class PopupLayout(BoxLayout): name = StringProperty() price = NumericProperty() description = StringProperty() img_path = StringProperty() quantity = BoundedNumericProperty(0, min=0, max=50) def __init__(self, **kwargs): super(PopupLayout, self).__init__(**kwargs) if item_quantity.get(self.name, 0) == 0: item_quantity[self.name] = [0, False] self.quantity = item_quantity[self.name][0] else: self.quantity = item_quantity.get(self.name)[0] # function called when confirm button in item popup pressed def save(self, *args): item_quantity[self.name][0] = self.quantity # function called when clear button in item popup pressed def clear(self, *args): item_quantity[self.name][0] = 0
class MDBottomNavigationHeader(BaseFlatButton, BasePressedButton): width = BoundedNumericProperty(dp(0), min=dp(80), max=dp(168), errorhandler=lambda x: small_error_warn(x)) tab = ObjectProperty(None) panel = ObjectProperty(None) _label = ObjectProperty() _label_font_size = NumericProperty(sp(12)) _current_color = ListProperty([0.0, 0.0, 0.0, 0.0]) text = StringProperty('') _capitalized_text = StringProperty('') active = BooleanProperty(False) def on_text(self, instance, value): self._capitalized_text = value.upper() def __init__(self, panel, height, tab): self.panel = panel self.height = height self.tab = tab super(MDBottomNavigationHeader, self).__init__() self._current_color = self.theme_cls.disabled_hint_text_color self._label = self.ids._label self._label_font_size = sp(12) self.theme_cls.bind(primary_color=self._update_theme_color, disabled_hint_text_color=self._update_theme_style) self.active = False def on_press(self): Animation(_label_font_size=sp(14), d=0.1).start(self) Animation(_current_color=self.theme_cls.primary_color, d=0.1).start(self) def _update_theme_color(self, instance, color): if self.active: self._current_color = self.theme_cls.primary_color def _update_theme_style(self, instance, color): if not self.active: self._current_color = self.theme_cls.disabled_hint_text_color
class BaseRectangularButton(RectangularRippleBehavior, BaseButton): """ Abstract base class for all rectangular buttons, bringing in the appropriate on-touch behavior. Also maintains the correct minimum width as stated in guidelines. """ width = BoundedNumericProperty( 88, min=88, max=None, errorhandler=lambda x: 88 ) text = StringProperty("") """Button text. :attr:`text` is an :class:`~kivy.properties.StringProperty` and defaults to `''`. """ increment_width = NumericProperty("32dp") """ Button extra width value. :attr:`increment_width` is an :class:`~kivy.properties.NumericProperty` and defaults to `'32dp'`. """ button_label = BooleanProperty(True) """ If ``False`` the text on the button will not be displayed. :attr:`button_label` is an :class:`~kivy.properties.BooleanProperty` and defaults to `True`. """ can_capitalize = BooleanProperty(True) _radius = NumericProperty("2dp") _height = NumericProperty(0)
def test_bounded_numeric_property_error_handler(self): from kivy.properties import BoundedNumericProperty bnp = BoundedNumericProperty(0, min=-5, max=5, errorhandler=lambda x: 5 if x > 5 else -5) bnp.link(wid, 'bnp') bnp.set(wid, 1) self.assertEqual(bnp.get(wid), 1) bnp.set(wid, 5) self.assertEqual(bnp.get(wid), 5) bnp.set(wid, 10) self.assertEqual(bnp.get(wid), 5) bnp.set(wid, -5) self.assertEqual(bnp.get(wid), -5) bnp.set(wid, -10) self.assertEqual(bnp.get(wid), -5)
class Timer(Widget): delay = BoundedNumericProperty(1, min=.005, max=3600) # seconds oneshot = BooleanProperty(False) autoplay = BooleanProperty(False) def __init__(self, **kwargs): super().__init__(**kwargs) self.register_event_type('on_timeout') self.bind(autoplay=self.start) self._clock = None self._scheduler = Clock.create_trigger self.delta = 0. def on_timeout(self): pass def start(self, *args): if self._clock and self._clock.is_triggered: return if not self._clock: self._clock = self._scheduler(self._tick, self.delay) self._clock() else: self._clock() def stop(self): if self._clock: self._clock.cancel() def _tick(self, dt): self.delta = dt self.dispatch('on_timeout') if not self.oneshot: self._clock()
class Layers(Widget): r1 = ObjectProperty(None) r2 = ObjectProperty(None) r_y = BoundedNumericProperty(0,min=-100,max=100) inc = 1 def __init__(self,*args,**kwargs): super(Layers,self).__init__(*args,**kwargs) Clock.schedule_interval(self.update,0) def update(self,t): try: self.r_y += self.inc self.r2.y += self.inc except ValueError: self.inc *= -1 children_sorted = sorted(self.children,key=lambda w: w.y) children_sorted.reverse() for i in children_sorted: self.remove_widget(i) self.add_widget(i)
class TestCar(Widget): angle = BoundedNumericProperty(0.) rotation = BoundedNumericProperty(0.) velocity_x = BoundedNumericProperty(0.) velocity_y = BoundedNumericProperty(0.) deceleration_x = BoundedNumericProperty(0.) deceleration_y = BoundedNumericProperty(0.) velocity = ReferenceListProperty(velocity_x, velocity_y) deceleration = ReferenceListProperty(deceleration_x, deceleration_y) def rotate(self, rotation): # print ("Entered rotate method") self.pos = Vector(*self.velocity) + self.pos self.rotation = float(rotation) self.angle = self.angle + self.rotation def accelerate(self, acceleration_x): # print ("Entered method: ", inspect.stack()[0][3]) self.velocity = Vector(*self.velocity) + Vector(acceleration_x, 0) if self.velocity_x > 1: self.velocity_x = 1 elif (self.velocity_x < 0): # To avoid car moving backwards self.velocity_x = 0 self.pos = Vector(*self.velocity) + self.pos # change property type # print ("pos1 :",self.pos , " vel1 : " , self.velocity) def decelerate(self, deceleration_x): # print ("Entered method: ", inspect.stack()[0][3]) self.velocity = Vector(*self.velocity) - Vector(deceleration_x, 0) if self.velocity_x > 1: self.velocity_x = 1 elif (self.velocity_x < 0): # To avoid car moving backwards self.velocity_x = 0 self.pos = Vector(*self.velocity) + self.pos
class PhotoAlbumScreen(Screen): """Base screen to run the photo album.""" # Reference to the screen manager photoscreen = ObjectProperty(None) # Value for the screen display time photoduration = BoundedNumericProperty(5, min=2, max=60, errorvalue=5) def __init__(self, **kwargs): #super(PhotoAlbumScreen, self).__init__(**kwargs) super(PhotoAlbumScreen, self).__init__() self.name = kwargs["name"] # Get the user's preferences self.folders = kwargs["params"]["folders"] self.exts = kwargs["params"]["extensions"] self.photoduration = kwargs["params"]["duration"] # Initialise some useful variables self.running = False self.photos = [] self.timer = None self.oldPhoto = None self.photoindex = 0 def on_enter(self): if not self.running: # The screen hasn't been run before so let's tell the user # that we need to get the photos self.loading = PhotoLoading(name="loading") self.photoscreen.add_widget(self.loading) self.photoscreen.current = "loading" # Retrieve photos Clock.schedule_once(self.getPhotos, 0.5) else: # We've been here before so just show the photos self.timer = Clock.schedule_interval(self.showPhoto, self.photoduration) def on_leave(self): # We can stop looping over photos Clock.unschedule(self.timer) def getPhotos(self, *args): """Method to retrieve list of photos based on user's preferences.""" # Get a list of extensions. Assumes all caps or all lower case. exts = [] for ext in ([x.upper(), x.lower()] for x in self.exts): exts.extend(ext) # Loop over the folders for folder in self.folders: # Look for each extension for ext in exts: # Get the photos photos = glob(os.path.join(folder, "*.{}".format(ext))) # Add to our master list self.photos.extend(photos) # Put the photos in order self.photos.sort() # We've got the photos so we can set the running flag self.running = True # and start the timer self.timer = Clock.schedule_interval(self.showPhoto, self.photoduration) # Show the first photo self.showPhoto() def showPhoto(self, *args): """Method to update the currently displayed photo.""" # Get the current photo photo = self.photos[self.photoindex] fehler = True while fehler: try: print("Foto laden") photo = self.photos[self.photoindex] fehler = False except: print("Es gab ein Problem beim Foto laden") fehler = True self.photoindex = (self.photoindex + 1) % len(self.photos) if fehler: print("Ich lad lieber mal das erste Bild") photo = self.photos[0] print("Photo: ", photo) print(type(photo)) # Create a screen pbject to show that photo scr = Photo(name=photo) # Add it to our screenmanager and display it self.photoscreen.add_widget(scr) self.photoscreen.current = photo # If we've got an old photo if self.oldPhoto: # We can unload it self.photoscreen.remove_widget(self.oldPhoto) # Create reference to the new photo self.oldPhoto = scr # Increase our index for the next photo self.photoindex = (self.photoindex + 1) % len(self.photos)
class ProgressSpinnerBase(Widget): '''ProgressSpinnerBase - base class for progress spinner widgets ''' color = ListProperty([1, 1, 1, 1]) '''Color to render the spinner. :attr:`color` is a :class:`~kivy.properties.ListProperty` and defaults to [1, 1, 1, 1] (white, full opacity). ''' speed = BoundedNumericProperty(1, min=0.1) '''Speed coefficient of the spinner. This value is multiplied by the base speed of 90 degrees per second. :attr:`speed` is a :class:`~kivy.properties.BoundedNumericProperty` and defaults to 1. ''' stroke_length = BoundedNumericProperty(25., min=1, max=180) '''Base length of the stroke in degrees. :attr:`stroke_length` is a :class:`~kivy.properties.BoundedNumericProperty` and defaults to 25. ''' stroke_width = NumericProperty(None, allownone=True) '''Width of the stroke in pixels. If set to None, the width will be calculated automatically as 1/20th of the radius. :attr:`stroke_width` is a :class:`~kivy.properties.NumericProperty` and defaults to None. ''' auto_start = BooleanProperty(False) '''Whether to automatically start spinning. :attr:`auto_start` is a :class:`~kivy.properties.BooleanProperty` and defaults to True. ''' # internal properties _angle_center = NumericProperty(0) _angle_start = NumericProperty() _angle_end = NumericProperty() _size = NumericProperty() _rsize = NumericProperty() _stroke = NumericProperty(1) _radius = NumericProperty(50) def __init__(self, **kwargs): super(ProgressSpinnerBase, self).__init__(**kwargs) self._state = 'wait1' self._next = None self._spinning = False if self.auto_start: self.start_spinning() def start_spinning(self, *args): '''Start spinning the progress spinner. Ignores all positional args for easy binding. ''' if not self._spinning: self._state = 'wait1' self._next = None self._angle_center = 0. self._angle_start = 360. self._angle_end = 360. + self.stroke_length Clock.schedule_interval(self._update, 0) Clock.schedule_once(self._rotate, 0.3) self._spinning = True def stop_spinning(self, *args): '''Stop spinning the progress spinner. Ignores all positional args for easy binding. If you intend to keep the spinner around, you should stop it when not using it and restart it when needed again. ''' if self._spinning: if self._next: if isinstance(self._next, Animation): self._next.cancel(self) else: self._next.cancel() Clock.unschedule(self._update) Clock.unschedule(self._rotate) self._angle_start = self._angle_end = 0 self._spinning = False def _update(self, dt): angle_speed = 90. * self.speed self._angle_center += dt * angle_speed if self._angle_center > 360: self._angle_center -= 360. def _rotate(self, *args): if not self._spinning: return rotate_speed = 0.6 / self.speed wait_speed = 0.3 / self.speed if self._state == 'wait1': self._state = 'rotate1' self._next = Animation(_angle_end=self._angle_start + 360. - self.stroke_length, d=rotate_speed, t='in_quad') self._next.bind(on_complete=self._rotate) self._next.start(self) elif self._state == 'rotate1': self._state = 'wait2' self._next = Clock.schedule_once(self._rotate, wait_speed) elif self._state == 'wait2': self._state = 'rotate2' self._next = Animation(_angle_start=self._angle_end - self.stroke_length, d=rotate_speed, t='in_quad') self._next.bind(on_complete=self._rotate) self._next.start(self) elif self._state == 'rotate2': self._state = 'wait1' self._next = Clock.schedule_once(self._rotate, wait_speed) while self._angle_end > 720.: self._angle_start -= 360. self._angle_end -= 360.
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 range, 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]) """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([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(.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(.5) """Font size scale factor fot the :class:`Number`s. :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): c = 0 for i in range(*self.range): if i % self.multiples_of == 0: c += 1 return c 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(CircularNumberPicker, self).__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 * .3 for i in esize] dpos = [i + esize[0] / 2. - dsize[0] / 2. for i in epos] csize = [i * .05 for i in esize] cpos = [i - csize[0] / 2. 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]) 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, c: setattr( self._selection_color, "rgba", c + [self.selector_alpha])) self.bind(selector_color=lambda ign, c: setattr( self._selection_dot_color, "rgb", c)) self.bind(color=lambda ign, c: setattr(self._center_color, "rgb", c)) Clock.schedule_once(self._genitems) Clock.schedule_once( self.on_selected) # Just to make sure pos/size are set def _genitems(self, *a): self.clear_widgets() for i in range(*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) def on_touch_move(self, touch): if touch.grab_current is not self: return super(CircularNumberPicker, self).on_touch_move(touch) self.selected = self.number_at_pos(*touch.pos) def on_touch_up(self, touch): if touch.grab_current is not self: return super(CircularNumberPicker, self).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 * .3 for i in esize] dpos = [i + esize[0] / 2. - dsize[0] / 2. for i in epos] csize = [i * .05 for i in esize] cpos = [i - csize[0] / 2. 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 # print self.selected 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. middle_r = radius * sum(self.radius_hint) / 2. cx = self.center_x + self.padding[0] - self.padding[2] cy = self.center_y + self.padding[3] - self.padding[1] sign = +1. angle_offset = radians(self.start_angle) if self.direction == 'cw': angle_offset = 2 * pi - angle_offset sign = -1. 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 and ly > 0: angle += pi if lx > 0 and ly < 0: 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 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]. .. versionchanged:: 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(.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. 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(CircularLayout, self).__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) # selfw = self.width # selfh = self.height 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. outer_r = radius * self.outer_radius_hint inner_r = radius * self.inner_radius_hint middle_r = radius * sum(self.radius_hint) / 2. delta_r = outer_r - inner_r # calculate maximum space used by size_hint stretch_weight_angle = 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. angle_offset = start_angle_r if direction == 'cw': angle_offset = 2 * pi - start_angle_r sign = -1. 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 Scatter(Widget): '''Scatter class. See module documentation for more information. :Events: `on_transform_with_touch`: Fired when the scatter has been transformed by user touch or multitouch, such as panning or zooming. `on_bring_to_front`: Fired when the scatter is brought to the front. .. versionchanged:: 1.9.0 Event `on_bring_to_front` added. .. versionchanged:: 1.8.0 Event `on_transform_with_touch` added. ''' __events__ = ('on_transform_with_touch', 'on_bring_to_front') auto_bring_to_front = BooleanProperty(True) '''If True, the widget will be automatically pushed on the top of parent widget list for drawing. :attr:`auto_bring_to_front` is a :class:`~kivy.properties.BooleanProperty` and defaults to True. ''' do_translation_x = BooleanProperty(True) '''Allow translation on the X axis. :attr:`do_translation_x` is a :class:`~kivy.properties.BooleanProperty` and defaults to True. ''' do_translation_y = BooleanProperty(True) '''Allow translation on Y axis. :attr:`do_translation_y` is a :class:`~kivy.properties.BooleanProperty` and defaults to True. ''' def _get_do_translation(self): return (self.do_translation_x, self.do_translation_y) def _set_do_translation(self, value): if type(value) in (list, tuple): self.do_translation_x, self.do_translation_y = value else: self.do_translation_x = self.do_translation_y = bool(value) do_translation = AliasProperty(_get_do_translation, _set_do_translation, bind=('do_translation_x', 'do_translation_y'), cache=True) '''Allow translation on the X or Y axis. :attr:`do_translation` is an :class:`~kivy.properties.AliasProperty` of (:attr:`do_translation_x` + :attr:`do_translation_y`) ''' translation_touches = BoundedNumericProperty(1, min=1) '''Determine whether translation was triggered by a single or multiple touches. This only has effect when :attr:`do_translation` = True. :attr:`translation_touches` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. .. versionadded:: 1.7.0 ''' do_rotation = BooleanProperty(True) '''Allow rotation. :attr:`do_rotation` is a :class:`~kivy.properties.BooleanProperty` and defaults to True. ''' do_scale = BooleanProperty(True) '''Allow scaling. :attr:`do_scale` is a :class:`~kivy.properties.BooleanProperty` and defaults to True. ''' do_collide_after_children = BooleanProperty(False) '''If True, the collision detection for limiting the touch inside the scatter will be done after dispaching the touch to the children. You can put children outside the bounding box of the scatter and still be able to touch them. :attr:`do_collide_after_children` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. .. versionadded:: 1.3.0 ''' scale_min = NumericProperty(0.01) '''Minimum scaling factor allowed. :attr:`scale_min` is a :class:`~kivy.properties.NumericProperty` and defaults to 0.01. ''' scale_max = NumericProperty(1e20) '''Maximum scaling factor allowed. :attr:`scale_max` is a :class:`~kivy.properties.NumericProperty` and defaults to 1e20. ''' transform = ObjectProperty(Matrix()) '''Transformation matrix. :attr:`transform` is an :class:`~kivy.properties.ObjectProperty` and defaults to the identity matrix. .. note:: This matrix reflects the current state of the transformation matrix but setting it directly will erase previously applied transformations. To apply a transformation considering context, please use the :attr:`~Scatter.apply_transform` method. ''' transform_inv = ObjectProperty(Matrix()) '''Inverse of the transformation matrix. :attr:`transform_inv` is an :class:`~kivy.properties.ObjectProperty` and defaults to the identity matrix. ''' def _get_bbox(self): xmin, ymin = xmax, ymax = self.to_parent(0, 0) for point in [(self.width, 0), (0, self.height), self.size]: x, y = self.to_parent(*point) if x < xmin: xmin = x if y < ymin: ymin = y if x > xmax: xmax = x if y > ymax: ymax = y return (xmin, ymin), (xmax - xmin, ymax - ymin) bbox = AliasProperty(_get_bbox, bind=('transform', 'width', 'height')) '''Bounding box of the widget in parent space:: ((x, y), (w, h)) # x, y = lower left corner :attr:`bbox` is an :class:`~kivy.properties.AliasProperty`. ''' def _get_rotation(self): v1 = Vector(0, 10) tp = self.to_parent v2 = Vector(*tp(*self.pos)) - tp(self.x, self.y + 10) return -1.0 * (v1.angle(v2) + 180) % 360 def _set_rotation(self, rotation): angle_change = self.rotation - rotation r = Matrix().rotate(-radians(angle_change), 0, 0, 1) self.apply_transform(r, post_multiply=True, anchor=self.to_local(*self.center)) rotation = AliasProperty(_get_rotation, _set_rotation, bind=('x', 'y', 'transform')) '''Rotation value of the scatter in degrees moving in a counterclockwise direction. :attr:`rotation` is an :class:`~kivy.properties.AliasProperty` and defaults to 0.0. ''' def _get_scale(self): p1 = Vector(*self.to_parent(0, 0)) p2 = Vector(*self.to_parent(1, 0)) scale = p1.distance(p2) # XXX float calculation are not accurate, and then, scale can be # throwed again even with only the position change. So to # prevent anything wrong with scale, just avoid to dispatch it # if the scale "visually" didn't change. #947 # Remove this ugly hack when we'll be Python 3 only. if hasattr(self, '_scale_p'): if str(scale) == str(self._scale_p): return self._scale_p self._scale_p = scale return scale def _set_scale(self, scale): rescale = scale * 1.0 / self.scale self.apply_transform(Matrix().scale(rescale, rescale, rescale), post_multiply=True, anchor=self.to_local(*self.center)) scale = AliasProperty(_get_scale, _set_scale, bind=('x', 'y', 'transform')) '''Scale value of the scatter. :attr:`scale` is an :class:`~kivy.properties.AliasProperty` and defaults to 1.0. ''' def _get_center(self): return (self.bbox[0][0] + self.bbox[1][0] / 2.0, self.bbox[0][1] + self.bbox[1][1] / 2.0) def _set_center(self, center): if center == self.center: return False t = Vector(*center) - self.center trans = Matrix().translate(t.x, t.y, 0) self.apply_transform(trans) center = AliasProperty(_get_center, _set_center, bind=('bbox', )) def _get_pos(self): return self.bbox[0] def _set_pos(self, pos): _pos = self.bbox[0] if pos == _pos: return t = Vector(*pos) - _pos trans = Matrix().translate(t.x, t.y, 0) self.apply_transform(trans) pos = AliasProperty(_get_pos, _set_pos, bind=('bbox', )) def _get_x(self): return self.bbox[0][0] def _set_x(self, x): if x == self.bbox[0][0]: return False self.pos = (x, self.y) return True x = AliasProperty(_get_x, _set_x, bind=('bbox', )) def _get_y(self): return self.bbox[0][1] def _set_y(self, y): if y == self.bbox[0][1]: return False self.pos = (self.x, y) return True y = AliasProperty(_get_y, _set_y, bind=('bbox', )) def get_right(self): return self.x + self.bbox[1][0] def set_right(self, value): self.x = value - self.bbox[1][0] right = AliasProperty(get_right, set_right, bind=('x', 'bbox')) def get_top(self): return self.y + self.bbox[1][1] def set_top(self, value): self.y = value - self.bbox[1][1] top = AliasProperty(get_top, set_top, bind=('y', 'bbox')) def get_center_x(self): return self.x + self.bbox[1][0] / 2. def set_center_x(self, value): self.x = value - self.bbox[1][0] / 2. center_x = AliasProperty(get_center_x, set_center_x, bind=('x', 'bbox')) def get_center_y(self): return self.y + self.bbox[1][1] / 2. def set_center_y(self, value): self.y = value - self.bbox[1][1] / 2. center_y = AliasProperty(get_center_y, set_center_y, bind=('y', 'bbox')) def __init__(self, **kwargs): self._touches = [] self._last_touch_pos = {} super(Scatter, self).__init__(**kwargs) def on_transform(self, instance, value): self.transform_inv = value.inverse() def collide_point(self, x, y): x, y = self.to_local(x, y) return 0 <= x <= self.width and 0 <= y <= self.height def to_parent(self, x, y, **k): p = self.transform.transform_point(x, y, 0) return (p[0], p[1]) def to_local(self, x, y, **k): p = self.transform_inv.transform_point(x, y, 0) return (p[0], p[1]) def _apply_transform(self, m, pos=None): m = self.transform.multiply(m) return super(Scatter, self)._apply_transform(m, (0, 0)) def apply_transform(self, trans, post_multiply=False, anchor=(0, 0)): ''' Transforms the scatter by applying the "trans" transformation matrix (on top of its current transformation state). The resultant matrix can be found in the :attr:`~Scatter.transform` property. :Parameters: `trans`: :class:`~kivy.graphics.transformation.Matrix`. Transformation matix to be applied to the scatter widget. `anchor`: tuple, defaults to (0, 0). The point to use as the origin of the transformation (uses local widget space). `post_multiply`: bool, defaults to False. If True, the transform matrix is post multiplied (as if applied before the current transform). Usage example:: from kivy.graphics.transformation import Matrix mat = Matrix().scale(3, 3, 3) scatter_instance.apply_transform(mat) ''' t = Matrix().translate(anchor[0], anchor[1], 0) t = t.multiply(trans) 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 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_transform(Matrix().translate(dx, dy, 0)) changed = True if len(self._touches) == 1: 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 self.apply_transform(Matrix().rotate(angle, 0, 0, 1), anchor=anchor) if self.do_scale: scale = new_line.length() / old_line.length() 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 self.apply_transform(Matrix().scale(scale, scale, scale), anchor=anchor) changed = True return changed def _bring_to_front(self, touch): # auto bring to front if self.auto_bring_to_front and self.parent: parent = self.parent if parent.children[0] is self: return parent.remove_widget(self) parent.add_widget(self) self.dispatch('on_bring_to_front', touch) def on_touch_down(self, touch): x, y = touch.x, touch.y # if the touch isnt on the widget we do nothing if not self.do_collide_after_children: if not self.collide_point(x, y): return False # let the child widgets handle the event if they want touch.push() touch.apply_transform_2d(self.to_local) if super(Scatter, self).on_touch_down(touch): touch.pop() self._bring_to_front(touch) return True touch.pop() # if our child didn't do anything, and if we don't have any active # interaction control, then don't accept the touch. if not self.do_translation_x and \ not self.do_translation_y and \ not self.do_rotation and \ not self.do_scale: return False if self.do_collide_after_children: if not self.collide_point(x, y): return False if 'multitouch_sim' in touch.profile: touch.multitouch_sim = True # grab the touch so we get all it later move events for sure self._bring_to_front(touch) touch.grab(self) self._touches.append(touch) self._last_touch_pos[touch] = touch.pos return True def on_touch_move(self, touch): x, y = touch.x, touch.y # let the child widgets handle the event if they want if self.collide_point(x, y) and not touch.grab_current == self: touch.push() touch.apply_transform_2d(self.to_local) if super(Scatter, self).on_touch_move(touch): touch.pop() return True touch.pop() # rotate/scale/translate if touch in self._touches and touch.grab_current == self: if self.transform_with_touch(touch): self.dispatch('on_transform_with_touch', touch) self._last_touch_pos[touch] = touch.pos # stop propagating if its within our bounds if self.collide_point(x, y): return True def on_transform_with_touch(self, touch): ''' Called when a touch event has transformed the scatter widget. By default this does nothing, but can be overriden by derived classes that need to react to transformations caused by user input. :Parameters: `touch`: The touch object which triggered the transformation. .. versionadded:: 1.8.0 ''' pass def on_bring_to_front(self, touch): ''' Called when a touch event causes the scatter to be brought to the front of the parent (only if :attr:`auto_bring_to_front` is True) :Parameters: `touch`: The touch object which brought the scatter to front. .. versionadded:: 1.9.0 ''' pass def on_touch_up(self, touch): x, y = touch.x, touch.y # if the touch isnt on the widget we do nothing, just try children if not touch.grab_current == self: touch.push() touch.apply_transform_2d(self.to_local) if super(Scatter, self).on_touch_up(touch): touch.pop() return True touch.pop() # remove it from our saved touches if touch in self._touches and touch.grab_state: touch.ungrab(self) del self._last_touch_pos[touch] self._touches.remove(touch) # stop propagating if its within our bounds if self.collide_point(x, y): return True
class Graph(Widget): '''Graph class, see module documentation for more information. ''' # triggers a full reload of graphics _trigger = ObjectProperty(None) # triggers only a repositioning of objects due to size/pos updates _trigger_size = ObjectProperty(None) # holds widget with the x-axis label _xlabel = ObjectProperty(None) # holds widget with the y-axis label _ylabel = ObjectProperty(None) # holds all the x-axis tick mark labels _x_grid_label = ListProperty([]) # holds all the y-axis tick mark labels _y_grid_label = ListProperty([]) # holds the stencil view that clipse the plots to graph area _plot_area = ObjectProperty(None) # the mesh drawing all the ticks/grids _mesh = ObjectProperty(None) # the mesh which draws the surrounding rectangle _mesh_rect = ObjectProperty(None) # a list of locations of major and minor ticks. The values are not # but is in the axis min - max range _ticks_majorx = ListProperty([]) _ticks_minorx = ListProperty([]) _ticks_majory = ListProperty([]) _ticks_minory = ListProperty([]) def __init__(self, **kwargs): super(Graph, self).__init__(**kwargs) self._mesh = Mesh(mode='lines') self._mesh_rect = Mesh(mode='line_strip') val = 0.25 self.canvas.add(Color(1 * val, 1 * val, 1 * val)) self.canvas.add(self._mesh) self.canvas.add(Color(1, 1, 1)) self.canvas.add(self._mesh_rect) mesh = self._mesh_rect mesh.vertices = [0] * (5 * 4) mesh.indices = [k for k in xrange(5)] self._plot_area = StencilView() self.add_widget(self._plot_area) self._trigger = Clock.create_trigger(self._redraw_all) self._trigger_size = Clock.create_trigger(self._redraw_size) self.bind(center=self._trigger_size, padding=self._trigger_size, font_size=self._trigger_size, plots=self._trigger_size, x_grid=self._trigger_size, y_grid=self._trigger_size, draw_border=self._trigger_size) self.bind(xmin=self._trigger, xmax=self._trigger, xlog=self._trigger, x_ticks_major=self._trigger, x_ticks_minor=self._trigger, xlabel=self._trigger, x_grid_label=self._trigger, ymin=self._trigger, ymax=self._trigger, ylog=self._trigger, y_ticks_major=self._trigger, y_ticks_minor=self._trigger, ylabel=self._trigger, y_grid_label=self._trigger) self._trigger() def _get_ticks(self, major, minor, log, s_min, s_max): if major and s_max > s_min: if log: s_min = log10(s_min) s_max = log10(s_max) # count the decades in min - max. This is in actual decades, # not logs. n_decades = floor(s_max - s_min) # for the fractional part of the last decade, we need to # convert the log value, x, to 10**x but need to handle # differently if the last incomplete decade has a decade # boundary in it if floor(s_min + n_decades) != floor(s_max): n_decades += 1 - (10 ** (s_min + n_decades + 1) - 10 ** s_max) / 10 ** floor(s_max + 1) else: n_decades += ((10 ** s_max - 10 ** (s_min + n_decades)) / 10 ** floor(s_max + 1)) # this might be larger than what is needed, but we delete # excess later n_ticks_major = n_decades / float(major) n_ticks = int(floor(n_ticks_major * (minor if minor >= 1. else 1.0))) + 2 # in decade multiples, e.g. 0.1 of the decade, the distance # between ticks decade_dist = major / float(minor if minor else 1.0) points_minor = [0] * n_ticks points_major = [0] * n_ticks k = 0 # position in points major k2 = 0 # position in points minor # because each decade is missing 0.1 of the decade, if a tick # falls in < min_pos skip it min_pos = 0.1 - 0.00001 * decade_dist s_min_low = floor(s_min) # first real tick location. value is in fractions of decades # from the start we have to use decimals here, otherwise # floating point inaccuracies results in bad values start_dec = ceil((10 ** Decimal(s_min - s_min_low - 1)) / Decimal(decade_dist)) * decade_dist count_min = (0 if not minor else floor(start_dec / decade_dist) % minor) start_dec += s_min_low count = 0 # number of ticks we currently have passed start while True: # this is the current position in decade that we are. # e.g. -0.9 means that we're at 0.1 of the 10**ceil(-0.9) # decade pos_dec = start_dec + decade_dist * count pos_dec_low = floor(pos_dec) diff = pos_dec - pos_dec_low zero = abs(diff) < 0.001 * decade_dist if zero: # the same value as pos_dec but in log scale pos_log = pos_dec_low else: pos_log = log10((pos_dec - pos_dec_low ) * 10 ** ceil(pos_dec)) if pos_log > s_max: break count += 1 if zero or diff >= min_pos: if minor and not count_min % minor: points_major[k] = pos_log k += 1 else: points_minor[k2] = pos_log k2 += 1 count_min += 1 # n_ticks = len(points) else: # distance between each tick tick_dist = major / float(minor if minor else 1.0) n_ticks = int(floor((s_max - s_min) / tick_dist) + 1) points_major = [0] * int( floor((s_max - s_min) / float(major)) + 1 ) points_minor = [0] * (n_ticks - len(points_major) + 1) k = 0 # position in points major k2 = 0 # position in points minor for m in xrange(0, n_ticks): if minor and m % minor: points_minor[k2] = m * tick_dist + s_min k2 += 1 else: points_major[k] = m * tick_dist + s_min k += 1 del points_major[k:] del points_minor[k2:] else: points_major = [] points_minor = [] return points_major, points_minor def _update_labels(self): xlabel = self._xlabel ylabel = self._ylabel x = self.x y = self.y width = self.width height = self.height padding = self.padding x_next = padding + x y_next = padding + y xextent = x + width yextent = y + height ymin = self.ymin ymax = self.ymax xmin = self.xmin precision = self.precision x_overlap = False y_overlap = False # set up x and y axis labels if xlabel: xlabel.text = self.xlabel xlabel.texture_update() xlabel.size = xlabel.texture_size xlabel.pos = (x + width / 2. - xlabel.width / 2., padding + y) y_next += padding + xlabel.height if ylabel: ylabel.text = self.ylabel ylabel.texture_update() ylabel.size = ylabel.texture_size ylabel.x = padding + x - (ylabel.width / 2. - ylabel.height / 2.) x_next += padding + ylabel.height xpoints = self._ticks_majorx xlabels = self._x_grid_label xlabel_grid = self.x_grid_label ylabel_grid = self.y_grid_label ypoints = self._ticks_majory ylabels = self._y_grid_label # now x and y tick mark labels if len(ylabels) and ylabel_grid: # horizontal size of the largest tick label, to have enough room ylabels[0].text = precision % ypoints[0] ylabels[0].texture_update() y1 = ylabels[0].texture_size y_start = y_next + ( padding + y1[1] if len(xlabels) and xlabel_grid else 0 ) + (padding + y1[1] if not y_next else 0) yextent = y + height - padding - y1[1] / 2. if self.ylog: ymax = log10(ymax) ymin = log10(ymin) ratio = (yextent - y_start) / float(ymax - ymin) y_start -= y1[1] / 2. func = (lambda x: 10 ** x) if self.ylog else lambda x: x y1 = y1[0] for k in xrange(len(ylabels)): ylabels[k].text = precision % func(ypoints[k]) ylabels[k].texture_update() ylabels[k].size = ylabels[k].texture_size y1 = max(y1, ylabels[k].texture_size[0]) ylabels[k].pos = (x_next, y_start + (ypoints[k] - ymin) * ratio) if len(ylabels) > 1 and ylabels[0].top > ylabels[1].y: y_overlap = True else: x_next += y1 + padding if len(xlabels) and xlabel_grid: func = log10 if self.xlog else lambda x: x # find the distance from the end that'll fit the last tick label xlabels[0].text = precision % func(xpoints[-1]) xlabels[0].texture_update() xextent = x + width - xlabels[0].texture_size[0] / 2. - padding # find the distance from the start that'll fit the first tick label if not x_next: xlabels[0].text = precision % func(xpoints[0]) xlabels[0].texture_update() x_next = padding + xlabels[0].texture_size[0] / 2. xmin = func(xmin) ratio = (xextent - x_next) / float(func(self.xmax) - xmin) func = (lambda x: 10 ** x) if self.xlog else lambda x: x right = -1 for k in xrange(len(xlabels)): xlabels[k].text = precision % func(xpoints[k]) # update the size so we can center the labels on ticks xlabels[k].texture_update() xlabels[k].size = xlabels[k].texture_size xlabels[k].pos = (x_next + (xpoints[k] - xmin) * ratio - xlabels[k].texture_size[0] / 2., y_next) if xlabels[k].x < right: x_overlap = True break right = xlabels[k].right if not x_overlap: y_next += padding + xlabels[0].texture_size[1] # now re-center the x and y axis labels if xlabel: xlabel.x = x_next + (xextent - x_next) / 2. - xlabel.width / 2. if ylabel: ylabel.y = y_next + (yextent - y_next) / 2. - ylabel.height / 2. t = Matrix().translate(ylabel.center[0], ylabel.center[1], 0) t = t.multiply(Matrix().rotate(-radians(270), 0, 0, 1)) ylabel.transform = t.multiply(Matrix().translate(-ylabel.center[0], -ylabel.center[1], 0)) if x_overlap: for k in xrange(len(xlabels)): xlabels[k].text = '' if y_overlap: for k in xrange(len(ylabels)): ylabels[k].text = '' return x_next, y_next, xextent, yextent def _update_ticks(self, size): # re-compute the positions of the bounding rectangle mesh = self._mesh_rect vert = mesh.vertices if self.draw_border: vert[0] = size[0] vert[1] = size[1] vert[4] = size[2] vert[5] = size[1] vert[8] = size[2] vert[9] = size[3] vert[12] = size[0] vert[13] = size[3] vert[16] = size[0] vert[17] = size[1] else: vert[0:18] = [0 for k in xrange(18)] mesh.vertices = vert # re-compute the positions of the x/y axis ticks mesh = self._mesh vert = mesh.vertices start = 0 xpoints = self._ticks_majorx ypoints = self._ticks_majory ylog = self.ylog xlog = self.xlog xmin = self.xmin xmax = self.xmax if xlog: xmin = log10(xmin) xmax = log10(xmax) ymin = self.ymin ymax = self.ymax if ylog: xmin = log10(ymin) ymax = log10(ymax) if len(xpoints): top = size[3] if self.x_grid else metrics.dp(12) + size[1] ratio = (size[2] - size[0]) / float(xmax - xmin) for k in xrange(start, len(xpoints) + start): vert[k * 8] = size[0] + (xpoints[k - start] - xmin) * ratio vert[k * 8 + 1] = size[1] vert[k * 8 + 4] = vert[k * 8] vert[k * 8 + 5] = top start += len(xpoints) if len(ypoints): top = size[2] if self.y_grid else metrics.dp(12) + size[0] ratio = (size[3] - size[1]) / float(ymax - ymin) for k in xrange(start, len(ypoints) + start): vert[k * 8 + 1] = size[1] + (ypoints[k - start] - ymin) * ratio vert[k * 8 + 5] = vert[k * 8 + 1] vert[k * 8] = size[0] vert[k * 8 + 4] = top mesh.vertices = vert def _update_plots(self, size): ylog = self.ylog xlog = self.xlog xmin = self.xmin xmax = self.xmax ymin = self.ymin ymax = self.ymax for plot in self.plots: plot._update(xlog, xmin, xmax, ylog, ymin, ymax, size) def _redraw_all(self, *args): # add/remove all the required labels font_size = self.font_size if self.xlabel: if not self._xlabel: xlabel = Label(font_size=font_size) self.add_widget(xlabel) self._xlabel = xlabel else: xlabel = self._xlabel if xlabel: self.remove_widget(xlabel) self._xlabel = None grids = self._x_grid_label xpoints_major, xpoints_minor = self._get_ticks(self.x_ticks_major, self.x_ticks_minor, self.xlog, self.xmin, self.xmax) self._ticks_majorx = xpoints_major self._ticks_minorx = xpoints_minor if not self.x_grid_label: n_labels = 0 else: n_labels = len(xpoints_major) for k in xrange(n_labels, len(grids)): self.remove_widget(grids[k]) del grids[n_labels:] grid_len = len(grids) grids.extend([None] * (n_labels - len(grids))) for k in xrange(grid_len, n_labels): grids[k] = Label(font_size=font_size) self.add_widget(grids[k]) if self.ylabel: if not self._ylabel: ylabel = RotateLabel(font_size=font_size) self.add_widget(ylabel) self._ylabel = ylabel else: ylabel = self._ylabel if ylabel: self.remove_widget(ylabel) self._ylabel = None grids = self._y_grid_label ypoints_major, ypoints_minor = self._get_ticks(self.y_ticks_major, self.y_ticks_minor, self.ylog, self.ymin, self.ymax) self._ticks_majory = ypoints_major self._ticks_minory = ypoints_minor if not self.y_grid_label: n_labels = 0 else: n_labels = len(ypoints_major) for k in xrange(n_labels, len(grids)): self.remove_widget(grids[k]) del grids[n_labels:] grid_len = len(grids) grids.extend([None] * (n_labels - len(grids))) for k in xrange(grid_len, n_labels): grids[k] = Label(font_size=font_size) self.add_widget(grids[k]) mesh = self._mesh n_points = (len(xpoints_major) + len(xpoints_minor) + len(ypoints_major) + len(ypoints_minor)) mesh.vertices = [0] * (n_points * 8) mesh.indices = [k for k in xrange(n_points * 2)] self._redraw_size() def _redraw_size(self, *args): # size a 4-tuple describing the bounding box in which we can draw # graphs, it's (x0, y0, x1, y1), which correspond with the bottom left # and top right corner locations, respectively size = self._update_labels() self._plot_area.pos = (size[0], size[1]) self._plot_area.size = (size[2] - size[0], size[3] - size[1]) self._update_ticks(size) self._update_plots(size) def add_plot(self, plot): '''Add a new plot to this graph. :Parameters: `plot`: Plot to add to this graph. >>> graph = Graph() >>> plot = MeshLinePlot(mode='line_strip', color=[1, 0, 0, 1]) >>> plot.points = [(x / 10., sin(x / 50.)) for x in xrange(-0, 101)] >>> graph.add_plot(plot) ''' area = self._plot_area for group in plot._get_drawings(): area.canvas.add(group) self.plots = self.plots + [plot] def remove_plot(self, plot): '''Remove a plot from this graph. :Parameters: `plot`: Plot to remove from this graph. >>> graph = Graph() >>> plot = MeshLinePlot(mode='line_strip', color=[1, 0, 0, 1]) >>> plot.points = [(x / 10., sin(x / 50.)) for x in xrange(-0, 101)] >>> graph.add_plot(plot) >>> graph.remove_plot(plot) ''' self._plot_area.canvas.remove_group(plot._get_group()) self.plots.remove(plot) xmin = NumericProperty(0.) '''The x-axis minimum value. If :data:`xlog` is True, xmin must be larger than zero. :data:`xmin` is a :class:`~kivy.properties.NumericProperty`, defaults to 0. ''' xmax = NumericProperty(100.) '''The x-axis maximum value, larger than xmin. :data:`xmax` is a :class:`~kivy.properties.NumericProperty`, defaults to 0. ''' xlog = BooleanProperty(False) '''Determines whether the x-axis should be displayed logarithmically (True) or linearly (False). :data:`xlog` is a :class:`~kivy.properties.BooleanProperty`, defaults to False. ''' x_ticks_major = BoundedNumericProperty(0, min=0) '''Distance between major tick marks on the x-axis. Determines the distance between the major tick marks. Major tick marks start from min and re-occur at every ticks_major until :data:`xmax`. If :data:`xmax` doesn't overlap with a integer multiple of ticks_major, no tick will occur at :data:`xmax`. Zero indicates no tick marks. If :data:`xlog` is true, then this indicates the distance between ticks in multiples of current decade. E.g. if :data:`xmin` is 0.1 and ticks_major is 0.1, it means there will be a tick at every 10th of the decade, i.e. 0.1 ... 0.9, 1, 2... If it is 0.3, the ticks will occur at 0.1, 0.3, 0.6, 0.9, 2, 5, 8, 10. You'll notice that it went from 8 to 10 instead of to 20, that's so that we can say 0.5 and have ticks at every half decade, e.g. 0.1, 0.5, 1, 5, 10, 50... Similarly, if ticks_major is 1.5, there will be ticks at 0.1, 5, 100, 5,000... Also notice, that there's always a major tick at the start. Finally, if e.g. :data:`xmin` is 0.6 and this 0.5 there will be ticks at 0.6, 1, 5... :data:`x_ticks_major` is a :class:`~kivy.properties.BoundedNumericProperty`, defaults to 0. ''' x_ticks_minor = BoundedNumericProperty(0, min=0) '''The number of sub-intervals that divide x_ticks_major. Determines the number of sub-intervals into which ticks_major is divided, if non-zero. The actual number of minor ticks between the major ticks is ticks_minor - 1. Only used if ticks_major is non-zero. If there's no major tick at xmax then the number of minor ticks after the last major tick will be however many ticks fit until xmax. If self.xlog is true, then this indicates the number of intervals the distance between major ticks is divided. The result is the number of multiples of decades between ticks. I.e. if ticks_minor is 10, then if ticks_major is 1, there will be ticks at 0.1, 0.2...0.9, 1, 2, 3... If ticks_major is 0.3, ticks will occur at 0.1, 0.12, 0.15, 0.18... Finally, as is common, if ticks major is 1, and ticks minor is 5, there will be ticks at 0.1, 0.2, 0.4... 0.8, 1, 2... :data:`x_ticks_minor` is a :class:`~kivy.properties.BoundedNumericProperty`, defaults to 0. ''' x_grid = BooleanProperty(False) '''Determines whether the x-axis has tick marks or a full grid. If :data:`x_ticks_major` is non-zero, then if x_grid is False tick marks will be displayed at every major tick. If x_grid is True, instead of ticks, a vertical line will be displayed at every major tick. :data:`x_grid` is a :class:`~kivy.properties.BooleanProperty`, defaults to False. ''' x_grid_label = BooleanProperty(False) '''Whether labels should be displayed beneath each major tick. If true, each major tick will have a label containing the axis value. :data:`x_grid_label` is a :class:`~kivy.properties.BooleanProperty`, defaults to False. ''' xlabel = StringProperty('') '''The label for the x-axis. If not empty it is displayed in the center of the axis. :data:`xlabel` is a :class:`~kivy.properties.StringProperty`, defaults to ''. ''' ymin = NumericProperty(0.) '''The y-axis minimum value. If :data:`ylog` is True, ymin must be larger than zero. :data:`ymin` is a :class:`~kivy.properties.NumericProperty`, defaults to 0. ''' ymax = NumericProperty(100.) '''The y-axis maximum value, larger than ymin. :data:`ymax` is a :class:`~kivy.properties.NumericProperty`, defaults to 0. ''' ylog = BooleanProperty(False) '''Determines whether the y-axis should be displayed logarithmically (True) or linearly (False). :data:`ylog` is a :class:`~kivy.properties.BooleanProperty`, defaults to False. ''' y_ticks_major = BoundedNumericProperty(0, min=0) '''Distance between major tick marks. See :data:`x_ticks_major`. :data:`y_ticks_major` is a :class:`~kivy.properties.BoundedNumericProperty`, defaults to 0. ''' y_ticks_minor = BoundedNumericProperty(0, min=0) '''The number of sub-intervals that divide ticks_major. See :data:`x_ticks_minor`. :data:`y_ticks_minor` is a :class:`~kivy.properties.BoundedNumericProperty`, defaults to 0. ''' y_grid = BooleanProperty(False) '''Determines whether the y-axis has tick marks or a full grid. See :data:`x_grid`. :data:`y_grid` is a :class:`~kivy.properties.BooleanProperty`, defaults to False. ''' y_grid_label = BooleanProperty(False) '''Whether labels should be displayed beneath each major tick. If true, each major tick will have a label containing the axis value. :data:`y_grid_label` is a :class:`~kivy.properties.BooleanProperty`, defaults to False. ''' ylabel = StringProperty('') '''The label for the y-axis. If not empty it is displayed in the center of the axis. :data:`ylabel` is a :class:`~kivy.properties.StringProperty`, defaults to ''. ''' padding = NumericProperty('5dp') '''Padding distances between the labels, titles and graph, as well between the widget and the objects near the boundaries. :data:`padding` is a :class:`~kivy.properties.NumericProperty`, defaults to 5dp. ''' font_size = NumericProperty('15sp') '''Font size of the labels. :data:`font_size` is a :class:`~kivy.properties.NumericProperty`, defaults to 15sp. ''' precision = StringProperty('%g') '''Determines the numerical precision of the tick mark labels. This value governs how the numbers are converted into string representation. Accepted values are those listed in Python's manual in the "String Formatting Operations" section. :data:`precision` is a :class:`~kivy.properties.StringProperty`, defaults to '%g'. ''' draw_border = BooleanProperty(True) '''Whether a border is drawn around the canvas of the graph where the plots are displayed. :data:`draw_border` is a :class:`~kivy.properties.BooleanProperty`, defaults to True. ''' plots = ListProperty([]) '''Holds a list of all the plots in the graph. To add and remove plots
class AndroidTabs(AnchorLayout): ''' The AndroidTabs class. You can use it to create your own custom tabbed panel. ''' default_tab = NumericProperty(0) ''' Index of the default tab. Default to 0. ''' tab_bar_height = NumericProperty('48dp') ''' Height of the tab bar. ''' tab_indicator_anim = BooleanProperty(True) ''' Tab indicator animation. Default to True. If you do not want animation set it to False. ''' tab_indicator_height = NumericProperty('2dp') ''' Height of the tab indicator. ''' tab_indicator_color = VariableListProperty([1]) ''' Color of the tab indicator. ''' anim_duration = NumericProperty(0.2) ''' Duration of the slide animation. Default to 0.2. ''' anim_threshold = BoundedNumericProperty(0.8, min=0.0, max=1.0, errorhandler=lambda x: 0.0 if x < 0.0 else 1.0) ''' Animation threshold allow you to change the tab indicator animation effect. Default to 0.8. ''' def on_carousel_index(self, carousel, index): # when the index of the carousel change, update # tab indicator, select the current tab and reset threshold data. current_tab_label = carousel.current_slide.tab_label if current_tab_label.state == 'normal': current_tab_label._do_press() self.tab_bar.update_indicator(current_tab_label.x, current_tab_label.width) def add_widget(self, widget): # You can add only subclass of AndroidTabsBase. if len(self.children) >= 2: if not issubclass(widget.__class__, AndroidTabsBase): raise AndroidTabsException( 'AndroidTabs accept only subclass of AndroidTabsBase') widget.tab_label.tab_bar = self.tab_bar self.tab_bar.layout.add_widget(widget.tab_label) self.carousel.add_widget(widget) return return super(AndroidTabs, self).add_widget(widget) def remove_widget(self, widget): # You can remove only subclass of AndroidTabsBase. if not issubclass(widget.__class__, AndroidTabsBase): raise AndroidTabsException( 'AndroidTabs can remove only subclass of AndroidTabBase') if widget.parent.parent == self.carousel: self.tab_bar.layout.remove_widget(widget.tab_label) self.carousel.remove_widget(widget)
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`. """ 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, *args): if self.background_origin: self._background_origin = self.background_origin else: self._background_origin = self.center
class ProductView(Screen): id = None amount = BoundedNumericProperty(1, min=1, max=100, errorvalue=1) progress = BoundedNumericProperty(0, min=0, max=100, errorvalue=1) def on_leave(self): self.stop_timer() self.ids.product_image.source = "" def getProduct(self): api = self.manager.api barcode = self.manager.scanned response = api.search(barcode, self.search_succes, self.search_failed) def search_failed(self, err): popup = Popup(title="Error", content=Label(text="%s" % (err)), size_hint=(None, None), size=(800, 300)) popup.open() self.manager.go_back("ScannerView") def search_succes(self, req, content): response = json.loads(content) api = self.manager.api if api.responseIsSuccess(response): barcode = self.manager.scanned print "zoeken van product met barcode %s: %s" % ( barcode, response["status"]["meaning"]) self.amount = 1 self.id = productId = response["data"]["searchResults"][0]["list"][ 0]["id"] productBrand = response["data"]["searchResults"][0]["list"][0][ "brand"] productDescription = response["data"]["searchResults"][0]["list"][ 0]["description"] productImagePath = response["data"]["searchResults"][0]["list"][0][ "overviewImage"] price = response["data"]["searchResults"][0]["list"][0]["price"] image = api.get_product_image(productImagePath) self.ids.product_image.source = image self.ids.product_description.text = "%s - %s : %s euro" % ( productBrand, productDescription, price) print "Product [%s] %s - %s : %s euro" % ( productId, productBrand, productDescription, price) self.progress = 0 Clock.schedule_interval( self.progress_callback, float(App.get_running_app().config.getdefaultint( "ColliScanner", "wait_time", 10)) / 100) else: self.search_failed("Search failed: %s" % (response["status"]["meaning"])) def progress_callback(self, dt): self.progress += 1 self.ids.pbProgress.value = self.progress if self.progress == 100: self.add_product() return False return True def increase(self): self.amount += 1 self.stop_timer() def decrease(self): self.amount -= 1 self.stop_timer() def stop_timer(self): Clock.unschedule(self.progress_callback) self.progress = 0 self.ids.pbProgress.value = self.progress def confirm(self): self.add_product() def add_product(self): self.manager.api.add(self.id, self.amount, "S", self.add_success, self.add_failed) def add_success(self, req, content): response = json.loads(content) if self.manager.api.responseIsSuccess(response): print "%s stuks toevoegen aan de winkelmand: %s" % ( self.amount, response["status"]["meaning"]) self.manager.go_back("ScannerView") else: self.add_failed("Fout bij toevoegen: %s" % (response["status"]["meaning"])) def add_failed(self, err): popup = Popup(title="Error", content=Label(text="%s" % (err)), size_hint=(None, None), size=(800, 300)) popup.open()
class RangeSlider(Widget): """Class for creating a RangeSlider widget. Check module documentation for more details. """ connector_color = ListProperty([1, .55, 0, 1]) '''Connector bar color, in the format (r, g, b, a). for disabling this bar use a = .0 ''' def _get_value(self): return [self.value1, self.value2] def _set_value(self, value): self.value1, self.value2 = value value = AliasProperty(_get_value, _set_value, bind=('value1', 'value2')) '''Current value used for the both sliders. :attr:`value` is an :class:`~kivy.properties.AliasProperty` and defaults to [0, 0].''' value1 = NumericProperty(0.) '''Current value used for the first slider. :attr:`value` is a :class:`~kivy.properties.NumericProperty` and defaults to 0.''' value2 = NumericProperty(100.) '''Current value used for the second 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.value1 = min(self.max, max(self.min, self.value1)) self.value2 = min(self.max, max(self.min, self.value2)) def on_max(self, *largs): self.value1 = min(self.max, max(self.min, self.value1)) self.value2 = min(self.max, max(self.min, self.value2)) def get_norm_value1(self): vmin = self.min d = self.max - vmin if d == 0: return 0 return (self.value1 - vmin) / float(d) def get_norm_value2(self): vmin = self.min d = self.max - vmin if d == 0: return 0 return (self.value2 - vmin) / float(d) def set_norm_value1(self, value): vmin = self.min step = self.step val = value * (self.max - vmin) + vmin if step == 0: self.value1 = val else: self.value1 = min( round((val - vmin) / step) * step + vmin, self.max) def set_norm_value2(self, value): vmin = self.min step = self.step val = value * (self.max - vmin) + vmin if step == 0: self.value2 = val else: self.value2 = min( round((val - vmin) / step) * step + vmin, self.max) value1_normalized = AliasProperty(get_norm_value1, set_norm_value1, bind=('value1', 'min', 'max', 'step')) value2_normalized = AliasProperty(get_norm_value2, set_norm_value2, bind=('value2', '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_value1_pos(self): padding = self.padding x = self.x y = self.y nval = self.value1_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 get_value2_pos(self): padding = self.padding x = self.x y = self.y nval = self.value2_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_value1_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.value1_normalized = 0 else: self.value1_normalized = ( x - self.x - padding) / float(self.width - 2 * padding) else: if self.height == 0: self.value1_normalized = 0 else: self.value1_normalized = ( y - self.y - padding) / float(self.height - 2 * padding) def set_value2_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.value2_normalized = 0 else: self.value2_normalized = ( x - self.x - padding) / float(self.width - 2 * padding) else: if self.height == 0: self.value2_normalized = 0 else: self.value2_normalized = ( y - self.y - padding) / float(self.height - 2 * padding) value1_pos = AliasProperty(get_value1_pos, set_value1_pos, bind=('x', 'y', 'width', 'height', 'min', 'max', 'value1_normalized', 'orientation')) value2_pos = AliasProperty(get_value2_pos, set_value2_pos, bind=('x', 'y', 'width', 'height', 'min', 'max', 'value2_normalized', 'orientation')) '''Position of the internal cursor, based on the normalized value. :attr:`value_pos` is an :class:`~kivy.properties.AliasProperty`. ''' def _touch_normalized_value(self, touch): pos = touch.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': value = (x - self.x - padding) / float(self.width - 2 * padding) else: value = (y - self.y - padding) / float(self.height - 2 * padding) return value def on_touch_down(self, touch): if self.disabled or not self.collide_point(*touch.pos): return touch.grab(self) t_value = self._touch_normalized_value(touch) if abs(self.value1_normalized - t_value) < abs(self.value2_normalized - t_value): self.value1_pos = touch.pos touch.ud['cursorid'] = 1 else: self.value2_pos = touch.pos touch.ud['cursorid'] = 2 return True def on_touch_move(self, touch): if touch.grab_current == self: if 'cursorid' in touch.ud: if touch.ud['cursorid'] == 1: self.value1_pos = touch.pos if self.value1 > self.value2: self.value1_pos = self.value2_pos elif touch.ud['cursorid'] == 2: self.value2_pos = touch.pos if self.value2 < self.value1: self.value2_pos = self.value1_pos return True def on_touch_up(self, touch): if touch.grab_current == self: touch.ungrab(self) return True
class MDTabs(ThemableBehavior, SpecificBackgroundColorBehavior, AnchorLayout): """ You can use this class to create your own tabbed panel. :Events: `on_tab_switch` Called when switching tabs. `on_slide_progress` Called while the slide is scrolling. `on_ref_press` The method will be called when the ``on_ref_press`` event occurs when you, for example, use markup text for tabs. """ default_tab = NumericProperty(0) """ Index of the default tab. :attr:`default_tab` is an :class:`~kivy.properties.NumericProperty` and defaults to `0`. """ tab_bar_height = NumericProperty("48dp") """ Height of the tab bar. :attr:`tab_bar_height` is an :class:`~kivy.properties.NumericProperty` and defaults to `'48dp'`. """ tab_indicator_anim = BooleanProperty(False) """ Tab indicator animation. If you want use animation set it to ``True``. :attr:`tab_indicator_anim` is an :class:`~kivy.properties.BooleanProperty` and defaults to `False`. """ tab_indicator_height = NumericProperty("2dp") """ Height of the tab indicator. :attr:`tab_indicator_height` is an :class:`~kivy.properties.NumericProperty` and defaults to `'2dp'`. """ tab_indicator_type = OptionProperty( "line", options=["line", "fill", "round", "line-round", "line-rect"]) """ Type of tab indicator. Available options are: `'line'`, `'fill'`, `'round'`, `'line-rect'` and `'line-round'`. :attr:`tab_indicator_type` is an :class:`~kivy.properties.OptionProperty` and defaults to `'line'`. """ anim_duration = NumericProperty(0.2) """ Duration of the slide animation. :attr:`anim_duration` is an :class:`~kivy.properties.NumericProperty` and defaults to `0.2`. """ anim_threshold = BoundedNumericProperty(0.8, min=0.0, max=1.0, errorhandler=lambda x: 0.0 if x < 0.0 else 1.0) """ Animation threshold allow you to change the tab indicator animation effect. :attr:`anim_threshold` is an :class:`~kivy.properties.BoundedNumericProperty` and defaults to `0.8`. """ allow_stretch = BooleanProperty(True) """ If False - tabs will not stretch to full screen. :attr:`allow_stretch` is an :class:`~kivy.properties.BooleanProperty` and defaults to `True`. """ background_color = ColorProperty(None) """ Background color of tabs in ``rgba`` format. :attr:`background_color` is an :class:`~kivy.properties.ColorProperty` and defaults to `None`. """ text_color_normal = ColorProperty(None) """ Text color of the label when it is not selected. :attr:`text_color_normal` is an :class:`~kivy.properties.ColorProperty` and defaults to `None`. """ text_color_active = ColorProperty(None) """ Text color of the label when it is selected. :attr:`text_color_active` is an :class:`~kivy.properties.ColorProperty` and defaults to `None`. """ elevation = NumericProperty(0) """ Tab value elevation. .. seealso:: `Behaviors/Elevation <https://kivymd.readthedocs.io/en/latest/behaviors/elevation/index.html>`_ :attr:`elevation` is an :class:`~kivy.properties.NumericProperty` and defaults to `0`. """ indicator_color = ColorProperty(None) """ Color indicator in ``rgba`` format. :attr:`indicator_color` is an :class:`~kivy.properties.ColorProperty` and defaults to `None`. """ lock_swiping = BooleanProperty(False) """ If True - disable switching tabs by swipe. :attr:`lock_swiping` is an :class:`~kivy.properties.BooleanProperty` and defaults to `False`. """ font_name = StringProperty("Roboto") """ Font name for tab text. :attr:`font_name` is an :class:`~kivy.properties.StringProperty` and defaults to `'Roboto'`. """ ripple_duration = NumericProperty(2) """ Ripple duration when long touching to tab. :attr:`ripple_duration` is an :class:`~kivy.properties.NumericProperty` and defaults to `2`. """ no_ripple_effect = BooleanProperty(True) """ Whether to use the ripple effect when tapping on a tab. :attr:`no_ripple_effect` is an :class:`~kivy.properties.BooleanProperty` and defaults to `True`. """ def __init__(self, **kwargs): super().__init__(**kwargs) self.register_event_type("on_tab_switch") self.register_event_type("on_ref_press") self.register_event_type("on_slide_progress") Clock.schedule_once(self._carousel_bind, 1) self.theme_cls.bind(primary_palette=self.update_icon_color) self.theme_cls.bind(theme_style=self.update_icon_color) def update_icon_color(self, instance, value): for tab_label in self.get_tab_list(): if not self.text_color_normal: tab_label.text_color_normal = self.theme_cls.text_color if not self.text_color_active: tab_label.text_color_active = self.specific_secondary_text_color def switch_tab(self, name_tab): """Switching the tab by name.""" for instance_tab in self.tab_bar.parent.carousel.slides: if instance_tab.text == name_tab: self.tab_bar.parent.carousel.load_slide(instance_tab) break for tab_label in self.get_tab_list(): if name_tab == tab_label.text: self.ids.scrollview.scroll_to(tab_label) break def get_tab_list(self): """Returns a list of tab objects.""" return self.tab_bar.layout.children def add_widget(self, widget, index=0, canvas=None): # You can add only subclass of MDTabsBase. if issubclass(widget.__class__, MDTabsBase): try: # FIXME: Can't set the value of the `no_ripple_effect` # and `ripple_duration` properties for widget.tab_label. widget.tab_label._no_ripple_effect = self.no_ripple_effect widget.tab_label.ripple_duration_in_slow = self.ripple_duration widget.tab_label.group = str(self) widget.tab_label.tab_bar = self.tab_bar widget.tab_label.text_color_normal = ( self.text_color_normal if self.text_color_normal else self.theme_cls.text_color) widget.tab_label.text_color_active = ( self.text_color_active if self.text_color_active else self.specific_secondary_text_color) self.bind(font_name=widget.tab_label.setter("font_name")) self.bind(text_color_active=widget.tab_label.setter( "text_color_active")) self.bind(text_color_normal=widget.tab_label.setter( "text_color_normal")) self.tab_bar.layout.add_widget(widget.tab_label) self.carousel.add_widget(widget) return except AttributeError: pass if isinstance(widget, (MDTabsMain, MDTabsBar)): return super().add_widget(widget) def remove_widget(self, widget): # You can remove only subclass of MDTabsLabel. if not issubclass(widget.__class__, MDTabsLabel): raise MDTabsException( "MDTabs can remove only subclass of MDTabsLabel") # The last tab is not deleted. if len(self.tab_bar.layout.children) == 1: return # Search object next tab. next_tab = None for i, tab in enumerate(self.tab_bar.layout.children): if tab == widget: next_tab = self.tab_bar.layout.children[i - 1] break self.tab_bar.layout.remove_widget(widget) # After deleting the tab, it updates the indicator width # on the next tab. if next_tab: self._update_indicator(next_tab) for slide in self.carousel.slides: if slide.text == widget.text: self.carousel.remove_widget(slide) break def on_slide_progress(self, *args): """Called while the slide is scrolling.""" def on_carousel_index(self, carousel, index): """Called when the carousel index changes.""" # When the index of the carousel change, update tab indicator, # select the current tab and reset threshold data. if carousel.current_slide: current_tab_label = carousel.current_slide.tab_label if current_tab_label.state == "normal": # current_tab_label._do_press() current_tab_label.dispatch("on_release") if self.tab_indicator_type == "round": self.tab_indicator_height = self.tab_bar_height if index == 0: radius = [ 0, self.tab_bar_height / 2, self.tab_bar_height / 2, 0, ] self.tab_bar.update_indicator(current_tab_label.x, current_tab_label.width, radius) elif index == len(self.get_tab_list()) - 1: radius = [ self.tab_bar_height / 2, 0, 0, self.tab_bar_height / 2, ] self.tab_bar.update_indicator(current_tab_label.x, current_tab_label.width, radius) else: radius = [ self.tab_bar_height / 2, ] self.tab_bar.update_indicator(current_tab_label.x, current_tab_label.width, radius) elif (self.tab_indicator_type == "fill" or self.tab_indicator_type == "line-round" or self.tab_indicator_type == "line-rect"): self.tab_indicator_height = self.tab_bar_height self.tab_bar.update_indicator(current_tab_label.x, current_tab_label.width) else: self.tab_bar.update_indicator(current_tab_label.x, current_tab_label.width) def on_ref_press(self, *args): """The method will be called when the ``on_ref_press`` event occurs when you, for example, use markup text for tabs.""" def on_tab_switch(self, *args): """Called when switching tabs.""" def on_size(self, *args): if self.carousel.current_slide: self._update_indicator(self.carousel.current_slide.tab_label) def _carousel_bind(self, interval): self.carousel.bind(on_slide_progress=self._on_slide_progress) def _on_slide_progress(self, *args): self.dispatch("on_slide_progress", args) def _update_indicator(self, current_tab_label): def update_indicator(interval): self.tab_bar.update_indicator(current_tab_label.x, current_tab_label.width) if not current_tab_label: current_tab_label = self.tab_bar.layout.children[-1] Clock.schedule_once(update_indicator)
class Sound(EventDispatcher): '''Represents a sound to play. This class is abstract, and cannot be used directly. Use SoundLoader to load a sound. :Events: `on_play`: None Fired when the sound is played. `on_stop`: None Fired when the sound is stopped. ''' source = StringProperty(None) '''Filename / source of your audio file. .. versionadded:: 1.3.0 :attr:`source` is a :class:`~kivy.properties.StringProperty` that defaults to None and is read-only. Use the :meth:`SoundLoader.load` for loading audio. ''' volume = NumericProperty(1.) '''Volume, in the range 0-1. 1 means full volume, 0 means mute. .. versionadded:: 1.3.0 :attr:`volume` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. ''' pitch = BoundedNumericProperty(1., min=float_info.epsilon) '''Pitch of a sound. 2 is an octave higher, .5 one below. This is only implemented for SDL2 audio provider yet. .. versionadded:: 1.10.0 :attr:`pitch` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. ''' state = OptionProperty('stop', options=('stop', 'play')) '''State of the sound, one of 'stop' or 'play'. .. versionadded:: 1.3.0 :attr:`state` is a read-only :class:`~kivy.properties.OptionProperty`.''' loop = BooleanProperty(False) '''Set to True if the sound should automatically loop when it finishes. .. versionadded:: 1.8.0 :attr:`loop` is a :class:`~kivy.properties.BooleanProperty` and defaults to False.''' # # deprecated # def _get_status(self): return self.state status = AliasProperty( _get_status, None, bind=('state',), deprecated=True) ''' .. deprecated:: 1.3.0 Use :attr:`state` instead. ''' def _get_filename(self): return self.source filename = AliasProperty( _get_filename, None, bind=('source',), deprecated=True) ''' .. deprecated:: 1.3.0 Use :attr:`source` instead. ''' __events__ = ('on_play', 'on_stop') def on_source(self, instance, filename): self.unload() if filename is None: return self.load() def get_pos(self): ''' Returns the current position of the audio file. Returns 0 if not playing. .. versionadded:: 1.4.1 ''' return 0 def _get_length(self): return 0 length = property(lambda self: self._get_length(), doc='Get length of the sound (in seconds).') def load(self): '''Load the file into memory.''' pass def unload(self): '''Unload the file from memory.''' pass def play(self): '''Play the file.''' self.state = 'play' self.dispatch('on_play') def stop(self): '''Stop playback.''' self.state = 'stop' self.dispatch('on_stop') def seek(self, position): '''Go to the <position> (in seconds). .. note:: Most sound providers cannot seek when the audio is stopped. Play then seek. ''' pass def on_play(self): pass def on_stop(self): pass
class GridLayout(Layout): '''Grid layout class. See module documentation for more information. ''' spacing = NumericProperty(0) '''Spacing between widget box and children, in pixels. :data:`spacing` is a :class:`~kivy.properties.NumericProperty`, default to 0. ''' padding = NumericProperty(0) '''Padding between widget box and children, in pixels. :data:`padding` is a :class:`~kivy.properties.NumericProperty`, default to 0. ''' cols = BoundedNumericProperty(None, min=0, allow_none=True) '''Number of columns in the grid .. versionadded:: 1.0.8 Change from NumericProperty to BoundedNumericProperty. You cannot set a negative value anymore. :data:`cols` is a :class:`~kivy.properties.NumericProperty`, default to 0. ''' rows = BoundedNumericProperty(None, min=0, allow_none=True) '''Number of rows in the grid .. versionadded:: 1.0.8 Change from NumericProperty to BoundedNumericProperty. You cannot set a negative value anymore. :data:`rows` is a :class:`~kivy.properties.NumericProperty`, default to 0. ''' col_default_width = NumericProperty(0) '''Default minimum size to use for column .. versionadded:: 1.0.7 :data:`col_default_width` is a :class:`~kivy.properties.NumericProperty`, default to 0. ''' row_default_height = NumericProperty(0) '''Default minimum size to use for row .. versionadded:: 1.0.7 :data:`row_default_height` is a :class:`~kivy.properties.NumericProperty`, default to 0. ''' col_force_default = BooleanProperty(False) '''If True, ignore the width and size_hint_x of the child, and use the default column width. .. versionadded:: 1.0.7 :data:`col_force_default` is a :class:`~kivy.properties.BooleanProperty`, default to False. ''' row_force_default = BooleanProperty(False) '''If True, ignore the height and size_hint_y of the child, and use the default row height. .. versionadded:: 1.0.7 :data:`row_force_default` is a :class:`~kivy.properties.BooleanProperty`, default to False. ''' cols_minimum = DictProperty({}) '''List of minimum size for each column. .. versionadded:: 1.0.7 :data:`cols_minimum` is a :class:`~kivy.properties.DictProperty`, default to {} ''' rows_minimum = DictProperty({}) '''List of minimum size for each row. .. versionadded:: 1.0.7 :data:`rows_minimum` is a :class:`~kivy.properties.DictProperty`, default to {} ''' minimum_width = NumericProperty(0) '''Minimum width needed to contain all childrens. .. versionadded:: 1.0.8 :data:`minimum_width` is a :class:`kivy.properties.NumericProperty`, default to 0. ''' minimum_height = NumericProperty(0) '''Minimum height needed to contain all childrens. .. versionadded:: 1.0.8 :data:`minimum_height` is a :class:`kivy.properties.NumericProperty`, default to 0. ''' minimum_size = ReferenceListProperty(minimum_width, minimum_height) '''Minimum size needed to contain all childrens. .. versionadded:: 1.0.8 :data:`minimum_size` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`minimum_width`, :data:`minimum_height`) properties. ''' def __init__(self, **kwargs): self._cols = self._rows = None super(GridLayout, self).__init__(**kwargs) self.bind( col_default_width = self._trigger_layout, row_default_height = self._trigger_layout, col_force_default = self._trigger_layout, row_force_default = self._trigger_layout, cols = self._trigger_layout, rows = self._trigger_layout, parent = self._trigger_layout, spacing = self._trigger_layout, padding = self._trigger_layout, children = self._trigger_layout, size = self._trigger_layout, pos = self._trigger_layout) def add_widget(self, widget, index=0): widget.bind( size = self._trigger_layout, size_hint = self._trigger_layout) return super(Layout, self).add_widget(widget, index) def remove_widget(self, widget): widget.unbind( size = self._trigger_layout, size_hint = self._trigger_layout) return super(Layout, self).remove_widget(widget) def get_max_widgets(self): if self.cols and not self.rows: return None if self.rows and not self.cols: return None if not self.cols and not self.rows: return None return self.rows * self.cols def on_children(self, instance, value): # if that makes impossible to construct things with deffered method, # migrate this test in do_layout, and/or issue a warning. smax = self.get_max_widgets() if smax and len(value) > smax: raise GridLayoutException( 'Too much children in GridLayout. Increase your rows/cols!') def update_minimum_size(self, *largs): # the goal here is to calculate the minimum size of every cols/rows # and determine if they have stretch or not current_cols = self.cols current_rows = self.rows children = self.children len_children = len(children) # if no cols or rows are set, we can't calculate minimum size. # the grid must be contrained at least on one side if not current_cols and not current_rows: Logger.warning('%r have no cols or rows set, ' 'layout are not triggered.' % self) return None if current_cols is None: current_cols = int(ceil(len_children / float(current_rows))) elif current_rows is None: current_rows = int(ceil(len_children / float(current_cols))) current_cols = max(1, current_cols) current_rows = max(1, current_rows) cols = [self.col_default_width] * current_cols cols_sh = [None] * current_cols rows = [self.row_default_height] * current_rows rows_sh = [None] * current_rows # update minimum size from the dicts # FIXME index might be outside the bounds ? for index, value in self.cols_minimum.iteritems(): cols[index] = value for index, value in self.rows_minimum.iteritems(): rows[index] = value # calculate minimum size for each columns and rows i = len_children - 1 for row in xrange(current_rows): for col in xrange(current_cols): # don't go further is we don't have child left if i < 0: break # get initial information from the child c = children[i] shw = c.size_hint_x shh = c.size_hint_y w = c.width h = c.height # compute minimum size / maximum stretch needed if shw is None: cols[col] = max(cols[col], w) else: cols_sh[col] = max(cols_sh[col], shw) if shh is None: rows[row] = max(rows[row], h) else: rows_sh[row] = max(rows_sh[row], shh) # next child i = i - 1 # calculate minimum width/height needed, starting from padding + spacing padding2 = self.padding * 2 spacing = self.spacing width = padding2 + spacing * (current_cols - 1) height = padding2 + spacing * (current_rows - 1) # then add the cell size width += sum(cols) height += sum(rows) # remember for layout self._cols = cols self._rows = rows self._cols_sh = cols_sh self._rows_sh = rows_sh # finally, set the minimum size self.minimum_size = (width, height) def do_layout(self, *largs): self.update_minimum_size() if self._cols is None: return if self.cols is None and self.rows is None: raise GridLayoutException('Need at least cols or rows restriction.') children = self.children len_children = len(children) if len_children == 0: return # speedup padding = self.padding spacing = self.spacing selfx = self.x selfw = self.width selfh = self.height # resolve size for each column if self.col_force_default: cols = [self.col_default_width] * len(self._cols) for index, value in self.cols_minimum.iteritems(): cols[index] = value else: cols = self._cols[:] cols_sh = self._cols_sh cols_weigth = sum([x for x in cols_sh if x]) strech_w = max(0, selfw - self.minimum_width) for index in xrange(len(cols)): # if the col don't have strech information, nothing to do col_stretch = cols_sh[index] if col_stretch is None: continue # calculate the column stretch, and take the maximum from # minimum size and the calculated stretch col_width = cols[index] col_width = max(col_width, strech_w * col_stretch / cols_weigth) cols[index] = col_width # same algo for rows if self.row_force_default: rows = [self.row_default_height] * len(self._rows) for index, value in self.rows_minimum.iteritems(): rows[index] = value else: rows = self._rows[:] rows_sh = self._rows_sh rows_weigth = sum([x for x in rows_sh if x]) strech_h = max(0, selfh - self.minimum_height) for index in xrange(len(rows)): # if the row don't have strech information, nothing to do row_stretch = rows_sh[index] if row_stretch is None: continue # calculate the row stretch, and take the maximum from minimum # size and the calculated stretch row_height = rows[index] row_height = max(row_height, strech_h * row_stretch / rows_weigth) rows[index] = row_height # reposition every child i = len_children - 1 y = self.top - padding reposition_child = self.reposition_child for row_height in rows: x = selfx + padding for col_width in cols: if i < 0: break c = children[i] c_pos = x, y - row_height c_size = (col_width, row_height) reposition_child(c, pos=c_pos, size=c_size) i = i - 1 x = x + col_width + spacing y -= row_height + spacing
class FloatingActionButton(ThemeBehaviour, CircularRippleBehavior, ElevationBehaviour, ButtonBehavior, AnchorLayout): _bg_color_down = ListProperty([]) def _get_bg_color_down(self): return self._bg_color_down def _set_bg_color_down(self, color, alpha=None): if len(color) == 2: self._bg_color_down = get_rgba_color(color, control_alpha=alpha) elif len(color) == 4: self._bg_color_down = color background_color_down = AliasProperty(_get_bg_color_down, _set_bg_color_down, bind=('_bg_color_down', )) _bg_color_disabled = ListProperty([]) def _get_bg_color_disabled(self): return self._bg_color_disabled def _set_bg_color_disabled(self, color, alpha=None): if len(color) == 2: self._bg_color_disabled = get_rgba_color(color, control_alpha=alpha) elif len(color) == 4: self._bg_color_disabled = color background_color_disabled = AliasProperty(_get_bg_color_disabled, _set_bg_color_disabled, bind=('_bg_color_disabled', )) theme_style = OptionProperty(None, options=['Light', 'Dark', 'Custom'], allownone=True) icon_color = ListProperty(None, allownone=True) elevation_normal = BoundedNumericProperty(2, min=0, max=12) elevation_raised = BoundedNumericProperty(0, min=0, max=12) icon = StringProperty('md-android') def __init__(self, **kwargs): self.elevation = self.elevation_normal if self.elevation_raised == 0 and self.elevation_normal + 6 <= 12: self.elevation_raised = self.elevation_normal + 6 elif self.elevation_raised == 0: self.elevation_raised = 12 super(FloatingActionButton, self).__init__(**kwargs) self.background_color = self._theme_cls.primary_color self.background_color_down = self._theme_cls.primary_dark self.background_color_disabled = self._theme_cls.disabled_bg_color() self.elevation_press_anim = Animation(elevation=self.elevation_raised, duration=.2, t='out_quad') self.elevation_release_anim = Animation( elevation=self.elevation_normal, duration=.2, t='out_quad') def _set_ellipse(self, instance, value): ellipse = self.ellipse ripple_rad = self.ripple_rad ellipse.size = (ripple_rad, ripple_rad) ellipse.pos = (self.center_x - ripple_rad / 2., self.center_y - ripple_rad / 2.) def on_disabled(self, instance, value): super(FloatingActionButton, self).on_disabled(instance, value) if self.disabled: self.elevation = 0 else: self.elevation = self.elevation_normal def on_touch_down(self, touch): if not self.disabled: if touch.is_mouse_scrolling: return False if not self.collide_point(touch.x, touch.y): return False if self in touch.ud: return False self.elevation_press_anim.stop(self) self.elevation_press_anim.start(self) return super(FloatingActionButton, self).on_touch_down(touch) def on_touch_up(self, touch): if not self.disabled: if touch.grab_current is not self: return super(ButtonBehavior, self).on_touch_up(touch) self.elevation_release_anim.stop(self) self.elevation_release_anim.start(self) return super(FloatingActionButton, self).on_touch_up(touch) def on_elevation_normal(self, instance, value): self.elevation = value def on_elevation_raised(self, instance, value): if self.elevation_raised == 0 and self.elevation_normal + 6 <= 12: self.elevation_raised = self.elevation_normal + 6 elif self.elevation_raised == 0: self.elevation_raised = 12
class Player(Widget): """Player class.""" partisans = BoundedNumericProperty(0, min=0, rebind=True) swing = BoundedNumericProperty(0, min=0) media = BoundedNumericProperty(1, min=1) news = BoundedNumericProperty(1, min=0) mojo = BoundedNumericProperty(1, min=1) hype = BoundedNumericProperty(1, min=0) money = BoundedNumericProperty(1, min=1) cash = BoundedNumericProperty(1, min=0) state_area = ObjectProperty('') cards_actions = ListProperty([]) def __init__(self, **kwargs): """Init player.""" super(Player, self).__init__(**kwargs) self.player_id = None self.player_name = None self.stats = None def late_init(self, **kwargs): """Init player resources.""" self.player_id = kwargs.pop('player_id') self.card_factory = kwargs.pop('card_factory') is_bot = kwargs.pop('is_bot') self.human = False if is_bot else True self.bot = True if is_bot else False self.player_name = PLAYERS[self.player_id] self.stats = kwargs for prop_name, value in self.stats.items(): self.property(prop_name).set(self, value) self.RESOURSES = {1: 'news', 2: 'cash', 3: 'hype'} self.ACTIONS = { 1: ['swing'], 2: ['partisans'], 3: ['news'], 4: ['hype'], 5: ['cash'], 6: ['media'], 7: ['mojo'], 8: ['money'], 9: ['news', 'hype', 'cash'], 10: ['media', 'mojo', 'money'] } self.active = False self.winner = None self.deck = Deck(self, self.card_factory) self.hand = Hand(self.deck) def set_opponent(self, opponent): """Set player's opponent.""" self.opponent = opponent def set_active(self, active): """Set player as active.""" self.active = active def get_active(self): """See if player is active.""" return self.active def is_bot(self): """See if player is bot.""" return self.bot def set_winner(self, winner): """Make player winner or loser.""" self.winner = winner def get_deck(self): """Get deck.""" return self.deck def get_hand(self): """Get player hand.""" return self.hand def get_player_id(self): """Get player id.""" return self.player_id def get_voters(self): """Get number of partisans.""" return self.partisans def play(self): pass def pay_for_card(self, card_color, card_value): """If possible pay for card.""" if card_color != 0 and card_color != 4: property = self.property(self.RESOURSES[card_color]) property_value = property.get(self) if (property_value - card_value) >= 0: property.set(self, property_value - card_value) else: return False else: for color, prop_name in self.RESOURSES.items(): property = self.property(prop_name) property_value = property.get(self) if (property_value - card_value) >= 0: property.set(self, property_value - card_value) else: return False return True def apply_card(self, type, value): """ Apply card actions. Return True if after applying this card the turn doesn't change. """ if type == 0: if value >= 0: self.swing += value # lose voters branch elif value < 0: diff = self.swing + value self.swing = max(0, self.swing + value) if diff < 0: self.partisans = max(0, self.partisans + diff) elif type == 11: return True else: for res in self.ACTIONS[type]: # TODO: # check with -value, it seems that it doesn't work(( old_value = self.property(res).get(self) min_value = self.property(res).get_min(self) # print self.player_id, res, type, value, old_value, min_value self.property(res).set(self, max(min_value, old_value + value)) return False def update_resources(self): """Update resources of players at the end of the turn.""" for increment, resource in (('media', 'news'), ('mojo', 'hype'), ('money', 'cash')): increment_property = self.property(increment) increment_property_value = increment_property.get(self) resource_property = self.property(resource) resource_property_value = resource_property.get(self) resource_property.set( self, increment_property_value + resource_property_value)
class GridLayout(Layout): '''Grid 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 one 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 its 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]. .. versionchanged:: 1.7.0 Replaced NumericProperty with VariableListProperty. :attr:`padding` is a :class:`~kivy.properties.VariableListProperty` and defaults to [0, 0, 0, 0]. ''' cols = BoundedNumericProperty(None, min=0, allownone=True) '''Number of columns in the grid. .. versionchanged:: 1.0.8 Changed from a NumericProperty to BoundedNumericProperty. You can no longer set this to a negative value. :attr:`cols` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' rows = BoundedNumericProperty(None, min=0, allownone=True) '''Number of rows in the grid. .. versionchanged:: 1.0.8 Changed from a NumericProperty to a BoundedNumericProperty. You can no longer set this to a negative value. :attr:`rows` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' col_default_width = NumericProperty(0) '''Default minimum size to use for a column. .. versionadded:: 1.0.7 :attr:`col_default_width` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' row_default_height = NumericProperty(0) '''Default minimum size to use for row. .. versionadded:: 1.0.7 :attr:`row_default_height` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' col_force_default = BooleanProperty(False) '''If True, ignore the width and size_hint_x of the child and use the default column width. .. versionadded:: 1.0.7 :attr:`col_force_default` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' row_force_default = BooleanProperty(False) '''If True, ignore the height and size_hint_y of the child and use the default row height. .. versionadded:: 1.0.7 :attr:`row_force_default` is a :class:`~kivy.properties.BooleanProperty` and defaults to False. ''' cols_minimum = DictProperty({}) '''Dict of minimum width for each column. The dictionary keys are the column numbers, e.g. 0, 1, 2... .. versionadded:: 1.0.7 :attr:`cols_minimum` is a :class:`~kivy.properties.DictProperty` and defaults to {}. ''' rows_minimum = DictProperty({}) '''Dict of minimum height for each row. The dictionary keys are the row numbers, e.g. 0, 1, 2... .. versionadded:: 1.0.7 :attr:`rows_minimum` is a :class:`~kivy.properties.DictProperty` and defaults to {}. ''' minimum_width = NumericProperty(0) '''Automatically computed minimum width needed to contain all children. .. versionadded:: 1.0.8 :attr:`minimum_width` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. It is read only. ''' minimum_height = NumericProperty(0) '''Automatically computed minimum height needed to contain all children. .. versionadded:: 1.0.8 :attr:`minimum_height` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. It is read only. ''' minimum_size = ReferenceListProperty(minimum_width, minimum_height) '''Automatically computed minimum size needed to contain all children. .. versionadded:: 1.0.8 :attr:`minimum_size` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`minimum_width`, :attr:`minimum_height`) properties. It is read only. ''' def __init__(self, **kwargs): self._cols = self._rows = None super(GridLayout, self).__init__(**kwargs) fbind = self.fbind update = self._trigger_layout fbind('col_default_width', update) fbind('row_default_height', update) fbind('col_force_default', update) fbind('row_force_default', update) fbind('cols', update) fbind('rows', update) fbind('parent', update) fbind('spacing', update) fbind('padding', update) fbind('children', update) fbind('size', update) fbind('pos', update) def get_max_widgets(self): if self.cols and self.rows: return self.rows * self.cols else: return None def on_children(self, instance, value): # if that makes impossible to construct things with deffered method, # migrate this test in do_layout, and/or issue a warning. smax = self.get_max_widgets() if smax and len(value) > smax: raise GridLayoutException( 'Too many children in GridLayout. Increase rows/cols!') def _init_rows_cols_sizes(self, count): # the goal here is to calculate the minimum size of every cols/rows # and determine if they have stretch or not current_cols = self.cols current_rows = self.rows # if no cols or rows are set, we can't calculate minimum size. # the grid must be contrained at least on one side if not current_cols and not current_rows: Logger.warning('%r have no cols or rows set, ' 'layout is not triggered.' % self) return if current_cols is None: current_cols = int(ceil(count / float(current_rows))) elif current_rows is None: current_rows = int(ceil(count / float(current_cols))) current_cols = max(1, current_cols) current_rows = max(1, current_rows) self._has_hint_bound_x = False self._has_hint_bound_y = False self._cols_min_size_none = 0. # min size from all the None hint self._rows_min_size_none = 0. # min size from all the None hint self._cols = cols = [self.col_default_width] * current_cols self._cols_sh = [None] * current_cols self._cols_sh_min = [None] * current_cols self._cols_sh_max = [None] * current_cols self._rows = rows = [self.row_default_height] * current_rows self._rows_sh = [None] * current_rows self._rows_sh_min = [None] * current_rows self._rows_sh_max = [None] * current_rows # update minimum size from the dicts items = (i for i in self.cols_minimum.items() if i[0] < len(cols)) for index, value in items: cols[index] = max(value, cols[index]) items = (i for i in self.rows_minimum.items() if i[0] < len(rows)) for index, value in items: rows[index] = max(value, rows[index]) return True def _fill_rows_cols_sizes(self): cols, rows = self._cols, self._rows cols_sh, rows_sh = self._cols_sh, self._rows_sh cols_sh_min, rows_sh_min = self._cols_sh_min, self._rows_sh_min cols_sh_max, rows_sh_max = self._cols_sh_max, self._rows_sh_max # calculate minimum size for each columns and rows n_cols = len(cols) has_bound_y = has_bound_x = False for i, child in enumerate(reversed(self.children)): (shw, shh), (w, h) = child.size_hint, child.size shw_min, shh_min = child.size_hint_min shw_max, shh_max = child.size_hint_max row, col = divmod(i, n_cols) # compute minimum size / maximum stretch needed if shw is None: cols[col] = nmax(cols[col], w) else: cols_sh[col] = nmax(cols_sh[col], shw) if shw_min is not None: has_bound_x = True cols_sh_min[col] = nmax(cols_sh_min[col], shw_min) if shw_max is not None: has_bound_x = True cols_sh_max[col] = nmin(cols_sh_max[col], shw_max) if shh is None: rows[row] = nmax(rows[row], h) else: rows_sh[row] = nmax(rows_sh[row], shh) if shh_min is not None: has_bound_y = True rows_sh_min[row] = nmax(rows_sh_min[row], shh_min) if shh_max is not None: has_bound_y = True rows_sh_max[row] = nmin(rows_sh_max[row], shh_max) self._has_hint_bound_x = has_bound_x self._has_hint_bound_y = has_bound_y def _update_minimum_size(self): # calculate minimum width/height needed, starting from padding + # spacing l, t, r, b = self.padding spacing_x, spacing_y = self.spacing cols, rows = self._cols, self._rows width = l + r + spacing_x * (len(cols) - 1) self._cols_min_size_none = sum(cols) + width # we need to subtract for the sh_max/min the already guaranteed size # due to having a None in the col. So sh_min gets smaller by that size # since it's already covered. Similarly for sh_max, because if we # already exceeded the max, the subtracted max will be zero, so # it won't get larger if self._has_hint_bound_x: cols_sh_min = self._cols_sh_min cols_sh_max = self._cols_sh_max for i, (c, sh_min, sh_max) in enumerate(zip(cols, cols_sh_min, cols_sh_max)): if sh_min is not None: width += max(c, sh_min) cols_sh_min[i] = max(0., sh_min - c) else: width += c if sh_max is not None: cols_sh_max[i] = max(0., sh_max - c) else: width = self._cols_min_size_none height = t + b + spacing_y * (len(rows) - 1) self._rows_min_size_none = sum(rows) + height if self._has_hint_bound_y: rows_sh_min = self._rows_sh_min rows_sh_max = self._rows_sh_max for i, (r, sh_min, sh_max) in enumerate(zip(rows, rows_sh_min, rows_sh_max)): if sh_min is not None: height += max(r, sh_min) rows_sh_min[i] = max(0., sh_min - r) else: height += r if sh_max is not None: rows_sh_max[i] = max(0., sh_max - r) else: height = self._rows_min_size_none # finally, set the minimum size self.minimum_size = (width, height) def _finalize_rows_cols_sizes(self): selfw = self.width selfh = self.height # resolve size for each column if self.col_force_default: cols = [self.col_default_width] * len(self._cols) for index, value in self.cols_minimum.items(): cols[index] = value self._cols = cols else: cols = self._cols cols_sh = self._cols_sh cols_sh_min = self._cols_sh_min cols_weight = float(sum((x for x in cols_sh if x is not None))) stretch_w = max(0., selfw - self._cols_min_size_none) if stretch_w > 1e-9: if self._has_hint_bound_x: # fix the hints to be within bounds self.layout_hint_with_bounds( cols_weight, stretch_w, sum((c for c in cols_sh_min if c is not None)), cols_sh_min, self._cols_sh_max, cols_sh) for index, col_stretch in enumerate(cols_sh): # if the col don't have stretch information, nothing to do if not col_stretch: continue # add to the min width whatever remains from size_hint cols[index] += stretch_w * col_stretch / cols_weight # same algo for rows if self.row_force_default: rows = [self.row_default_height] * len(self._rows) for index, value in self.rows_minimum.items(): rows[index] = value self._rows = rows else: rows = self._rows rows_sh = self._rows_sh rows_sh_min = self._rows_sh_min rows_weight = float(sum((x for x in rows_sh if x is not None))) stretch_h = max(0., selfh - self._rows_min_size_none) if stretch_h > 1e-9: if self._has_hint_bound_y: # fix the hints to be within bounds self.layout_hint_with_bounds( rows_weight, stretch_h, sum((r for r in rows_sh_min if r is not None)), rows_sh_min, self._rows_sh_max, rows_sh) for index, row_stretch in enumerate(rows_sh): # if the row don't have stretch information, nothing to do if not row_stretch: continue # add to the min height whatever remains from size_hint rows[index] += stretch_h * row_stretch / rows_weight def _iterate_layout(self, count): selfx = self.x padding_left = self.padding[0] padding_top = self.padding[1] spacing_x, spacing_y = self.spacing i = count - 1 y = self.top - padding_top cols = self._cols for row_height in self._rows: x = selfx + padding_left for col_width in cols: if i < 0: break yield i, x, y - row_height, col_width, row_height i = i - 1 x = x + col_width + spacing_x y -= row_height + spacing_y def do_layout(self, *largs): children = self.children if not children or not self._init_rows_cols_sizes(len(children)): l, t, r, b = self.padding self.minimum_size = l + r, t + b return self._fill_rows_cols_sizes() self._update_minimum_size() self._finalize_rows_cols_sizes() for i, x, y, w, h in self._iterate_layout(len(children)): c = children[i] c.pos = x, y shw, shh = c.size_hint shw_min, shh_min = c.size_hint_min shw_max, shh_max = c.size_hint_max if shw_min is not None: if shw_max is not None: w = max(min(w, shw_max), shw_min) else: w = max(w, shw_min) else: if shw_max is not None: w = min(w, shw_max) if shh_min is not None: if shh_max is not None: h = max(min(h, shh_max), shh_min) else: h = max(h, shh_min) else: if shh_max is not None: h = min(h, shh_max) if shw is None: if shh is not None: c.height = h else: if shh is None: c.width = w else: c.size = (w, h)
class MainScreen(Screen): """A master layout that contains one board and some menus. This contains three elements: a scrollview (containing the board), a menu, and the time control panel. This class has some support methods for handling interactions with the menu and the character sheet, but if neither of those happen, the scrollview handles touches on its own. """ manager = ObjectProperty() boards = DictProperty() boardview = ObjectProperty() charmenu = ObjectProperty() statlist = ObjectProperty() statpanel = ObjectProperty() timepanel = ObjectProperty() kv = StringProperty() use_kv = BooleanProperty() play_speed = NumericProperty() playbut = ObjectProperty() portaladdbut = ObjectProperty() portaldirbut = ObjectProperty() dummyplace = ObjectProperty() dummything = ObjectProperty() dummies = ReferenceListProperty(dummyplace, dummything) dialoglayout = ObjectProperty() visible = BooleanProperty() _touch = ObjectProperty(None, allownone=True) rules_per_frame = BoundedNumericProperty(10, min=1) app = ObjectProperty() def on_statpanel(self, *args): if not self.app: Clock.schedule_once(self.on_statpanel, 0) return self.app.bind(selected_proxy=self.statpanel.setter('proxy')) def pull_visibility(self, *args): self.visible = self.manager.current == 'main' def on_manager(self, *args): self.pull_visibility() self.manager.bind(current=self.pull_visibility) def on_play_speed(self, *args): """Change the interval at which ``self.play`` is called to match my current ``play_speed``. """ Clock.unschedule(self.play) Clock.schedule_interval(self.play, 1.0 / self.play_speed) def remake_display(self, *args): """Remake any affected widgets after a change in my ``kv``. """ Builder.load_string(self.kv) if hasattr(self, '_kv_layout'): self.remove_widget(self._kv_layout) del self._kv_layout self._kv_layout = KvLayout() self.add_widget(self._kv_layout) _trigger_remake_display = trigger(remake_display) def on_touch_down(self, touch): if self.visible: touch.grab(self) for interceptor in (self.timepanel, self.charmenu, self.statpanel, self.dummyplace, self.dummything): if interceptor.collide_point(*touch.pos): interceptor.dispatch('on_touch_down', touch) self.boardview.keep_selection = True return True if self.dialoglayout.dispatch('on_touch_down', touch): return True return self.boardview.dispatch('on_touch_down', touch) def on_touch_up(self, touch): if self.timepanel.collide_point(*touch.pos): return self.timepanel.dispatch('on_touch_up', touch) elif self.charmenu.collide_point(*touch.pos): return self.charmenu.dispatch('on_touch_up', touch) elif self.statpanel.collide_point(*touch.pos): return self.statpanel.dispatch('on_touch_up', touch) return self.boardview.dispatch('on_touch_up', touch) def on_dummies(self, *args): """Give the dummies numbers such that, when appended to their names, they give a unique name for the resulting new :class:`board.Pawn` or :class:`board.Spot`. """ def renum_dummy(dummy, *args): dummy.num = dummynum(self.app.character, dummy.prefix) + 1 for dummy in self.dummies: if dummy is None or hasattr(dummy, '_numbered'): continue if dummy == self.dummything: self.app.pawncfg.bind(imgpaths=self._propagate_thing_paths) if dummy == self.dummyplace: self.app.spotcfg.bind(imgpaths=self._propagate_place_paths) dummy.num = dummynum(self.app.character, dummy.prefix) + 1 Logger.debug("MainScreen: dummy #{}".format(dummy.num)) dummy.bind(prefix=partial(renum_dummy, dummy)) dummy._numbered = True def _propagate_thing_paths(self, *args): # horrible hack self.dummything.paths = self.app.pawncfg.imgpaths def _propagate_place_paths(self, *args): # horrible hack self.dummyplace.paths = self.app.spotcfg.imgpaths def _update_from_time_travel(self, command, branch, turn, tick, result, **kwargs): self._update_from_delta(command, branch, turn, tick, result[-1]) def _update_from_delta(self, cmd, branch, turn, tick, delta, **kwargs): self.app.branch = branch self.app.turn = turn self.app.tick = tick chardelta = delta.get(self.boardview.board.character.name, {}) for unwanted in ('character_rulebook', 'avatar_rulebook', 'character_thing_rulebook', 'character_place_rulebook', 'character_portal_rulebook'): if unwanted in chardelta: del chardelta[unwanted] self.boardview.board.trigger_update_from_delta(chardelta) self.statpanel.statlist.mirror = dict(self.app.selected_proxy) def play(self, *args): """If the 'play' button is pressed, advance a turn. If you want to disable this, set ``engine.universal['block'] = True`` """ if self.playbut.state == 'normal': return self.next_turn() def _update_from_next_turn(self, command, branch, turn, tick, result): todo, deltas = result if isinstance(todo, list): self.dialoglayout.todo = todo self.dialoglayout.idx = 0 self._update_from_delta(command, branch, turn, tick, deltas) self.dialoglayout.advance_dialog() def next_turn(self, *args): """Advance time by one turn, if it's not blocked. Block time by setting ``engine.universal['block'] = True``""" eng = self.app.engine dial = self.dialoglayout if eng.universal.get('block'): Logger.info( "MainScreen: next_turn blocked, delete universal['block'] to unblock" ) return if dial.idx < len(dial.todo): Logger.info( "MainScreen: not advancing time while there's a dialog") return eng.next_turn(cb=self._update_from_next_turn)
class CircularTimePicker(BoxLayout): """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. """ 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={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]) """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(.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): return datetime.time(*self.time_list) def _set_time(self, dt): 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( *self.selector_color) if self.picker == "hours" else rgb_to_hex( *self.color) mc = rgb_to_hex( *self.selector_color) if self.picker == "minutes" else rgb_to_hex( *self.color) h = self.hours == 0 and 12 or self.hours <= 12 and self.hours or self.hours - 12 m = self.minutes return self.time_format.format(hours_color=hc, minutes_color=mc, hours=h, minutes=m) time_text = AliasProperty(_get_time_text, None, bind=("hours", "minutes", "time_format", "picker")) def _get_ampm_text(self): amc = rgb_to_hex(*self.selector_color) if self._am else rgb_to_hex( *self.color) pmc = rgb_to_hex(*self.selector_color) if not self._am else rgb_to_hex( *self.color) 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, as_popup=False, touch_switch=False, *args, **kwargs): super(CircularTimePicker, self).__init__(*args, **kwargs) if self.hours >= 12: self._am = False self.bind(time_list=self.on_time_list, picker=self._switch_picker, _am=self.on_ampm) self._h_picker = CircularHourPicker() self._m_picker = CircularMinutePicker() 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)) #print "TIMEee", self.time 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 ref == "hours": self.picker = "hours" elif ref == "minutes": self.picker = "minutes" elif 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): #print "TIME", self.time if not self._picker: return if self.picker == "hours": self._picker.selected = self.hours == 0 and 12 or self._am and self.hours or self.hours - 12 elif self.picker == "minutes": self._picker.selected = self.minutes 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 _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: # print "noanim" if prevpicker in container.children: container.remove_widget(prevpicker) if picker.parent: picker.parent.remove_widget(picker) container.add_widget(picker) else: if prevpicker in container.children: anim = Animation(scale=1.5, d=.5, t="in_back") & Animation( opacity=0, d=.5, t="in_cubic") anim.start(prevpicker) Clock.schedule_once( lambda *a: container.remove_widget(prevpicker), .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=.5, t="out_back") & Animation( opacity=1, d=.5, t="out_cubic") Clock.schedule_once(lambda *a: anim.start(picker), .3)
class PlayScreen(Screen): game_mode = "" current_level = {} rows = 3 cols = 3 moves_made = BoundedNumericProperty(0) max_moves = BoundedNumericProperty(15) time_limit = BoundedNumericProperty(15) time_elapsed = BoundedNumericProperty(0) timer = None gridlayout = None answerlayout = None button_ids = {} random = False resume = False game_tile_sound = None filename = '' level = None level_stars = 0 stars = [0] * 20 is_paused = False hint_count = 0 def on_enter(self): self.set_mode() self.set_level() if not self.resume: self.gridlayout = GridLayout(rows=self.rows, cols=self.cols) self.answerlayout = GridLayout(rows=self.rows, cols=self.cols) # generate answer key self.generate_answer() if self.game_mode != "Expert": self.answerlayout.size_hint = [0.3, 0.17] self.answerlayout.pos = (0.35 * self.width, 0.695 * self.height) self.add_widget(self.answerlayout) # generate game board self.generate_grid() self.gridlayout.size_hint = [0.75, 0.43] # height, width self.gridlayout.pos = (0.13 * self.width, 0.25 * self.height ) # x, y self.add_widget(self.gridlayout) self.user_key = "0" * self.rows * self.cols self.resume = True if self.game_mode == "Expert": self.open_answer("init") self.answer_button = Button( background_normal="../Art/SHOWANS.png", background_down='../Art/SHOWANS_DOWN.png', size=(99.8, 67), size_hint=(None, None), pos=(430, 843)) self.answer_button.bind(on_release=self.open_answer) self.add_widget(self.answer_button) else: self.hint_button = Button(background_normal="../Art/HINT.png", background_down='../Art/HINT_DOWN.png', size=(99.8, 67), size_hint=(None, None), pos=(430, 845)) self.hint_button.bind(on_release=self.get_hint) self.add_widget(self.hint_button) self.game_tile_sound = SoundLoader.load('../Audio/GAME_TILE_PRESS.wav') def generate_grid(self): for i in range(self.rows): for j in range(self.cols): button = Button(background_normal="../Art/TILE.png", background_down="../Art/TILE.png") button.bind(on_release=self.move_made) self.button_ids[button] = "{},{}".format(i, j) self.gridlayout.add_widget(button, len(self.gridlayout.children)) def generate_answer(self): for _ in range(self.rows * self.cols): button = Button(background_normal="../Art/TILE.png", background_down="../Art/TILE.png") self.answerlayout.add_widget(button, len(self.answerlayout.children)) # if random, generate new answer_key if self.random: # while loop to make sure at least one tile is pressed while True: self.answer_key = "" for _ in range(self.rows * self.cols): self.answer_key = self.answer_key + str( random.randint(0, 1)) if "1" in self.answer_key: break self.minimum_moves = self.answer_key.count("1") for index in range(len(self.answerlayout.children)): if self.answer_key[index] == "1": row, col = self.get_row_col_by_index(index) self.change_surrounding_tiles(index, row, col, is_answer_grid=True) def get_index_by_tile_id(self, col, row): return row * self.cols + col def get_row_col_by_index(self, index): return (index // self.cols, index % self.cols) def move_made(self, instance): self.game_tile_sound.play() row, col = (int(d) for d in self.button_ids[instance].split(',')) index = self.get_index_by_tile_id(col, row) self.moves_made += 1 self.user_key = self.user_key[:index] + ( "1" if self.user_key[index] == "0" else "0") + self.user_key[index + 1:] print("Pressed button row: {}, col: {}".format(row, col)) self.change_surrounding_tiles(index, row, col) self.goal_reached() def change_surrounding_tiles(self, index, row, col, is_answer_grid=False): self.change_tile_color(index, is_answer_grid) # check if NOT top row if (row < self.rows - 1): top_index = self.get_index_by_tile_id(col, row + 1) self.change_tile_color(top_index, is_answer_grid) # check if NOT bottom row if (row > 0): bottom_index = self.get_index_by_tile_id(col, row - 1) self.change_tile_color(bottom_index, is_answer_grid) # check if NOT left column if (col < self.cols - 1): left_index = self.get_index_by_tile_id(col + 1, row) self.change_tile_color(left_index, is_answer_grid) # check if NOT right column if (col > 0): right_index = self.get_index_by_tile_id(col - 1, row) self.change_tile_color(right_index, is_answer_grid) def change_tile_color(self, index, is_answer_grid=False): grid = self.gridlayout if not is_answer_grid else self.answerlayout if grid.children[index].background_normal == "../Art/TILE_HINT.png": if grid.children[index].background_down == "../Art/TILE_DOWN.png": grid.children[index].background_normal = "../Art/TILE.png" else: grid.children[index].background_normal = "../Art/TILE_DOWN.png" elif grid.children[ index].background_normal == "../Art/TILE.png" or grid.children[ index].background_normal == "tile": grid.children[index].background_normal = "../Art/TILE_DOWN.png" grid.children[index].background_down = "../Art/TILE_DOWN.png" else: grid.children[index].background_normal = "../Art/TILE.png" grid.children[index].background_down = "../Art/TILE.png" def goal_reached(self): if self.user_key == self.answer_key: print("Yay, you won!") self.number_stars() self.open_won() self.clear_game() else: if self.game_mode != "Classic": self.ids.moves.text = "Moves Left: " + str(self.max_moves - self.moves_made) if self.moves_made == self.max_moves: print("Oops, you lost!") self.open_lost() self.clear_game() else: self.ids.moves.text = "Moves Made: " + str(self.moves_made) def number_stars(self): intervals = 5 hint_weight = self.hint_count / self.minimum_moves * 3 * intervals print("Hints:", self.hint_count) if self.moves_made + hint_weight < self.minimum_moves + intervals: self.level_stars = 3 elif self.moves_made + hint_weight <= self.minimum_moves + intervals * 2: self.level_stars = 2 else: self.level_stars = 1 if self.game_mode == 'Classic': if self.stars[self.current_level[self.game_mode] - 1] < self.level_stars: self.stars[self.current_level[self.game_mode] - 1] = self.level_stars print("Number of stars:", sum(self.stars)) def reset_board(self): self.moves_made = 0 self.time_elapsed = 0 self.hint_count = 0 self.is_paused = False if self.game_mode == "Expert": if self.timer: self.timer.cancel() self.start_timer() self.user_key = "0" * self.rows * self.cols if self.game_mode == "Classic": self.ids.moves.text = "Moves Made: " + str(self.moves_made) else: self.ids.moves.text = "Moves Left: " + str(self.max_moves - self.moves_made) for tile in self.gridlayout.children: tile.background_normal = "../Art/TILE.png" tile.background_down = "../Art/TILE_DOWN.png" def clear_game(self): self.is_paused = False if self.game_mode == "Expert": if self.timer: self.timer.cancel() self.remove_widget(self.answer_button) if self.answertimer: self.answertimer.cancel() if self.resume: if self.game_mode != "Expert": self.remove_widget(self.hint_button) self.ids.extra_settings.text = "" # to clear up numbers from timer self.ids.moves.text = "" # clear up move counters (slight glitch in which user can see 'Moves Made' changed to "Moves Left" after switching from Classic to Challenger/Expert) self.moves_made = 0 self.time_elapsed = 0 self.hint_count = 0 self.gridlayout.clear_widgets() self.answerlayout.clear_widgets() self.clear_widgets([self.gridlayout, self.answerlayout]) self.resume = False def open_pause(self): self.is_paused = True if self.game_mode == "Expert": self.timer.cancel() popup = Pause() popup.open() def open_won(self): if self.game_mode == "Expert": self.timer.cancel() #self.current_level[self.game_mode] = self.current_level[self.game_mode] + 1 popup = GameWin() popup.set_stars(self.level_stars) popup.open() self.clear_game() def open_lost(self): if self.game_mode == "Expert": self.timer.cancel() popup = GameLose() popup.open() self.clear_game() # shows the answer for 5 seconds # need to change the color of the background or the tiles def open_answer(self, instance): self.answer = ExpertAnswer() if instance == "init": self.answer.init(self.answerlayout, True) else: self.answer.init(self.answerlayout, False) self.answer.open() if instance == "init": self.answertimer = Timer(5.0, self.answer.clean) self.answertimer.start() def get_hint(self, instance=0): self.hint_count += 1 #compare the user and answer key and the first place with a difference changes color for 2 seconds for i in range(self.rows * self.cols): if self.user_key[i] != self.answer_key[i]: self.hintloc = i break self.gridlayout.children[ self.hintloc].background_normal = "../Art/TILE_HINT.png" timer = Timer(2.0, self.reverse_hint) timer.start() def reverse_hint(self): if self.gridlayout.children[ self.hintloc].background_normal == "../Art/TILE_HINT.png": if self.gridlayout.children[ self.hintloc].background_down == "../Art/TILE_DOWN.png": self.gridlayout.children[ self.hintloc].background_normal = "../Art/TILE_DOWN.png" elif self.gridlayout.children[ self.hintloc].background_down == "../Art/TILE.png": self.gridlayout.children[ self.hintloc].background_normal = "../Art/TILE.png" def set_mode(self): app = App.get_running_app() self.game_mode = app.DIFFICULTY if app.STARTLEVEL: self.current_level[self.game_mode] = app.STARTLEVEL # Initialize what level we are on for each difficulty level if self.game_mode not in self.current_level: self.current_level[self.game_mode] = 1 self.ids.moves.text = "" if self.game_mode == "Classic": self.filename = os.path.join(dirname, '../Levels/Classic.txt') self.ids.moves.text = "Moves Made: " + str(self.moves_made) elif self.game_mode == "Challenge": self.filename = os.path.join(dirname, '../Levels/Challenge.txt') self.ids.moves.text = "Moves Left: " + str(self.max_moves - self.moves_made) elif self.game_mode == "Expert": self.filename = os.path.join(dirname, '../Levels/Expert.txt') self.ids.moves.text = "Moves Left: " + str(self.max_moves - self.moves_made) else: self.random = True with open(self.filename) as f: level_info = "" for i, line in enumerate(f): if i + 1 == self.current_level[self.game_mode]: level_info = line # level data in the format col row answerkey level_info = level_info.rstrip('\n').split(' ') # if no more pre-determined level data, then set to randomized level generation self.random = level_info == [''] if self.random: # if randomized, set defaults (including time limit) rows, cols, time_limit = 3, 3, 15 return elif self.game_mode == 'Expert': # if expert, set time limit too rows, cols, self.answer_key, time_limit = level_info self.time_limit = int(time_limit) else: rows, cols, self.answer_key = level_info self.rows = int(rows) self.cols = int(cols) def set_level(self): app = App.get_running_app() level_number = app.STARTLEVEL self.current_level[self.game_mode] = level_number def start_timer(self): if self.game_mode == 'Expert': #MUST have this if statement here if self.timer: # make sure only one timer is running at a time self.timer.cancel() self.ids.extra_settings.text = "Time Left: " + str( self.time_limit - self.time_elapsed) self.timer = Clock.schedule_interval(partial(self.timer_tick), 1) #update the timer every sec def timer_tick(self, *largs): if not self.is_paused: self.time_elapsed += 1 self.ids.extra_settings.text = "Time Left: " + str( self.time_limit - self.time_elapsed) if self.time_limit - self.time_elapsed <= 0: self.timer.cancel() self.open_lost()
class Gauge(Widget): ''' Gauge class ''' unit = NumericProperty(1.8) file_gauge = StringProperty("cadran.png") file_needle = StringProperty("needle.png") size_text = NumericProperty(10) value = BoundedNumericProperty(0, min_value=0, max_value=100, errorvalue=0) def __init__(self, curent_value=0, min_value=0, max_value=100, half_widget=True, show_progress=True, **kwargs): super(Gauge, self).__init__(**kwargs) self.property('value').set_min(self, min_value) self.property('value').set_max(self, max_value) self.value = curent_value self.midle_value = (max_value - min_value) / 2 self._show_progress = show_progress self._gauge = Scatter( size=self.size, do_rotation=False, ) # _img_gauge = Image(source=get_module_resource_path(self.file_gauge), size=(self.size_gauge, self.size_gauge)) self._img_gauge = get_module_resource_path(self.file_gauge, size=self.size, resource_package=__name__) self._needle = Scatter( size=self.size, do_rotation=False, ) self._img_needle = get_module_resource_path(self.file_needle, size=self.size, resource_package=__name__) self._glab = Label(font_size=self.size_text, markup=True) self._gauge.add_widget(self._img_gauge) self._needle.add_widget(self._img_needle) self.add_widget(self._gauge) self.add_widget(self._needle) self.add_widget(self._glab) if show_progress: self._progress = ProgressBar(max=max_value, height=20, value=curent_value) self.add_widget(self._progress) self.bind(pos=self._update) self.bind(size=self._update) self.bind(value=self._turn) self.unit = 90 / self.midle_value if half_widget else 180 / self.midle_value self._turn() def _update(self, *args): ''' Update gauge and needle positions after sizing or positioning. ''' self._img_gauge.size = self.size self._img_needle.size = self._img_gauge.size self._needle.size = self.size self._gauge.size = self.size self._gauge.pos = self.pos self._gauge.center = self.center self._needle.pos = (self.x, self.y) self._needle.center = self._gauge.center self._glab.center_x = self._gauge.center_x self._glab.center_y = self.center_y + (self.height / 4) if self._show_progress: element_size = min(self.height, self.width) self._progress.x = self._gauge.center_x - element_size / 2 self._progress.y = self._gauge.y + (self.height / 4) self._progress.width = element_size def _turn(self, *args): ''' Turn needle, 1 degree = 1 unit, 0 degree point start on 50 value. ''' self._needle.center_x = self._gauge.center_x self._needle.center_y = self._gauge.center_y self._needle.rotation = (self.midle_value * self.unit) - (self.value * self.unit) self._glab.text = "[b]{0:.0f}[/b]".format(self.value) if self._show_progress: self._progress.value = self.value def set_animate(self, value, easing='in_out_quad', speed=1): from kivy.animation import Animation Animation(value=value, duration=speed, t=easing).start(self)
class Slider(Widget): '''Class for creating Slider widget. Check module documentation for more details. ''' value = NumericProperty(0.) '''Current value used for the slider. :data:`value` is a :class:`~kivy.properties.NumericProperty`, default to 0. ''' min = NumericProperty(0.) '''Minimum value allowed for :data:`value`. :data:`min` is a :class:`~kivy.properties.NumericProperty`, default to 0. ''' max = NumericProperty(100.) '''Maximum value allowed for :data:`value`. :data:`max` is a :class:`~kivy.properties.NumericProperty`, default to 100. ''' padding = NumericProperty(10) '''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 10. The range of the slider is reduced from padding * 2 on the screen. It allows drawing a cursor of 20px width, without having the cursor going out of the widget. :data:`padding` is a :class:`~kivy.properties.NumericProperty`, default to 10. ''' orientation = OptionProperty('horizontal', options=( 'vertical', 'horizontal')) '''Orientation of the slider. :data:`orientation` is an :class:`~kivy.properties.OptionProperty`, default 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 :data:`range` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`min`, :data:`max`) ''' 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 :data:`step` is a :class:`~kivy.properties.NumericProperty`, default to 1. ''' 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 step = self.step val = value * (self.max - vmin) + vmin if step == 0: self.value = val else: self.value = min(round((val - vmin) / step) * step, self.max) value_normalized = AliasProperty(get_norm_value, set_norm_value, bind=('value', 'min', 'max', 'step')) '''Normalized value inside the :data:`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 = 1 >>> 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 :data:`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): x = min(self.right, max(pos[0], self.x)) y = min(self.top, max(pos[1], self.y)) if self.orientation == 'horizontal': if self.width == 0: self.value_normalized = 0 else: self.value_normalized = (x - self.x) / float(self.width) else: if self.height == 0: self.value_normalized = 0 else: self.value_normalized = (y - self.y) / float(self.height) 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. :data:`value_pos` is an :class:`~kivy.properties.AliasProperty`. ''' def on_touch_down(self, touch): if self.collide_point(*touch.pos): 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 Gauge(Widget): ''' Gauge class ''' unit = NumericProperty(225/10) # 1 needle tick = 180+45 degrees divided by range hrpm = BoundedNumericProperty(0, min = 0, max = 10, errorvalue = 10) speed = BoundedNumericProperty(0, min = 0, max = 15, errorvalue = 15) distance = BoundedNumericProperty(0, min = 0, max = 50000, errorvalue = 0) elapsed = StringProperty(str(datetime.now())) path = dirname(abspath(__file__)) file_gauge = StringProperty(join(path, "lm_tacho_dial_768.png")) file_needle = StringProperty(join(path, "lm_tacho_needle_768.png")) size_gauge = BoundedNumericProperty(128, min = 128, max = 768, errorvalue = 128) size_text = NumericProperty(24) def __init__(self, **kwargs): super(Gauge, self).__init__(**kwargs) self._gauge = Scatter( size=(self.size_gauge, self.size_gauge), do_rotation=False, do_scale=False, do_translation=False ) _img_gauge = Image( source=self.file_gauge, size=(self.size_gauge, self.size_gauge) ) self._needle = Scatter( size=(self.size_gauge, self.size_gauge), do_rotation=False, do_scale=False, do_translation=False ) _img_needle = Image( source=self.file_needle, size=(self.size_gauge, self.size_gauge) ) self._speed = Label(font_size=self.size_text*3, markup=True) self._distance = Label(font_size=self.size_text*1.5, markup=True) self._elapsed = Label(font_size=self.size_text*1.5, markup=True) self._gauge.add_widget(_img_gauge) self._needle.add_widget(_img_needle) self.add_widget(self._gauge) self.add_widget(self._needle) self.add_widget(self._speed) self.add_widget(self._distance) self.add_widget(self._elapsed) self.bind(pos=self._update) self.bind(size=self._update) self.bind(hrpm=self._turn) self.bind(speed=self._turn) self.bind(distance=self._turn) self.bind(elapsed=self._turn) def _update(self, *args): ''' Update gauge and needle positions after sizing or positioning. ''' self._gauge.pos = self.pos self._needle.pos = (self.x, self.y) self._needle.center = self._gauge.center self._speed.center_x = self._gauge.center_x self._speed.center_y = self._gauge.center_y - (self.size_gauge / 7) self._distance.center_x = self._gauge.center_x self._distance.center_y = self._gauge.center_y - (self.size_gauge / 4.4) self._elapsed.center_x = self._gauge.center_x self._elapsed.center_y = self._gauge.center_y - (self.size_gauge / 3.4) def _turn(self, *args): ''' Turn needle, 1 degree = 1 unit, 0 degree point start on 50 value. ''' self._needle.center_x = self._gauge.center_x self._needle.center_y = self._gauge.center_y self._needle.rotation = -self.hrpm * self.unit self._speed.text = "[color=5599ff][b]{0:.1f}[/b][/color]".format(self.speed) self._distance.text = "[color=99ff55][b]{0:.0f}m[/b][/color]".format(self.distance) self._elapsed.text = "[b]{}[/b]".format(self.elapsed)
class Position(EventDispatcher): """ Position manager for fish, hooks, boat, etc. Enables a wrapped X axis and a bounded Y axis. """ pos_x = BoundedNumericProperty(0, min=0, max=1) pos_y = BoundedNumericProperty(0, min=0, max=1) def __init__(self, parent, space_subdivisions): super().__init__() self.parent = parent self.space_subdivisions = space_subdivisions self.unit = 0.5 / self.space_subdivisions self.bind(pos_x=parent.on_state) self.bind(pos_y=parent.on_state) @property def x(self): """X axis""" cur_pos = self.pos_x state_centering = (cur_pos - self.unit + 1.0) % 1.0 state = self.space_subdivisions * state_centering return int(round(state)) % self.space_subdivisions def increase_x(self, state_amount): """ Increase the x axis by given (small) amount :param state_amount: double. amount to increase in the x axis :return: """ pos_amount = state_amount / self.space_subdivisions self.pos_x = (self.pos_x + pos_amount) % 1.0 @property def y(self): """Y axis""" cur_pos = self.pos_y state_centering = (cur_pos - self.unit + 1.0) % 1.0 state = self.space_subdivisions * state_centering return int(round(state)) % self.space_subdivisions def increase_y(self, state_amount): """ Increase the y axis by given (small) amount :param state_amount: double. amount to increase in the y axis :return: """ pos_amount = state_amount / self.space_subdivisions if self.pos_y + pos_amount < self.unit: self.pos_y = self.unit elif self.pos_y + pos_amount > 1.0 - self.unit: self.pos_y = 1.0 - self.unit else: self.pos_y = self.pos_y + pos_amount def set_x(self, state_value): """ Set the x axis decimal position :param state_value: decimal position in range [0, 1] :return: """ val = state_value / self.space_subdivisions + self.unit if not self.unit <= val <= 1.0 - self.unit: raise AttributeError("Value out of bounds") self.pos_x = val def set_y(self, state_value): """ Set the y axis decimal position :param state_value: decimal position in range [0, 1] :return: """ val = state_value / self.space_subdivisions + self.unit if not self.unit <= val <= 1.0 - self.unit: raise AttributeError("Value out of bounds") self.pos_y = val def __str__(self): return self.__repr__() def __eq__(self, other): """Equivalent states in order to check fish and hooks in same position (caught fish)""" return self.x == other.x and self.y == other.y def copy(self): """ Copy the current positions :return: new position instance """ s = Position(self.parent, self.space_subdivisions) s.pos_x = self.pos_x s.pos_y = self.pos_y return s