def test_reference_child_update(self): from kivy.properties import NumericProperty, ReferenceListProperty x = NumericProperty(0) x.link(wid, 'x') x.link_deps(wid, 'x') y = NumericProperty(0) y.link(wid, 'y') y.link_deps(wid, 'y') pos = ReferenceListProperty(x, y) pos.link(wid, 'pos') pos.link_deps(wid, 'pos') pos.get(wid)[0] = 10 self.assertEqual(pos.get(wid), [10, 0]) pos.get(wid)[:] = (20, 30) self.assertEqual(pos.get(wid), [20, 30])
class CommonElevationBehavior(Widget): """Common base class for rectangular and circular elevation behavior.""" elevation = BoundedNumericProperty(0, min=0) """ Elevation of the widget .. note:: However this value does not represent the current elevation of the widget. `_elevation` can be used to animate the current elevation and come back using the `elevation` property directly. For example: .. code-block:: python from kivy.lang import Builder from kivy.uix.behaviors import ButtonBehavior from kivymd.app import MDApp from kivymd.uix.behaviors import CircularElevationBehavior, CircularRippleBehavior from kivymd.uix.boxlayout import MDBoxLayout KV = ''' #:import Animation kivy.animation.Animation MDScreen: CircularRippleButton: size_hint: None, None size: "50dp", "50dp" md_bg_color: .2, .2, .2, 1 pos_hint: {"center_x": .5, "center_y": .5} on_press: Animation(_elevation=40, d=1).start(self) on_release: Animation(_elevation=6, d=1).start(self) ''' class CircularRippleButton( CircularElevationBehavior, CircularRippleBehavior, ButtonBehavior, MDBoxLayout, ): def __init__(self, **kv): super().__init__(**kv) self.elevation = 6 def on_size(self, *args): self.radius = [self.size[0] / 2] class Example(MDApp): def build(self): return Builder.load_string(KV) Example().run() """ # Shadow rendering porpoerties # Shadow rotation meomry - SHARED ACROSS OTHER CLASSES. angle = NumericProperty(0) """ Angle of rotation in degrees of the current shadow. This value is shared across different widgets. .. note:: This value will affect both, hard and soft shadows. Each shadow has his own origin point that's computed every time the elevation changes. :attr:`angle` is an :class:`~kivy.properties.NumericProperty` and defaults to `0`. """ radius = VariableListProperty([0]) """ Radius of the Corners of the shadow. This values represents each corner of the shadow, starting from top-left corner and going clockwise. .. code-block:: python radius = [ "top-left", "top-right", "bottom-right", "bottom-left", ] This value can be expanded thus allowing this settings to be valid: .. code-block:: python widget.radius = [0] # Translates to [0, 0, 0, 0] widget.radius = [10, 3] # Translates to [10, 3, 10, 3] widget.radius = [7.0, 8.7, 1.5, 3.0] # Translates to [7, 8, 1, 3] .. note:: This value will affect both, hard and soft shadows. this value only affects RoundedRectangularElevationBehavior for now, but can be stored and used by custom shadow Draw functions. :attr:`radius` is an :class:`~kivy.properties.VariableListProperty` and defaults to `[0, 0, 0, 0]`. """ # Position of the shadow _shadow_origin_x = NumericProperty(0) """ Shadow origin x position for the rotation origin. Managed by `_shadow_origin`. :attr:`_shadow_origin_x` is an :class:`~kivy.properties.NumericProperty` and defaults to `0`. .. note:: This property is automatically processed. by _shadow_origin """ _shadow_origin_y = NumericProperty(0) """ Shadow origin y position for the rotation origin. Managed by `_shadow_origin`. :attr:`_shadow_origin_y` is an :class:`~kivy.properties.NumericProperty` and defaults to `0`. .. note:: This property is automatically processed. """ _shadow_origin = ReferenceListProperty(_shadow_origin_x, _shadow_origin_y) """ Soft Shadow Rotation origin point. :attr:`_shadow_origin` is an :class:`~kivy.properties.ReferenceListProperty` and defaults to `[0, 0]`. .. note:: This property is automatically processed and relative to the canvas center. """ _shadow_pos = ListProperty([0, 0]) # custom offset """ Soft Shadow origin point. :attr:`_shadow_pos` is an :class:`~kivy.properties.ListProperty` and defaults to `[0, 0]`. .. note:: This property is automatically processed and relative to the Widget's canvas center. """ shadow_pos = ListProperty([0, 0]) # bottom left corner """ Custom shadow Origin point. if this property is set, _shadow_pos will be ommited. This property allows userts to fake light source. :attr:`shadow_pos` is an :class:`~kivy.properties.ListProperty` and defaults to `[0, 0]`. .. note:: this value overwrite the _shadow_pos processing """ # Shadow Group shared memory __shadow_groups = {"global": []} shadow_group = StringProperty("global") """ Widget's shadow Group. By default every widget with a shadow is saved inside the memory `__shadow_groups` as a weakref. This means that you can have multiple light sources, one for every shadow group. To fake a light source use force_shadow_pos. :attr:`shadow_group` is an :class:`~kivy.properties.StringProperty` and defaults to `"global"`. """ _elevation = NumericProperty(0) """ inner memory for the elevation. .. warning:: This property is the current elevation of the widget, do not use this property directly, instead, use `CommonElevationBehavior` elevation. :attr:`_elevation` is an :class:`~kivy.properties.NumericProperty` and defaults to `0`. """ # soft shadow _soft_shadow_texture = ObjectProperty() """ Texture of the Soft Shadow texture for the canvas. :attr:`_soft_shadow_texture` is an :class:`~kivy.core.image.Image` and defaults to `None`. .. note:: This property is automatically processed. """ soft_shadow_size = ListProperty([0, 0]) """ Size of the soft Shadow texture over the canvas. :attr:`soft_shadow_size` is an :class:`~kivy.properties.ListProperty` and defaults to `[0, 0]`. .. note:: This property is automatically processed. """ soft_shadow_pos = ListProperty([0, 0]) """ Position of the Hard Shadow texture over the canvas. :attr:`soft_shadow_pos` is an :class:`~kivy.properties.ListProperty` and defaults to `[0, 0]`. .. note:: This property is automatically processed. """ soft_shadow_cl = ListProperty([0, 0, 0, 0.50]) """ Color of the soft Shadow :attr:`soft_shadow_cl` is an :class:`~kivy.properties.ListProperty` and defaults to `[0, 0, 0, 0.15]`. """ # hard shadow hard_shadow_texture = ObjectProperty() """ Texture of the Hard Shadow texture for the canvas. :attr:`hard_shadow_texture` is an :class:`~kivy.core.image.Image` and defaults to `None`. .. note:: This property is automatically processed when elevation is changed. """ hard_shadow_size = ListProperty([0, 0]) """ Size of the Hard Shadow texture over the canvas. :attr:`hard_shadow_size` is an :class:`~kivy.properties.ListProperty` and defaults to `[0, 0]`. .. note:: This property is automatically processed when elevation is changed. """ hard_shadow_pos = ListProperty([0, 0]) """ Position of the Hard Shadow texture over the canvas. :attr:`hard_shadow_pos` is an :class:`~kivy.properties.ListProperty` and defaults to `[0, 0]`. .. note:: This property is automatically processed when elevation is changed. """ hard_shadow_cl = ListProperty([0, 0, 0, 0.15]) """ Color of the Hard Shadow. .. note:: :attr:`hard_shadow_cl` is an :class:`~kivy.properties.ListProperty` and defaults to `[0, 0, 0, 0.15]`. """ # Shared property for some calculations. hard_shadow_offset = BoundedNumericProperty(2, min=0, errorhandler=lambda x: 0 if x < 0 else x) """ This value sets a special offset to the shadow canvas, this offset allows a correct draw of the canvas size. allowing the effect to correctly blur the image in the given space. :attr:`hard_shadow_offset` is an :class:`~kivy.properties.BoundedNumericProperty` and defaults to `2`. """ soft_shadow_offset = BoundedNumericProperty(4, min=0, errorhandler=lambda x: 0 if x < 0 else x) """ This value sets a special offset to the shadow canvas, this offset allows a correct draw of the canvas size. allowing the effect to correctly blur the image in the given space. :attr:`soft_shadow_offset` is an :class:`~kivy.properties.BoundedNumericProperty` and defaults to `4`. """ draw_shadow = ObjectProperty(None) """ This property controls the draw call of the context. This property is automatically set to `__draw_shadow__` inside the `super().__init__ call.` unless the property is different of None. To set a different drawing instruction function, set this property before the `super(),__init__` call inside the `__init__` definition of the new class. You can use the source for this clases as example of how to draw over with the context: #. "RectangularElevationBehavior" #. "CircularElevationBehavior" #. "RoundedRectangularElevationBehavior" :attr:`draw_shadow` is an :class:`~kivy.properties.ObjectProperty` and defaults to None. .. note:: If this property is left to None the CommonElevationBehavior will set to a function that will raise a `NotImplementedError` inside `super().__init__`. Follow the next example to set a new draw instruction for the class inside `__init__`: .. code-block:: python class RoundedRectangularElevationBehavior(CommonElevationBehavior): ''' Shadow class for the RoundedRectangular shadow behavior. Controls the size and position of the shadow.''' def __init__(self, **kwargs): self._draw_shadow = WeakMethod(self.__draw_shadow__) super().__init__(**kwargs) def __draw_shadow__(self, origin, end, context=None): context.draw(...) Context is a Pillow `ImageDraw` class. For more information check the [Pillow official documentation](https://github.com/python-pillow/Pillow/). """ def __init__(self, **kwargs): if self.draw_shadow is None: self.draw_shadow = WeakMethod(self.__draw_shadow__) self.prev_shadow_group = None im = BytesIO() Image.new("RGBA", (4, 4), color=(0, 0, 0, 0)).save(im, format="png") im.seek(0) self._soft_shadow_texture = self.hard_shadow_texture = CoreImage( im, ext="png").texture Clock.schedule_once(self.shadow_preset, -1) self.on_shadow_group(self, self.shadow_group) self.bind( pos=self._update_shadow, size=self._update_shadow, radius=self._update_shadow, ) super().__init__(**kwargs) def on_shadow_group(self, instance, value): """ This function controls the shadow group of the widget. Do not use Directly to change the group. instead, use the shadow_group :attr:`property`. """ groups = CommonElevationBehavior.__shadow_groups if self.prev_shadow_group: group = groups[self.prev_shadow_group] for widget in group[:]: if widget() is self: group.remove(widget) group = self.prev_shadow_group = self.shadow_group if group not in groups: groups[group] = [] r = ref(self, CommonElevationBehavior._clear_shadow_groups) groups[group].append(r) @staticmethod def _clear_shadow_groups(wk): # auto flush the element when the weak reference have been deleted groups = CommonElevationBehavior.__shadow_groups for group in list(groups.values()): if not group: break if wk in group: group.remove(wk) break def force_shadow_pos(self, shadow_pos): """ This property forces the shadow position in every widget inside the widget. The argument :attr:`shadow_pos` is expected as a <class 'list'> or <class 'tuple'>. """ if self.shadow_group is None: return group = CommonElevationBehavior.__shadow_groups[self.shadow_group] for wk in group[:]: widget = wk() if widget is None: group.remove(wk) widget.shadow_pos = shadow_pos del group def update_group_property(self, property_name, value): """ This functions allows to change properties of every widget inside the shadow group. """ if self.shadow_group is None: return group = CommonElevationBehavior.__shadow_groups[self.shadow_group] for wk in group[:]: widget = wk() if widget is None: group.remove(wk) setattr(widget, property_name, value) del group def shadow_preset(self, *dt): """ This function is meant to set the default configuration of the elevation. After a new instance is created, the elevation property will be launched and thus this function will update the elevation if the KV lang have not done it already. Works similar to an `__after_init__` call inside a widget. """ if self.elevation is None: self.elevation = 10 self._update_shadow(self, self.elevation) self.bind( pos=self._update_shadow, size=self._update_shadow, _elevation=self._update_shadow, ) def on_elevation(self, instance, value): """ Elevation event that sets the current elevation value to _elevation """ if value is not None: self._elevation = value def _set_soft_shadow_a(self, value): value = 0 if value < 0 else (1 if value > 1 else value) self.soft_shadow_cl[-1] = value return True def _set_hard_shadow_a(self, value): value = 0 if value < 0 else (1 if value > 1 else value) self.hard_shadow_cl[-1] = value return True def _get_soft_shadow_a(self): return self.soft_shadow_cl[-1] def _get_hard_shadow_a(self): return self.hard_shadow_cl[-1] _soft_shadow_a = AliasProperty(_get_soft_shadow_a, _set_soft_shadow_a, bind=["soft_shadow_cl"]) _hard_shadow_a = AliasProperty(_get_hard_shadow_a, _set_hard_shadow_a, bind=["hard_shadow_cl"]) def on_disabled(self, instance, value): """ This function hides the shadow when the widget is disabled. it sets the shadow to 0. """ if self.disabled is True: self._elevation = 0 else: self._elevation = 0 if self.elevation is None else self.elevation self._update_shadow(self, self._elevation) try: super().on_disabled(instance, value) except Exception: pass def _update_elevation(self, instance, value): self._elevation = value self._update_shadow(instance, value) def _update_shadow_pos(self, instance, value): self.hard_shadow_pos = [ self.x - dp(self.hard_shadow_offset), # + self.shadow_pos[0], self.y - dp(self.hard_shadow_offset), # + self.shadow_pos[1], ] if self.shadow_pos == [0, 0]: self.soft_shadow_pos = [ self.x + self._shadow_pos[0] - self._elevation - dp(self.soft_shadow_offset), self.y + self._shadow_pos[1] - self._elevation - dp(self.soft_shadow_offset), ] else: self.soft_shadow_pos = [ self.x + self.shadow_pos[0] - self._elevation - dp(self.soft_shadow_offset), self.y + self.shadow_pos[1] - self._elevation - dp(self.soft_shadow_offset), ] self._shadow_origin = [ self.soft_shadow_pos[0] + self.soft_shadow_size[0] / 2, self.soft_shadow_pos[1] + self.soft_shadow_size[1] / 2, ] def on__shadow_pos(self, ins, val): """ Updates the shadow with the computed value. Call this function every time you need to force a shadow update. """ self._update_shadow_pos(ins, val) def on_shadow_pos(self, ins, val): """ Updates the shadow with the fixed value. Call this function every time you need to force a shadow update. """ self._update_shadow_pos(ins, val) def _update_shadow(self, instance, value): self._update_shadow_pos(instance, value) if self._elevation > 0: # dynamic elecation position for the shadow if self.shadow_pos == [0, 0]: self._shadow_pos = [0, -self._elevation * 0.4] # HARD Shadow offset = int(dp(self.hard_shadow_offset)) size = [ int(self.size[0] + (offset * 2)), int(self.size[1] + (offset * 2)), ] im = BytesIO() # Context img = Image.new("RGBA", tuple(size), color=(0, 0, 0, 0)) # Draw context shadow = ImageDraw.Draw(img) self.draw_shadow()([offset, offset], [ int(size[0] - 1 - offset), int(size[1] - 1 - offset), ], context=shadow # context=ref(shadow) ) img = img.filter( ImageFilter.GaussianBlur( radius=int(dp(1 + self.hard_shadow_offset / 3)))) img.save(im, format="png") im.seek(0) self.hard_shadow_size = size self.hard_shadow_texture = CoreImage(im, ext="png").texture # soft shadow offset = dp(self.soft_shadow_offset) size = [ int(self.size[0] + dp(self._elevation * 2) + (offset * 2)), int(self.size[1] + dp(self._elevation * 2) + (offset * 2)), # ((self._elevation)*2) + x + (offset*2)) for x in self.size ] im = BytesIO() img = Image.new("RGBA", tuple(size), color=((0, ) * 4)) shadow = ImageDraw.Draw(img) _offset = int(dp(self._elevation + offset)) self.draw_shadow()([ _offset, _offset, ], [int(size[0] - _offset - 1), int(size[1] - _offset - 1)], context=shadow # context=ref(shadow) ) img = img.filter( ImageFilter.GaussianBlur(radius=self._elevation // 2)) shadow = ImageDraw.Draw(img) img.save(im, format="png") im.seek(0) self.soft_shadow_size = size self._soft_shadow_texture = CoreImage(im, ext="png").texture else: im = BytesIO() Image.new("RGBA", (4, 4), color=(0, 0, 0, 0)).save(im, format="png") im.seek(0) # self._soft_shadow_texture = self.hard_shadow_texture = CoreImage( im, ext="png").texture return def __draw_shadow__(self, origin, end, context=None): raise NotImplementedError( "KivyMD:\n" "If you see this error, this means that either youre using " "CommonElevationBehavior directly or your 'shader' dont have a " "_draw_shadow instruction, remember to overwrite this function to " "draw over the image context. the figure you would like.")
class Agent(Widget): angle = NumericProperty(0) rotation = NumericProperty(0) velocity_x = NumericProperty(0) velocity_y = NumericProperty(0) velocity = ReferenceListProperty(velocity_x, velocity_y) sensor1_x = NumericProperty(0) sensor1_y = NumericProperty(0) sensor1 = ReferenceListProperty(sensor1_x, sensor1_y) sensor2_x = NumericProperty(0) sensor2_y = NumericProperty(0) sensor2 = ReferenceListProperty(sensor2_x, sensor2_y) sensor3_x = NumericProperty(0) sensor3_y = NumericProperty(0) sensor3 = ReferenceListProperty(sensor3_x, sensor3_y) signal1 = NumericProperty(0) signal2 = NumericProperty(0) signal3 = NumericProperty(0) def move(self, rotation): self.pos = Vector(*self.velocity) + self.pos self.rotation = rotation self.angle = self.angle + self.rotation self.sensor1 = Vector(30, 0).rotate(self.angle) + self.pos self.sensor2 = Vector(30, 0).rotate((self.angle + 30) % 360) + self.pos self.sensor3 = Vector(30, 0).rotate((self.angle - 30) % 360) + self.pos self.signal1 = 1 if ( int( np.sum(white_space[ int(self.sensor1_x) - 10:int(self.sensor1_x) + 10, int(self.sensor1_y) - 10:int(self.sensor1_y) + 10])) / 400 > 0.5) else int( np.sum(white_space[int(self.sensor1_x) - 10:int(self.sensor1_x) + 10, int(self.sensor1_y) - 10:int(self.sensor1_y) + 10])) / 400. self.signal2 = 1 if ( int( np.sum(white_space[ int(self.sensor2_x) - 10:int(self.sensor2_x) + 10, int(self.sensor2_y) - 10:int(self.sensor2_y) + 10])) / 400 > 0.5) else int( np.sum(white_space[int(self.sensor2_x) - 10:int(self.sensor2_x) + 10, int(self.sensor2_y) - 10:int(self.sensor2_y) + 10])) / 400. self.signal3 = 1 if ( int( np.sum(white_space[ int(self.sensor3_x) - 10:int(self.sensor3_x) + 10, int(self.sensor3_y) - 10:int(self.sensor3_y) + 10])) / 400 > 0.5) else int( np.sum(white_space[int(self.sensor3_x) - 10:int(self.sensor3_x) + 10, int(self.sensor3_y) - 10:int(self.sensor3_y) + 10])) / 400. if self.sensor1_x > maze_width - 10 or self.sensor1_x < 10 or self.sensor1_y > maze_height - 10 or self.sensor1_y < 10: self.signal1 = 1. if self.sensor2_x > maze_width - 10 or self.sensor2_x < 10 or self.sensor2_y > maze_height - 10 or self.sensor2_y < 10: self.signal2 = 1. if self.sensor3_x > maze_width - 10 or self.sensor3_x < 10 or self.sensor3_y > maze_height - 10 or self.sensor3_y < 10: self.signal3 = 1.
class CropScatter(ScatterLayout): image_width = NumericProperty(1) image_height = NumericProperty(1) image_size = ReferenceListProperty(image_width, image_height) image_x = NumericProperty(0) image_y = NumericProperty(0) image_pos = ReferenceListProperty(image_x, image_y) crop_width = NumericProperty(0) crop_height = NumericProperty(0) crop_size = ReferenceListProperty(crop_width, crop_height) crop_x = NumericProperty(0) crop_y = NumericProperty(0) crop_pos = ReferenceListProperty(crop_x, crop_y) frame_width = NumericProperty(0) frame_height = NumericProperty(0) frame_size = ReferenceListProperty(frame_width, frame_height) texture = ObjectProperty(None) source = StringProperty(None) zoom_delta = NumericProperty(0.05) def on_touch_down(self, touch): result = super().on_touch_down(touch) if result: if touch.button == 'scrollup': self.apply_image_transform(scale=self.clamp_scale(1 - self.zoom_delta), anchor=(touch.x, touch.y)) elif touch.button == 'scrolldown': self.apply_image_transform(scale=self.clamp_scale(1 + self.zoom_delta), anchor=(touch.x, touch.y)) return result def clamp_scale(self, scale): new_scale = scale * self.scale if new_scale < self.scale_min: scale = self.scale_min / self.scale elif new_scale > self.scale_max: scale = self.scale_max / self.scale return scale def transform_with_touch(self, touch): # just do a simple one finger drag changed = False if len(self._touches) == self.translation_touches: # _last_touch_pos has last pos in correct parent space, # just like incoming touch dx = (touch.x - self._last_touch_pos[touch][0]) * self.do_translation_x dy = (touch.y - self._last_touch_pos[touch][1]) * self.do_translation_y dx = dx / self.translation_touches dy = dy / self.translation_touches self.apply_image_transform(x=dx, y=dy) changed = True if len(self._touches) == 1: # If we only have one touch, we want to scale on a scroll return changed # we have more than one touch... list of last known pos points = [Vector(self._last_touch_pos[t]) for t in self._touches if t is not touch] # add current touch last points.append(Vector(touch.pos)) # we only want to transform if the touch is part of the two touches # farthest apart! So first we find anchor, the point to transform # around as another touch farthest away from current touch's pos anchor = max(points[:-1], key=lambda p: p.distance(touch.pos)) # now we find the touch farthest away from anchor, if its not the # same as touch. Touch is not one of the two touches used to transform farthest = max(points, key=anchor.distance) if farthest is not points[-1]: return changed # ok, so we have touch, and anchor, so we can actually compute the # transformation old_line = Vector(*touch.ppos) - anchor new_line = Vector(*touch.pos) - anchor if not old_line.length(): # div by zero return changed angle = radians(new_line.angle(old_line)) * self.do_rotation if angle: changed = True if self.do_scale: scale = new_line.length() / old_line.length() self.apply_image_transform(scale=self.clamp_scale(scale), anchor=anchor) changed = True return changed def apply_image_transform(self, x=0, y=0, scale=1, post_multiply=False, anchor=(0, 0)): # Test if the x move is valid t = Matrix() t = t.translate(x, 0, 0) if not self.test_valid(t.multiply(self.transform)): x = 0 # Test if the y move is valid t = Matrix() t = t.translate(0, y, 0) if not self.test_valid(t.multiply(self.transform)): y = 0 # Test if the scale is valid t = Matrix().translate(anchor[0], anchor[1], 0) t = t.scale(scale, scale, scale) t = t.multiply(Matrix().translate(-anchor[0], -anchor[1], 0)) if not self.test_valid(t.multiply(self.transform)): scale = 1 # Compile final matrix t = Matrix().translate(anchor[0], anchor[1], 0) t = t.translate(x, y, 0) t = t.scale(scale, scale, scale) t = t.multiply(Matrix().translate(-anchor[0], -anchor[1], 0)) if post_multiply: self.transform = self.transform.multiply(t) else: self.transform = t.multiply(self.transform) def test_valid(self, matrix): crop_x, crop_y = (self.width - self.crop_width) / 2, (self.height - self.crop_height) / 2 x, y, z = matrix.transform_point(self.image_x, self.image_y, 0) mx, my, mz = matrix.transform_point(self.image_x + self.image_width, self.image_y + self.image_height, 0) if x > crop_x: return False if y > crop_y: return False if mx < crop_x + self.crop_width: return False if my < crop_y + self.crop_height: return False return True def on_texture(self, instance, value): self.calculate() self.reset() def on_size(self, *args): self.calculate() def reset(self): self.matrix = Matrix() def calculate(self): if 'image' not in self.ids or self.ids.image.texture is None: return width, height = self.ids.image.texture.size frame_width, frame_height = self.width, self.height if width > height: self.image_width = frame_width self.image_height = height / width * frame_height self.crop_size = self.image_height, self.image_height self.frame_size = frame_width, frame_width self.image_y = (self.height - self.image_height) / 2 else: self.image_width = width / height * frame_width self.image_height = frame_height self.frame_size = frame_height, frame_height self.crop_size = self.image_width, self.image_width self.image_x = (self.width - self.image_width) / 2 def get_image_crop(self): x, y, z = self.transform.transform_point(self.image_x, self.image_y, 0) mx, my, mz = self.transform.transform_point(self.image_x + self.image_width, self.image_y + self.image_height, 0) ccenx, cceny = (self.width - self.crop_width) / 2 + self.crop_width / 2, (self.height - self.crop_height) / 2 + self.crop_height / 2 cx, cy = (self.width - self.crop_width) / 2, (self.height - self.crop_height) / 2 cmx, cmy = cx + self.crop_width, cy + self.crop_height cw = int(round(self.texture.width * ((cmx - cx) / (mx - x)))) ch = int(round(self.texture.height * ((cmy - cy) / (my - y)))) cenx, ceny = int(round((ccenx - x) * (self.texture.width / (mx - x)))), int(round((cceny - y) * (self.texture.height / (my - y)))) return cw, ch, cenx, ceny
class CircularNumberPicker(CircularLayout): """A circular number picker based on CircularLayout. A selector will help you pick a number. You can also set :attr:`multiples_of` to make it show only some numbers and use the space in between for the other numbers. """ min = NumericProperty(0) """The first value of the range. :attr:`min` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. """ max = NumericProperty(0) """The last value of the range. Note that it behaves like xrange, so the actual last displayed value will be :attr:`max` - 1. :attr:`max` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. """ range = ReferenceListProperty(min, max) """Packs :attr:`min` and :attr:`max` into a list for convenience. See their documentation for further information. :attr:`range` is a :class:`~kivy.properties.ReferenceListProperty`. """ multiples_of = NumericProperty(1) """Only show numbers that are multiples of this number. The other numbers will be selectable, but won't have their own label. :attr:`multiples_of` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. """ # selector_color = ListProperty([.337, .439, .490]) selector_color = ListProperty([1, 1, 1]) """Color of the number selector. RGB. :attr:`selector_color` is a :class:`~kivy.properties.ListProperty` and defaults to [.337, .439, .490] (material green). """ color = ListProperty([0, 0, 0]) """Color of the number labels and of the center dot. RGB. :attr:`color` is a :class:`~kivy.properties.ListProperty` and defaults to [1, 1, 1] (white). """ selector_alpha = BoundedNumericProperty(0.3, min=0, max=1) """Alpha value for the transparent parts of the selector. :attr:`selector_alpha` is a :class:`~kivy.properties.BoundedNumericProperty` and defaults to 0.3 (min=0, max=1). """ selected = NumericProperty(None) """Currently selected number. :attr:`selected` is a :class:`~kivy.properties.NumericProperty` and defaults to :attr:`min`. """ number_size_factor = NumericProperty(0.5) """Font size scale factor for the :class:`~Number`. :attr:`number_size_factor` is a :class:`~kivy.properties.NumericProperty` and defaults to 0.5. """ number_format_string = StringProperty("{}") """String that will be formatted with the selected number as the first argument. Can be anything supported by :meth:`str.format` (es. "{:02d}"). :attr:`number_format_string` is a :class:`~kivy.properties.StringProperty` and defaults to "{}". """ scale = NumericProperty(1) """Canvas scale factor. Used in :class:`CircularTimePicker` transitions. :attr:`scale` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. """ _selection_circle = ObjectProperty(None) _selection_line = ObjectProperty(None) _selection_dot = ObjectProperty(None) _selection_dot_color = ObjectProperty(None) _selection_color = ObjectProperty(None) _center_dot = ObjectProperty(None) _center_color = ObjectProperty(None) def _get_items(self): return self.max - self.min items = AliasProperty(_get_items, None) def _get_shown_items(self): sh = 0 for i in xrange(*self.range): if i % self.multiples_of == 0: sh += 1 return sh shown_items = AliasProperty(_get_shown_items, None) def __init__(self, **kw): self._trigger_genitems = Clock.create_trigger(self._genitems, -1) self.bind( min=self._trigger_genitems, max=self._trigger_genitems, multiples_of=self._trigger_genitems, ) super().__init__(**kw) self.selected = self.min self.bind( selected=self.on_selected, pos=self.on_selected, size=self.on_selected, ) cx = self.center_x + self.padding[0] - self.padding[2] cy = self.center_y + self.padding[3] - self.padding[1] sx, sy = self.pos_for_number(self.selected) epos = [ i - (self.delta_radii * self.number_size_factor) for i in (sx, sy) ] esize = [self.delta_radii * self.number_size_factor * 2] * 2 dsize = [i * 0.3 for i in esize] dpos = [i + esize[0] / 2.0 - dsize[0] / 2.0 for i in epos] csize = [i * 0.05 for i in esize] cpos = [i - csize[0] / 2.0 for i in (cx, cy)] dot_alpha = 0 if self.selected % self.multiples_of == 0 else 1 color = list(self.selector_color) with self.canvas: self._selection_color = Color(*(color + [self.selector_alpha])) self._selection_circle = Ellipse(pos=epos, size=esize) self._selection_line = Line(points=[cx, cy, sx, sy], width=dp(1.25)) self._selection_dot_color = Color(*(color + [dot_alpha])) self._selection_dot = Ellipse(pos=dpos, size=dsize) self._center_color = Color(*self.color) self._center_dot = Ellipse(pos=cpos, size=csize) self.bind(selector_color=lambda ign, u: setattr( self._selection_color, "rgba", u + [self.selector_alpha])) self.bind(selector_color=lambda ign, u: setattr( self._selection_dot_color, "rgb", u)) self.bind(selector_color=lambda ign, u: self.dot_is_none()) self.bind(color=lambda ign, u: setattr(self._center_color, "rgb", u)) Clock.schedule_once(self._genitems) # Just to make sure pos/size are set Clock.schedule_once(self.on_selected) def dot_is_none(self, *args): dot_alpha = 0 if self.selected % self.multiples_of == 0 else 1 if self._selection_dot_color: self._selection_dot_color.a = dot_alpha def _genitems(self, *a): self.clear_widgets() for i in xrange(*self.range): if i % self.multiples_of != 0: continue n = Number( text=self.number_format_string.format(i), size_factor=self.number_size_factor, color=self.color, ) self.bind(color=n.setter("color")) self.add_widget(n) def on_touch_down(self, touch): if not self.collide_point(*touch.pos): return touch.grab(self) self.selected = self.number_at_pos(*touch.pos) if self.selected == 60: self.selected = 0 def on_touch_move(self, touch): if touch.grab_current is not self: return super().on_touch_move(touch) self.selected = self.number_at_pos(*touch.pos) if self.selected == 60: self.selected = 0 def on_touch_up(self, touch): if touch.grab_current is not self: return super().on_touch_up(touch) touch.ungrab(self) def on_selected(self, *a): cx = self.center_x + self.padding[0] - self.padding[2] cy = self.center_y + self.padding[3] - self.padding[1] sx, sy = self.pos_for_number(self.selected) epos = [ i - (self.delta_radii * self.number_size_factor) for i in (sx, sy) ] esize = [self.delta_radii * self.number_size_factor * 2] * 2 dsize = [i * 0.3 for i in esize] dpos = [i + esize[0] / 2.0 - dsize[0] / 2.0 for i in epos] csize = [i * 0.05 for i in esize] cpos = [i - csize[0] / 2.0 for i in (cx, cy)] dot_alpha = 0 if self.selected % self.multiples_of == 0 else 1 if self._selection_circle: self._selection_circle.pos = epos self._selection_circle.size = esize if self._selection_line: self._selection_line.points = [cx, cy, sx, sy] if self._selection_dot: self._selection_dot.pos = dpos self._selection_dot.size = dsize if self._selection_dot_color: self._selection_dot_color.a = dot_alpha if self._center_dot: self._center_dot.pos = cpos self._center_dot.size = csize def pos_for_number(self, n): """Returns the center x, y coordinates for a given number. """ if self.items == 0: return 0, 0 radius = (min( self.width - self.padding[0] - self.padding[2], self.height - self.padding[1] - self.padding[3], ) / 2.0) middle_r = radius * sum(self.radius_hint) / 2.0 cx = self.center_x + self.padding[0] - self.padding[2] cy = self.center_y + self.padding[3] - self.padding[1] sign = +1.0 angle_offset = radians(self.start_angle) if self.direction == "cw": angle_offset = 2 * pi - angle_offset sign = -1.0 quota = 2 * pi / self.items mult_quota = 2 * pi / self.shown_items angle = angle_offset + n * sign * quota if self.items == self.shown_items: angle += quota / 2 else: angle -= mult_quota / 2 # kived: looking it up, yes. x = cos(angle) * radius + centerx; # y = sin(angle) * radius + centery x = cos(angle) * middle_r + cx y = sin(angle) * middle_r + cy return x, y def number_at_pos(self, x, y): """Returns the number at a given x, y position. The number is found using the widget's center as a starting point for angle calculations. Not thoroughly tested, may yield wrong results. """ if self.items == 0: return self.min cx = self.center_x + self.padding[0] - self.padding[2] cy = self.center_y + self.padding[3] - self.padding[1] lx = x - cx ly = y - cy quota = 2 * pi / self.items mult_quota = 2 * pi / self.shown_items if lx == 0 and ly > 0: angle = pi / 2 elif lx == 0 and ly < 0: angle = 3 * pi / 2 else: angle = atan(ly / lx) if lx < 0 < ly: angle += pi if lx > 0 > ly: angle += 2 * pi if lx < 0 and ly < 0: angle += pi angle += radians(self.start_angle) if self.direction == "cw": angle = 2 * pi - angle if mult_quota != quota: angle -= mult_quota / 2 if angle < 0: angle += 2 * pi elif angle > 2 * pi: angle -= 2 * pi return int(angle / quota) + self.min
class Car(Widget): angle = NumericProperty(0) rotation = NumericProperty(0) velocity_x = NumericProperty(0) velocity_y = NumericProperty(0) velocity = ReferenceListProperty(velocity_x, velocity_y) signal1 = NumericProperty(0) signal2 = NumericProperty(0) signal3 = NumericProperty(0) sand = None sand_width = 0 sand_height = 0 points = None red = None blue = None yellow = None def move(self, rotation): self.pos = Vector(*self.velocity) + self.pos self.rotation = rotation self.angle = self.angle + self.rotation self.red = Vector(25, 0).rotate(self.angle) + self.pos self.blue = Vector(25, 0).rotate((self.angle + 30) % 360) + self.pos self.yellow = Vector(25, 0).rotate((self.angle - 30) % 360) + self.pos if self.sensor_onborder(self.red): self.signal1 = 1. else: self.signal1 = self.sensor_sanddensity(self.red) if self.sensor_onborder(self.blue): self.signal2 = 1. else: self.signal2 = self.sensor_sanddensity(self.blue) if self.sensor_onborder(self.yellow): self.signal3 = 1. else: self.signal3 = self.sensor_sanddensity(self.yellow) def sensor_sanddensity(self, vector): int_sensorx = int(vector[0]) int_sensory = int(vector[1]) return int(np.sum(self.sand[int_sensorx:int_sensorx + 10, int_sensory:int_sensory + 10])) / 100. def get_vec_corners(self, vector): return { 'bottom_left': vector, 'top_left': Vector(vector[0], vector[1] + 10), 'top_right': Vector(vector[0] + 10, vector[1] + 10), 'bottom_right': Vector(vector[0] + 10, vector[1]), } def sensor_onborder(self, vector): corners = self.get_vec_corners(vector) margin = 2 # left border for corner in corners.values(): x_diff = margin - corner.x if x_diff > 0: return True # right border for corner in corners.values(): x_diff = corner.x - (self.sand_width - margin) if x_diff > 0: return True # top border for corner in corners.values(): y_diff = corner.y - (self.sand_height - margin) if y_diff > 0: return True # bottom border for corner in corners.values(): y_diff = margin - corner.y if y_diff > 0: return True return False def get_carfront(self): return Vector(self.pos[0] + self.width, self.center[1]) def get_corners(self): return { 'bottom_left': Vector(*self.pos), 'top_left': Vector(self.pos[0], self.pos[1] + self.height), 'top_right': Vector(self.pos[0] + self.width, self.pos[1] + self.height), 'bottom_right': Vector(self.pos[0] + self.width, self.pos[1]), }
class Knob(Widget): """Class for creating a Knob widget.""" min = NumericProperty(0) '''Minimum value for value :attr:`value`. :attr:`min` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' max = NumericProperty(100) '''Maximum value for value :attr:`value`. :attr:`max` is a :class:`~kivy.properties.NumericProperty` and defaults to 100. ''' range = ReferenceListProperty(min, max) ''' Range of the values for Knob. :attr:`range` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`min`, :attr:`max`). ''' value = NumericProperty(0) '''Current value of the knob. Set :attr:`value` when creating a knob to set its initial position. An internal :attr:`_angle` is calculated to set the position of the knob. :attr:`value` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' step = BoundedNumericProperty(1, min=0) '''Step interval of knob to go from min to max. An internal :attr:`_angle_step` is calculated to set knob step in degrees. :attr:`step` is a :class:`~kivy.properties.BoundedNumericProperty` and defaults to 1 (min=0). ''' curve = BoundedNumericProperty(1, min=1) '''This parameter determines the shape of the map function. It represent the reciprocal of a power function's exponent used to map the input value. So, for higher values of curve the contol is more reactive, and conversely. ''' knobimg_source = StringProperty("") '''Path of texture image that visually represents the knob. Use PNG for transparency support. The texture is rendered on a centered rectangle of size = :attr:`size` * :attr:`knobimg_size`. :attr:`knobimg_source` is a :class:`~kivy.properties.StringProperty` and defaults to empty string. ''' knobimg_color = ListProperty([1, 1, 1, 1]) '''Color to apply to :attr:`knobimg_source` texture when loaded. :attr:`knobimg_color` is a :class:`~kivy.properties.ListProperty` and defaults to [1,1,1,1]. ''' knobimg_size = BoundedNumericProperty(0.9, max=1.0, min=0.1) ''' Internal proportional size of rectangle that holds :attr:`knobimg_source` texture. :attr:`knobimg_size` is a :class:`~kivy.properties.BoundedNumericProperty` and defaults to 0.9. ''' show_marker = BooleanProperty(True) ''' Shows/hides marker surrounding knob. use :attr:`knob_size` < 1.0 to leave space to marker. :attr:`show_marker` is a :class:`~kivy.properties.BooleanProperty` and defaults to True. ''' marker_img = StringProperty("") '''Path of texture image that visually represents the knob marker. The marker is rendered in a centered Ellipse (Circle) with the same size of the widget and goes from angle_start=:attr:`marker_startangle` to angle_end=:attr:`_angle`. :attr:`marker_img` is a :class:`~kivy.properties.StringProperty` and defaults to "". ''' marker_color = ListProperty([1, 1, 1, 1]) '''Color to apply to :attr:`marker_img` texture when loaded. :attr:`marker_color` is a :class:`~kivy.properties.ListProperty` and defaults to [1,1,1,1]. ''' knobimg_bgcolor = ListProperty([0, 0, 0, 1]) ''' Background color behind :attr:`knobimg_source` texture. :attr:`value` is a :class:`~kivy.properties.ListProperty` and defaults to [0,0,0,1]. ''' markeroff_img = StringProperty("") '''Path of texture image that visually represents the knob marker where it's off, that is, parts of the marker that haven't been reached yet by the knob (:attr:`value`). :attr:`markeroff_img` is a :class:`~kivy.properties.StringProperty` and defaults to "". ''' markeroff_color = ListProperty([0, 0, 0, 0]) '''Color applied to :attr:`markeroff_img` int the Canvas. :attr:`markeroff_color` is a :class:`~kivy.properties.ListProperty` and defaults to [0,0,0,0]. ''' marker_startangle = NumericProperty(0) '''Starting angle of Ellipse where :attr:`marker_img` is rendered. :attr:`value` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' marker_ahead = NumericProperty(0) ''' Adds degrees to angle_end of marker (except when :attr:`value` == 0). :attr:`marker_ahead` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' _angle = NumericProperty(0) # Internal angle calculated from value. _angle_step = NumericProperty(0) # Internal angle_step calculated from step. def __init__(self, *args, **kwargs): super(Knob, self).__init__(*args, **kwargs) self.bind(show_marker = self._show_marker) self.bind(value = self._value) def _value(self, instance, value): self._angle = pow( (value - self.min)/(self.max - self.min), 1./self.curve) * 360. self.on_knob(value) def _show_marker(self, instance, flag): # "show/hide" marker. if flag: self.knobimg_bgcolor[3] = 1 self.marker_color[3] = 1 self.markeroff_color[3] = 1 else: self.knobimg_bgcolor[3] = 0 self.marker_color[3] = 0 self.markeroff_color[3] = 0 def on_touch_down(self, touch): if self.collide_point(*touch.pos): self.update_angle(touch) def on_touch_move(self, touch): if self.collide_point(*touch.pos): self.update_angle(touch) def update_angle(self, touch): posx, posy = touch.pos cx, cy = self.center rx, ry = posx - cx, posy - cy if ry >= 0: # Quadrants are clockwise. quadrant = 1 if rx >= 0 else 4 else: quadrant = 3 if rx <= 0 else 2 try: angle = math.atan(rx / ry) * (180./math.pi) if quadrant == 2 or quadrant == 3: angle = 180 + angle elif quadrant == 4: angle = 360 + angle except: # atan not def for angle 90 and 270 angle = 90 if quadrant <= 2 else 270 self._angle_step = (self.step*360)/(self.max - self.min) self._angle = self._angle_step while self._angle < angle: self._angle = self._angle + self._angle_step relativeValue = pow((angle/360.), 1./self.curve) self.value = (relativeValue * (self.max - self.min)) + self.min #TO OVERRIDE def on_knob(self, value): pass #Knob values listenerr
class ScrollView(StencilView): '''ScrollView class. See module documentation for more information. :Events: `on_scroll_start` Generic event fired when scrolling starts from touch. `on_scroll_move` Generic event fired when scrolling move from touch. `on_scroll_stop` Generic event fired when scrolling stops from touch. .. versionchanged:: 1.9.0 `on_scroll_start`, `on_scroll_move` and `on_scroll_stop` events are now dispatched when scrolling to handle nested ScrollViews. .. versionchanged:: 1.7.0 `auto_scroll`, `scroll_friction`, `scroll_moves`, `scroll_stoptime' has been deprecated, use :attr:`effect_cls` instead. ''' scroll_distance = NumericProperty(_scroll_distance) '''Distance to move before scrolling the :class:`ScrollView`, in pixels. As soon as the distance has been traveled, the :class:`ScrollView` will start to scroll, and no touch event will go to children. It is advisable that you base this value on the dpi of your target device's screen. :attr:`scroll_distance` is a :class:`~kivy.properties.NumericProperty` and defaults to 20 (pixels), according to the default value in user configuration. ''' scroll_wheel_distance = NumericProperty('20sp') '''Distance to move when scrolling with a mouse wheel. It is advisable that you base this value on the dpi of your target device's screen. .. versionadded:: 1.8.0 :attr:`scroll_wheel_distance` is a :class:`~kivy.properties.NumericProperty` , defaults to 20 pixels. ''' scroll_timeout = NumericProperty(_scroll_timeout) '''Timeout allowed to trigger the :attr:`scroll_distance`, in milliseconds. If the user has not moved :attr:`scroll_distance` within the timeout, the scrolling will be disabled, and the touch event will go to the children. :attr:`scroll_timeout` is a :class:`~kivy.properties.NumericProperty` and defaults to 55 (milliseconds) according to the default value in user configuration. .. versionchanged:: 1.5.0 Default value changed from 250 to 55. ''' scroll_x = NumericProperty(0.) '''X scrolling value, between 0 and 1. If 0, the content's left side will touch the left side of the ScrollView. If 1, the content's right side will touch the right side. This property is controled by :class:`ScrollView` only if :attr:`do_scroll_x` is True. :attr:`scroll_x` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' scroll_y = NumericProperty(1.) '''Y scrolling value, between 0 and 1. If 0, the content's bottom side will touch the bottom side of the ScrollView. If 1, the content's top side will touch the top side. This property is controled by :class:`ScrollView` only if :attr:`do_scroll_y` is True. :attr:`scroll_y` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. ''' do_scroll_x = BooleanProperty(True) '''Allow scroll on X axis. :attr:`do_scroll_x` is a :class:`~kivy.properties.BooleanProperty` and defaults to True. ''' do_scroll_y = BooleanProperty(True) '''Allow scroll on Y axis. :attr:`do_scroll_y` is a :class:`~kivy.properties.BooleanProperty` and defaults to True. ''' def _get_do_scroll(self): return (self.do_scroll_x, self.do_scroll_y) def _set_do_scroll(self, value): if type(value) in (list, tuple): self.do_scroll_x, self.do_scroll_y = value else: self.do_scroll_x = self.do_scroll_y = bool(value) do_scroll = AliasProperty(_get_do_scroll, _set_do_scroll, bind=('do_scroll_x', 'do_scroll_y')) '''Allow scroll on X or Y axis. :attr:`do_scroll` is a :class:`~kivy.properties.AliasProperty` of (:attr:`do_scroll_x` + :attr:`do_scroll_y`) ''' def _get_vbar(self): # must return (y, height) in % # calculate the viewport size / scrollview size % if self._viewport is None: return 0, 1. vh = self._viewport.height h = self.height if vh < h or vh == 0: return 0, 1. ph = max(0.01, h / float(vh)) sy = min(1.0, max(0.0, self.scroll_y)) py = (1. - ph) * sy return (py, ph) vbar = AliasProperty(_get_vbar, None, bind=('scroll_y', '_viewport', 'viewport_size')) '''Return a tuple of (position, size) of the vertical scrolling bar. .. versionadded:: 1.2.0 The position and size are normalized between 0-1, and represent a percentage of the current scrollview height. This property is used internally for drawing the little vertical bar when you're scrolling. :attr:`vbar` is a :class:`~kivy.properties.AliasProperty`, readonly. ''' def _get_hbar(self): # must return (x, width) in % # calculate the viewport size / scrollview size % if self._viewport is None: return 0, 1. vw = self._viewport.width w = self.width if vw < w or vw == 0: return 0, 1. pw = max(0.01, w / float(vw)) sx = min(1.0, max(0.0, self.scroll_x)) px = (1. - pw) * sx return (px, pw) hbar = AliasProperty(_get_hbar, None, bind=('scroll_x', '_viewport', 'viewport_size')) '''Return a tuple of (position, size) of the horizontal scrolling bar. .. versionadded:: 1.2.0 The position and size are normalized between 0-1, and represent a percentage of the current scrollview height. This property is used internally for drawing the little horizontal bar when you're scrolling. :attr:`vbar` is a :class:`~kivy.properties.AliasProperty`, readonly. ''' bar_color = ListProperty([.7, .7, .7, .9]) '''Color of horizontal / vertical scroll bar, in RGBA format. .. versionadded:: 1.2.0 :attr:`bar_color` is a :class:`~kivy.properties.ListProperty` and defaults to [.7, .7, .7, .9]. ''' bar_inactive_color = ListProperty([.7, .7, .7, .2]) '''Color of horizontal / vertical scroll bar (in RGBA format), when no scroll is happening. .. versionadded:: 1.9.0 :attr:`bar_inactive_color` is a :class:`~kivy.properties.ListProperty` and defaults to [.7, .7, .7, .2]. ''' bar_width = NumericProperty('2dp') '''Width of the horizontal / vertical scroll bar. The width is interpreted as a height for the horizontal bar. .. versionadded:: 1.2.0 :attr:`bar_width` is a :class:`~kivy.properties.NumericProperty` and defaults to 2. ''' bar_pos_x = OptionProperty('bottom', options=('top', 'bottom')) '''Which side of the ScrollView the horizontal scroll bar should go on. Possible values are 'top' and 'bottom'. .. versionadded:: 1.8.0 :attr:`bar_pos_x` is an :class:`~kivy.properties.OptionProperty`, defaults to 'bottom'. ''' bar_pos_y = OptionProperty('right', options=('left', 'right')) '''Which side of the ScrollView the vertical scroll bar should go on. Possible values are 'left' and 'right'. .. versionadded:: 1.8.0 :attr:`bar_pos_y` is an :class:`~kivy.properties.OptionProperty` and defaults to 'right'. ''' bar_pos = ReferenceListProperty(bar_pos_x, bar_pos_y) '''Which side of the scroll view to place each of the bars on. :attr:`bar_pos` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`bar_pos_x`, :attr:`bar_pos_y`) ''' bar_margin = NumericProperty(0) '''Margin between the bottom / right side of the scrollview when drawing the horizontal / vertical scroll bar. .. versionadded:: 1.2.0 :attr:`bar_margin` is a :class:`~kivy.properties.NumericProperty`, default to 0 ''' effect_cls = ObjectProperty(DampedScrollEffect, allownone=True) '''Class effect to instantiate for X and Y axis. .. versionadded:: 1.7.0 :attr:`effect_cls` is an :class:`~kivy.properties.ObjectProperty` and defaults to :class:`DampedScrollEffect`. .. versionchanged:: 1.8.0 If you set a string, the :class:`~kivy.factory.Factory` will be used to resolve the class. ''' effect_x = ObjectProperty(None, allownone=True) '''Effect to apply for the X axis. If None is set, an instance of :attr:`effect_cls` will be created. .. versionadded:: 1.7.0 :attr:`effect_x` is an :class:`~kivy.properties.ObjectProperty` and defaults to None. ''' effect_y = ObjectProperty(None, allownone=True) '''Effect to apply for the Y axis. If None is set, an instance of :attr:`effect_cls` will be created. .. versionadded:: 1.7.0 :attr:`effect_y` is an :class:`~kivy.properties.ObjectProperty` and defaults to None, read-only. ''' viewport_size = ListProperty([0, 0]) '''(internal) Size of the internal viewport. This is the size of your only child in the scrollview. ''' scroll_type = OptionProperty(['content'], options=(['content'], ['bars'], ['bars', 'content'], ['content', 'bars'])) '''Sets the type of scrolling to use for the content of the scrollview. Available options are: ['content'], ['bars'], ['bars', 'content']. .. versionadded:: 1.8.0 :attr:`scroll_type` is a :class:`~kivy.properties.OptionProperty`, defaults to ['content']. ''' # private, for internal use only _viewport = ObjectProperty(None, allownone=True) _bar_color = ListProperty([0, 0, 0, 0]) _effect_x_start_width = None _effect_y_start_height = None _update_effect_bounds_ev = None _bind_inactive_bar_color_ev = None def _set_viewport_size(self, instance, value): self.viewport_size = value def on__viewport(self, instance, value): if value: value.bind(size=self._set_viewport_size) self.viewport_size = value.size __events__ = ('on_scroll_start', 'on_scroll_move', 'on_scroll_stop') def __init__(self, **kwargs): self._touch = None self._trigger_update_from_scroll = Clock.create_trigger( self.update_from_scroll, -1) # create a specific canvas for the viewport from kivy.graphics import PushMatrix, Translate, PopMatrix, Canvas self.canvas_viewport = Canvas() self.canvas = Canvas() with self.canvas_viewport.before: PushMatrix() self.g_translate = Translate(0, 0) with self.canvas_viewport.after: PopMatrix() super(ScrollView, self).__init__(**kwargs) self.register_event_type('on_scroll_start') self.register_event_type('on_scroll_move') self.register_event_type('on_scroll_stop') # now add the viewport canvas to our canvas self.canvas.add(self.canvas_viewport) effect_cls = self.effect_cls if isinstance(effect_cls, string_types): effect_cls = Factory.get(effect_cls) if self.effect_x is None and effect_cls is not None: self.effect_x = effect_cls(target_widget=self._viewport) if self.effect_y is None and effect_cls is not None: self.effect_y = effect_cls(target_widget=self._viewport) trigger_update_from_scroll = self._trigger_update_from_scroll update_effect_widget = self._update_effect_widget update_effect_x_bounds = self._update_effect_x_bounds update_effect_y_bounds = self._update_effect_y_bounds fbind = self.fbind fbind('width', update_effect_x_bounds) fbind('height', update_effect_y_bounds) fbind('viewport_size', self._update_effect_bounds) fbind('_viewport', update_effect_widget) fbind('scroll_x', trigger_update_from_scroll) fbind('scroll_y', trigger_update_from_scroll) fbind('pos', trigger_update_from_scroll) fbind('size', trigger_update_from_scroll) update_effect_widget() update_effect_x_bounds() update_effect_y_bounds() def on_effect_x(self, instance, value): if value: value.bind(scroll=self._update_effect_x) value.target_widget = self._viewport def on_effect_y(self, instance, value): if value: value.bind(scroll=self._update_effect_y) value.target_widget = self._viewport def on_effect_cls(self, instance, cls): if isinstance(cls, string_types): cls = Factory.get(cls) self.effect_x = cls(target_widget=self._viewport) self.effect_x.bind(scroll=self._update_effect_x) self.effect_y = cls(target_widget=self._viewport) self.effect_y.bind(scroll=self._update_effect_y) def _update_effect_widget(self, *args): if self.effect_x: self.effect_x.target_widget = self._viewport if self.effect_y: self.effect_y.target_widget = self._viewport def _update_effect_x_bounds(self, *args): if not self._viewport or not self.effect_x: return self.effect_x.min = -(self.viewport_size[0] - self.width) self.effect_x.max = 0 self.effect_x.value = self.effect_x.min * self.scroll_x def _update_effect_y_bounds(self, *args): if not self._viewport or not self.effect_y: return self.effect_y.min = -(self.viewport_size[1] - self.height) self.effect_y.max = 0 self.effect_y.value = self.effect_y.min * self.scroll_y def _update_effect_bounds(self, *args): if not self._viewport: return if self.effect_x: self._update_effect_x_bounds() if self.effect_y: self._update_effect_y_bounds() def _update_effect_x(self, *args): vp = self._viewport if not vp or not self.effect_x: return if self.effect_x.is_manual: sw = vp.width - self._effect_x_start_width else: sw = vp.width - self.width if sw < 1: return sx = self.effect_x.scroll / float(sw) self.scroll_x = -sx self._trigger_update_from_scroll() def _update_effect_y(self, *args): vp = self._viewport if not vp or not self.effect_y: return if self.effect_y.is_manual: sh = vp.height - self._effect_y_start_height else: sh = vp.height - self.height if sh < 1: return sy = self.effect_y.scroll / float(sh) self.scroll_y = -sy self._trigger_update_from_scroll() def to_local(self, x, y, **k): tx, ty = self.g_translate.xy return x - tx, y - ty def to_parent(self, x, y, **k): tx, ty = self.g_translate.xy return x + tx, y + ty def _apply_transform(self, m, pos=None): tx, ty = self.g_translate.xy m.translate(tx, ty, 0) return super(ScrollView, self)._apply_transform(m, (0, 0)) def simulate_touch_down(self, touch): # at this point the touch is in parent coords touch.push() touch.apply_transform_2d(self.to_local) ret = super(ScrollView, self).on_touch_down(touch) touch.pop() return ret def on_touch_down(self, touch): if self.dispatch('on_scroll_start', touch): self._touch = touch touch.grab(self) return True def _touch_in_handle(self, pos, size, touch): x, y = pos width, height = size return x <= touch.x <= x + width and y <= touch.y <= y + height def on_scroll_start(self, touch, check_children=True): if check_children: touch.push() touch.apply_transform_2d(self.to_local) if self.dispatch_children('on_scroll_start', touch): touch.pop() return True touch.pop() if not self.collide_point(*touch.pos): touch.ud[self._get_uid('svavoid')] = True return if self.disabled: return True if self._touch or (not (self.do_scroll_x or self.do_scroll_y)): return self.simulate_touch_down(touch) # handle mouse scrolling, only if the viewport size is bigger than the # scrollview size, and if the user allowed to do it vp = self._viewport if not vp: return True scroll_type = self.scroll_type ud = touch.ud scroll_bar = 'bars' in scroll_type # check if touch is in bar_x(horizontal) or bay_y(bertical) ud['in_bar_x'] = ud['in_bar_y'] = False width_scrollable = vp.width > self.width height_scrollable = vp.height > self.height bar_pos_x = self.bar_pos_x[0] bar_pos_y = self.bar_pos_y[0] d = { 'b': True if touch.y < self.y + self.bar_width else False, 't': True if touch.y > self.top - self.bar_width else False, 'l': True if touch.x < self.x + self.bar_width else False, 'r': True if touch.x > self.right - self.bar_width else False } if scroll_bar: if (width_scrollable and d[bar_pos_x]): ud['in_bar_x'] = True if (height_scrollable and d[bar_pos_y]): ud['in_bar_y'] = True if vp and 'button' in touch.profile and \ touch.button.startswith('scroll'): btn = touch.button m = self.scroll_wheel_distance e = None if ((btn == 'scrolldown' and self.scroll_y >= 1) or (btn == 'scrollup' and self.scroll_y <= 0) or (btn == 'scrollleft' and self.scroll_x >= 1) or (btn == 'scrollright' and self.scroll_x <= 0)): return False if (self.effect_x and self.do_scroll_y and height_scrollable and btn in ('scrolldown', 'scrollup')): e = self.effect_x if ud['in_bar_x'] else self.effect_y elif (self.effect_y and self.do_scroll_x and width_scrollable and btn in ('scrollleft', 'scrollright')): e = self.effect_y if ud['in_bar_y'] else self.effect_x if e: if btn in ('scrolldown', 'scrollleft'): e.value = max(e.value - m, e.min) e.velocity = 0 elif btn in ('scrollup', 'scrollright'): e.value = min(e.value + m, e.max) e.velocity = 0 touch.ud[self._get_uid('svavoid')] = True e.trigger_velocity_update() return True in_bar = ud['in_bar_x'] or ud['in_bar_y'] if scroll_type == ['bars'] and not in_bar: return self.simulate_touch_down(touch) if in_bar: if (ud['in_bar_y'] and not self._touch_in_handle( self._handle_y_pos, self._handle_y_size, touch)): self.scroll_y = (touch.y - self.y) / self.height elif (ud['in_bar_x'] and not self._touch_in_handle( self._handle_x_pos, self._handle_x_size, touch)): self.scroll_x = (touch.x - self.x) / self.width # no mouse scrolling, so the user is going to drag the scrollview with # this touch. self._touch = touch uid = self._get_uid() ud[uid] = { 'mode': 'unknown', 'dx': 0, 'dy': 0, 'user_stopped': in_bar, 'frames': Clock.frames, 'time': touch.time_start } if self.do_scroll_x and self.effect_x and not ud['in_bar_x']: self._effect_x_start_width = self.width self.effect_x.start(touch.x) self._scroll_x_mouse = self.scroll_x if self.do_scroll_y and self.effect_y and not ud['in_bar_y']: self._effect_y_start_height = self.height self.effect_y.start(touch.y) self._scroll_y_mouse = self.scroll_y if not in_bar: Clock.schedule_once(self._change_touch_mode, self.scroll_timeout / 1000.) return True def on_touch_move(self, touch): if self._touch is not touch: # touch is in parent touch.push() touch.apply_transform_2d(self.to_local) super(ScrollView, self).on_touch_move(touch) touch.pop() return self._get_uid() in touch.ud if touch.grab_current is not self: return True if touch.ud.get(self._get_uid()) is None: return super(ScrollView, self).on_touch_move(touch) touch.ud['sv.handled'] = {'x': False, 'y': False} if self.dispatch('on_scroll_move', touch): return True def on_scroll_move(self, touch): if self._get_uid('svavoid') in touch.ud: return False touch.push() touch.apply_transform_2d(self.to_local) if self.dispatch_children('on_scroll_move', touch): touch.pop() return True touch.pop() rv = True # By default this touch can be used to defocus currently focused # widget, like any touch outside of ScrollView. touch.ud['sv.can_defocus'] = True uid = self._get_uid() if not uid in touch.ud: self._touch = False return self.on_scroll_start(touch, False) ud = touch.ud[uid] mode = ud['mode'] # check if the minimum distance has been travelled if mode == 'unknown' or mode == 'scroll': if not touch.ud['sv.handled']['x'] and self.do_scroll_x \ and self.effect_x: width = self.width if touch.ud.get('in_bar_x', False): dx = touch.dx / float(width - width * self.hbar[1]) self.scroll_x = min(max(self.scroll_x + dx, 0.), 1.) self._trigger_update_from_scroll() else: if self.scroll_type != ['bars']: self.effect_x.update(touch.x) if self.scroll_x < 0 or self.scroll_x > 1: rv = False else: touch.ud['sv.handled']['x'] = True # Touch resulted in scroll should not defocus focused widget touch.ud['sv.can_defocus'] = False if not touch.ud['sv.handled']['y'] and self.do_scroll_y \ and self.effect_y: height = self.height if touch.ud.get('in_bar_y', False): dy = touch.dy / float(height - height * self.vbar[1]) self.scroll_y = min(max(self.scroll_y + dy, 0.), 1.) self._trigger_update_from_scroll() else: if self.scroll_type != ['bars']: self.effect_y.update(touch.y) if self.scroll_y < 0 or self.scroll_y > 1: rv = False else: touch.ud['sv.handled']['y'] = True # Touch resulted in scroll should not defocus focused widget touch.ud['sv.can_defocus'] = False if mode == 'unknown': ud['dx'] += abs(touch.dx) ud['dy'] += abs(touch.dy) if ((ud['dx'] > self.scroll_distance) or (ud['dy'] > self.scroll_distance)): if not self.do_scroll_x and not self.do_scroll_y: # touch is in parent, but _change expects window coords touch.push() touch.apply_transform_2d(self.to_local) touch.apply_transform_2d(self.to_window) self._change_touch_mode() touch.pop() return mode = 'scroll' ud['mode'] = mode if mode == 'scroll': ud['dt'] = touch.time_update - ud['time'] ud['time'] = touch.time_update ud['user_stopped'] = True return rv def on_touch_up(self, touch): if self._touch is not touch and self._get_uid( 'svavoid') not in touch.ud: # touch is in parents touch.push() touch.apply_transform_2d(self.to_local) if super(ScrollView, self).on_touch_up(touch): touch.pop() return True touch.pop() return False if self.dispatch('on_scroll_stop', touch): touch.ungrab(self) if not touch.ud.get('sv.can_defocus', True): # Focused widget should stay focused FocusBehavior.ignored_touch.append(touch) return True def on_scroll_stop(self, touch, check_children=True): self._touch = None if check_children: touch.push() touch.apply_transform_2d(self.to_local) if self.dispatch_children('on_scroll_stop', touch): touch.pop() return True touch.pop() if self._get_uid('svavoid') in touch.ud: return if self._get_uid() not in touch.ud: return False self._touch = None uid = self._get_uid() ud = touch.ud[uid] if self.do_scroll_x and self.effect_x: if not touch.ud.get('in_bar_x', False) and\ self.scroll_type != ['bars']: self.effect_x.stop(touch.x) if self.do_scroll_y and self.effect_y and\ self.scroll_type != ['bars']: if not touch.ud.get('in_bar_y', False): self.effect_y.stop(touch.y) if ud['mode'] == 'unknown': # we must do the click at least.. # only send the click if it was not a click to stop # autoscrolling if not ud['user_stopped']: self.simulate_touch_down(touch) Clock.schedule_once(partial(self._do_touch_up, touch), .2) ev = self._update_effect_bounds_ev if ev is None: ev = self._update_effect_bounds_ev = Clock.create_trigger( self._update_effect_bounds) ev() # if we do mouse scrolling, always accept it if 'button' in touch.profile and touch.button.startswith('scroll'): return True return self._get_uid() in touch.ud def scroll_to(self, widget, padding=10, animate=True): '''Scrolls the viewport to ensure that the given widget is visible, optionally with padding and animation. If animate is True (the default), then the default animation parameters will be used. Otherwise, it should be a dict containing arguments to pass to :class:`~kivy.animation.Animation` constructor. .. versionadded:: 1.9.1 ''' if not self.parent: return if isinstance(padding, (int, float)): padding = (padding, padding) pos = self.parent.to_widget(*widget.to_window(*widget.pos)) cor = self.parent.to_widget( *widget.to_window(widget.right, widget.top)) dx = dy = 0 if pos[1] < self.y: dy = self.y - pos[1] + dp(padding[1]) elif cor[1] > self.top: dy = self.top - cor[1] - dp(padding[1]) if pos[0] < self.x: dx = self.x - pos[0] + dp(padding[0]) elif cor[0] > self.right: dx = self.right - cor[0] - dp(padding[0]) dsx, dsy = self.convert_distance_to_scroll(dx, dy) sxp = min(1, max(0, self.scroll_x - dsx)) syp = min(1, max(0, self.scroll_y - dsy)) if animate: if animate is True: animate = {'d': 0.2, 't': 'out_quad'} Animation.stop_all(self, 'scroll_x', 'scroll_y') Animation(scroll_x=sxp, scroll_y=syp, **animate).start(self) else: self.scroll_x = sxp self.scroll_y = syp def convert_distance_to_scroll(self, dx, dy): '''Convert a distance in pixels to a scroll distance, depending on the content size and the scrollview size. The result will be a tuple of scroll distance that can be added to :data:`scroll_x` and :data:`scroll_y` ''' if not self._viewport: return 0, 0 vp = self._viewport if vp.width > self.width: sw = vp.width - self.width sx = dx / float(sw) else: sx = 0 if vp.height > self.height: sh = vp.height - self.height sy = dy / float(sh) else: sy = 1 return sx, sy def update_from_scroll(self, *largs): '''Force the reposition of the content, according to current value of :attr:`scroll_x` and :attr:`scroll_y`. This method is automatically called when one of the :attr:`scroll_x`, :attr:`scroll_y`, :attr:`pos` or :attr:`size` properties change, or if the size of the content changes. ''' if not self._viewport: return vp = self._viewport # update from size_hint if vp.size_hint_x is not None: vp.width = vp.size_hint_x * self.width if vp.size_hint_y is not None: vp.height = vp.size_hint_y * self.height if vp.width > self.width: sw = vp.width - self.width x = self.x - self.scroll_x * sw else: x = self.x if vp.height > self.height: sh = vp.height - self.height y = self.y - self.scroll_y * sh else: y = self.top - vp.height # from 1.8.0, we now use a matrix by default, instead of moving the # widget position behind. We set it here, but it will be a no-op most of # the time. vp.pos = 0, 0 self.g_translate.xy = x, y # New in 1.2.0, show bar when scrolling happens and (changed in 1.9.0) # fade to bar_inactive_color when no scroll is happening. ev = self._bind_inactive_bar_color_ev if ev is None: ev = self._bind_inactive_bar_color_ev = Clock.create_trigger( self._bind_inactive_bar_color, .5) self.funbind('bar_inactive_color', self._change_bar_color) Animation.stop_all(self, '_bar_color') self.fbind('bar_color', self._change_bar_color) self._bar_color = self.bar_color ev() def _bind_inactive_bar_color(self, *l): self.funbind('bar_color', self._change_bar_color) self.fbind('bar_inactive_color', self._change_bar_color) Animation(_bar_color=self.bar_inactive_color, d=.5, t='out_quart').start(self) def _change_bar_color(self, inst, value): self._bar_color = value # # Private # def add_widget(self, widget, index=0): if self._viewport: raise Exception('ScrollView accept only one widget') canvas = self.canvas self.canvas = self.canvas_viewport super(ScrollView, self).add_widget(widget, index) self.canvas = canvas self._viewport = widget widget.bind(size=self._trigger_update_from_scroll) self._trigger_update_from_scroll() def remove_widget(self, widget): canvas = self.canvas self.canvas = self.canvas_viewport super(ScrollView, self).remove_widget(widget) self.canvas = canvas if widget is self._viewport: self._viewport = None def _get_uid(self, prefix='sv'): return '{0}.{1}'.format(prefix, self.uid) def _change_touch_mode(self, *largs): if not self._touch: return uid = self._get_uid() touch = self._touch if uid not in touch.ud: self._touch = False return ud = touch.ud[uid] if ud['mode'] != 'unknown' or ud['user_stopped']: return diff_frames = Clock.frames - ud['frames'] # in order to be able to scroll on very slow devices, let at least 3 # frames displayed to accumulate some velocity. And then, change the # touch mode. Otherwise, we might never be able to compute velocity, and # no way to scroll it. See #1464 and #1499 if diff_frames < 3: Clock.schedule_once(self._change_touch_mode, 0) return if self.do_scroll_x and self.effect_x: self.effect_x.cancel() if self.do_scroll_y and self.effect_y: self.effect_y.cancel() # XXX the next line was in the condition. But this stop # the possibility to "drag" an object out of the scrollview in the # non-used direction: if you have an horizontal scrollview, a # vertical gesture will not "stop" the scroll view to look for an # horizontal gesture, until the timeout is done. # and touch.dx + touch.dy == 0: touch.ungrab(self) self._touch = None # touch is in window coords touch.push() touch.apply_transform_2d(self.to_widget) touch.apply_transform_2d(self.to_parent) self.simulate_touch_down(touch) touch.pop() return def _do_touch_up(self, touch, *largs): # touch is in window coords touch.push() touch.apply_transform_2d(self.to_widget) super(ScrollView, self).on_touch_up(touch) touch.pop() # don't forget about grab event! for x in touch.grab_list[:]: touch.grab_list.remove(x) x = x() if not x: continue touch.grab_current = x # touch is in window coords touch.push() touch.apply_transform_2d(self.to_widget) super(ScrollView, self).on_touch_up(touch) touch.pop() touch.grab_current = None
class Label(Widget): '''Label class, see module documentation for more information. :Events: `on_ref_press` Fired when the user clicks on a word referenced with a ``[ref]`` tag in a text markup. ''' __events__ = ['on_ref_press'] _font_properties = ('text', 'font_size', 'font_name', 'bold', 'italic', 'halign', 'valign', 'padding_x', 'padding_y', 'text_size', 'shorten', 'mipmap', 'markup', 'line_height', 'max_lines', 'strip', 'shorten_from', 'split_str') def __init__(self, **kwargs): self._trigger_texture = Clock.create_trigger(self.texture_update, -1) super(Label, self).__init__(**kwargs) # bind all the property for recreating the texture d = Label._font_properties dkw = {} for x in d: dkw[x] = partial(self._trigger_texture_update, x) self.bind(**dkw) self._label = None self._create_label() # force the texture creation self._trigger_texture() def _create_label(self): # create the core label class according to markup value if self._label is not None: cls = self._label.__class__ else: cls = None markup = self.markup if (markup and cls is not CoreMarkupLabel) or \ (not markup and cls is not CoreLabel): # markup have change, we need to change our rendering method. d = Label._font_properties dkw = dict(list(zip(d, [getattr(self, x) for x in d]))) if markup: self._label = CoreMarkupLabel(**dkw) else: self._label = CoreLabel(**dkw) def _trigger_texture_update(self, name=None, source=None, value=None): # check if the label core class need to be switch to a new one if name == 'markup': self._create_label() if source: if name == 'text': self._label.text = value elif name == 'text_size': self._label.usersize = value elif name == 'font_size': self._label.options[name] = value else: self._label.options[name] = value self._trigger_texture() def texture_update(self, *largs): '''Force texture recreation with the current Label properties. After this function call, the :attr:`texture` and :attr:`texture_size` will be updated in this order. ''' mrkup = self._label.__class__ is CoreMarkupLabel self.texture = None if (not self._label.text or (self.halign[-1] == 'y' or self.strip) and not self._label.text.strip()): self.texture_size = (0, 0) if mrkup: self.refs, self._label._refs = {}, {} self.anchors, self._label._anchors = {}, {} else: if mrkup: text = self._label.text # we must strip here, otherwise, if the last line is empty, # markup will retain the last empty line since it only strips # line by line within markup if self.halign[-1] == 'y' or self.strip: text = text.strip() self._label.text = ''.join( ('[color=', get_hex_from_color(self.color), ']', text, '[/color]')) self._label.refresh() # force the rendering to get the references if self._label.texture: self._label.texture.bind() self.refs = self._label.refs self.anchors = self._label.anchors else: self._label.refresh() texture = self._label.texture if texture is not None: self.texture = self._label.texture self.texture_size = list(self.texture.size) def on_touch_down(self, touch): if super(Label, self).on_touch_down(touch): return True if not len(self.refs): return False tx, ty = touch.pos tx -= self.center_x - self.texture_size[0] / 2. ty -= self.center_y - self.texture_size[1] / 2. ty = self.texture_size[1] - ty for uid, zones in self.refs.items(): for zone in zones: x, y, w, h = zone if x <= tx <= w and y <= ty <= h: self.dispatch('on_ref_press', uid) return True return False def on_ref_press(self, ref): pass # # Properties # disabled_color = ListProperty([1, 1, 1, .3]) '''Text color, in the format (r, g, b, a) .. versionadded:: 1.8.0 :attr:`disabled_color` is a :class:`~kivy.properties.ListProperty` and defaults to [1, 1, 1, .5]. ''' text = StringProperty('') '''Text of the label. Creation of a simple hello world:: widget = Label(text='Hello world') If you want to create the widget with an unicode string, use:: widget = Label(text=u'My unicode string') :attr:`text` is a :class:`~kivy.properties.StringProperty` and defaults to ''. ''' text_size = ListProperty([None, None]) '''By default, the label is not constrained to any bounding box. You can set the size constraint of the label with this property. The text will autoflow into the constrains. So although the font size will not be reduced, the text will be arranged to fit into the box as best as possible, with any text still outside the box clipped. This sets and clips :attr:`texture_size` to text_size if not None. .. versionadded:: 1.0.4 For example, whatever your current widget size is, if you want the label to be created in a box with width=200 and unlimited height:: Label(text='Very big big line', text_size=(200, None)) .. note:: This text_size property is the same as the :attr:`~kivy.core.text.Label.usersize` property in the :class:`~kivy.core.text.Label` class. (It is named size= in the constructor.) :attr:`text_size` is a :class:`~kivy.properties.ListProperty` and defaults to (None, None), meaning no size restriction by default. ''' font_name = StringProperty('DroidSans') '''Filename of the font to use. The path can be absolute or relative. Relative paths are resolved by the :func:`~kivy.resources.resource_find` function. .. warning:: Depending of your text provider, the font file can be ignored. However, you can mostly use this without problems. If the font used lacks the glyphs for the particular language/symbols you are using, you will see '[]' blank box characters instead of the actual glyphs. The solution is to use a font that has the glyphs you need to display. For example, to display |unicodechar|, use a font such as freesans.ttf that has the glyph. .. |unicodechar| image:: images/unicode-char.png :attr:`font_name` is a :class:`~kivy.properties.StringProperty` and defaults to 'DroidSans'. ''' font_size = NumericProperty('15sp') '''Font size of the text, in pixels. :attr:`font_size` is a :class:`~kivy.properties.NumericProperty` and defaults to 12dp. ''' line_height = NumericProperty(1.0) '''Line Height for the text. e.g. line_height = 2 will cause the spacing between lines to be twice the size. :attr:`line_height` is a :class:`~kivy.properties.NumericProperty` and defaults to 1.0. .. versionadded:: 1.5.0 ''' bold = BooleanProperty(False) '''Indicates use of the bold version of your font. .. note:: Depending of your font, the bold attribute may have no impact on your text rendering. :attr:`bold` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' italic = BooleanProperty(False) '''Indicates use of the italic version of your font. .. note:: Depending of your font, the italic attribute may have no impact on your text rendering. :attr:`italic` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' padding_x = NumericProperty(0) '''Horizontal padding of the text inside the widget box. :attr:`padding_x` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. .. versionchanged:: 1.9.0 `padding_x` has been fixed to work as expected. In the past, the text was padded by the negative of its values. ''' padding_y = NumericProperty(0) '''Vertical padding of the text inside the widget box. :attr:`padding_y` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. .. versionchanged:: 1.9.0 `padding_y` has been fixed to work as expected. In the past, the text was padded by the negative of its values. ''' padding = ReferenceListProperty(padding_x, padding_y) '''Padding of the text in the format (padding_x, padding_y) :attr:`padding` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`padding_x`, :attr:`padding_y`) properties. ''' halign = OptionProperty('left', options=['left', 'center', 'right', 'justify']) '''Horizontal alignment of the text. :attr:`halign` is an :class:`~kivy.properties.OptionProperty` and defaults to 'left'. Available options are : left, center, right and justify. .. warning:: This doesn't change the position of the text texture of the Label (centered), only the position of the text in this texture. You probably want to bind the size of the Label to the :attr:`texture_size` or set a :attr:`text_size`. .. versionchanged:: 1.6.0 A new option was added to :attr:`halign`, namely `justify`. ''' valign = OptionProperty('bottom', options=['bottom', 'middle', 'top']) '''Vertical alignment of the text. :attr:`valign` is an :class:`~kivy.properties.OptionProperty` and defaults to 'bottom'. Available options are : bottom, middle and top. .. warning:: This doesn't change the position of the text texture of the Label (centered), only the position of the text within this texture. You probably want to bind the size of the Label to the :attr:`texture_size` or set a :attr:`text_size` to change this behavior. ''' color = ListProperty([1, 1, 1, 1]) '''Text color, in the format (r, g, b, a) :attr:`color` is a :class:`~kivy.properties.ListProperty` and defaults to [1, 1, 1, 1]. ''' texture = ObjectProperty(None, allownone=True) '''Texture object of the text. The text is rendered automatically when a property changes. The OpenGL texture created in this operation is stored in this property. You can use this :attr:`texture` for any graphics elements. Depending on the texture creation, the value will be a :class:`~kivy.graphics.texture.Texture` or :class:`~kivy.graphics.texture.TextureRegion` object. .. warning:: The :attr:`texture` update is scheduled for the next frame. If you need the texture immediately after changing a property, you have to call the :meth:`texture_update` method before accessing :attr:`texture`:: l = Label(text='Hello world') # l.texture is good l.font_size = '50sp' # l.texture is not updated yet l.texture_update() # l.texture is good now. :attr:`texture` is an :class:`~kivy.properties.ObjectProperty` and defaults to None. ''' texture_size = ListProperty([0, 0]) '''Texture size of the text. The size is determined by the font size and text. If :attr:`text_size` is [None, None], the texture will be the size required to fit the text, otherwise it's clipped to fit :attr:`text_size`. When :attr:`text_size` is [None, None], one can bind to texture_size and rescale it proportionally to fit the size of the label in order to make the text fit maximally in the label. .. warning:: The :attr:`texture_size` is set after the :attr:`texture` property. If you listen for changes to :attr:`texture`, :attr:`texture_size` will not be up-to-date in your callback. Bind to :attr:`texture_size` instead. ''' mipmap = BooleanProperty(False) '''Indicates whether OpenGL mipmapping is applied to the texture or not. Read :ref:`mipmap` for more information. .. versionadded:: 1.0.7 :attr:`mipmap` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' shorten = BooleanProperty(False) ''' Indicates whether the label should attempt to shorten its textual contents as much as possible if a :attr:`text_size` is given. Setting this to True without an appropriately set :attr:`text_size` will lead to unexpected results. :attr:`shorten_from` and :attr:`split_str` control the direction from which the :attr:`text` is split, as well as where in the :attr:`text` we are allowed to split. :attr:`shorten` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' shorten_from = OptionProperty('center', options=['left', 'center', 'right']) '''The side from which we should shorten the text from, can be left, right, or center. For example, if left, the ellipsis will appear towards the left side and we will display as much text starting from the right as possible. Similar to :attr:`shorten`, this option only applies when :attr:`text_size` [0] is not None, In this case, the string is shortened to fit within the specified width. .. versionadded:: 1.9.0 :attr:`shorten_from` is a :class:`~kivy.properties.OptionProperty` and defaults to `center`. ''' split_str = StringProperty('') '''The string used to split the :attr:`text` while shortening the string when :attr:`shorten` is True. For example, if it's a space, the string will be broken into words and as many whole words that can fit into a single line will be displayed. If :attr:`shorten_from` is the empty string, `''`, we split on every character fitting as much text as possible into the line. .. versionadded:: 1.9.0 :attr:`split_str` is a :class:`~kivy.properties.StringProperty` and defaults to `''` (the empty string). ''' markup = BooleanProperty(False) ''' .. versionadded:: 1.1.0 If True, the text will be rendered using the :class:`~kivy.core.text.markup.MarkupLabel`: you can change the style of the text using tags. Check the :doc:`api-kivy.core.text.markup` documentation for more information. :attr:`markup` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' refs = DictProperty({}) ''' .. versionadded:: 1.1.0 List of ``[ref=xxx]`` markup items in the text with the bounding box of all the words contained in a ref, available only after rendering. For example, if you wrote:: Check out my [ref=hello]link[/ref] The refs will be set with:: {'hello': ((64, 0, 78, 16), )} The references marked "hello" have a bounding box at (x1, y1, x2, y2). These co-ordinates are relative to the top left corner of the text, with the y value increasing downwards. You can define multiple refs with the same name: each occurence will be added as another (x1, y1, x2, y2) tuple to this list. The current Label implementation uses these references if they exist in your markup text, automatically doing the collision with the touch and dispatching an `on_ref_press` event. You can bind a ref event like this:: def print_it(instance, value): print('User click on', value) widget = Label(text='Hello [ref=world]World[/ref]', markup=True) widget.on_ref_press(print_it) .. note:: This works only with markup text. You need :attr:`markup` set to True. ''' anchors = DictProperty({}) ''' .. versionadded:: 1.1.0 Position of all the ``[anchor=xxx]`` markup in the text. These co-ordinates are relative to the top left corner of the text, with the y value increasing downwards. Anchors names should be unique and only the first occurence of any duplicate anchors will be recorded. You can place anchors in your markup text as follows:: text = """ [anchor=title1][size=24]This is my Big title.[/size] [anchor=content]Hello world """ Then, all the ``[anchor=]`` references will be removed and you'll get all the anchor positions in this property (only after rendering):: >>> widget = Label(text=text, markup=True) >>> widget.texture_update() >>> widget.anchors {"content": (20, 32), "title1": (20, 16)} .. note:: This works only with markup text. You need :attr:`markup` set to True. ''' max_lines = NumericProperty(0) '''Maximum number of lines to use, defaults to 0, which means unlimited. Please note that :attr:`shorten` take over this property. (with shorten, the text is always one line.) .. versionadded:: 1.8.0 :attr:`max_lines` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' strip = BooleanProperty(False) '''Whether leading and trailing spaces and newlines should be stripped from
class ColorWheel(Widget): '''Chromatic wheel for the ColorPicker. .. versionchanged:: 1.7.1 `font_size`, `font_name` and `foreground_color` have been removed. The sizing is now the same as others widget, based on 'sp'. Orientation is also automatically determined according to the width/height ratio. ''' r = BoundedNumericProperty(0, min=0, max=1) '''The Red value of the color currently selected. :attr:`r` is a :class:`~kivy.properties.BoundedNumericProperty` and can be a value from 0 to 1. It defaults to 0. ''' g = BoundedNumericProperty(0, min=0, max=1) '''The Green value of the color currently selected. :attr:`g` is a :class:`~kivy.properties.BoundedNumericProperty` and can be a value from 0 to 1. ''' b = BoundedNumericProperty(0, min=0, max=1) '''The Blue value of the color currently selected. :attr:`b` is a :class:`~kivy.properties.BoundedNumericProperty` and can be a value from 0 to 1. ''' a = BoundedNumericProperty(0, min=0, max=1) '''The Alpha value of the color currently selected. :attr:`a` is a :class:`~kivy.properties.BoundedNumericProperty` and can be a value from 0 to 1. ''' color = ReferenceListProperty(r, g, b, a) '''The holds the color currently selected. :attr:`color` is a :class:`~kivy.properties.ReferenceListProperty` and contains a list of `r`, `g`, `b`, `a` values. ''' _origin = ListProperty((100, 100)) _radius = NumericProperty(100) _piece_divisions = NumericProperty(10) _pieces_of_pie = NumericProperty(16) _inertia_slowdown = 1.25 _inertia_cutoff = .25 _num_touches = 0 _pinch_flag = False _hsv = ListProperty([1, 1, 1, 0]) def __init__(self, **kwargs): super(ColorWheel, self).__init__(**kwargs) pdv = self._piece_divisions self.sv_s = [(float(x) / pdv, 1) for x in range(pdv)] + [ (1, float(y) / pdv) for y in reversed(range(pdv))] def on__origin(self, instance, value): self.init_wheel(None) def on__radius(self, instance, value): self.init_wheel(None) def init_wheel(self, dt): # initialize list to hold all meshes self.canvas.clear() self.arcs = [] self.sv_idx = 0 pdv = self._piece_divisions ppie = self._pieces_of_pie for r in range(pdv): for t in range(ppie): self.arcs.append( _ColorArc( self._radius * (float(r) / float(pdv)), self._radius * (float(r + 1) / float(pdv)), 2 * pi * (float(t) / float(ppie)), 2 * pi * (float(t + 1) / float(ppie)), origin=self._origin, color=(float(t) / ppie, self.sv_s[self.sv_idx + r][0], self.sv_s[self.sv_idx + r][1], 1))) self.canvas.add(self.arcs[-1]) def recolor_wheel(self): ppie = self._pieces_of_pie for idx, segment in enumerate(self.arcs): segment.change_color( sv=self.sv_s[int(self.sv_idx + idx / ppie)]) def change_alpha(self, val): for idx, segment in enumerate(self.arcs): segment.change_color(a=val) def inertial_incr_sv_idx(self, dt): # if its already zoomed all the way out, cancel the inertial zoom if self.sv_idx == len(self.sv_s) - self._piece_divisions: return False self.sv_idx += 1 self.recolor_wheel() if dt * self._inertia_slowdown > self._inertia_cutoff: return False else: Clock.schedule_once(self.inertial_incr_sv_idx, dt * self._inertia_slowdown) def inertial_decr_sv_idx(self, dt): # if its already zoomed all the way in, cancel the inertial zoom if self.sv_idx == 0: return False self.sv_idx -= 1 self.recolor_wheel() if dt * self._inertia_slowdown > self._inertia_cutoff: return False else: Clock.schedule_once(self.inertial_decr_sv_idx, dt * self._inertia_slowdown) def on_touch_down(self, touch): r = self._get_touch_r(touch.pos) if r > self._radius: return False # code is still set up to allow pinch to zoom, but this is # disabled for now since it was fiddly with small wheels. # Comment out these lines and adjust on_touch_move to reenable # this. if self._num_touches != 0: return False touch.grab(self) self._num_touches += 1 touch.ud['anchor_r'] = r touch.ud['orig_sv_idx'] = self.sv_idx touch.ud['orig_time'] = Clock.get_time() def on_touch_move(self, touch): if touch.grab_current is not self: return r = self._get_touch_r(touch.pos) goal_sv_idx = (touch.ud['orig_sv_idx'] - int((r - touch.ud['anchor_r']) / (float(self._radius) / self._piece_divisions))) if ( goal_sv_idx != self.sv_idx and goal_sv_idx >= 0 and goal_sv_idx <= len(self.sv_s) - self._piece_divisions ): # this is a pinch to zoom self._pinch_flag = True self.sv_idx = goal_sv_idx self.recolor_wheel() def on_touch_up(self, touch): if touch.grab_current is not self: return touch.ungrab(self) self._num_touches -= 1 if self._pinch_flag: if self._num_touches == 0: # user was pinching, and now both fingers are up. Return # to normal if self.sv_idx > touch.ud['orig_sv_idx']: Clock.schedule_once( self.inertial_incr_sv_idx, (Clock.get_time() - touch.ud['orig_time']) / (self.sv_idx - touch.ud['orig_sv_idx'])) if self.sv_idx < touch.ud['orig_sv_idx']: Clock.schedule_once( self.inertial_decr_sv_idx, (Clock.get_time() - touch.ud['orig_time']) / (self.sv_idx - touch.ud['orig_sv_idx'])) self._pinch_flag = False return else: # user was pinching, and at least one finger remains. We # don't want to treat the remaining fingers as touches return else: r, theta = rect_to_polar(self._origin, *touch.pos) # if touch up is outside the wheel, ignore if r >= self._radius: return # compute which ColorArc is being touched (they aren't # widgets so we don't get collide_point) and set # _hsv based on the selected ColorArc piece = int((theta / (2 * pi)) * self._pieces_of_pie) division = int((r / self._radius) * self._piece_divisions) self._hsv = \ self.arcs[self._pieces_of_pie * division + piece].color def on__hsv(self, instance, value): c_hsv = Color(*value, mode='hsv') self.r = c_hsv.r self.g = c_hsv.g self.b = c_hsv.b self.a = c_hsv.a self.rgba = (self.r, self.g, self.b, self.a) def _get_touch_r(self, pos): return distance(pos, self._origin)
class Text(Widget): widget_type_name = 'Text' var_finder = re.compile("(?<=\()[a-zA-Z_0-9|]+(?=\))") string_finder = re.compile("(?<=\$)[a-zA-Z_0-9]+") merge_settings = ('font_name', 'font_size', 'bold', 'italic', 'halign', 'valign', 'padding_x', 'padding_y', 'text_size', 'shorten', 'mipmap', 'markup', 'line_height', 'max_lines', 'strip', 'shorten_from', 'split_str', 'unicode_errors', 'color') animation_properties = ('x', 'y', 'font_size', 'color', 'opacity', 'rotation', 'scale') def __init__(self, mc: "MpfMc", config: dict, key: Optional[str]=None, play_kwargs: Optional[dict]=None, **kwargs) -> None: self._label = CustomLabel(mc, config) self._label.fbind('texture', self.on_label_texture) super().__init__(mc=mc, config=config, key=key) # Special handling for baseline anchor if self.config['anchor_y'] == 'baseline': self.anchor_y = 'bottom' self.adjust_bottom = self._label._label.get_descent() * -1 self.original_text = self._get_text_string(config.get('text', '')) self.text_variables = dict() if play_kwargs: self.event_replacements = play_kwargs else: self.event_replacements = kwargs self._process_text(self.original_text) # Bind to all properties that when changed need to force # the widget to be redrawn self.bind(pos=self._draw_widget, size=self._draw_widget, color=self._draw_widget, rotation=self._draw_widget, scale=self._draw_widget) def __repr__(self) -> str: if hasattr(self, '_label') and self._label: return '<Text Widget text={}>'.format(self._label.text) else: return '<Text Widget text=None>' def _draw_widget(self, *args): """Draws the image (draws a rectangle using the image texture)""" del args anchor = (self.x - self.anchor_offset_pos[0], self.y - self.anchor_offset_pos[1]) self.canvas.clear() if len(self._label.text) > 0: with self.canvas: Color(*self.color) Rotate(angle=self.rotation, origin=anchor) Scale(self.scale).origin = anchor Rectangle(pos=self.pos, size=self.size, texture=self._label.texture) def on_label_texture(self, instance, texture): del instance if texture: self.size = texture.size if self.config['anchor_y'] == 'baseline': self.adjust_bottom = self._label._label.get_descent() * -1 def update_kwargs(self, **kwargs) -> None: self.event_replacements.update(kwargs) self._process_text(self.original_text) def _get_text_string(self, text: str) -> str: if '$' not in text: return text for text_string in Text.string_finder.findall(text): text = text.replace('${}'.format(text_string), self._do_get_text_string(text_string)) return text def _do_get_text_string(self, text_string: str) -> str: try: return str(self.mc.machine_config['text_strings'][text_string]) except KeyError: # if the text string is not found, put the $ back on return '${}'.format(text_string) def _get_text_vars(self, text: str): return Text.var_finder.findall(text) def _process_text(self, text: str) -> None: for var_string in self._get_text_vars(text): if var_string in self.event_replacements: text = text.replace('({})'.format(var_string), str(self.event_replacements[var_string])) if self._get_text_vars(text): # monitors won't be added twice, so it's ok to blindly call this self._setup_variable_monitors(text) self.update_vars_in_text(text) def update_vars_in_text(self, text: str) -> None: for var_string in self._get_text_vars(text): if var_string.startswith('machine|'): try: text = text.replace('(' + var_string + ')', str(self.mc.machine_vars[ var_string.split('|')[1]])) except KeyError: text = '' elif self.mc.player: if var_string.startswith('player|'): text = text.replace('(' + var_string + ')', str(self.mc.player[ var_string.split('|')[1]])) continue elif var_string.startswith('player') and '|' in var_string: player_num, var_name = var_string.lstrip('player').split( '|') try: value = self.mc.player_list[int(player_num) - 1][ var_name] if value is not None: text = text.replace('(' + var_string + ')', str(value)) else: text = '' except IndexError: text = '' continue elif self.mc.player.is_player_var(var_string): text = text.replace('(' + var_string + ')', str(self.mc.player[var_string])) continue if var_string in self.event_replacements: text = text.replace('({})'.format(var_string), str(self.event_replacements[var_string])) self.update_text(text) def update_text(self, text: str) -> None: if text: if self.config['min_digits']: text = text.zfill(self.config['min_digits']) if self.config['number_grouping']: # find the numbers in the string number_list = [s for s in text.split() if s.isdigit()] # group the numbers and replace them in the string for item in number_list: grouped_item = Text.group_digits(item) text = text.replace(str(item), grouped_item) self._label.text = text self._label.texture_update() def _player_var_change(self, **kwargs) -> None: del kwargs self.update_vars_in_text(self.original_text) def _machine_var_change(self, **kwargs) -> None: del kwargs self.update_vars_in_text(self.original_text) def _setup_variable_monitors(self, text: str) -> None: for var_string in self._get_text_vars(text): if '|' not in var_string: self.add_player_var_handler(name=var_string) self.add_current_player_handler() else: source, variable_name = var_string.split('|') if source.lower().startswith('player'): if source.lstrip('player'): # we have player num self.add_player_var_handler(name=variable_name) else: # no player num self.add_player_var_handler(name=var_string) self.add_current_player_handler() elif source.lower() == 'machine': self.add_machine_var_handler(name=variable_name) def add_player_var_handler(self, name: str) -> None: self.mc.events.add_handler('player_{}'.format(name), self._player_var_change) def add_current_player_handler(self) -> None: self.mc.events.add_handler('player_turn_start', self._player_var_change) def add_machine_var_handler(self, name: str) -> None: self.mc.events.add_handler('machine_var_{}'.format(name), self._machine_var_change) def prepare_for_removal(self) -> None: self.mc.events.remove_handler(self._player_var_change) self.mc.events.remove_handler(self._machine_var_change) @staticmethod def group_digits(text: str, separator: str=',', group_size: int=3) -> str: """Enables digit grouping (i.e. adds comma separators between thousands digits). Args: text: The incoming string of text separator: String of the character(s) you'd like to add between the digit groups. Default is a comma. (",") group_size: How many digits you want in each group. Default is 3. Returns: A string with the separator added. MPF uses this method instead of the Python locale settings because the locale settings are a mess. They're set system-wide and it's really hard to make them work cross-platform and there are all sorts of external dependencies, so this is just way easier. """ digit_list = list(text.split('.')[0]) for i in range(len(digit_list))[::-group_size][1:]: digit_list.insert(i + 1, separator) return ''.join(digit_list) # # Properties # disabled_color = ListProperty([1, 1, 1, .3]) '''The color of the text when the widget is disabled, in the (r, g, b, a) format. .. versionadded:: 1.8.0 :attr:`disabled_color` is a :class:`~kivy.properties.ListProperty` and defaults to [1, 1, 1, .3]. ''' def _get_text(self) -> str: return self._label.text def _set_text(self, text: str) -> None: self._label.text = text text = AliasProperty(_get_text, _set_text) '''Text of the label. ''' def _get_text_size(self) -> list: return self._label.text_size def _set_text_size(self, text_size: list) -> None: self._label.text_size = text_size text_size = AliasProperty(_get_text_size, _set_text_size) '''By default, the label is not constrained to any bounding box. You can set the size constraint of the label with this property. The text will autoflow into the constraints. So although the font size will not be reduced, the text will be arranged to fit into the box as best as possible, with any text still outside the box clipped. This sets and clips :attr:`texture_size` to text_size if not None. .. versionadded:: 1.0.4 For example, whatever your current widget size is, if you want the label to be created in a box with width=200 and unlimited height:: Label(text='Very big big line', text_size=(200, None)) .. note:: This text_size property is the same as the :attr:`~kivy.core.text.Label.usersize` property in the :class:`~kivy.core.text.Label` class. (It is named size= in the constructor.) :attr:`text_size` is a :class:`~kivy.properties.ListProperty` and defaults to (None, None), meaning no size restriction by default. ''' def _get_font_name(self) -> str: return self._label.font_name def _set_font_name(self, font_name: str) -> None: self._label.font_name = font_name font_name = AliasProperty(_get_font_name, _set_font_name) '''Filename of the font to use. The path can be absolute or relative. Relative paths are resolved by the :func:`~kivy.resources.resource_find` function. .. warning:: Depending of your text provider, the font file can be ignored. However, you can mostly use this without problems. If the font used lacks the glyphs for the particular language/symbols you are using, you will see '[]' blank box characters instead of the actual glyphs. The solution is to use a font that has the glyphs you need to display. For example, to display |unicodechar|, use a font such as freesans.ttf that has the glyph. .. |unicodechar| image:: images/unicode-char.png :attr:`font_name` is a :class:`~kivy.properties.StringProperty` and defaults to 'Roboto'. This value is taken from :class:`~kivy.config.Config`. ''' def _get_font_size(self): return self._label.font_size def _set_font_size(self, font_size) -> None: self._label.font_size = font_size font_size = AliasProperty(_get_font_size, _set_font_size) '''Font size of the text, in pixels. :attr:`font_size` is a :class:`~kivy.properties.NumericProperty` and defaults to 15sp. ''' def _get_line_height(self) -> float: return self._label.line_height def _set_line_height(self, line_height: float) -> None: self._label.line_height = line_height bitmap_font = BooleanProperty(False) '''Flag indicating whether or not the font_name attribute refers to a bitmap font.''' line_height = AliasProperty(_get_line_height, _set_line_height) '''Line Height for the text. e.g. line_height = 2 will cause the spacing between lines to be twice the size. :attr:`line_height` is a :class:`~kivy.properties.NumericProperty` and defaults to 1.0. .. versionadded:: 1.5.0 ''' def _get_bold(self) -> bool: return self._label.bold def _set_bold(self, bold: bool) -> None: self._label.bold = bold bold = AliasProperty(_get_bold, _set_bold) '''Indicates use of the bold version of your font. .. note:: Depending of your font, the bold attribute may have no impact on your text rendering. :attr:`bold` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' def _get_italic(self) -> bool: return self._label.italic def _set_italic(self, italic: bool) -> None: self._label.italic = italic italic = AliasProperty(_get_italic, _set_italic) '''Indicates use of the italic version of your font. .. note:: Depending of your font, the italic attribute may have no impact on your text rendering. :attr:`italic` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' def _get_underline(self) -> bool: return self._label.underline def _set_underline(self, underline: bool) -> None: self._label.underline = underline underline = AliasProperty(_get_underline, _set_underline) '''Adds an underline to the text. .. note:: This feature requires the SDL2 text provider. .. versionadded:: 1.10.0 :attr:`underline` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' def _get_strikethrough(self) -> bool: return self._label.strikethrough def _set_strikethrough(self, strikethrough: bool) -> None: self._label.strikethrough = strikethrough strikethrough = AliasProperty(_get_strikethrough, _set_strikethrough) '''Adds a strikethrough line to the text. .. note:: This feature requires the SDL2 text provider. ''' def _get_padding_x(self): return self._label.padding_x def _set_padding_x(self, padding_x): self._label.padding_x = padding_x padding_x = AliasProperty(_get_padding_x, _set_padding_x) '''Horizontal padding of the text inside the widget box. ''' def _get_padding_y(self): return self._label.padding_y def _set_padding_y(self, padding_y): self._label.padding_y = padding_y padding_y = AliasProperty(_get_padding_y, _set_padding_y) '''Vertical padding of the text inside the widget box. ''' padding = ReferenceListProperty(padding_x, padding_y) '''Padding of the text in the format (padding_x, padding_y) :attr:`padding` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`padding_x`, :attr:`padding_y`) properties. ''' def _get_halign(self) -> str: return self._label.halign def _set_halign(self, halign: str) -> None: self._label.halign = halign halign = AliasProperty(_get_halign, _set_halign) '''Horizontal alignment of the text. Available options are : left, center, right and justify. .. warning:: This doesn't change the position of the text texture of the Label (centered), only the position of the text in this texture. You probably want to bind the size of the Label to the :attr:`texture_size` or set a :attr:`text_size`. ''' def _get_valign(self) -> str: return self._label.valign def _set_valign(self, valign: str) -> None: self._label.valign = valign valign = AliasProperty(_get_valign, _set_valign) '''Vertical alignment of the text. Available options are : `'bottom'`, `'middle'` (or `'center'`) and `'top'`. .. warning:: This doesn't change the position of the text texture of the Label (centered), only the position of the text within this texture. You probably want to bind the size of the Label to the :attr:`texture_size` or set a :attr:`text_size` to change this behavior. ''' def _get_outline_width(self) -> Optional[int]: return self._label.outline_width def _set_outline_width(self, outline_width: Optional[int]) -> None: self._label.outline_width = outline_width outline_width = AliasProperty(_get_outline_width, _set_outline_width) '''Width in pixels for the outline around the text. No outline will be rendered if the value is None. .. note:: This feature requires the SDL2 text provider. ''' def _get_outline_color(self) -> list: return self._label.outline_color def _set_outline_color(self, outline_color: list) -> None: self._label.outline_color = outline_color outline_color = AliasProperty(_get_outline_color, _set_outline_color) '''The color of the text outline, in the (r, g, b) format. .. note:: This feature requires the SDL2 text provider. ''' def _get_disabled_outline_color(self) -> list: return self._label.disabled_outline_color def _set_disabled_outline_color(self, disabled_outline_color: list) -> None: self._label.disabled_outline_color = disabled_outline_color disabled_outline_color = AliasProperty(_get_disabled_outline_color, _set_disabled_outline_color) '''The color of the text outline when the widget is disabled, in the (r, g, b) format. .. note:: This feature requires the SDL2 text provider. ''' def _get_mipmap(self) -> bool: return self._label.mipmap def _set_mipmap(self, mipmap: bool) -> None: self._label.mipmap = mipmap mipmap = AliasProperty(_get_mipmap, _set_mipmap) '''Indicates whether OpenGL mipmapping is applied to the texture or not. Read :ref:`mipmap` for more information. ''' def _get_shorten(self) -> bool: return self._label.shorten def _set_shorten(self, shorten: bool) -> None: self._label.shorten = shorten shorten = AliasProperty(_get_shorten, _set_shorten) ''' Indicates whether the label should attempt to shorten its textual contents as much as possible if a :attr:`text_size` is given. Setting this to True without an appropriately set :attr:`text_size` will lead to unexpected results. :attr:`shorten_from` and :attr:`split_str` control the direction from which the :attr:`text` is split, as well as where in the :attr:`text` we are allowed to split. ''' def _get_shorten_from(self) -> str: return self._label.shorten_from def _set_shorten_from(self, shorten_from: str) -> None: self._label.shorten_from = shorten_from shorten_from = AliasProperty(_get_shorten_from, _set_shorten_from) '''The side from which we should shorten the text from, can be left, right, or center. For example, if left, the ellipsis will appear towards the left side and we will display as much text starting from the right as possible. Similar to :attr:`shorten`, this option only applies when :attr:`text_size` [0] is not None, In this case, the string is shortened to fit within the specified width. ''' def _get_is_shortened(self) -> bool: return self._label.is_shortened is_shortened = AliasProperty(_get_is_shortened, None) '''This property indicates if :attr:`text` was rendered with or without shortening when :attr:`shorten` is True. ''' def _get_split_str(self) -> str: return self._label.split_str def _set_split_str(self, split_str: str) -> None: self._label.split_str = split_str split_str = AliasProperty(_get_split_str, _set_split_str) '''The string used to split the :attr:`text` while shortening the string when :attr:`shorten` is True. For example, if it's a space, the string will be broken into words and as many whole words that can fit into a single line will be displayed. If :attr:`split_str` is the empty string, `''`, we split on every character fitting as much text as possible into the line. ''' def _get_ellipsis_options(self) -> dict: return self._label.ellipsis_options def _set_ellipsis_options(self, ellipsis_options: dict) -> None: self._label.ellipsis_options = ellipsis_options ellipsis_options = AliasProperty(_get_ellipsis_options, _set_ellipsis_options) '''Font options for the ellipsis string('...') used to split the text. Accepts a dict as option name with the value. Only applied when :attr:`markup` is true and text is shortened. All font options which work for :class:`Label` will work for :attr:`ellipsis_options`. Defaults for the options not specified are taken from the surronding text. .. code-block:: kv Label: text: 'Some very long line which will be cut' markup: True shorten: True ellipsis_options: {'color':(1,0.5,0.5,1),'underline':True} ''' def _get_unicode_errors(self) -> str: return self._label.unicode_errors def _set_unicode_errors(self, unicode_errors: str) -> None: self._label.unicode_errors = unicode_errors unicode_errors = AliasProperty(_get_unicode_errors, _set_unicode_errors) '''How to handle unicode decode errors. Can be `'strict'`, `'replace'` or `'ignore'`. ''' def _get_markup(self) -> bool: return self._label.markup def _set_markup(self, markup: bool) -> None: self._label.markup = markup markup = AliasProperty(_get_markup, _set_markup) '''If True, the text will be rendered using the :class:`~kivy.core.text.markup.MarkupLabel`: you can change the style of the text using tags. Check the :doc:`api-kivy.core.text.markup` documentation for more information. ''' def _get_refs(self) -> dict: return self._label.refs refs = AliasProperty(_get_refs, None) '''List of ``[ref=xxx]`` markup items in the text with the bounding box of all the words contained in a ref, available only after rendering. For example, if you wrote:: Check out my [ref=hello]link[/ref] The refs will be set with:: {'hello': ((64, 0, 78, 16), )} The references marked "hello" have a bounding box at (x1, y1, x2, y2). These co-ordinates are relative to the top left corner of the text, with the y value increasing downwards. You can define multiple refs with the same name: each occurrence will be added as another (x1, y1, x2, y2) tuple to this list. The current Label implementation uses these references if they exist in your markup text, automatically doing the collision with the touch and dispatching an `on_ref_press` event. You can bind a ref event like this:: def print_it(instance, value): print('User click on', value) widget = Label(text='Hello [ref=world]World[/ref]', markup=True) widget.on_ref_press(print_it) .. note:: This works only with markup text. You need :attr:`markup` set to True. ''' def _get_max_lines(self) -> Optional[int]: return self._label.max_lines def _set_max_lines(self, max_lines: Optional[int]) -> None: self._label.max_lines = max_lines max_lines = AliasProperty(_get_max_lines, _set_max_lines) '''Maximum number of lines to use, defaults to 0, which means unlimited. Please note that :attr:`shorten` take over this property. (with shorten, the text is always one line.) ''' def _get_strip(self) -> bool: return self._label.strip def _set_strip(self, strip: bool) -> None: self._label.strip = strip strip = AliasProperty(_get_strip, _set_strip) '''Whether leading and trailing spaces and newlines should be stripped from each displayed line. If True, every line will start at the right or left edge, depending on :attr:`halign`. If :attr:`halign` is `justify` it is implicitly True. .. versionadded:: 1.9.0 :attr:`strip` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' def _get_font_hinting(self) -> Optional[str]: return self._label.font_hinting def _set_font_hinting(self, font_hinting: Optional[str]): self._label.font_hinting = font_hinting font_hinting = AliasProperty(_get_font_hinting, _set_font_hinting) '''What hinting option to use for font rendering. Can be one of `'normal'`, `'light'`, `'mono'` or None. .. note:: This feature requires the SDL2 text provider. ''' def _get_font_kerning(self) -> bool: return self._label.font_kerning def _set_font_kerning(self, font_kerning: bool) -> None: self._label.font_kerning = font_kerning font_kerning = AliasProperty(_get_font_kerning, _set_font_kerning) '''Whether kerning is enabled for font rendering. .. note:: This feature requires the SDL2 text provider. ''' def _get_font_blended(self) -> bool: return self._label.font_blended def _set_font_blended(self, font_blended: bool) -> None: self._label.font_blended = font_blended font_blended = AliasProperty(_get_font_blended, _set_font_blended) '''Whether blended or solid font rendering should be used. .. note:: This feature requires the SDL2 text provider. ''' rotation = NumericProperty(0) '''Rotation angle value of the widget. :attr:`rotation` is an :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' scale = NumericProperty(1.0) '''Scale value of the widget.
class Dummy(ImageStack): """A widget that looks like the ones on the board, which, when dragged onto the board, creates one of them. """ _touch = ObjectProperty(None, allownone=True) name = StringProperty() prefix = StringProperty() num = NumericProperty() x_start = NumericProperty(0) y_start = NumericProperty(0) pos_start = ReferenceListProperty(x_start, y_start) x_down = NumericProperty(0) y_down = NumericProperty(0) pos_down = ReferenceListProperty(x_down, y_down) x_up = NumericProperty(0) y_up = NumericProperty(0) pos_up = ReferenceListProperty(x_up, y_up) x_center_up = NumericProperty(0) y_center_up = NumericProperty(0) center_up = ReferenceListProperty(x_center_up, y_center_up) right_up = NumericProperty(0) top_up = NumericProperty(0) def on_paths(self, *args, **kwargs): super().on_paths(*args, **kwargs) Logger.debug("Dummy: {} got paths {}".format(self.name, self.paths)) def on_touch_down(self, touch): """If hit, record my starting position, that I may return to it in ``on_touch_up`` after creating a real :class:`board.Spot` or :class:`board.Pawn` instance. """ if not self.collide_point(*touch.pos): return False self.pos_start = self.pos self.pos_down = (self.x - touch.x, self.y - touch.y) touch.grab(self) self._touch = touch return True def on_touch_move(self, touch): """Follow the touch""" if touch is not self._touch: return False self.pos = (touch.x + self.x_down, touch.y + self.y_down) return True def on_touch_up(self, touch): """Return to ``pos_start``, but first, save my current ``pos`` into ``pos_up``, so that the layout knows where to put the real :class:`board.Spot` or :class:`board.Pawn` instance. """ if touch is not self._touch: return False self.pos_up = self.pos self.pos = self.pos_start self._touch = None return True
class FlexSlider(Widget): value = NumericProperty(0.) min = NumericProperty(0.) max = NumericProperty(100.) padding = NumericProperty('16sp') # default: 16sp orientation = OptionProperty('horizontal', options=('vertical', 'horizontal')) range = ReferenceListProperty(min, max) step = BoundedNumericProperty(0, min=0) background_horizontal = StringProperty( 'atlas://data/images/defaulttheme/sliderh_background') background_disabled_horizontal = StringProperty( 'atlas://data/images/defaulttheme/sliderh_background_disabled') background_vertical = StringProperty( 'atlas://data/images/defaulttheme/sliderv_background') background_disabled_vertical = StringProperty( 'atlas://data/images/defaulttheme/sliderv_background_disabled') background_width = NumericProperty('36sp') cursor_image = StringProperty( 'atlas://data/images/defaulttheme/slider_cursor') cursor_disabled_image = StringProperty( 'atlas://data/images/defaulttheme/slider_cursor_disabled') cursor_width = NumericProperty('32sp') cursor_height = NumericProperty('32sp') cursor_size = ReferenceListProperty(cursor_width, cursor_height) border_horizontal = ListProperty([0, 18, 0, 18]) border_vertical = ListProperty([18, 0, 18, 0]) value_track = BooleanProperty(False) value_track_color = ListProperty([1, 1, 1, 1]) value_track_width = NumericProperty('3dp') sensitivity = OptionProperty('all', options=('all', 'handle')) def on_min(self, *largs): self.value = min(self.max, max(self.min, self.value)) def on_max(self, *largs): self.value = min(self.max, max(self.min, self.value)) def get_norm_value(self): vmin = self.min d = self.max - vmin if d == 0: return 0 return (self.value - vmin) / float(d) def set_norm_value(self, value): vmin = self.min vmax = self.max step = self.step val = min(value * (vmax - vmin) + vmin, vmax) if step == 0: self.value = val else: self.value = min(round((val - vmin) / step) * step + vmin, vmax) value_normalized = AliasProperty(get_norm_value, set_norm_value, bind=('value', 'min', 'max'), cache=True) def get_value_pos(self): padding = self.padding x = self.x y = self.y nval = self.value_normalized if self.orientation == 'horizontal': return (x + padding + nval * (self.width - 2 * padding), y) else: return (x, y + padding + nval * (self.height - 2 * padding)) def set_value_pos(self, pos): padding = self.padding x = min(self.right - padding, max(pos[0], self.x + padding)) y = min(self.top - padding, max(pos[1], self.y + padding)) if self.orientation == 'horizontal': if self.width == 0: self.value_normalized = 0 else: self.value_normalized = ( x - self.x - padding) / float(self.width - 2 * padding) else: if self.height == 0: self.value_normalized = 0 else: self.value_normalized = ( y - self.y - padding) / float(self.height - 2 * padding) value_pos = AliasProperty(get_value_pos, set_value_pos, bind=('pos', 'size', 'min', 'max', 'padding', 'value_normalized', 'orientation'), cache=True) def on_touch_down(self, touch): if self.disabled or not self.collide_point(*touch.pos): return if touch.is_mouse_scrolling: if 'down' in touch.button or 'left' in touch.button: if self.step: self.value = min(self.max, self.value + self.step) else: self.value = min(self.max, self.value + (self.max - self.min) / 20) if 'up' in touch.button or 'right' in touch.button: if self.step: self.value = max(self.min, self.value - self.step) else: self.value = max(self.min, self.value - (self.max - self.min) / 20) elif self.sensitivity == 'handle': if self.children[0].collide_point(*touch.pos): touch.grab(self) else: touch.grab(self) self.value_pos = touch.pos return True def on_touch_move(self, touch): if touch.grab_current == self: self.value_pos = touch.pos self.loading_value_pos = touch.pos[0] - 10, touch.pos[1] return True def on_touch_up(self, touch): if touch.grab_current == self: self.value_pos = touch.pos return True
class Slider(Widget): """Class for creating a Slider widget. Check module documentation for more details. """ value = NumericProperty(0.) '''Current value used for the slider. :attr:`value` is a :class:`~kivy.properties.NumericProperty` and defaults to 0.''' min = NumericProperty(0.) '''Minimum value allowed for :attr:`value`. :attr:`min` is a :class:`~kivy.properties.NumericProperty` and defaults to 0.''' max = NumericProperty(100.) '''Maximum value allowed for :attr:`value`. :attr:`max` is a :class:`~kivy.properties.NumericProperty` and defaults to 100.''' padding = NumericProperty(sp(16)) '''Padding of the slider. The padding is used for graphical representation and interaction. It prevents the cursor from going out of the bounds of the slider bounding box. By default, padding is sp(16). The range of the slider is reduced from padding \*2 on the screen. It allows drawing the default cursor of sp(32) width without having the cursor go out of the widget. :attr:`padding` is a :class:`~kivy.properties.NumericProperty` and defaults to sp(16).''' orientation = OptionProperty('horizontal', options=('vertical', 'horizontal')) '''Orientation of the slider. :attr:`orientation` is an :class:`~kivy.properties.OptionProperty` and defaults to 'horizontal'. Can take a value of 'vertical' or 'horizontal'. ''' range = ReferenceListProperty(min, max) '''Range of the slider in the format (minimum value, maximum value):: >>> slider = Slider(min=10, max=80) >>> slider.range [10, 80] >>> slider.range = (20, 100) >>> slider.min 20 >>> slider.max 100 :attr:`range` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`min`, :attr:`max`) properties. ''' step = BoundedNumericProperty(0, min=0) '''Step size of the slider. .. versionadded:: 1.4.0 Determines the size of each interval or step the slider takes between min and max. If the value range can't be evenly divisible by step the last step will be capped by slider.max :attr:`step` is a :class:`~kivy.properties.NumericProperty` and defaults to 1.''' # The following two methods constrain the slider's value # to range(min,max). Otherwise it may happen that self.value < self.min # at init. def on_min(self, *largs): self.value = min(self.max, max(self.min, self.value)) def on_max(self, *largs): self.value = min(self.max, max(self.min, self.value)) def get_norm_value(self): vmin = self.min d = self.max - vmin if d == 0: return 0 return (self.value - vmin) / float(d) def set_norm_value(self, value): vmin = self.min vmax = self.max step = self.step val = min(value * (vmax - vmin) + vmin, vmax) if step == 0: self.value = val else: self.value = min(round((val - vmin) / step) * step + vmin, vmax) value_normalized = AliasProperty(get_norm_value, set_norm_value, bind=('value', 'min', 'max', 'step')) '''Normalized value inside the :attr:`range` (min/max) to 0-1 range:: >>> slider = Slider(value=50, min=0, max=100) >>> slider.value 50 >>> slider.value_normalized 0.5 >>> slider.value = 0 >>> slider.value_normalized 0 >>> slider.value = 100 >>> slider.value_normalized 1 You can also use it for setting the real value without knowing the minimum and maximum:: >>> slider = Slider(min=0, max=200) >>> slider.value_normalized = .5 >>> slider.value 100 >>> slider.value_normalized = 1. >>> slider.value 200 :attr:`value_normalized` is an :class:`~kivy.properties.AliasProperty`. ''' def get_value_pos(self): padding = self.padding x = self.x y = self.y nval = self.value_normalized if self.orientation == 'horizontal': return (x + padding + nval * (self.width - 2 * padding), y) else: return (x, y + padding + nval * (self.height - 2 * padding)) def set_value_pos(self, pos): padding = self.padding x = min(self.right - padding, max(pos[0], self.x + padding)) y = min(self.top - padding, max(pos[1], self.y + padding)) if self.orientation == 'horizontal': if self.width == 0: self.value_normalized = 0 else: self.value_normalized = ( x - self.x - padding) / float(self.width - 2 * padding) else: if self.height == 0: self.value_normalized = 0 else: self.value_normalized = ( y - self.y - padding) / float(self.height - 2 * padding) value_pos = AliasProperty(get_value_pos, set_value_pos, bind=('x', 'y', 'width', 'height', 'min', 'max', 'value_normalized', 'orientation')) '''Position of the internal cursor, based on the normalized value. :attr:`value_pos` is an :class:`~kivy.properties.AliasProperty`. ''' def on_touch_down(self, touch): if self.disabled or not self.collide_point(*touch.pos): return if touch.is_mouse_scrolling: if 'down' in touch.button or 'left' in touch.button: if self.step: self.value = min(self.max, self.value + self.step) else: self.value = min(self.max, self.value + (self.max - self.min) / 20) if 'up' in touch.button or 'right' in touch.button: if self.step: self.value = max(self.min, self.value - self.step) else: self.value = max(self.min, self.value - (self.max - self.min) / 20) else: touch.grab(self) self.value_pos = touch.pos return True def on_touch_move(self, touch): if touch.grab_current == self: self.value_pos = touch.pos return True def on_touch_up(self, touch): if touch.grab_current == self: self.value_pos = touch.pos return True
class Car(Widget): angle = NumericProperty(0) # inicialização do ângulo do carro rotation = NumericProperty( 0 ) # inicialização da última rotação do carro (depois de uma ação, o caro faz uma rotação de 0, 20 ou -20 graus) velocity_x = NumericProperty( 0) # inicialização da coordenada de velocidade x velocity_y = NumericProperty( 0) # inicialização da coordenada de velocidade y velocity = ReferenceListProperty( velocity_x, velocity_y) # vetor com a velocidade x e y sensor1_x = NumericProperty( 0) # inicialização da coordenada x do primeiro sensor (frente) sensor1_y = NumericProperty( 0) # inicialização da coordenadao y do primeiro sensor (frente) sensor1 = ReferenceListProperty(sensor1_x, sensor1_y) # primeiro sensor sensor2_x = NumericProperty( 0 ) # inicialização da coordenada x do segundo sensor (30 graus para a esquerda) sensor2_y = NumericProperty( 0 ) # inicialização da coordenada y do segundo sensor (30 graus para a esquerda) sensor2 = ReferenceListProperty(sensor2_x, sensor2_y) # segundo sensor sensor3_x = NumericProperty( 0 ) # inicialização da coordenada x do terceiro sensor (30 graus para a direita) sensor3_y = NumericProperty( 0 ) # inicialização da coordenada y do terceiro sensor (30 graus para a direita) sensor3 = ReferenceListProperty(sensor3_x, sensor3_y) # terceiro sensor signal1 = NumericProperty( 0) # inicialização do sinal recebido pelo sensor 1 signal2 = NumericProperty( 0) # inicialização do sinal recebido pelo sensor 2 signal3 = NumericProperty( 0) # inicialização do sinal recebido pelo sensor 3 def move(self, rotation): self.pos = Vector( *self.velocity ) + self.pos # atualiza a posição do carro de acordo com sua última posição e velocidade self.rotation = rotation # busca a rotação do carro self.angle = self.angle + self.rotation # atualiza o ângulo self.sensor1 = Vector(30, 0).rotate( self.angle) + self.pos # atualiza a posição do sensor 1 self.sensor2 = Vector(30, 0).rotate( (self.angle + 30) % 360) + self.pos # atualiza a posição do sensor 2 self.sensor3 = Vector(30, 0).rotate( (self.angle - 30) % 360) + self.pos # atualiza a posição do sensor 3 self.signal1 = int( np.sum(sand[int(self.sensor1_x) - 10:int(self.sensor1_x) + 10, int(self.sensor1_y) - 10:int(self.sensor1_y) + 10]) ) / 400. # calcula o sinal recebido do sensor 1 (densidade de areia ao redor do sensor 1) self.signal2 = int( np.sum(sand[int(self.sensor2_x) - 10:int(self.sensor2_x) + 10, int(self.sensor2_y) - 10:int(self.sensor2_y) + 10]) ) / 400. # calcula o sinal recebido do sensor 1 (densidade de areia ao redor do sensor 2) self.signal3 = int( np.sum(sand[int(self.sensor3_x) - 10:int(self.sensor3_x) + 10, int(self.sensor3_y) - 10:int(self.sensor3_y) + 10]) ) / 400. # calcula o sinal recebido do sensor 1 (densidade de areia ao redor do sensor 3) if self.sensor1_x > longueur - 10 or self.sensor1_x < 10 or self.sensor1_y > largeur - 10 or self.sensor1_y < 10: # se o sensor 1 saiu do mapa self.signal1 = 1. # sensor 1 detecta areia if self.sensor2_x > longueur - 10 or self.sensor2_x < 10 or self.sensor2_y > largeur - 10 or self.sensor2_y < 10: # se o sensor 2 saiu do mapa self.signal2 = 1. # sensor 2 detecta areia if self.sensor3_x > longueur - 10 or self.sensor3_x < 10 or self.sensor3_y > largeur - 10 or self.sensor3_y < 10: # se o sensor 3 saiu do mapa self.signal3 = 1. # sensor 3 detecta areia
class BackgroundColorBehavior(CommonElevationBehavior): background = StringProperty() """ Background image path. :attr:`background` is a :class:`~kivy.properties.StringProperty` and defaults to `None`. """ r = BoundedNumericProperty(1.0, min=0.0, max=1.0) """ The value of ``red`` in the ``rgba`` palette. :attr:`r` is an :class:`~kivy.properties.BoundedNumericProperty` and defaults to `1.0`. """ g = BoundedNumericProperty(1.0, min=0.0, max=1.0) """ The value of ``green`` in the ``rgba`` palette. :attr:`g` is an :class:`~kivy.properties.BoundedNumericProperty` and defaults to `1.0`. """ b = BoundedNumericProperty(1.0, min=0.0, max=1.0) """ The value of ``blue`` in the ``rgba`` palette. :attr:`b` is an :class:`~kivy.properties.BoundedNumericProperty` and defaults to `1.0`. """ a = BoundedNumericProperty(0.0, min=0.0, max=1.0) """ The value of ``alpha channel`` in the ``rgba`` palette. :attr:`a` is an :class:`~kivy.properties.BoundedNumericProperty` and defaults to `0.0`. """ radius = VariableListProperty([0], length=4) """ Canvas radius. .. code-block:: python # Top left corner slice. MDBoxLayout: md_bg_color: app.theme_cls.primary_color radius: [25, 0, 0, 0] :attr:`radius` is an :class:`~kivy.properties.VariableListProperty` and defaults to `[0, 0, 0, 0]`. """ md_bg_color = ReferenceListProperty(r, g, b, a) """ The background color of the widget (:class:`~kivy.uix.widget.Widget`) that will be inherited from the :attr:`BackgroundColorBehavior` class. For example: .. code-block:: kv Widget: canvas: Color: rgba: 0, 1, 1, 1 Rectangle: size: self.size pos: self.pos similar to code: .. code-block:: kv <MyWidget@BackgroundColorBehavior> md_bg_color: 0, 1, 1, 1 :attr:`md_bg_color` is an :class:`~kivy.properties.ReferenceListProperty` and defaults to :attr:`r`, :attr:`g`, :attr:`b`, :attr:`a`. """ line_color = ColorProperty([0, 0, 0, 0]) """ If a custom value is specified for the `line_color parameter`, the border of the specified color will be used to border the widget: .. code-block:: kv MDBoxLayout: size_hint: .5, .2 md_bg_color: 0, 1, 1, .5 line_color: 0, 0, 1, 1 radius: [24, ] .. versionadded:: 0.104.2 :attr:`line_color` is an :class:`~kivy.properties.ColorProperty` and defaults to `[0, 0, 0, 0]`. """ line_width = NumericProperty(1) """ Border of the specified width will be used to border the widget. .. versionadded:: 1.0.0 :attr:`line_width` is an :class:`~kivy.properties.NumericProperty` and defaults to `1`. """ angle = NumericProperty(0) background_origin = ListProperty(None) _background_x = NumericProperty(0) _background_y = NumericProperty(0) _background_origin = ReferenceListProperty( _background_x, _background_y, ) def __init__(self, **kwarg): super().__init__(**kwarg) self.bind(pos=self.update_background_origin) def update_background_origin(self, instance_md_widget, pos: List[float]) -> None: if self.background_origin: self._background_origin = self.background_origin else: self._background_origin = self.center
class Pallet(StackLayout): """Many :class:`SwatchButton`, gathered from an :class:`kivy.atlas.Atlas`.""" atlas = ObjectProperty() """:class:`kivy.atlas.Atlas` object I'll make :class:`SwatchButton` from.""" filename = StringProperty() """Path to an atlas; will construct :class:`kivy.atlas.Atlas` when set""" swatches = DictProperty({}) """:class:`SwatchButton` widgets here, keyed by name of their graphic""" swatch_width = NumericProperty(100) """Width of each and every :class:`SwatchButton` here""" swatch_height = NumericProperty(75) """Height of each and every :class:`SwatchButton` here""" swatch_size = ReferenceListProperty(swatch_width, swatch_height) """Size of each and every :class:`SwatchButton` here""" selection = ListProperty([]) """List of :class:`SwatchButton`s that are selected""" selection_mode = OptionProperty('single', options=['single', 'multiple']) """Whether to allow only a 'single' selected :class:`SwatchButton` (default), or 'multiple'""" def on_selection(self, *args): Logger.debug( 'Pallet: {} got selection {}'.format( self.filename, self.selection ) ) def on_filename(self, *args): if not self.filename: return resource = resource_find(self.filename) if not resource: raise ValueError("Couldn't find atlas: {}".format(self.filename)) self.atlas = Atlas(resource) def on_atlas(self, *args): if self.atlas is None: return self.upd_textures() self.atlas.bind(textures=self._trigger_upd_textures) def upd_textures(self, *args): """Create one :class:`SwatchButton` for each texture""" if self.canvas is None: Clock.schedule_once(self.upd_textures, 0) return swatches = self.swatches atlas_textures = self.atlas.textures remove_widget = self.remove_widget add_widget = self.add_widget swatch_size = self.swatch_size for name, swatch in list(swatches.items()): if name not in atlas_textures: remove_widget(swatch) del swatches[name] for (name, tex) in atlas_textures.items(): if name in swatches and swatches[name] != tex: remove_widget(swatches[name]) if name not in swatches or swatches[name] != tex: swatches[name] = SwatchButton( text=name, tex=tex, size_hint=(None, None), size=swatch_size ) add_widget(swatches[name]) def _trigger_upd_textures(self, *args): Clock.unschedule(self.upd_textures) Clock.schedule_once(self._trigger_upd_textures)
class Piece(RelativeLayout, GridEntry): name = StringProperty('L') mode = OptionProperty('up', options=['up', 'dn', 'lt', 'rt']) brick_a = ObjectProperty(None) brick_b = ObjectProperty(None) brick_c = ObjectProperty(None) brick_0 = ObjectProperty(None) bricks = ReferenceListProperty(brick_a, brick_b, brick_c, brick_0) color = ListProperty([1, 1, 1, 1]) def on_grid_pos(self, *args, **kwargs): self.pos_hint = { 'x': self.size_hint[0] * self.column, 'y': self.size_hint[1] * self.row } def fall(self, *args): self.row = self.row - 1 def strifel(self): self.column = self.column - 1 def strifer(self): self.column = self.column + 1 def shift_left(self): if self.mode is 'up': self.mode = 'lt' elif self.mode is 'dn': self.mode = 'rt' elif self.mode is 'rt': self.mode = 'up' elif self.mode is 'lt': self.mode = 'dn' def shift_right(self): if self.mode is 'up': self.mode = 'rt' elif self.mode is 'dn': self.mode = 'lt' elif self.mode is 'rt': self.mode = 'dn' elif self.mode is 'lt': self.mode = 'up' def release_bricks(self): for brick in self.bricks: self.remove_widget(brick) def factory(name): if name is 'L': return LPiece() if name is 'J': return JPiece() if name is 'S': return SPiece() if name is 'O': return OPiece() if name is 'T': return TPiece() if name is 'I': return IPiece() if name is 'Z': return ZPiece() factory = staticmethod(factory)
class CommonElevationBehavior(Widget): """Common base class for rectangular and circular elevation behavior.""" elevation = BoundedNumericProperty(0, min=0, errorvalue=0) """ Elevation of the widget. .. note:: Although, this value does not represent the current elevation of the widget. :attr:`~CommonElevationBehavior._elevation` can be used to animate the current elevation and come back using the :attr:`~CommonElevationBehavior.elevation` property directly. For example: .. code-block:: python from kivy.lang import Builder from kivy.uix.behaviors import ButtonBehavior from kivymd.app import MDApp from kivymd.uix.behaviors import CircularElevationBehavior, CircularRippleBehavior from kivymd.uix.boxlayout import MDBoxLayout KV = ''' #:import Animation kivy.animation.Animation <WidgetWithShadow> size_hint: [None, None] elevation: 6 animation_: None md_bg_color: [1] * 4 on_size: self.radius = [self.height / 2] * 4 on_press: if self.animation_: \ self.animation_.cancel(self); \ self.animation_ = Animation(_elevation=self.elevation + 6, d=0.08); \ self.animation_.start(self) on_release: if self.animation_: \ self.animation_.cancel(self); \ self.animation_ = Animation(_elevation = self.elevation, d=0.08); \ self.animation_.start(self) MDFloatLayout: WidgetWithShadow: size: [root.size[1] / 2] * 2 pos_hint: {"center": [0.5, 0.5]} ''' class WidgetWithShadow( CircularElevationBehavior, CircularRippleBehavior, ButtonBehavior, MDBoxLayout, ): def __init__(self, **kwargs): # always set the elevation before the super().__init__ call # self.elevation = 6 super().__init__(**kwargs) def on_size(self, *args): self.radius = [self.size[0] / 2] class Example(MDApp): def build(self): return Builder.load_string(KV) Example().run() :attr:`elevation` is an :class:`~kivy.properties.BoundedNumericProperty` and defaults to `0`. """ # Shadow rendering properties. # Shadow rotation memory - SHARED ACROSS OTHER CLASSES. angle = NumericProperty(0) """ Angle of rotation in degrees of the current shadow. This value is shared across different widgets. .. note:: This value will affect both, hard and soft shadows. Each shadow has his own origin point that's computed every time the elevation changes. .. warning:: Do not add `PushMatrix` inside the canvas before and add `PopMatrix` in the next layer, this will cause visual errors, because the stack used will clip the push and pop matrix already inside the canvas.before canvas layer. Incorrect: .. code-block:: kv <TiltedWidget> canvas.before: PushMatrix [...] canvas: PopMatrix Correct: .. code-block:: kv <TiltedWidget> canvas.before: PushMatrix [...] PopMatrix :attr:`angle` is an :class:`~kivy.properties.NumericProperty` and defaults to `0`. """ radius = VariableListProperty([0]) """ Radius of the corners of the shadow. This values represents each corner of the shadow, starting from `top-left` corner and going clockwise. .. code-block:: python radius = [ "top-left", "top-right", "bottom-right", "bottom-left", ] This value can be expanded thus allowing this settings to be valid: .. code-block:: python widget.radius=[0] # Translates to [0, 0, 0, 0] widget.radius=[10, 3] # Translates to [10, 3, 10, 3] widget.radius=[7.0, 8.7, 1.5, 3.0] # Translates to [7, 8, 1, 3] .. note:: This value will affect both, hard and soft shadows. This value only affects :class:`~RoundedRectangularElevationBehavior` for now, but can be stored and used by custom shadow draw functions. :attr:`radius` is an :class:`~kivy.properties.VariableListProperty` and defaults to `[0, 0, 0, 0]`. """ # Position of the shadow. _shadow_origin_x = NumericProperty(0) """ Shadow origin `x` position for the rotation origin. Managed by `_shadow_origin`. :attr:`_shadow_origin_x` is an :class:`~kivy.properties.NumericProperty` and defaults to `0`. .. note:: This property is automatically processed. by _shadow_origin. """ _shadow_origin_y = NumericProperty(0) """ Shadow origin y position for the rotation origin. Managed by :attr:`_shadow_origin`. :attr:`_shadow_origin_y` is an :class:`~kivy.properties.NumericProperty` and defaults to `0`. .. note:: This property is automatically processed. """ _shadow_origin = ReferenceListProperty(_shadow_origin_x, _shadow_origin_y) """ Soft shadow rotation origin point. :attr:`_shadow_origin` is an :class:`~kivy.properties.ReferenceListProperty` and defaults to `[0, 0]`. .. note:: This property is automatically processed and relative to the canvas center. """ _shadow_pos = ListProperty([0, 0]) # custom offset """ Soft shadow origin point. :attr:`_shadow_pos` is an :class:`~kivy.properties.ListProperty` and defaults to `[0, 0]`. .. note:: This property is automatically processed and relative to the widget's canvas center. """ shadow_pos = ListProperty([0, 0]) # bottom left corner """ Custom shadow origin point. If this property is set, :attr:`_shadow_pos` will be ommited. This property allows users to fake light source. :attr:`shadow_pos` is an :class:`~kivy.properties.ListProperty` and defaults to `[0, 0]`. .. note:: this value overwrite the :attr:`_shadow_pos` processing. """ # Shadow Group shared memory __shadow_groups = {"global": []} shadow_group = StringProperty("global") """ Widget's shadow group. By default every widget with a shadow is saved inside the memory :attr:`__shadow_groups` as a weakref. This means that you can have multiple light sources, one for every shadow group. To fake a light source use :attr:`force_shadow_pos`. :attr:`shadow_group` is an :class:`~kivy.properties.StringProperty` and defaults to `"global"`. """ _elevation = BoundedNumericProperty(0, min=0, errorvalue=0) """ Current elevation of the widget. .. warning:: This property is the current elevation of the widget, do not use this property directly, instead, use :class:`~CommonElevationBehavior` elevation. :attr:`_elevation` is an :class:`~kivy.properties.NumericProperty` and defaults to `0`. """ # soft shadow _soft_shadow_texture = ObjectProperty() """ Texture of the soft shadow texture for the canvas. :attr:`_soft_shadow_texture` is an :class:`~kivy.core.image.Image` and defaults to `None`. .. note:: This property is automatically processed. """ soft_shadow_size = ListProperty([0, 0]) """ Size of the soft shadow texture over the canvas. :attr:`soft_shadow_size` is an :class:`~kivy.properties.ListProperty` and defaults to `[0, 0]`. .. note:: This property is automatically processed. """ soft_shadow_pos = ListProperty([0, 0]) """ Position of the hard shadow texture over the canvas. :attr:`soft_shadow_pos` is an :class:`~kivy.properties.ListProperty` and defaults to `[0, 0]`. .. note:: This property is automatically processed. """ soft_shadow_cl = ListProperty([0, 0, 0, 0.50]) """ Color of the soft shadow. :attr:`soft_shadow_cl` is an :class:`~kivy.properties.ListProperty` and defaults to `[0, 0, 0, 0.15]`. """ # hard shadow hard_shadow_texture = ObjectProperty() """ Texture of the hard shadow texture for the canvas. :attr:`hard_shadow_texture` is an :class:`~kivy.core.image.Image` and defaults to `None`. .. note:: This property is automatically processed when elevation is changed. """ hard_shadow_size = ListProperty([0, 0]) """ Size of the hard shadow texture over the canvas. :attr:`hard_shadow_size` is an :class:`~kivy.properties.ListProperty` and defaults to `[0, 0]`. .. note:: This property is automatically processed when elevation is changed. """ hard_shadow_pos = ListProperty([0, 0]) """ Position of the hard shadow texture over the canvas. :attr:`hard_shadow_pos` is an :class:`~kivy.properties.ListProperty` and defaults to `[0, 0]`. .. note:: This property is automatically processed when elevation is changed. """ hard_shadow_cl = ListProperty([0, 0, 0, 0.15]) """ Color of the hard shadow. .. note:: :attr:`hard_shadow_cl` is an :class:`~kivy.properties.ListProperty` and defaults to `[0, 0, 0, 0.15]`. """ # Shared property for some calculations. # This values are used to improve the gaussain blur and avoid that # the blur goes outside the texture. hard_shadow_offset = BoundedNumericProperty(2, min=0, errorhandler=lambda x: 0 if x < 0 else x) """ This value sets a special offset to the shadow canvas, this offset allows a correct draw of the canvas size. allowing the effect to correctly blur the image in the given space. :attr:`hard_shadow_offset` is an :class:`~kivy.properties.BoundedNumericProperty` and defaults to `2`. """ soft_shadow_offset = BoundedNumericProperty(4, min=0, errorhandler=lambda x: 0 if x < 0 else x) """ This value sets a special offset to the shadow canvas, this offset allows a correct draw of the canvas size. allowing the effect to correctly blur the image in the given space. :attr:`soft_shadow_offset` is an :class:`~kivy.properties.BoundedNumericProperty` and defaults to `4`. """ draw_shadow = ObjectProperty(None) """ This property controls the draw call of the context. This property is automatically set to :attr:`__draw_shadow__` inside the `super().__init__ call.` unless the property is different of None. To set a different drawing instruction function, set this property before the `super(),__init__` call inside the `__init__` definition of the new class. You can use the source for this classes as example of how to draw over with the context: Real time shadows: #. :class:`~RectangularElevationBehavior` #. :class:`~CircularElevationBehavior` #. :class:`~RoundedRectangularElevationBehavior` #. :class:`~ObservableShadow` Fake shadows (d`ont use this property): #. :class:`~FakeRectangularElevationBehavior` #. :class:`~FakeCircularElevationBehavior` :attr:`draw_shadow` is an :class:`~kivy.properties.ObjectProperty` and defaults to `None`. .. note:: If this property is left to `None` the :class:`~CommonElevationBehavior` will set to a function that will raise a `NotImplementedError` inside `super().__init__`. Follow the next example to set a new draw instruction for the class inside `__init__`: .. code-block:: python class RoundedRectangularElevationBehavior(CommonElevationBehavior): ''' Shadow class for the RoundedRectangular shadow behavior. Controls the size and position of the shadow. ''' def __init__(self, **kwargs): self._draw_shadow = WeakMethod(self.__draw_shadow__) super().__init__(**kwargs) def __draw_shadow__(self, origin, end, context=None): context.draw(...) Context is a `Pillow` `ImageDraw` class. For more information check the [Pillow official documentation](https://github.com/python-pillow/Pillow/). """ # All classes that uses a fake shadow shall set this value as `True` # for performance. _fake_elevation = BooleanProperty(False) def __init__(self, **kwargs): if self.draw_shadow is None: self.draw_shadow = WeakMethod(self.__draw_shadow__) self.prev_shadow_group = None im = BytesIO() Image.new("RGBA", (4, 4), color=(0, 0, 0, 0)).save(im, format="png") im.seek(0) # Setting a empty image as texture, improves performance. self._soft_shadow_texture = self.hard_shadow_texture = CoreImage( im, ext="png").texture Clock.schedule_once(self.shadow_preset, -1) self.on_shadow_group(self, self.shadow_group) self.bind( pos=self._update_shadow, size=self._update_shadow, radius=self._update_shadow, ) super().__init__(**kwargs) def on_shadow_group(self, instance, value): """ This function controls the shadow group of the widget. Do not use Directly to change the group. instead, use the shadow_group :attr:`property`. """ groups = CommonElevationBehavior.__shadow_groups if self.prev_shadow_group: group = groups[self.prev_shadow_group] for widget in group[:]: if widget() is self: group.remove(widget) group = self.prev_shadow_group = self.shadow_group if group not in groups: groups[group] = [] r = ref(self, CommonElevationBehavior._clear_shadow_groups) groups[group].append(r) @staticmethod def _clear_shadow_groups(wk): # auto flush the element when the weak reference have been deleted groups = CommonElevationBehavior.__shadow_groups for group in list(groups.values()): if not group: break if wk in group: group.remove(wk) break def force_shadow_pos(self, shadow_pos): """ This property forces the shadow position in every widget inside the widget. The argument :attr:`shadow_pos` is expected as a <class 'list'> or <class 'tuple'>. """ if self.shadow_group is None: return group = CommonElevationBehavior.__shadow_groups[self.shadow_group] for wk in group[:]: widget = wk() if widget is None: group.remove(wk) widget.shadow_pos = shadow_pos del group def update_group_property(self, property_name, value): """ This functions allows to change properties of every widget inside the shadow group. """ if self.shadow_group is None: return group = CommonElevationBehavior.__shadow_groups[self.shadow_group] for wk in group[:]: widget = wk() if widget is None: group.remove(wk) setattr(widget, property_name, value) del group def shadow_preset(self, *args): """ This function is meant to set the default configuration of the elevation. After a new instance is created, the elevation property will be launched and thus this function will update the elevation if the KV lang have not done it already. Works similar to an `__after_init__` call inside a widget. """ from kivymd.uix.card import MDCard if self.elevation is None and not issubclass(self.__class__, MDCard): self.elevation = 10 if self._fake_elevation is False: self._update_shadow(self, self.elevation) self.bind( pos=self._update_shadow, size=self._update_shadow, _elevation=self._update_shadow, ) def on_elevation(self, instance, value): """ Elevation event that sets the current elevation value to `_elevation`. """ if value is not None: self._elevation = value def _set_soft_shadow_a(self, value): value = 0 if value < 0 else (1 if value > 1 else value) self.soft_shadow_cl[-1] = value return True def _set_hard_shadow_a(self, value): value = 0 if value < 0 else (1 if value > 1 else value) self.hard_shadow_cl[-1] = value return True def _get_soft_shadow_a(self): return self.soft_shadow_cl[-1] def _get_hard_shadow_a(self): return self.hard_shadow_cl[-1] _soft_shadow_a = AliasProperty(_get_soft_shadow_a, _set_soft_shadow_a, bind=["soft_shadow_cl"]) _hard_shadow_a = AliasProperty(_get_hard_shadow_a, _set_hard_shadow_a, bind=["hard_shadow_cl"]) def on_disabled(self, instance, value): """ This function hides the shadow when the widget is disabled. It sets the shadow to `0`. """ if self.disabled is True: self._elevation = 0 else: self._elevation = 0 if self.elevation is None else self.elevation self._update_shadow(self, self._elevation) try: super().on_disabled(instance, value) except Exception: pass def _update_elevation(self, instance, value): self._elevation = value self._update_shadow(instance, value) def _update_shadow_pos(self, instance, value): if self._elevation > 0: self.hard_shadow_pos = [ self.x - dp(self.hard_shadow_offset), # + self.shadow_pos[0], self.y - dp(self.hard_shadow_offset), # + self.shadow_pos[1], ] if self.shadow_pos == [0, 0]: self.soft_shadow_pos = [ self.x + self._shadow_pos[0] - self._elevation - dp(self.soft_shadow_offset), self.y + self._shadow_pos[1] - self._elevation - dp(self.soft_shadow_offset), ] else: self.soft_shadow_pos = [ self.x + self.shadow_pos[0] - self._elevation - dp(self.soft_shadow_offset), self.y + self.shadow_pos[1] - self._elevation - dp(self.soft_shadow_offset), ] self._shadow_origin = [ self.soft_shadow_pos[0] + self.soft_shadow_size[0] / 2, self.soft_shadow_pos[1] + self.soft_shadow_size[1] / 2, ] def on__shadow_pos(self, ins, val): """ Updates the shadow with the computed value. Call this function every time you need to force a shadow update. """ self._update_shadow_pos(ins, val) def on_shadow_pos(self, ins, val): """ Updates the shadow with the fixed value. Call this function every time you need to force a shadow update. """ self._update_shadow_pos(ins, val) def _update_shadow(self, instance, value): self._update_shadow_pos(instance, value) if self._elevation > 0 and self._fake_elevation is False: # dynamic elevation position for the shadow if self.shadow_pos == [0, 0]: self._shadow_pos = [0, -self._elevation * 0.4] # HARD Shadow offset = int(dp(self.hard_shadow_offset)) size = [ int(self.size[0] + (offset * 2)), int(self.size[1] + (offset * 2)), ] im = BytesIO() # context img = Image.new("RGBA", tuple(size), color=(0, 0, 0, 0)) # draw context shadow = ImageDraw.Draw(img) self.draw_shadow()([offset, offset], [ int(size[0] - 1 - offset), int(size[1] - 1 - offset), ], context=shadow # context=ref(shadow) ) img = img.filter( ImageFilter.GaussianBlur( radius=int(dp(1 + self.hard_shadow_offset / 3)))) img.save(im, format="png") im.seek(0) self.hard_shadow_size = size self.hard_shadow_texture = CoreImage(im, ext="png").texture # soft shadow if self.soft_shadow_cl[-1] > 0: offset = dp(self.soft_shadow_offset) size = [ int(self.size[0] + dp(self._elevation * 2) + (offset * 2)), int(self.size[1] + dp(self._elevation * 2) + (offset * 2)), # ((self._elevation)*2) + x + (offset*2)) for x in self.size ] im = BytesIO() img = Image.new("RGBA", tuple(size), color=((0, ) * 4)) shadow = ImageDraw.Draw(img) _offset = int(dp(self._elevation + offset)) self.draw_shadow()([ _offset, _offset, ], [int(size[0] - _offset - 1), int(size[1] - _offset - 1)], context=shadow # context=ref(shadow) ) img = img.filter( ImageFilter.GaussianBlur(radius=self._elevation // 2)) shadow = ImageDraw.Draw(img) img.save(im, format="png") im.seek(0) self.soft_shadow_size = size self._soft_shadow_texture = CoreImage(im, ext="png").texture else: im = BytesIO() Image.new("RGBA", (4, 4), color=(0, 0, 0, 0)).save(im, format="png") im.seek(0) self._soft_shadow_texture = self.hard_shadow_texture = CoreImage( im, ext="png").texture return def _get_center(self): center = [self.pos[0] + self.width / 2, self.pos[1] + self.height / 2] return center def __draw_shadow__(self, origin, end, context=None): Logger.warning( f"KivyMD: " f"If you see this error, this means that either youre using " f"`CommonElevationBehavio`r directly or your 'shader' dont have a " f"`_draw_shadow` instruction, remember to overwrite this function" f"to draw over the image context. Тhe figure you would like. " f"Or your class {self.__class__.__name__} is not inherited from " f"any of the classes {__all__}")
def test_reference(self): from kivy.properties import NumericProperty, ReferenceListProperty x = NumericProperty(0) x.link(wid, 'x') x.link_deps(wid, 'x') y = NumericProperty(0) y.link(wid, 'y') y.link_deps(wid, 'y') pos = ReferenceListProperty(x, y) pos.link(wid, 'pos') pos.link_deps(wid, 'pos') self.assertEqual(x.get(wid), 0) self.assertEqual(y.get(wid), 0) self.assertEqual(pos.get(wid), [0, 0]) x.set(wid, 50) self.assertEqual(pos.get(wid), [50, 0]) y.set(wid, 50) self.assertEqual(pos.get(wid), [50, 50]) pos.set(wid, [0, 0]) self.assertEqual(pos.get(wid), [0, 0]) self.assertEqual(x.get(wid), 0) self.assertEqual(y.get(wid), 0) # test observer global observe_called observe_called = 0 def observe(obj, value): global observe_called observe_called = 1 pos.bind(wid, observe) self.assertEqual(observe_called, 0) x.set(wid, 99) self.assertEqual(observe_called, 1)
class Slider(Widget): """Class for creating a Slider widget. Check module documentation for more details. """ value = NumericProperty(0.) '''Current value used for the slider. :attr:`value` is a :class:`~kivy.properties.NumericProperty` and defaults to 0.''' min = NumericProperty(0.) '''Minimum value allowed for :attr:`value`. :attr:`min` is a :class:`~kivy.properties.NumericProperty` and defaults to 0.''' max = NumericProperty(100.) '''Maximum value allowed for :attr:`value`. :attr:`max` is a :class:`~kivy.properties.NumericProperty` and defaults to 100.''' padding = NumericProperty('16sp') '''Padding of the slider. The padding is used for graphical representation and interaction. It prevents the cursor from going out of the bounds of the slider bounding box. By default, padding is 16sp. The range of the slider is reduced from padding \\*2 on the screen. It allows drawing the default cursor of 32sp width without having the cursor go out of the widget. :attr:`padding` is a :class:`~kivy.properties.NumericProperty` and defaults to 16sp.''' orientation = OptionProperty('horizontal', options=('vertical', 'horizontal')) '''Orientation of the slider. :attr:`orientation` is an :class:`~kivy.properties.OptionProperty` and defaults to 'horizontal'. Can take a value of 'vertical' or 'horizontal'. ''' range = ReferenceListProperty(min, max) '''Range of the slider in the format (minimum value, maximum value):: >>> slider = Slider(min=10, max=80) >>> slider.range [10, 80] >>> slider.range = (20, 100) >>> slider.min 20 >>> slider.max 100 :attr:`range` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`min`, :attr:`max`) properties. ''' step = BoundedNumericProperty(0, min=0) '''Step size of the slider. .. versionadded:: 1.4.0 Determines the size of each interval or step the slider takes between min and max. If the value range can't be evenly divisible by step the last step will be capped by slider.max :attr:`step` is a :class:`~kivy.properties.NumericProperty` and defaults to 1.''' background_horizontal = StringProperty( 'atlas://data/images/defaulttheme/sliderh_background') """Background of the slider used in the horizontal orientation. .. versionadded:: 1.10.0 :attr:`background_horizontal` is a :class:`~kivy.properties.StringProperty` and defaults to `atlas://data/images/defaulttheme/sliderh_background`. """ background_disabled_horizontal = StringProperty( 'atlas://data/images/defaulttheme/sliderh_background_disabled') """Background of the disabled slider used in the horizontal orientation. .. versionadded:: 1.10.0 :attr:`background_disabled_horizontal` is a :class:`~kivy.properties.StringProperty` and defaults to `atlas://data/images/defaulttheme/sliderh_background_disabled`. """ background_vertical = StringProperty( 'atlas://data/images/defaulttheme/sliderv_background') """Background of the slider used in the vertical orientation. .. versionadded:: 1.10.0 :attr:`background_vertical` is a :class:`~kivy.properties.StringProperty` and defaults to `atlas://data/images/defaulttheme/sliderv_background`. """ background_disabled_vertical = StringProperty( 'atlas://data/images/defaulttheme/sliderv_background_disabled') """Background of the disabled slider used in the vertical orientation. .. versionadded:: 1.10.0 :attr:`background_disabled_vertical` is a :class:`~kivy.properties.StringProperty` and defaults to `atlas://data/images/defaulttheme/sliderv_background_disabled`. """ background_width = NumericProperty('36sp') """Slider's background's width (thickness), used in both horizontal and vertical orientations. .. versionadded 1.10.0 :attr:`background_width` is a :class:`~kivy.properties.NumericProperty` and defaults to 36sp. """ cursor_image = StringProperty( 'atlas://data/images/defaulttheme/slider_cursor') """Path of the image used to draw the slider cursor. .. versionadded 1.10.0 :attr:`cursor_image` is a :class:`~kivy.properties.StringProperty` and defaults to `atlas://data/images/defaulttheme/slider_cursor`. """ cursor_disabled_image = StringProperty( 'atlas://data/images/defaulttheme/slider_cursor_disabled') """Path of the image used to draw the disabled slider cursor. .. versionadded 1.10.0 :attr:`cursor_image` is a :class:`~kivy.properties.StringProperty` and defaults to `atlas://data/images/defaulttheme/slider_cursor_disabled`. """ cursor_width = NumericProperty('32sp') """Width of the cursor image. .. versionadded 1.10.0 :attr:`cursor_width` is a :class:`~kivy.properties.NumericProperty` and defaults to 32sp. """ cursor_height = NumericProperty('32sp') """Height of the cursor image. .. versionadded 1.10.0 :attr:`cursor_height` is a :class:`~kivy.properties.NumericProperty` and defaults to 32sp. """ cursor_size = ReferenceListProperty(cursor_width, cursor_height) """Size of the cursor image. .. versionadded 1.10.0 :attr:`cursor_size` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`cursor_width`, :attr:`cursor_height`) properties. """ border_horizontal = ListProperty([0, 18, 0, 18]) """Border used to draw the slider background in horizontal orientation. .. versionadded 1.10.0 :attr:`border_horizontal` is a :class:`~kivy.properties.ListProperty` and defaults to [0, 18, 0, 18]. """ border_vertical = ListProperty([18, 0, 18, 0]) """Border used to draw the slider background in vertical orientation. .. versionadded 1.10.0 :attr:`border_horizontal` is a :class:`~kivy.properties.ListProperty` and defaults to [18, 0, 18, 0]. """ value_track = BooleanProperty(False) """Decides if slider should draw the line indicating the space between :attr:`min` and :attr:`value` properties values. .. versionadded 1.10.0 :attr:`value_track` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. """ value_track_color = ListProperty([1, 1, 1, 1]) """Color of the :attr:`value_line` in rgba format. .. versionadded 1.10.0 :attr:`value_track_color` is a :class:`~kivy.properties.ListProperty` and defaults to [1, 1, 1, 1]. """ value_track_width = NumericProperty('3dp') """Width of the track line. .. versionadded 1.10.0 :attr:`value_track_width` is a :class:`~kivy.properties.NumericProperty` and defaults to 3dp. """ sensitivity = OptionProperty('all', options=('all', 'handle')) """Whether the touch collides with the whole body of the widget or with the slider handle part only. .. versionadded:: 1.10.1 :attr:`sensitivity` is a :class:`~kivy.properties.OptionProperty` and defaults to 'all'. Can take a value of 'all' or 'handle'. """ # The following two methods constrain the slider's value # to range(min,max). Otherwise it may happen that self.value < self.min # at init. def on_min(self, *largs): self.value = min(self.max, max(self.min, self.value)) def on_max(self, *largs): self.value = min(self.max, max(self.min, self.value)) def get_norm_value(self): vmin = self.min d = self.max - vmin if d == 0: return 0 return (self.value - vmin) / float(d) def set_norm_value(self, value): vmin = self.min vmax = self.max step = self.step val = min(value * (vmax - vmin) + vmin, vmax) if step == 0: self.value = val else: self.value = min(round((val - vmin) / step) * step + vmin, vmax) value_normalized = AliasProperty(get_norm_value, set_norm_value, bind=('value', 'min', 'max'), cache=True) '''Normalized value inside the :attr:`range` (min/max) to 0-1 range:: >>> slider = Slider(value=50, min=0, max=100) >>> slider.value 50 >>> slider.value_normalized 0.5 >>> slider.value = 0 >>> slider.value_normalized 0 >>> slider.value = 100 >>> slider.value_normalized 1 You can also use it for setting the real value without knowing the minimum and maximum:: >>> slider = Slider(min=0, max=200) >>> slider.value_normalized = .5 >>> slider.value 100 >>> slider.value_normalized = 1. >>> slider.value 200 :attr:`value_normalized` is an :class:`~kivy.properties.AliasProperty`. ''' def get_value_pos(self): padding = self.padding x = self.x y = self.y nval = self.value_normalized if self.orientation == 'horizontal': return (x + padding + nval * (self.width - 2 * padding), y) else: return (x, y + padding + nval * (self.height - 2 * padding)) def set_value_pos(self, pos): padding = self.padding x = min(self.right - padding, max(pos[0], self.x + padding)) y = min(self.top - padding, max(pos[1], self.y + padding)) if self.orientation == 'horizontal': if self.width == 0: self.value_normalized = 0 else: self.value_normalized = ( x - self.x - padding) / float(self.width - 2 * padding) else: if self.height == 0: self.value_normalized = 0 else: self.value_normalized = ( y - self.y - padding) / float(self.height - 2 * padding) value_pos = AliasProperty(get_value_pos, set_value_pos, bind=('pos', 'size', 'min', 'max', 'padding', 'value_normalized', 'orientation'), cache=True) '''Position of the internal cursor, based on the normalized value. :attr:`value_pos` is an :class:`~kivy.properties.AliasProperty`. ''' def on_touch_down(self, touch): if self.disabled or not self.collide_point(*touch.pos): return if touch.is_mouse_scrolling: if 'down' in touch.button or 'left' in touch.button: if self.step: self.value = min(self.max, self.value + self.step) else: self.value = min(self.max, self.value + (self.max - self.min) / 20) if 'up' in touch.button or 'right' in touch.button: if self.step: self.value = max(self.min, self.value - self.step) else: self.value = max(self.min, self.value - (self.max - self.min) / 20) elif self.sensitivity == 'handle': if self.children[0].collide_point(*touch.pos): touch.grab(self) else: touch.grab(self) self.value_pos = touch.pos return True def on_touch_move(self, touch): if touch.grab_current == self: self.value_pos = touch.pos return True def on_touch_up(self, touch): if touch.grab_current == self: self.value_pos = touch.pos return True
class StackLayout(Layout): '''Stack layout class. See module documentation for more information. ''' spacing = VariableListProperty([0, 0], length=2) '''Spacing between children: [spacing_horizontal, spacing_vertical]. spacing also accepts a single argument form [spacing]. :attr:`spacing` is a :class:`~kivy.properties.VariableListProperty` and defaults to [0, 0]. ''' padding = VariableListProperty([0, 0, 0, 0]) '''Padding between the layout box and it's children: [padding_left, padding_top, padding_right, padding_bottom]. padding also accepts a two argument form [padding_horizontal, padding_vertical] and a single argument form [padding]. .. versionchanged:: 1.7.0 Replaced the NumericProperty with a VariableListProperty. :attr:`padding` is a :class:`~kivy.properties.VariableListProperty` and defaults to [0, 0, 0, 0]. ''' orientation = OptionProperty('lr-tb', options=('lr-tb', 'tb-lr', 'rl-tb', 'tb-rl', 'lr-bt', 'bt-lr', 'rl-bt', 'bt-rl')) '''Orientation of the layout. :attr:`orientation` is an :class:`~kivy.properties.OptionProperty` and defaults to 'lr-tb'. Valid orientations are 'lr-tb', 'tb-lr', 'rl-tb', 'tb-rl', 'lr-bt', 'bt-lr', 'rl-bt' and 'bt-rl'. .. versionchanged:: 1.5.0 :attr:`orientation` now correctly handles all valid combinations of 'lr','rl','tb','bt'. Before this version only 'lr-tb' and 'tb-lr' were supported, and 'tb-lr' was misnamed and placed widgets from bottom to top and from right to left (reversed compared to what was expected). .. note:: 'lr' means Left to Right. 'rl' means Right to Left. 'tb' means Top to Bottom. 'bt' means Bottom to Top. ''' minimum_width = NumericProperty(0) '''Minimum width needed to contain all children. It is automatically set by the layout. .. versionadded:: 1.0.8 :attr:`minimum_width` is a :class:`kivy.properties.NumericProperty` and defaults to 0. ''' minimum_height = NumericProperty(0) '''Minimum height needed to contain all children. It is automatically set by the layout. .. versionadded:: 1.0.8 :attr:`minimum_height` is a :class:`kivy.properties.NumericProperty` and defaults to 0. ''' minimum_size = ReferenceListProperty(minimum_width, minimum_height) '''Minimum size needed to contain all children. It is automatically set by the layout. .. versionadded:: 1.0.8 :attr:`minimum_size` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`minimum_width`, :attr:`minimum_height`) properties. ''' def __init__(self, **kwargs): super(StackLayout, self).__init__(**kwargs) trigger = self._trigger_layout fbind = self.fbind fbind('padding', trigger) fbind('spacing', trigger) fbind('children', trigger) fbind('orientation', trigger) fbind('size', trigger) fbind('pos', trigger) def do_layout(self, *largs): if not self.children: self.minimum_size = (0., 0.) return # optimize layout by preventing looking at the same attribute in a loop selfpos = self.pos selfsize = self.size orientation = self.orientation.split('-') padding_left = self.padding[0] padding_top = self.padding[1] padding_right = self.padding[2] padding_bottom = self.padding[3] padding_x = padding_left + padding_right padding_y = padding_top + padding_bottom spacing_x, spacing_y = self.spacing # Determine which direction and in what order to place the widgets posattr = [0] * 2 posdelta = [0] * 2 posstart = [0] * 2 for i in (0, 1): posattr[i] = 1 * (orientation[i] in ('tb', 'bt')) k = posattr[i] if orientation[i] == 'lr': # left to right posdelta[i] = 1 posstart[i] = selfpos[k] + padding_left elif orientation[i] == 'bt': # bottom to top posdelta[i] = 1 posstart[i] = selfpos[k] + padding_bottom elif orientation[i] == 'rl': # right to left posdelta[i] = -1 posstart[i] = selfpos[k] + selfsize[k] - padding_right else: # top to bottom posdelta[i] = -1 posstart[i] = selfpos[k] + selfsize[k] - padding_top innerattr, outerattr = posattr ustart, vstart = posstart deltau, deltav = posdelta del posattr, posdelta, posstart u = ustart # inner loop position variable v = vstart # outer loop position variable # space calculation, used for determining when a row or column is full if orientation[0] in ('lr', 'rl'): sv = padding_y # size in v-direction, for minimum_size property su = padding_x # size in h-direction spacing_u = spacing_x spacing_v = spacing_y padding_u = padding_x padding_v = padding_y else: sv = padding_x # size in v-direction, for minimum_size property su = padding_y # size in h-direction spacing_u = spacing_y spacing_v = spacing_x padding_u = padding_y padding_v = padding_x # space calculation, row height or column width, for arranging widgets lv = 0 urev = (deltau < 0) vrev = (deltav < 0) firstchild = self.children[0] sizes = [] lc = [] for c in reversed(self.children): if c.size_hint[outerattr] is not None: c.size[outerattr] = max( 1, _compute_size(c, selfsize[outerattr] - padding_v, outerattr)) # does the widget fit in the row/column? ccount = len(lc) totalsize = availsize = max( 0, selfsize[innerattr] - padding_u - spacing_u * ccount) if not lc: if c.size_hint[innerattr] is not None: childsize = max(1, _compute_size(c, totalsize, innerattr)) else: childsize = max(0, c.size[innerattr]) availsize = selfsize[innerattr] - padding_u - childsize testsizes = [childsize] else: testsizes = [0] * (ccount + 1) for i, child in enumerate(lc): if availsize <= 0: # no space left but we're trying to add another widget. availsize = -1 break if child.size_hint[innerattr] is not None: testsizes[i] = childsize = max( 1, _compute_size(child, totalsize, innerattr)) else: childsize = max(0, child.size[innerattr]) testsizes[i] = childsize availsize -= childsize if c.size_hint[innerattr] is not None: testsizes[-1] = max(1, _compute_size(c, totalsize, innerattr)) else: testsizes[-1] = max(0, c.size[innerattr]) availsize -= testsizes[-1] # Tiny value added in order to avoid issues with float precision # causing unexpected children reordering when parent resizes. # e.g. if size is 101 and children size_hint_x is 1./5 # 5 children would not fit in one line because 101*(1./5) > 101/5 if (availsize + 1e-10) >= 0 or not lc: # even if there's no space, we always add one widget to a row lc.append(c) sizes = testsizes lv = max(lv, c.size[outerattr]) continue # apply the sizes for i, child in enumerate(lc): if child.size_hint[innerattr] is not None: child.size[innerattr] = sizes[i] # push the line sv += lv + spacing_v for c2 in lc: if urev: u -= c2.size[innerattr] c2.pos[innerattr] = u pos_outer = v if vrev: # v position is actually the top/right side of the widget # when going from high to low coordinate values, # we need to subtract the height/width from the position. pos_outer -= c2.size[outerattr] c2.pos[outerattr] = pos_outer if urev: u -= spacing_u else: u += c2.size[innerattr] + spacing_u v += deltav * lv v += deltav * spacing_v lc = [c] lv = c.size[outerattr] if c.size_hint[innerattr] is not None: sizes = [ max( 1, _compute_size(c, selfsize[innerattr] - padding_u, innerattr)) ] else: sizes = [max(0, c.size[innerattr])] u = ustart if lc: # apply the sizes for i, child in enumerate(lc): if child.size_hint[innerattr] is not None: child.size[innerattr] = sizes[i] # push the last (incomplete) line sv += lv + spacing_v for c2 in lc: if urev: u -= c2.size[innerattr] c2.pos[innerattr] = u pos_outer = v if vrev: pos_outer -= c2.size[outerattr] c2.pos[outerattr] = pos_outer if urev: u -= spacing_u else: u += c2.size[innerattr] + spacing_u self.minimum_size[outerattr] = sv
class CircularLayout(Layout): """ Circular layout class. See module documentation for more information. """ padding = VariableListProperty([0, 0, 0, 0]) """Padding between the layout box and it's children: [padding_left, padding_top, padding_right, padding_bottom]. padding also accepts a two argument form [padding_horizontal, padding_vertical] and a one argument form [padding]. .. version changed:: 1.7.0 Replaced NumericProperty with VariableListProperty. :attr:`padding` is a :class:`~kivy.properties.VariableListProperty` and defaults to [0, 0, 0, 0]. """ start_angle = NumericProperty(0) """Angle (in degrees) at which the first widget will be placed. Start counting angles from the X axis, going counterclockwise. :attr:`start_angle` is a :class:`~kivy.properties.NumericProperty` and defaults to 0 (start from the right). """ circle_quota = BoundedNumericProperty(360, min=0, max=360) """Size (in degrees) of the part of the circumference that will actually be used to place widgets. :attr:`circle_quota` is a :class:`~kivy.properties.BoundedNumericProperty` and defaults to 360 (all the circumference). """ direction = OptionProperty("ccw", options=("cw", "ccw")) """Direction of widgets in the circle. :attr:`direction` is an :class:`~kivy.properties.OptionProperty` and defaults to 'ccw'. Can be 'ccw' (counterclockwise) or 'cw' (clockwise). """ outer_radius_hint = NumericProperty(1) """Sets the size of the outer circle. A number greater than 1 will make the widgets larger than the actual widget, a number smaller than 1 will leave a gap. :attr:`outer_radius_hint` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. """ inner_radius_hint = NumericProperty(0.6) """Sets the size of the inner circle. A number greater than :attr:`outer_radius_hint` will cause glitches. The closest it is to :attr:`outer_radius_hint`, the smallest will be the widget in the layout. :attr:`outer_radius_hint` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. """ radius_hint = ReferenceListProperty(inner_radius_hint, outer_radius_hint) """Combined :attr:`outer_radius_hint` and :attr:`inner_radius_hint` in a list for convenience. See their documentation for more details. :attr:`radius_hint` is a :class:`~kivy.properties.ReferenceListProperty`. """ def _get_delta_radii(self): radius = (min( self.width - self.padding[0] - self.padding[2], self.height - self.padding[1] - self.padding[3], ) / 2.0) outer_r = radius * self.outer_radius_hint inner_r = radius * self.inner_radius_hint return outer_r - inner_r delta_radii = AliasProperty(_get_delta_radii, None, bind=("radius_hint", "padding", "size")) def __init__(self, **kwargs): super().__init__(**kwargs) self.bind( start_angle=self._trigger_layout, parent=self._trigger_layout, # padding=self._trigger_layout, children=self._trigger_layout, size=self._trigger_layout, radius_hint=self._trigger_layout, pos=self._trigger_layout, ) def do_layout(self, *largs): # optimize layout by preventing looking at the same attribute in a loop len_children = len(self.children) if len_children == 0: return selfcx = self.center_x selfcy = self.center_y direction = self.direction cquota = radians(self.circle_quota) start_angle_r = radians(self.start_angle) padding_left = self.padding[0] padding_top = self.padding[1] padding_right = self.padding[2] padding_bottom = self.padding[3] padding_x = padding_left + padding_right padding_y = padding_top + padding_bottom radius = min(self.width - padding_x, self.height - padding_y) / 2.0 outer_r = radius * self.outer_radius_hint inner_r = radius * self.inner_radius_hint middle_r = radius * sum(self.radius_hint) / 2.0 delta_r = outer_r - inner_r stretch_weight_angle = 0.0 for w in self.children: sha = w.size_hint_x if sha is None: raise ValueError( "size_hint_x cannot be None in a CircularLayout") else: stretch_weight_angle += sha sign = +1.0 angle_offset = start_angle_r if direction == "cw": angle_offset = 2 * pi - start_angle_r sign = -1.0 for c in reversed(self.children): sha = c.size_hint_x shs = c.size_hint_y angle_quota = cquota / stretch_weight_angle * sha angle = angle_offset + (sign * angle_quota / 2) angle_offset += sign * angle_quota # kived: looking it up, yes. x = cos(angle) * radius + centerx; # y = sin(angle) * radius + centery ccx = cos(angle) * middle_r + selfcx + padding_left - padding_right ccy = sin(angle) * middle_r + selfcy + padding_bottom - padding_top c.center_x = ccx c.center_y = ccy if shs: s = delta_r * shs c.width = s c.height = s
class CircularTimePicker(BoxLayout, ThemableBehavior): """Widget that makes use of :class:`CircularHourPicker` and :class:`CircularMinutePicker` to create a user-friendly, animated time picker like the one seen on Android. See module documentation for more details. """ primary_dark = ListProperty([1, 1, 1]) hours = NumericProperty(0) """The hours, in military format (0-23). :attr:`hours` is a :class:`~kivy.properties.NumericProperty` and defaults to 0 (12am). """ minutes = NumericProperty(0) """The minutes. :attr:`minutes` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. """ time_list = ReferenceListProperty(hours, minutes) """Packs :attr:`hours` and :attr:`minutes` in a list for convenience. :attr:`time_list` is a :class:`~kivy.properties.ReferenceListProperty`. """ # military = BooleanProperty(False) time_format = StringProperty( "[color={hours_color}][ref=hours]{hours}[/ref][/color][color={primary_dark}][ref=colon]:[/ref][/color]\ [color={minutes_color}][ref=minutes]{minutes:02d}[/ref][/color]") """String that will be formatted with the time and shown in the time label. Can be anything supported by :meth:`str.format`. Make sure you don't remove the refs. See the default for the arguments passed to format. :attr:`time_format` is a :class:`~kivy.properties.StringProperty` and defaults to "[color={hours_color}][ref=hours]{hours}[/ref][/color]:[color={minutes_color}][ref=minutes]\ {minutes:02d}[/ref][/color]". """ ampm_format = StringProperty("[color={am_color}][ref=am]AM[/ref][/color]\n" "[color={pm_color}][ref=pm]PM[/ref][/color]") """String that will be formatted and shown in the AM/PM label. Can be anything supported by :meth:`str.format`. Make sure you don't remove the refs. See the default for the arguments passed to format. :attr:`ampm_format` is a :class:`~kivy.properties.StringProperty` and defaults to "[color={am_color}][ref=am]AM[/ref][/color]\n [color={pm_color}][ref=pm]PM[/ref][/color]". """ picker = OptionProperty("hours", options=("minutes", "hours")) """Currently shown time picker. Can be one of "minutes", "hours". :attr:`picker` is a :class:`~kivy.properties.OptionProperty` and defaults to "hours". """ # selector_color = ListProperty([.337, .439, .490]) selector_color = ListProperty([0, 0, 0]) """Color of the number selector and of the highlighted text. RGB. :attr:`selector_color` is a :class:`~kivy.properties.ListProperty` and defaults to [.337, .439, .490] (material green). """ color = ListProperty([1, 1, 1]) """Color of the number labels and of the center dot. RGB. :attr:`color` is a :class:`~kivy.properties.ListProperty` and defaults to [1, 1, 1] (white). """ selector_alpha = BoundedNumericProperty(0.3, min=0, max=1) """Alpha value for the transparent parts of the selector. :attr:`selector_alpha` is a :class:`~kivy.properties.BoundedNumericProperty` and defaults to 0.3 (min=0, max=1). """ _am = BooleanProperty(True) _h_picker = ObjectProperty(None) _m_picker = ObjectProperty(None) _bound = DictProperty({}) def _get_time(self): try: return datetime.time(*self.time_list) except ValueError: self.time_list = [self.hours, 0] return datetime.time(*self.time_list) def set_time(self, dt): if dt.hour >= 12: dt.strftime("%I:%M") self._am = False self.time_list = [dt.hour, dt.minute] time = AliasProperty(_get_time, set_time, bind=("time_list", )) """Selected time as a datetime.time object. :attr:`time` is an :class:`~kivy.properties.AliasProperty`. """ def _get_picker(self): if self.picker == "hours": return self._h_picker return self._m_picker _picker = AliasProperty(_get_picker, None) def _get_time_text(self): hc = (rgb_to_hex(0, 0, 0) if self.picker == "hours" else rgb_to_hex( *self.primary_dark)) mc = (rgb_to_hex(0, 0, 0) if self.picker == "minutes" else rgb_to_hex( *self.primary_dark)) h = (self.hours == 0 and 12 or self.hours <= 12 and self.hours or self.hours - 12) m = self.minutes primary_dark = rgb_to_hex(*self.primary_dark) return self.time_format.format( hours_color=hc, minutes_color=mc, hours=h, minutes=m, primary_dark=primary_dark, ) time_text = AliasProperty(_get_time_text, None, bind=("hours", "minutes", "time_format", "picker")) def _get_ampm_text(self, *args): amc = (rgb_to_hex(0, 0, 0) if self._am else rgb_to_hex( *self.primary_dark)) pmc = (rgb_to_hex(0, 0, 0) if not self._am else rgb_to_hex( *self.primary_dark)) return self.ampm_format.format(am_color=amc, pm_color=pmc) ampm_text = AliasProperty(_get_ampm_text, None, bind=("hours", "ampm_format", "_am")) def __init__(self, **kw): super().__init__(**kw) self.selector_color = ( self.theme_cls.primary_color[0], self.theme_cls.primary_color[1], self.theme_cls.primary_color[2], ) self.color = self.theme_cls.text_color self.primary_dark = ( self.theme_cls.primary_dark[0] / 2, self.theme_cls.primary_dark[1] / 2, self.theme_cls.primary_dark[2] / 2, ) self.on_ampm() if self.hours >= 12: self._am = False self.bind( time_list=self.on_time_list, picker=self._switch_picker, _am=self.on_ampm, primary_dark=self._get_ampm_text, ) self._h_picker = CircularHourPicker() self.h_picker_touch = False self._m_picker = CircularMinutePicker() self.animating = False Clock.schedule_once(self.on_selected) Clock.schedule_once(self.on_time_list) Clock.schedule_once(self._init_later) Clock.schedule_once(lambda *a: self._switch_picker(noanim=True)) def _init_later(self, *args): self.ids.timelabel.bind(on_ref_press=self.on_ref_press) self.ids.ampmlabel.bind(on_ref_press=self.on_ref_press) def on_ref_press(self, ign, ref): if not self.animating: if ref == "hours": self.picker = "hours" elif ref == "minutes": self.picker = "minutes" if ref == "am": self._am = True elif ref == "pm": self._am = False def on_selected(self, *a): if not self._picker: return if self.picker == "hours": hours = (self._picker.selected if self._am else self._picker.selected + 12) if hours == 24 and not self._am: hours = 12 elif hours == 12 and self._am: hours = 0 self.hours = hours elif self.picker == "minutes": self.minutes = self._picker.selected def on_time_list(self, *a): if not self._picker: return self._h_picker.selected = (self.hours == 0 and 12 or self._am and self.hours or self.hours - 12) self._m_picker.selected = self.minutes self.on_selected() def on_ampm(self, *a): if self._am: self.hours = self.hours if self.hours < 12 else self.hours - 12 else: self.hours = self.hours if self.hours >= 12 else self.hours + 12 def is_animating(self, *args): self.animating = True def is_not_animating(self, *args): self.animating = False def on_touch_down(self, touch): if not self._h_picker.collide_point(*touch.pos): self.h_picker_touch = False else: self.h_picker_touch = True super().on_touch_down(touch) def on_touch_up(self, touch): try: if not self.h_picker_touch: return if not self.animating: if touch.grab_current is not self: if self.picker == "hours": self.picker = "minutes" except AttributeError: pass super().on_touch_up(touch) def _switch_picker(self, *a, **kw): noanim = "noanim" in kw if noanim: noanim = kw["noanim"] try: container = self.ids.picker_container except (AttributeError, NameError): Clock.schedule_once(lambda *a: self._switch_picker(noanim=noanim)) if self.picker == "hours": picker = self._h_picker prevpicker = self._m_picker elif self.picker == "minutes": picker = self._m_picker prevpicker = self._h_picker if len(self._bound) > 0: prevpicker.unbind(selected=self.on_selected) self.unbind(**self._bound) picker.bind(selected=self.on_selected) self._bound = { "selector_color": picker.setter("selector_color"), "color": picker.setter("color"), "selector_alpha": picker.setter("selector_alpha"), } self.bind(**self._bound) if len(container._bound) > 0: container.unbind(**container._bound) container._bound = { "size": picker.setter("size"), "pos": picker.setter("pos"), } container.bind(**container._bound) picker.pos = container.pos picker.size = container.size picker.selector_color = self.selector_color picker.color = self.color picker.selector_alpha = self.selector_alpha if noanim: if prevpicker in container.children: container.remove_widget(prevpicker) if picker.parent: picker.parent.remove_widget(picker) container.add_widget(picker) else: self.is_animating() if prevpicker in container.children: anim = Animation(scale=1.5, d=0.5, t="in_back") & Animation( opacity=0, d=0.5, t="in_cubic") anim.start(prevpicker) Clock.schedule_once( lambda *y: container.remove_widget(prevpicker), 0.5) # .31) picker.scale = 1.5 picker.opacity = 0 if picker.parent: picker.parent.remove_widget(picker) container.add_widget(picker) anim = Animation(scale=1, d=0.5, t="out_back") & Animation( opacity=1, d=0.5, t="out_cubic") anim.bind(on_complete=self.is_not_animating) Clock.schedule_once(lambda *y: anim.start(picker), 0.3)
class Widget(WidgetBase): '''Widget class. See module documentation for more information. :Events: `on_touch_down`: Fired when a new touch event occurs `on_touch_move`: Fired when an existing touch moves `on_touch_up`: Fired when an existing touch disappears ..warning:: Adding a `__del__` method to a class derived from Widget with python prior to 3.4, will disable automatic garbage collection for instances of that class. That is because Widget creates ref cycles, thereby removing it from garbage collection as described `here <https://docs.python.org/2/library/gc.html#gc.garbage>`_. .. versionchanged:: 1.0.9 Everything related to event properties has been moved to the :class:`~kivy.event.EventDispatcher`. Event properties can now be used when contructing a simple class without subclassing :class:`Widget`. .. versionchanged:: 1.5.0 The constructor now accepts on_* arguments to automatically bind callbacks to properties or events, as in the Kv language. ''' __metaclass__ = WidgetMetaclass __events__ = ('on_touch_down', 'on_touch_move', 'on_touch_up') def __init__(self, **kwargs): # Before doing anything, ensure the windows exist. EventLoop.ensure_window() # assign the default context of the widget creation if not hasattr(self, '_context'): self._context = get_current_context() super(Widget, self).__init__(**kwargs) # Create the default canvas if not exist if self.canvas is None: self.canvas = Canvas(opacity=self.opacity) # Apply all the styles if '__no_builder' not in kwargs: #current_root = Builder.idmap.get('root') #Builder.idmap['root'] = self Builder.apply(self) #if current_root is not None: # Builder.idmap['root'] = current_root #else: # Builder.idmap.pop('root') # Bind all the events for argument in kwargs: if argument[:3] == 'on_': self.bind(**{argument: kwargs[argument]}) @property def proxy_ref(self): '''Return a proxy reference to the widget, i.e. without creating a reference to the widget. See `weakref.proxy <http://docs.python.org/2/library/weakref.html?highlight\ =proxy#weakref.proxy>`_ for more information. .. versionadded:: 1.7.2 ''' if hasattr(self, '_proxy_ref'): return self._proxy_ref f = partial(_widget_destructor, self.uid) self._proxy_ref = _proxy_ref = proxy(self, f) # only f should be enough here, but it appears that is a very # specific case, the proxy destructor is not called if both f and # _proxy_ref are not together in a tuple _widget_destructors[self.uid] = (f, _proxy_ref) return _proxy_ref def __eq__(self, other): if not isinstance(other, Widget): return False return self.proxy_ref is other.proxy_ref def __hash__(self): return id(self) @property def __self__(self): return self # # Collision # def collide_point(self, x, y): '''Check if a point (x, y) is inside the widget's axis aligned bounding box. :Parameters: `x`: numeric X position of the point (in window coordinates) `y`: numeric Y position of the point (in window coordinates) :Returns: bool, True if the point is inside the bounding box. >>> Widget(pos=(10, 10), size=(50, 50)).collide_point(40, 40) True ''' return self.x <= x <= self.right and self.y <= y <= self.top def collide_widget(self, wid): '''Check if the other widget collides with this widget. Performs an axis-aligned bounding box intersection test by default. :Parameters: `wid`: :class:`Widget` class Widget to collide with. :Returns: bool, True if the other widget collides with this widget. >>> wid = Widget(size=(50, 50)) >>> wid2 = Widget(size=(50, 50), pos=(25, 25)) >>> wid.collide_widget(wid2) True >>> wid2.pos = (55, 55) >>> wid.collide_widget(wid2) False ''' if self.right < wid.x: return False if self.x > wid.right: return False if self.top < wid.y: return False if self.y > wid.top: return False return True # # Default event handlers # def on_touch_down(self, touch): '''Receive a touch down event. :Parameters: `touch`: :class:`~kivy.input.motionevent.MotionEvent` class Touch received. The touch is in parent coordinates. See :mod:`~kivy.uix.relativelayout` for a discussion on coordinate systems. :Returns: bool. If True, the dispatching of the touch event will stop. ''' if self.disabled and self.collide_point(*touch.pos): return True for child in self.children[:]: if child.dispatch('on_touch_down', touch): return True def on_touch_move(self, touch): '''Receive a touch move event. The touch is in parent coordinates. See :meth:`on_touch_down` for more information. ''' if self.disabled: return for child in self.children[:]: if child.dispatch('on_touch_move', touch): return True def on_touch_up(self, touch): '''Receive a touch up event. The touch is in parent coordinates. See :meth:`on_touch_down` for more information. ''' if self.disabled: return for child in self.children[:]: if child.dispatch('on_touch_up', touch): return True def on_disabled(self, instance, value): for child in self.children: child.disabled = value # # Tree management # def add_widget(self, widget, index=0): '''Add a new widget as a child of this widget. :Parameters: `widget`: :class:`Widget` Widget to add to our list of children. `index`: int, defaults to 0 Index to insert the widget in the list .. versionadded:: 1.0.5 >>> from kivy.uix.button import Button >>> from kivy.uix.slider import Slider >>> root = Widget() >>> root.add_widget(Button()) >>> slider = Slider() >>> root.add_widget(slider) ''' if not isinstance(widget, Widget): raise WidgetException( 'add_widget() can be used only with Widget classes.') widget = widget.__self__ if widget is self: raise WidgetException('You cannot add yourself in a Widget') parent = widget.parent # check if widget is already a child of another widget if parent: raise WidgetException('Cannot add %r, it already has a parent %r' % (widget, parent)) widget.parent = parent = self # child will be disabled if added to a disabled parent if parent.disabled: widget.disabled = True if index == 0 or len(self.children) == 0: self.children.insert(0, widget) self.canvas.add(widget.canvas) else: canvas = self.canvas children = self.children if index >= len(children): index = len(children) next_index = 0 else: next_child = children[index] next_index = canvas.indexof(next_child.canvas) if next_index == -1: next_index = canvas.length() else: next_index += 1 children.insert(index, widget) # we never want to insert widget _before_ canvas.before. if next_index == 0 and canvas.has_before: next_index = 1 canvas.insert(next_index, widget.canvas) def remove_widget(self, widget): '''Remove a widget from the children of this widget. :Parameters: `widget`: :class:`Widget` Widget to remove from our children list. >>> from kivy.uix.button import Button >>> root = Widget() >>> button = Button() >>> root.add_widget(button) >>> root.remove_widget(button) ''' if widget not in self.children: return self.children.remove(widget) self.canvas.remove(widget.canvas) widget.parent = None def clear_widgets(self, children=None): '''Remove all widgets added to this widget. .. versionchanged:: 1.8.0 `children` argument can be used to select the children we want to remove. It should be a list of children (or filtered list) of the current widget. ''' if not children: children = self.children remove_widget = self.remove_widget for child in children[:]: remove_widget(child) def export_to_png(self, filename, *args): '''Saves an image of the widget and its children in png format at the specified filename. Works by removing the widget canvas from its parent, rendering to an :class:`~kivy.graphics.fbo.Fbo`, and calling :meth:`~kivy.graphics.texture.Texture.save`. .. note:: The image includes only this widget and its children. If you want to include widgets elsewhere in the tree, you must call :meth:`~Widget.export_to_png` from their common parent, or use :meth:`~kivy.core.window.Window.screenshot` to capture the whole window. .. note:: The image will be saved in png format, you should include the extension in your filename. .. versionadded:: 1.8.1 ''' if self.parent is not None: canvas_parent_index = self.parent.canvas.indexof(self.canvas) self.parent.canvas.remove(self.canvas) fbo = Fbo(size=self.size) with fbo: ClearColor(0, 0, 0, 1) ClearBuffers() Translate(-self.x, -self.y, 0) fbo.add(self.canvas) fbo.draw() fbo.texture.save(filename) fbo.remove(self.canvas) if self.parent is not None: self.parent.canvas.insert(canvas_parent_index, self.canvas) return True def get_root_window(self): '''Return the root window. :Returns: Instance of the root window. Can be a :class:`~kivy.core.window.WindowBase` or :class:`Widget`. ''' if self.parent: return self.parent.get_root_window() def get_parent_window(self): '''Return the parent window. :Returns: Instance of the parent window. Can be a :class:`~kivy.core.window.WindowBase` or :class:`Widget`. ''' if self.parent: return self.parent.get_parent_window() def _walk(self, restrict=False, loopback=False, index=None): # we pass index only when we are going on the parent. # so don't yield the parent as well. if index is None: index = len(self.children) yield self for child in reversed(self.children[:index]): for walk_child in child._walk(restrict=True): yield walk_child # if we want to continue with our parent, just do it if not restrict: parent = self.parent try: if parent is None or not isinstance(parent, Widget): raise ValueError index = parent.children.index(self) except ValueError: # self is root, if wanted to loopback from first element then -> if not loopback: return # if we started with root (i.e. index==None), then we have to # start from root again, so we return self again. Otherwise, we # never returned it, so return it now starting with it parent = self index = None for walk_child in parent._walk(loopback=loopback, index=index): yield walk_child def walk(self, restrict=False, loopback=False): ''' Iterator that walks the widget tree starting with this widget and goes forward returning widgets in the order in which layouts display them. :Parameters: `restrict`: If True, it will only iterate through the widget and its children (or children of its children etc.). Defaults to False. `loopback`: If True, when the last widget in the tree is reached, it'll loop back to the uppermost root and start walking until we hit this widget again. Naturally, it can only loop back when `restrict` is False. Defaults to False. :return: A generator that walks the tree, returning widgets in the forward layout order. For example, given a tree with the following structure:: GridLayout: Button BoxLayout: id: box Widget Button Widget walking this tree:: >>> # Call walk on box with loopback True, and restrict False >>> [type(widget) for widget in box.walk(loopback=True)] [<class 'BoxLayout'>, <class 'Widget'>, <class 'Button'>, <class 'Widget'>, <class 'GridLayout'>, <class 'Button'>] >>> # Now with loopback False, and restrict False >>> [type(widget) for widget in box.walk()] [<class 'BoxLayout'>, <class 'Widget'>, <class 'Button'>, <class 'Widget'>] >>> # Now with restrict True >>> [type(widget) for widget in box.walk(restrict=True)] [<class 'BoxLayout'>, <class 'Widget'>, <class 'Button'>] .. versionadded:: 1.8.1 ''' gen = self._walk(restrict, loopback) yield next(gen) for node in gen: if node is self: return yield node def _walk_reverse(self, loopback=False, go_up=False): # process is walk up level, walk down its children tree, then walk up # next level etc. # default just walk down the children tree root = self index = 0 # we need to go up a level before walking tree if go_up: root = self.parent try: if root is None or not isinstance(root, Widget): raise ValueError index = root.children.index(self) + 1 except ValueError: if not loopback: return index = 0 go_up = False root = self # now walk children tree starting with last-most child for child in islice(root.children, index, None): for walk_child in child._walk_reverse(loopback=loopback): yield walk_child # we need to return ourself last, in all cases yield root # if going up, continue walking up the parent tree if go_up: for walk_child in root._walk_reverse(loopback=loopback, go_up=go_up): yield walk_child def walk_reverse(self, loopback=False): ''' Iterator that walks the widget tree backwards starting with the widget before this, and going backwards returning widgets in the reverse order in which layouts display them. This walks in the opposite direction of :meth:`walk`, so a list of the tree generated with :meth:`walk` will be in reverse order compared to the list generated with this, provided `loopback` is True. :Parameters: `loopback`: If True, when the uppermost root in the tree is reached, it'll loop back to the last widget and start walking back until after we hit widget again. Defaults to False :return: A generator that walks the tree, returning widgets in the reverse layout order. For example, given a tree with the following structure:: GridLayout: Button BoxLayout: id: box Widget Button Widget walking this tree:: >>> # Call walk on box with loopback True >>> [type(widget) for widget in box.walk_reverse(loopback=True)] [<class 'Button'>, <class 'GridLayout'>, <class 'Widget'>, <class 'Button'>, <class 'Widget'>, <class 'BoxLayout'>] >>> # Now with loopback False >>> [type(widget) for widget in box.walk_reverse()] [<class 'Button'>, <class 'GridLayout'>] >>> forward = [w for w in box.walk(loopback=True)] >>> backward = [w for w in box.walk_reverse(loopback=True)] >>> forward == backward[::-1] True .. versionadded:: 1.8.1 ''' for node in self._walk_reverse(loopback=loopback, go_up=True): yield node if node is self: return def to_widget(self, x, y, relative=False): '''Convert the given coordinate from window to local widget coordinates. See :mod:`~kivy.uix.relativelayout` for details on the coordinate systems. ''' if self.parent: x, y = self.parent.to_widget(x, y) return self.to_local(x, y, relative=relative) def to_window(self, x, y, initial=True, relative=False): '''Transform local coordinates to window coordinates. See :mod:`~kivy.uix.relativelayout` for details on the coordinate systems. ''' if not initial: x, y = self.to_parent(x, y, relative=relative) if self.parent: return self.parent.to_window(x, y, initial=False, relative=relative) return (x, y) def to_parent(self, x, y, relative=False): '''Transform local coordinates to parent coordinates. See :mod:`~kivy.uix.relativelayout` for details on the coordinate systems. :Parameters: `relative`: bool, defaults to False Change to True if you want to translate relative positions from a widget to its parent coordinates. ''' if relative: return (x + self.x, y + self.y) return (x, y) def to_local(self, x, y, relative=False): '''Transform parent coordinates to local coordinates. See :mod:`~kivy.uix.relativelayout` for details on the coordinate systems. :Parameters: `relative`: bool, defaults to False Change to True if you want to translate coordinates to relative widget coordinates. ''' if relative: return (x - self.x, y - self.y) return (x, y) x = NumericProperty(0) '''X position of the widget. :attr:`x` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' y = NumericProperty(0) '''Y position of the widget. :attr:`y` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' width = NumericProperty(100) '''Width of the widget. :attr:`width` is a :class:`~kivy.properties.NumericProperty` ans defaults to 100. .. warning:: Keep in mind that the `width` property is subject to layout logic and that this has not yet happened at the time of the widget's `__init__` method. ''' height = NumericProperty(100) '''Height of the widget. :attr:`height` is a :class:`~kivy.properties.NumericProperty` and defaults to 100. .. warning:: Keep in mind that the `height` property is subject to layout logic and that this has not yet happened at the time of the widget's `__init__` method. ''' pos = ReferenceListProperty(x, y) '''Position of the widget. :attr:`pos` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`x`, :attr:`y`) properties. ''' size = ReferenceListProperty(width, height) '''Size of the widget. :attr:`size` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`width`, :attr:`height`) properties. ''' def get_right(self): return self.x + self.width def set_right(self, value): self.x = value - self.width right = AliasProperty(get_right, set_right, bind=('x', 'width')) '''Right position of the widget. :attr:`right` is an :class:`~kivy.properties.AliasProperty` of (:attr:`x` + :attr:`width`), ''' def get_top(self): return self.y + self.height def set_top(self, value): self.y = value - self.height top = AliasProperty(get_top, set_top, bind=('y', 'height')) '''Top position of the widget. :attr:`top` is an :class:`~kivy.properties.AliasProperty` of (:attr:`y` + :attr:`height`), ''' def get_center_x(self): return self.x + self.width / 2. def set_center_x(self, value): self.x = value - self.width / 2. center_x = AliasProperty(get_center_x, set_center_x, bind=('x', 'width')) '''X center position of the widget. :attr:`center_x` is an :class:`~kivy.properties.AliasProperty` of (:attr:`x` + :attr:`width` / 2.), ''' def get_center_y(self): return self.y + self.height / 2. def set_center_y(self, value): self.y = value - self.height / 2. center_y = AliasProperty(get_center_y, set_center_y, bind=('y', 'height')) '''Y center position of the widget. :attr:`center_y` is an :class:`~kivy.properties.AliasProperty` of (:attr:`y` + :attr:`height` / 2.) ''' center = ReferenceListProperty(center_x, center_y) '''Center position of the widget. :attr:`center` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`center_x`, :attr:`center_y`) ''' cls = ListProperty([]) '''Class of the widget, used for styling. ''' id = StringProperty(None, allownone=True) '''Unique identifier of the widget in the tree. :attr:`id` is a :class:`~kivy.properties.StringProperty` and defaults to None. .. warning:: If the :attr:`id` is already used in the tree, an exception will be raised. ''' children = ListProperty([]) '''List of children of this widget. :attr:`children` is a :class:`~kivy.properties.ListProperty` and defaults to an empty list. Use :meth:`add_widget` and :meth:`remove_widget` for manipulating the children list. Don't manipulate the children list directly unless you know what you are doing. ''' parent = ObjectProperty(None, allownone=True) '''Parent of this widget. :attr:`parent` is an :class:`~kivy.properties.ObjectProperty` and defaults to None. The parent of a widget is set when the widget is added to another widget and unset when the widget is removed from its parent. ''' size_hint_x = NumericProperty(1, allownone=True) '''X size hint. Represents how much space the widget should use in the direction of the X axis relative to its parent's width. Only the :class:`~kivy.uix.layout.Layout` and :class:`~kivy.core.window.Window` classes make use of the hint. The value is in percent as a float from 0. to 1., where 1. means the full size of his parent. 0.5 represents 50%. :attr:`size_hint_x` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. ''' size_hint_y = NumericProperty(1, allownone=True) '''Y size hint. :attr:`size_hint_y` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. See :attr:`size_hint_x` for more information ''' size_hint = ReferenceListProperty(size_hint_x, size_hint_y) '''Size hint. :attr:`size_hint` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`size_hint_x`, :attr:`size_hint_y`). See :attr:`size_hint_x` for more information ''' pos_hint = ObjectProperty({}) '''Position hint. This property allows you to set the position of the widget inside its parent layout, in percent (similar to size_hint). For example, if you want to set the top of the widget to be at 90% height of its parent layout, you can write:: widget = Widget(pos_hint={'top': 0.9}) The keys 'x', 'right' and 'center_x' will use the parent width. The keys 'y', 'top' and 'center_y' will use the parent height. See :doc:`api-kivy.uix.floatlayout` for further reference. Position hint is only used by the :class:`~kivy.uix.floatlayout.FloatLayout` and :class:`~kivy.core.window.Window`. :attr:`pos_hint` is an :class:`~kivy.properties.ObjectProperty` containing a dict. ''' ids = DictProperty({}) '''This is a Dictionary of id's defined in your kv language. This will only be populated if you use id's in your kv language code. .. versionadded:: 1.7.0 :attr:`ids` is a :class:`~kivy.properties.DictProperty` and defaults to a empty dict {}. ''' opacity = NumericProperty(1.0) '''Opacity of the widget and all the children. .. versionadded:: 1.4.1 The opacity attribute controls the opacity of the widget and its children. Be careful, it's a cumulative attribute: the value is multiplied by the current global opacity and the result is applied to the current context color. For example, if the parent has an opacity of 0.5 and a child has an opacity of 0.2, the real opacity of the child will be 0.5 * 0.2 = 0.1. Then, the opacity is applied by the shader as:: frag_color = color * vec4(1.0, 1.0, 1.0, opacity); :attr:`opacity` is a :class:`~kivy.properties.NumericProperty` and defaults to 1.0. ''' def on_opacity(self, instance, value): canvas = self.canvas if canvas is not None: canvas.opacity = value canvas = None '''Canvas of the widget. The canvas is a graphics object that contains all the drawing instructions for the graphical representation of the widget. There are no general properties for the Widget class, such as background color, to keep the design simple and lean. Some derived classes, such as Button, do add such convenience properties but generally the developer is responsible for implementing the graphics representation for a custom widget from the ground up. See the derived widget classes for patterns to follow and extend. See :class:`~kivy.graphics.Canvas` for more information about the usage. ''' disabled = BooleanProperty(False) '''Indicates whether this widget can interact with input or not.
class MainApp(App): initpos_x = NumericProperty(start_pointX[maze_number]) initpos_y = NumericProperty(start_pointY[maze_number]) position = ReferenceListProperty(initpos_x, initpos_y) print("Start : ", initpos_x, " - ", initpos_y) def build(self): self.parent = MainWidget() #self.parent.serve_agent() self.parent.agent.pos = self.position self.parent.agent.velocity = Vector(6, 0) self.My_Clock = Clock if (maze_file[maze_number].find("maze_train") != -1): pass else: dateTimeObj = datetime.now() # log file log_file = open(maze_file[maze_number] + "_logfile.txt", "a") log_file.writelines("\nStart : " + str(dateTimeObj) + "\n") log_file.close() self.My_Clock.schedule_interval(self.parent.update, 1.0 / 45.0) mind.load() if ddqn: mind.target_load() print("Target Loaded") self.painter = MyPaintWidget() startbtn = Button(text='Start') pausebtn = Button(text='Pause', pos=(self.parent.width, 0)) savebtn = Button(text='Save Memory', pos=(2 * self.parent.width, 0)) loadbtn = Button(text='Load Memory', pos=(3 * self.parent.width, 0)) clearbtn = Button(text='Clear frame', pos=(4 * self.parent.width, 0)) startbtn.bind(on_release=self.start) savebtn.bind(on_release=self.save) loadbtn.bind(on_release=self.load) pausebtn.bind(on_release=self.on_pause) clearbtn.bind(on_release=self.clear_canvas) self.parent.add_widget(self.painter) self.parent.add_widget(startbtn) self.parent.add_widget(savebtn) self.parent.add_widget(loadbtn) self.parent.add_widget(pausebtn) self.parent.add_widget(clearbtn) return self.parent def start(self, obj): self.My_Clock.schedule_interval(self.parent.update, 1.0 / 60.0) def save(self, obj): print("saving mind...") dateTimeObj = datetime.now() file_name = str(maze_file[maze_number]) + '_' + str( dateTimeObj.year) + '_' + str(dateTimeObj.month) + '_' + str( dateTimeObj.day) + str(dateTimeObj.hour) + '_' + str( dateTimeObj.minute) + '_' + str( dateTimeObj.second) + '_' + str( dateTimeObj.microsecond) file_name = file_name + "_DDQN_" + str(ddqn) mind.save(scores, file_name) plt.plot(scores) plt.savefig(file_name + ".png") plt.show() def load(self, obj): print("loading last saved mind...") mind.load() def on_pause(self, obj): self.My_Clock.unschedule(self.parent.update) print("Paused") def clear_canvas(self, obj): global white_space self.painter.canvas.clear() white_space = maze_magnified.copy()
class TreeView(Widget): '''TreeView class. See module documentation for more information. :Events: `on_node_expand`: (node, ) Fired when a node is being expanded `on_node_collapse`: (node, ) Fired when a node is being collapsed ''' __events__ = ('on_node_expand', 'on_node_collapse') def __init__(self, **kwargs): self._trigger_layout = Clock.create_trigger(self._do_layout, -1) super(TreeView, self).__init__(**kwargs) tvlabel = TreeViewLabel(text='Root', is_open=True, level=0) for key, value in self.root_options.items(): setattr(tvlabel, key, value) self._root = self.add_node(tvlabel, None) self.bind(pos=self._trigger_layout, size=self._trigger_layout, indent_level=self._trigger_layout, indent_start=self._trigger_layout) self._trigger_layout() def add_node(self, node, parent=None): '''Add a new node in the tree. :Parameters: `node`: instance of a :class:`TreeViewNode` Node to add into the tree `parent`: instance of a :class:`TreeViewNode`, defaults to None Parent node to attach the new node ''' # check if the widget is "ok" for a node if not isinstance(node, TreeViewNode): raise TreeViewException( 'The node must be a subclass of TreeViewNode') # create node if parent is None and self._root: parent = self._root if parent: parent.is_leaf = False parent.nodes.append(node) node.parent_node = parent node.level = parent.level + 1 node.bind(size=self._trigger_layout) self._trigger_layout() return node def remove_node(self, node): '''Remove a node in a tree. .. versionadded:: 1.0.7 :Parameters: `node`: instance of a :class:`TreeViewNode` Node to remove from the tree ''' # check if the widget is "ok" for a node if not isinstance(node, TreeViewNode): raise TreeViewException( 'The node must be a subclass of TreeViewNode') parent = node.parent_node if parent is not None: nodes = parent.nodes if node in nodes: nodes.remove(node) parent.is_leaf = not bool(len(nodes)) node.parent_node = None node.unbind(size=self._trigger_layout) self._trigger_layout() def on_node_expand(self, node): pass def on_node_collapse(self, node): pass def select_node(self, node): '''Select a node in the tree. ''' if node.no_selection: return if self._selected_node: self._selected_node.is_selected = False node.is_selected = True self._selected_node = node def toggle_node(self, node): '''Toggle the state of the node (open/collapsed). ''' node.is_open = not node.is_open if node.is_open: if self.load_func and not node.is_loaded: self._do_node_load(node) self.dispatch('on_node_expand', node) else: self.dispatch('on_node_collapse', node) self._trigger_layout() def get_node_at_pos(self, pos): '''Get the node at the position (x, y). ''' x, y = pos for node in self.iterate_open_nodes(self.root): if self.x <= x <= self.right and \ node.y <= y <= node.top: return node def iterate_open_nodes(self, node=None): '''Generator to iterate over expanded nodes. To get all the open nodes:: treeview = TreeView() # ... add nodes ... for node in treeview.iterate_open_nodes(): print(node) ''' if not node: node = self.root if self.hide_root and node is self.root: pass else: yield node if not node.is_open: return f = self.iterate_open_nodes for cnode in node.nodes: for ynode in f(cnode): yield ynode def iterate_all_nodes(self, node=None): '''Generator to iterate over all nodes, expanded or not. ''' if not node: node = self.root yield node f = self.iterate_all_nodes for cnode in node.nodes: for ynode in f(cnode): yield ynode # # Private # def on_load_func(self, instance, value): if value: Clock.schedule_once(self._do_initial_load) def _do_initial_load(self, *largs): if not self.load_func: return self._do_node_load(None) def _do_node_load(self, node): gen = self.load_func(self, node) if node: node.is_loaded = True if not gen: return for cnode in gen: self.add_node(cnode, node) def on_root_options(self, instance, value): if not self.root: return for key, value in value.items(): setattr(self.root, key, value) def _do_layout(self, *largs): self.clear_widgets() # display only the one who are is_open self._do_open_node(self.root) # now do layout self._do_layout_node(self.root, 0, self.top) # now iterate for calculating minimum size min_width = min_height = 0 count = 0 for node in self.iterate_open_nodes(self.root): node.odd = False if count % 2 else True count += 1 min_width = max( min_width, node.width + self.indent_level + node.level * self.indent_level) min_height += node.height self.minimum_size = (min_width, min_height) def _do_open_node(self, node): if self.hide_root and node is self.root: height = 0 else: self.add_widget(node) height = node.height if not node.is_open: return height for cnode in node.nodes: height += self._do_open_node(cnode) return height def _do_layout_node(self, node, level, y): if self.hide_root and node is self.root: level -= 1 else: node.x = self.x + self.indent_start + level * self.indent_level node.top = y if node.size_hint_x: node.width = (self.width - (node.x - self.x)) * node.size_hint_x y -= node.height if not node.is_open: return y for cnode in node.nodes: y = self._do_layout_node(cnode, level + 1, y) return y def on_touch_down(self, touch): node = self.get_node_at_pos(touch.pos) if not node: return if node.disabled: return # toggle node or selection ? if node.x - self.indent_start <= touch.x < node.x: self.toggle_node(node) elif node.x <= touch.x: self.select_node(node) node.dispatch('on_touch_down', touch) return True # # Private properties # _root = ObjectProperty(None) _selected_node = ObjectProperty(None) # # Properties # minimum_width = NumericProperty(0) '''Minimum width needed to contain all children. .. versionadded:: 1.0.9 :data:`minimum_width` is a :class:`kivy.properties.NumericProperty` and defaults to 0. ''' minimum_height = NumericProperty(0) '''Minimum height needed to contain all children. .. versionadded:: 1.0.9 :data:`minimum_height` is a :class:`kivy.properties.NumericProperty` and defaults to 0. ''' minimum_size = ReferenceListProperty(minimum_width, minimum_height) '''Minimum size needed to contain all children. .. versionadded:: 1.0.9 :data:`minimum_size` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`minimum_width`, :data:`minimum_height`) properties. ''' indent_level = NumericProperty('16dp') '''Width used for the indentation of each level except the first level. Computation of spacing for each level of tree:: :data:`indent_start` + level * :data:`indent_level` :data:`indent_level` is a :class:`~kivy.properties.NumericProperty` and defaults to 16. ''' indent_start = NumericProperty('24dp') '''Indentation width of the level 0 / root node. This is mostly the initial size to accommodate a tree icon (collapsed / expanded). See :data:`indent_level` for more information about the computation of level indentation. :data:`indent_start` is a :class:`~kivy.properties.NumericProperty` and defaults to 24. ''' hide_root = BooleanProperty(False) '''Use this property to show/hide the initial root node. If True, the root node will be appear as a closed node. :data:`hide_root` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' def get_selected_node(self): return self._selected_node selected_node = AliasProperty(get_selected_node, None, bind=('_selected_node', )) '''Node selected by :meth:`TreeView.select_node` or by touch. :data:`selected_node` is a :class:`~kivy.properties.AliasProperty` and defaults to None. It is read-only. ''' def get_root(self): return self._root root = AliasProperty(get_root, None, bind=('_root', )) '''Root node. By default, the root node widget is a :class:`TreeViewLabel` with text 'Root'. If you want to change the default options passed to the widget creation, use the :data:`root_options` property:: treeview = TreeView(root_options={ 'text': 'Root directory', 'font_size': 15}) :data:`root_options` will change the properties of the :class:`TreeViewLabel` instance. However, you cannot change the class used for root node yet. :data:`root` is an :class:`~kivy.properties.AliasProperty` and defaults to None. It is read-only. However, the content of the widget can be changed. ''' root_options = ObjectProperty({}) '''Default root options to pass for root widget. See :data:`root` property for more information about the usage of root_options. :data:`root_options` is an :class:`~kivy.properties.ObjectProperty` and defaults to {}. ''' load_func = ObjectProperty(None) '''Callback to use for asynchronous loading. If set, asynchronous loading
class Robot(Widget): # Initialise the robot's angle (this is the angle between the x-axis and the robot's axis). angle = NumericProperty(0) # Initialise the robot's rotations # This allows the robot to: # rotate 0 degrees (move forward), # rotate -21 degrees (turn left), or # rotate +21 degrees (turn right). rotation = NumericProperty(0) # Initialise x co-ordinate of the velocity vector for the robot. velocity_x = NumericProperty(0) # Initialise y co-ordinate of the velocity vector for the robot. velocity_y = NumericProperty(0) # Initialise the robot's velocity vector. # This uses velocity_x and velocity_y as the x and y co-ordinates. velocity = ReferenceListProperty(velocity_x, velocity_y) # Initialising the x and y co-ordinates of the centre sensor. sensor_centre_x = NumericProperty(0) sensor_centre_y = NumericProperty(0) # The vector of the centre sensor sensor_centre = ReferenceListProperty(sensor_centre_x, sensor_centre_y) # Initialising the x and y co-ordinates of the left sensor. sensor_left_x = NumericProperty(0) sensor_left_y = NumericProperty(0) # The vector of the left sensor sensor_left = ReferenceListProperty(sensor_left_x, sensor_left_y) # Initialising the x and y co-ordinates of the right sensor. sensor_right_x = NumericProperty(0) sensor_right_y = NumericProperty(0) # The vector of the right sensor sensor_right = ReferenceListProperty(sensor_right_x, sensor_right_y) # Initialising the input signals from the sensors. signal_centre = NumericProperty(0) signal_left = NumericProperty(0) signal_right = NumericProperty(0) def move(self, rotation): # Update the position of the robot. # This uses the robot's previous velocity and position. self.pos = Vector(*self.velocity) + self.pos # This is the rotatio =n of the robot. self.rotation = rotation # This updates the angle of turn (of the robot). self.angle = self.angle + self.rotation # Updating the positions of the sensors. self.sensor_centre = Vector(30, 0).rotate(self.angle) + self.pos self.sensor_right = Vector(30, 0).rotate( (self.angle - 30) % 360) + self.pos self.sensor_left = Vector(30, 0).rotate( (self.angle + 30) % 360) + self.pos # Calculating the density of the obstacles around the sensors. self.signal_centre = int( np.sum(obstacle[int(self.sensor_centre_x) - 10:int(self.sensor_centre_x) + 10, int(self.sensor_centre_y) - 10:int(self.sensor_centre_y) + 10])) / 400. self.signal_left = int( np.sum(obstacle[int(self.sensor_left_x) - 10:int(self.sensor_left_x) + 10, int(self.sensor_left_y) - 10:int(self.sensor_left_y) + 10])) / 400. self.signal_right = int( np.sum(obstacle[int(self.sensor_right_x) - 10:int(self.sensor_right_x) + 10, int(self.sensor_right_y) - 10:int(self.sensor_right_y) + 10])) / 400. # Check if the sensor is out of the map. if self.sensor_centre_x > map_width - 10 or self.sensor_centre_x < 10 or self.sensor_centre_y > map_height - 10 or self.sensor_centre_y < 10: # This makes the density of the obstacles very high, making sure that the robot doesn't wander off the map. self.signal_centre = 1. if self.sensor_left_x > map_width - 10 or self.sensor_left_x < 10 or self.sensor_left_y > map_height - 10 or self.sensor_left_y < 10: self.signal_left = 1. if self.sensor_right_x > map_width - 10 or self.sensor_right_x < 10 or self.sensor_right_y > map_height - 10 or self.sensor_right_y < 10: self.signal_right = 1.
class ProfileService(EventDispatcher): ''' Simple service for managing profiles. ''' active_profile = StringProperty('') profiles = ListProperty([]) left_488 = NumericProperty(0) right_488 = NumericProperty(0) left_405 = NumericProperty(0) right_405 = NumericProperty(0) left_561 = NumericProperty(0) right_561 = NumericProperty(0) left_638 = NumericProperty(0) right_638 = NumericProperty(0) saved_offsets = ListProperty([]) applied_offsets = ListProperty([]) current_offsets = ReferenceListProperty(left_488, right_488, left_405, right_405, left_561, right_561, left_638, right_638) def __init__(self, path, initial_profile='default', **kwargs): super(ProfileService, self).__init__(**kwargs) self._path = self._ensure_file(path) self._config = ConfigParser() self._config.read(self._path) self.set_profile(initial_profile) self.profiles = filter(lambda x: 'file_path' not in x, self._config.sections()) @property def parameters(self): keys = list( filter( lambda attr: attr.startswith('left_') or attr.startswith( 'right_'), dir(self))) return {key: getattr(self, key) for key in keys} def _ensure_file(self, path): if not os.path.isfile(path): open(path, 'w').close() return path def set_profile(self, section): if hasattr(section, 'text'): section = section.text self.active_profile = section for item in self._config.items(section): param, val = item setattr(self, param, round(float(val), 6)) self.saved_offsets = self.current_offsets def save_profile(self, widget=None): print('Profile saved.') #self.saved_offsets = self.current_offsets for param, val in self.parameters.items(): self._config.set(self.active_profile, param, str(val)) with open(self._path, 'r+') as f: self._config.write(f) def add_profile(self, widget): section = widget.text if section in self._config.sections(): return self.profiles.append(section) self._config.add_section(section) for param, val in self.parameters.items(): self._config.set(section, param, str(val)) self.save_profile() self.set_profile(section) def delete_profile(self, section, _): self._config.remove_section(section) self.profiles.pop(self.profiles.index(section)) self.set_profile(self.profiles[0]) self.save_profile() return True
class Brick(DragBehavior, Label): # NOTE: values of properties without defaults # shall be set in the .kv file background_color = ListProperty() border_color = ListProperty() # (declaring the following class-wide constants as properties makes it # easier to specify/inherit/overridde their values just in the .kv file) detached_border_color = ListProperty() move_border_color = ListProperty() attached_border_color = ListProperty() equal_border_color = ListProperty() final_border_color = ListProperty() max_snap_x_distance = NumericProperty() max_snap_y_distance = NumericProperty() max_double_attach_x_distance = NumericProperty() max_double_attach_y_distance = NumericProperty() target_x = NumericProperty(0) target_y = NumericProperty(0) target_pos = ReferenceListProperty(target_x, target_y) def get_target_right(self): return self.target_x + self.width def set_target_right(self, value): self.target_x = value - self.width target_right = AliasProperty(get_target_right, set_target_right, bind=('target_x', 'width')) target_right_pos = ReferenceListProperty(target_right, target_y) state = OptionProperty('detached', options=[ 'detached', 'move', 'attached', 'equal', 'final', ]) left_attached_brick = None right_attached_brick = None @property def symbol(self): return self.text # event dispatch def on_touch_down(self, touch): if self.state != 'final' and super(Brick, self).on_touch_down(touch): self.update_states_before_detach() self.detach() self.state = 'move' return True return False def on_touch_up(self, touch): if self.state != 'final' and super(Brick, self).on_touch_up(touch): self.target_pos = self.pos assert self.state == 'move' if self.attach(): self.update_states_after_attach() else: self.state = 'detached' return True return False # detaching def update_states_before_detach(self): for brick_seq in (self.collect_all_left(), self.collect_all_right()): if brick_seq: if len(brick_seq) == 1: brick_seq[0].state = 'detached' elif self.is_brick_seq_equal(brick_seq): for brick in brick_seq: brick.state = 'equal' else: for brick in brick_seq: brick.state = 'attached' def detach(self): left_brick = self.left_attached_brick if left_brick is not None: left_brick.right_attached_brick = None self.left_attached_brick = None right_brick = self.right_attached_brick if right_brick is not None: right_brick.left_attached_brick = None self.right_attached_brick = None # attaching def attach(self): (left_brick, right_brick, target_pos) = self.get_bricks_and_target_pos() if target_pos is not None: if left_brick is not None: left_brick.right_attached_brick = self.proxy_ref self.left_attached_brick = left_brick.proxy_ref if right_brick is not None: right_brick.left_attached_brick = self.proxy_ref self.right_attached_brick = right_brick.proxy_ref self.target_pos = target_pos return True return False def get_bricks_and_target_pos(self): left_brick = self.choose_left_brick() right_brick = self.choose_right_brick() if right_brick is not None: target_pos_by_right = (right_brick.target_x - self.width, right_brick.target_y) if left_brick is not None: target_pos_by_left = tuple(left_brick.target_right_pos) distance_from_left = (Vector(self.target_pos).distance( left_brick.target_right_pos)) distance_from_right = (Vector(self.target_right_pos).distance( right_brick.target_pos)) if self.can_attach_to_both(left_brick, right_brick, target_pos_by_left, target_pos_by_right, distance_from_left, distance_from_right): target_pos = interpolate(target_pos_by_left, target_pos_by_right, step=2) elif self.should_attach_to_left(left_brick, right_brick, distance_from_left, distance_from_right): target_pos = target_pos_by_left right_brick = None else: target_pos = target_pos_by_right left_brick = None else: target_pos = target_pos_by_right elif left_brick is not None: target_pos = left_brick.target_right_pos else: target_pos = None return left_brick, right_brick, target_pos def choose_left_brick(self): _distance = Vector(self.target_pos).distance bricks_and_distances = [(brick, _distance(brick.target_right_pos), abs(self.target_x - brick.target_right)) for brick in self.iter_all_bricks() if brick.right_attached_brick is None] return self.get_attachable_brick(bricks_and_distances) def choose_right_brick(self): _distance = Vector(self.target_right_pos).distance bricks_and_distances = [(brick, _distance(brick.target_pos), abs(self.target_right - brick.target_x)) for brick in self.iter_all_bricks() if brick.left_attached_brick is None] return self.get_attachable_brick(bricks_and_distances) def get_attachable_brick(self, bricks_and_distances): bricks_and_distances.sort(key=operator.itemgetter(1)) for brick, _, x_distance in bricks_and_distances: if brick == self: continue # (for checking snap limits, using x and y separately # plays better than using the real x*y distance) y_distance = abs(self.target_y - brick.target_y) if (x_distance > self.max_snap_x_distance or y_distance > self.max_snap_y_distance): return None if self.can_be_attached_to(brick): return brick def can_be_attached_to(self, brick): return brick.state != 'move' def can_attach_to_both(self, left_brick, right_brick, target_pos_by_left, target_pos_by_right, distance_from_left, distance_from_right): return (Vector(target_pos_by_left).distance(target_pos_by_right) < self.width / 3) or ( (distance_from_right / 3.5 <= distance_from_left <= 3.5 * distance_from_right) and (abs(self.target_x - left_brick.target_right) <= self.max_double_attach_x_distance) and (abs(self.target_y - left_brick.target_y) <= self.max_double_attach_y_distance) and (abs(self.target_right - right_brick.target_x) <= self.max_double_attach_x_distance) and (abs(self.target_y - right_brick.target_y) <= self.max_double_attach_y_distance)) def should_attach_to_left(self, left_brick, right_brick, distance_from_left, distance_from_right): from_left = abs(self.target_y - left_brick.target_y) from_right = abs(self.target_y - right_brick.target_y) if (from_left < self.height / 4 and from_right < self.height / 4) or ( from_right / 1.4 <= from_left <= 1.4 * from_right): # y distances are too small or too similar to # be conclusive => let's use real x*y distances from_left = distance_from_left from_right = distance_from_right if from_left <= from_right: return True else: assert from_left > from_right return False def update_states_after_attach(self): brick_seq = self.collect_all_left() brick_seq.append(self) self.collect_all_right(brick_seq) if self.is_brick_seq_equal(brick_seq): for brick in brick_seq: brick.state = 'equal' all_bricks = list(self.iter_all_bricks()) if all(brick.state == 'equal' for brick in all_bricks): for brick in all_bricks: brick.state = 'final' self.parent.finish_game() else: for brick in brick_seq: brick.state = 'attached' # commons def collect_all_left(self, brick_seq=None): if brick_seq is None: brick_seq = [] left_attached_brick = self.left_attached_brick if left_attached_brick is not None: left_attached_brick.collect_all_left(brick_seq) brick_seq.append(left_attached_brick) return brick_seq def collect_all_right(self, brick_seq=None): if brick_seq is None: brick_seq = [] right_attached_brick = self.right_attached_brick if right_attached_brick is not None: brick_seq.append(right_attached_brick) right_attached_brick.collect_all_right(brick_seq) return brick_seq def is_brick_seq_equal(self, brick_seq): expr_str = ''.join(brick.symbol for brick in brick_seq) if '==' not in expr_str: return False try: return eval(expr_str) except (SyntaxError, ArithmeticError): return False def iter_all_bricks(self): return self.parent.iter_all_bricks()