def build(self): #self.load_kv('MyApp.kv') self.title = "Graphic Test Framework" widget = MyWidget() widget.add_widget(Label(text="prueba")) myCanvas = Canvas() myCanvas.add(Rectangle(size=(350, 1), pos=(0, 500))) widget.canvas = myCanvas widget.add_widget(Button(text="Boton", pos=(100, 500))) return widget return MyWidget() return PageLayout() return StackLayout() return BoxLayout() return GridLayout() return FloatLayout() return CustomWidget()
class HorizontalLine(Widget): separator_color = Popup.separator_color.defaultvalue separator_height = Popup.separator_height def __init__(self, **kwargs): self.size_hint_y = None self.height = 6 self.canvas = Canvas() self.rect = Rectangle() self.bind(size=self.on_change) self.bind(pos=self.on_change) super(HorizontalLine, self).__init__(**kwargs) def on_change(self, widget, size): self.color = Color() # Gets broken when moving self.color.rgba = self.separator_color # GETTING DEFAULT COLOR self.canvas.add(self.color) self.rect.pos = self.x, self.y + (self.height - self.separator_height) / 2.0 self.rect.size = self.width, self.separator_height self.canvas.add(self.rect)
class ScrollView(StencilView): '''ScrollView class. See module documentation for more information. .. versionchanged:: 1.7.0 `auto_scroll`, `scroll_friction`, `scroll_moves`, `scroll_stoptime' has been deprecated, use :attr:`effect_cls` instead. ''' scroll_distance = NumericProperty(_scroll_distance) '''Distance to move before scrolling the :class:`ScrollView`, in pixels. As soon as the distance has been traveled, the :class:`ScrollView` will start to scroll, and no touch event will go to children. It is advisable that you base this value on the dpi of your target device's screen. :attr:`scroll_distance` is a :class:`~kivy.properties.NumericProperty` and defaults to 20 (pixels), according to the default value in user configuration. ''' scroll_wheel_distance = NumericProperty(20) '''Distance to move when scrolling with a mouse wheel. It is advisable that you base this value on the dpi of your target device's screen. .. versionadded:: 1.8.0 :attr:`scroll_wheel_distance` is a :class:`~kivy.properties.NumericProperty` , defaults to 20 pixels. ''' scroll_timeout = NumericProperty(_scroll_timeout) '''Timeout allowed to trigger the :attr:`scroll_distance`, in milliseconds. If the user has not moved :attr:`scroll_distance` within the timeout, the scrolling will be disabled, and the touch event will go to the children. :attr:`scroll_timeout` is a :class:`~kivy.properties.NumericProperty` and defaults to 55 (milliseconds) according to the default value in user configuration. .. versionchanged:: 1.5.0 Default value changed from 250 to 55. ''' scroll_x = NumericProperty(0.) '''X scrolling value, between 0 and 1. If 0, the content's left side will touch the left side of the ScrollView. If 1, the content's right side will touch the right side. This property is controled by :class:`ScrollView` only if :attr:`do_scroll_x` is True. :attr:`scroll_x` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' scroll_y = NumericProperty(1.) '''Y scrolling value, between 0 and 1. If 0, the content's bottom side will touch the bottom side of the ScrollView. If 1, the content's top side will touch the top side. This property is controled by :class:`ScrollView` only if :attr:`do_scroll_y` is True. :attr:`scroll_y` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. ''' do_scroll_x = BooleanProperty(True) '''Allow scroll on X axis. :attr:`do_scroll_x` is a :class:`~kivy.properties.BooleanProperty` and defaults to True. ''' do_scroll_y = BooleanProperty(True) '''Allow scroll on Y axis. :attr:`do_scroll_y` is a :class:`~kivy.properties.BooleanProperty` and defaults to True. ''' def _get_do_scroll(self): return (self.do_scroll_x, self.do_scroll_y) def _set_do_scroll(self, value): if type(value) in (list, tuple): self.do_scroll_x, self.do_scroll_y = value else: self.do_scroll_x = self.do_scroll_y = bool(value) do_scroll = AliasProperty(_get_do_scroll, _set_do_scroll, bind=('do_scroll_x', 'do_scroll_y')) '''Allow scroll on X or Y axis. :attr:`do_scroll` is a :class:`~kivy.properties.AliasProperty` of (:attr:`do_scroll_x` + :attr:`do_scroll_y`) ''' def _get_vbar(self): # must return (y, height) in % # calculate the viewport size / scrollview size % if self._viewport is None: return 0, 1. vh = self._viewport.height h = self.height if vh < h or vh == 0: return 0, 1. ph = max(0.01, h / float(vh)) sy = min(1.0, max(0.0, self.scroll_y)) py = (1. - ph) * sy return (py, ph) vbar = AliasProperty(_get_vbar, None, bind=( 'scroll_y', '_viewport', 'viewport_size')) '''Return a tuple of (position, size) of the vertical scrolling bar. .. versionadded:: 1.2.0 The position and size are normalized between 0-1, and represent a percentage of the current scrollview height. This property is used internally for drawing the little vertical bar when you're scrolling. :attr:`vbar` is a :class:`~kivy.properties.AliasProperty`, readonly. ''' def _get_hbar(self): # must return (x, width) in % # calculate the viewport size / scrollview size % if self._viewport is None: return 0, 1. vw = self._viewport.width w = self.width if vw < w or vw == 0: return 0, 1. pw = max(0.01, w / float(vw)) sx = min(1.0, max(0.0, self.scroll_x)) px = (1. - pw) * sx return (px, pw) hbar = AliasProperty(_get_hbar, None, bind=( 'scroll_x', '_viewport', 'viewport_size')) '''Return a tuple of (position, size) of the horizontal scrolling bar. .. versionadded:: 1.2.0 The position and size are normalized between 0-1, and represent a percentage of the current scrollview height. This property is used internally for drawing the little horizontal bar when you're scrolling. :attr:`vbar` is a :class:`~kivy.properties.AliasProperty`, readonly. ''' bar_color = ListProperty([.7, .7, .7, .9]) '''Color of horizontal / vertical scroll bar, in RGBA format. .. versionadded:: 1.2.0 :attr:`bar_color` is a :class:`~kivy.properties.ListProperty` and defaults to [.7, .7, .7, .9]. ''' bar_inactive_color = ListProperty([.7, .7, .7, .2]) '''Color of horizontal / vertical scroll bar (in RGBA format), when no scroll is happening. .. versionadded:: 1.9.0 :attr:`bar_inactive_color` is a :class:`~kivy.properties.ListProperty` and defaults to [.7, .7, .7, .2]. ''' bar_width = NumericProperty('2dp') '''Width of the horizontal / vertical scroll bar. The width is interpreted as a height for the horizontal bar. .. versionadded:: 1.2.0 :attr:`bar_width` is a :class:`~kivy.properties.NumericProperty` and defaults to 2. ''' bar_pos_x = OptionProperty('bottom', options=('top', 'bottom')) '''Which side of the ScrollView the horizontal scroll bar should go on. Possible values are 'top' and 'bottom'. .. versionadded:: 1.8.0 :attr:`bar_pos_x` is an :class:`~kivy.properties.OptionProperty`, defaults to 'bottom'. ''' bar_pos_y = OptionProperty('right', options=('left', 'right')) '''Which side of the ScrollView the vertical scroll bar should go on. Possible values are 'left' and 'right'. .. versionadded:: 1.8.0 :attr:`bar_pos_y` is an :class:`~kivy.properties.OptionProperty` and defaults to 'right'. ''' bar_pos = ReferenceListProperty(bar_pos_x, bar_pos_y) '''Which side of the scroll view to place each of the bars on. :attr:`bar_pos` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`bar_pos_x`, :attr:`bar_pos_y`) ''' bar_margin = NumericProperty(0) '''Margin between the bottom / right side of the scrollview when drawing the horizontal / vertical scroll bar. .. versionadded:: 1.2.0 :attr:`bar_margin` is a :class:`~kivy.properties.NumericProperty`, default to 0 ''' effect_cls = ObjectProperty(DampedScrollEffect, allownone=True) '''Class effect to instanciate for X and Y axis. .. versionadded:: 1.7.0 :attr:`effect_cls` is an :class:`~kivy.properties.ObjectProperty` and defaults to :class:`DampedScrollEffect`. .. versionchanged:: 1.8.0 If you set a string, the :class:`~kivy.factory.Factory` will be used to resolve the class. ''' effect_x = ObjectProperty(None, allownone=True) '''Effect to apply for the X axis. If None is set, an instance of :attr:`effect_cls` will be created. .. versionadded:: 1.7.0 :attr:`effect_x` is an :class:`~kivy.properties.ObjectProperty` and defaults to None. ''' effect_y = ObjectProperty(None, allownone=True) '''Effect to apply for the Y axis. If None is set, an instance of :attr:`effect_cls` will be created. .. versionadded:: 1.7.0 :attr:`effect_y` is an :class:`~kivy.properties.ObjectProperty` and defaults to None, read-only. ''' viewport_size = ListProperty([0, 0]) '''(internal) Size of the internal viewport. This is the size of your only child in the scrollview. ''' scroll_type = OptionProperty(['content'], options=(['content'], ['bars'], ['bars', 'content'], ['content', 'bars'])) '''Sets the type of scrolling to use for the content of the scrollview. Available options are: ['content'], ['bars'], ['bars', 'content']. .. versionadded:: 1.8.0 :attr:`scroll_type` is a :class:`~kivy.properties.OptionProperty`, defaults to ['content']. ''' # private, for internal use only _viewport = ObjectProperty(None, allownone=True) _bar_color = ListProperty([0, 0, 0, 0]) def _set_viewport_size(self, instance, value): self.viewport_size = value def on__viewport(self, instance, value): if value: value.bind(size=self._set_viewport_size) self.viewport_size = value.size def __init__(self, **kwargs): self._touch = None self._trigger_update_from_scroll = Clock.create_trigger( self.update_from_scroll, -1) # create a specific canvas for the viewport from kivy.graphics import PushMatrix, Translate, PopMatrix, Canvas self.canvas_viewport = Canvas() self.canvas = Canvas() with self.canvas_viewport.before: PushMatrix() self.g_translate = Translate(0, 0) with self.canvas_viewport.after: PopMatrix() super(ScrollView, self).__init__(**kwargs) self.register_event_type('on_scroll_start') self.register_event_type('on_scroll_move') self.register_event_type('on_scroll_stop') # now add the viewport canvas to our canvas self.canvas.add(self.canvas_viewport) effect_cls = self.effect_cls if isinstance(effect_cls, string_types): effect_cls = Factory.get(effect_cls) if self.effect_x is None and effect_cls is not None: self.effect_x = effect_cls(target_widget=self._viewport) if self.effect_y is None and effect_cls is not None: self.effect_y = effect_cls(target_widget=self._viewport) self.bind( width=self._update_effect_x_bounds, height=self._update_effect_y_bounds, viewport_size=self._update_effect_bounds, _viewport=self._update_effect_widget, scroll_x=self._trigger_update_from_scroll, scroll_y=self._trigger_update_from_scroll, pos=self._trigger_update_from_scroll, size=self._trigger_update_from_scroll) self._update_effect_widget() self._update_effect_x_bounds() self._update_effect_y_bounds() def on_effect_x(self, instance, value): if value: value.bind(scroll=self._update_effect_x) value.target_widget = self._viewport def on_effect_y(self, instance, value): if value: value.bind(scroll=self._update_effect_y) value.target_widget = self._viewport def on_effect_cls(self, instance, cls): if isinstance(cls, string_types): cls = Factory.get(cls) self.effect_x = cls(target_widget=self._viewport) self.effect_x.bind(scroll=self._update_effect_x) self.effect_y = cls(target_widget=self._viewport) self.effect_y.bind(scroll=self._update_effect_y) def _update_effect_widget(self, *args): if self.effect_x: self.effect_x.target_widget = self._viewport if self.effect_y: self.effect_y.target_widget = self._viewport def _update_effect_x_bounds(self, *args): if not self._viewport or not self.effect_x: return self.effect_x.min = -(self.viewport_size[0] - self.width) self.effect_x.max = 0 self.effect_x.value = self.effect_x.min * self.scroll_x def _update_effect_y_bounds(self, *args): if not self._viewport or not self.effect_y: return self.effect_y.min = -(self.viewport_size[1] - self.height) self.effect_y.max = 0 self.effect_y.value = self.effect_y.min * self.scroll_y def _update_effect_bounds(self, *args): if not self._viewport: return if self.effect_x: self._update_effect_x_bounds() if self.effect_y: self._update_effect_y_bounds() def _update_effect_x(self, *args): vp = self._viewport if not vp or not self.effect_x: return sw = vp.width - self.width if sw < 1: return sx = self.effect_x.scroll / float(sw) self.scroll_x = -sx self._trigger_update_from_scroll() def _update_effect_y(self, *args): vp = self._viewport if not vp or not self.effect_y: return sh = vp.height - self.height if sh < 1: return sy = self.effect_y.scroll / float(sh) self.scroll_y = -sy self._trigger_update_from_scroll() def to_local(self, x, y, **k): tx, ty = self.g_translate.xy return x - tx, y - ty def to_parent(self, x, y, **k): tx, ty = self.g_translate.xy return x + tx, y + ty def _apply_transform(self, m): tx, ty = self.g_translate.xy m.translate(tx, ty, 0) m.translate(self.x, self.y, 0) return super(ScrollView, self)._apply_transform(m) def simulate_touch_down(self, touch): # at this point the touch is in parent coords touch.push() touch.apply_transform_2d(self.to_local) ret = super(ScrollView, self).on_touch_down(touch) touch.pop() return ret def on_touch_down(self, touch): if self.dispatch('on_scroll_start', touch): self._touch = touch touch.grab(self) return True def on_scroll_start(self, touch, check_children=True): if check_children: touch.push() touch.apply_transform_2d(self.to_local) if self.dispatch_children('on_scroll_start', touch): return True touch.pop() if not self.collide_point(*touch.pos): touch.ud[self._get_uid('svavoid')] = True return if self.disabled: return True if self._touch or (not (self.do_scroll_x or self.do_scroll_y)): return self.simulate_touch_down(touch) # handle mouse scrolling, only if the viewport size is bigger than the # scrollview size, and if the user allowed to do it vp = self._viewport if not vp: return True scroll_type = self.scroll_type ud = touch.ud scroll_bar = 'bars' in scroll_type # check if touch is in bar_x(horizontal) or bay_y(bertical) ud['in_bar_x'] = ud['in_bar_y'] = False width_scrollable = vp.width > self.width height_scrollable = vp.height > self.height bar_pos_x = self.bar_pos_x[0] bar_pos_y = self.bar_pos_y[0] d = {'b': True if touch.y < self.y + self.bar_width else False, 't': True if touch.y > self.top - self.bar_width else False, 'l': True if touch.x < self.x + self.bar_width else False, 'r': True if touch.x > self.right - self.bar_width else False} if scroll_bar: if (width_scrollable and d[bar_pos_x]): ud['in_bar_x'] = True if (height_scrollable and d[bar_pos_y]): ud['in_bar_y'] = True if vp and 'button' in touch.profile and \ touch.button.startswith('scroll'): btn = touch.button m = sp(self.scroll_wheel_distance) e = None if ((btn == 'scrolldown' and self.scroll_y >= 1) or (btn == 'scrollup' and self.scroll_y <= 0) or (btn == 'scrollleft' and self.scroll_x >= 1) or (btn == 'scrollright' and self.scroll_x <= 0)): return False if (self.effect_x and self.do_scroll_y and height_scrollable and btn in ('scrolldown', 'scrollup')): e = self.effect_x if ud['in_bar_x'] else self.effect_y elif (self.effect_y and self.do_scroll_x and width_scrollable and btn in ('scrollleft', 'scrollright')): e = self.effect_y if ud['in_bar_y'] else self.effect_x if e: if btn in ('scrolldown', 'scrollleft'): e.value = max(e.value - m, e.min) e.velocity = 0 elif btn in ('scrollup', 'scrollright'): e.value = min(e.value + m, e.max) e.velocity = 0 touch.ud[self._get_uid('svavoid')] = True e.trigger_velocity_update() return True # no mouse scrolling, so the user is going to drag the scrollview with # this touch. self._touch = touch uid = self._get_uid() ud[uid] = { 'mode': 'unknown', 'dx': 0, 'dy': 0, 'user_stopped': False, 'frames': Clock.frames, 'time': touch.time_start} if self.do_scroll_x and self.effect_x and not ud['in_bar_x']: self.effect_x.start(touch.x) self._scroll_x_mouse = self.scroll_x if self.do_scroll_y and self.effect_y and not ud['in_bar_y']: self.effect_y.start(touch.y) self._scroll_y_mouse = self.scroll_y if (ud.get('in_bar_x', False) or ud.get('in_bar_y', False)): return True Clock.schedule_once(self._change_touch_mode, self.scroll_timeout / 1000.) if scroll_type == ['bars']: return False else: return True def on_touch_move(self, touch): if self._touch is not touch: # touch is in parent touch.push() touch.apply_transform_2d(self.to_local) super(ScrollView, self).on_touch_move(touch) touch.pop() return self._get_uid() in touch.ud if touch.grab_current is not self: return True if not (self.do_scroll_y or self.do_scroll_x): return super(ScrollView, self).on_touch_move(touch) touch.ud['sv.handled'] = {'x': False, 'y': False} if self.dispatch('on_scroll_move', touch): return True def on_scroll_move(self, touch): if self._get_uid('svavoid') in touch.ud: return False touch.push() touch.apply_transform_2d(self.to_local) if self.dispatch_children('on_scroll_move', touch): return True touch.pop() rv = True uid = self._get_uid() if not uid in touch.ud: self._touch = False return self.on_scroll_start(touch, False) ud = touch.ud[uid] mode = ud['mode'] # check if the minimum distance has been travelled if mode == 'unknown' or mode == 'scroll': if not touch.ud['sv.handled']['x'] and self.do_scroll_x \ and self.effect_x: width = self.width if touch.ud.get('in_bar_x', False): dx = touch.dx / float(width - width * self.hbar[1]) self.scroll_x = min(max(self.scroll_x + dx, 0.), 1.) self._trigger_update_from_scroll() else: if self.scroll_type != ['bars']: self.effect_x.update(touch.x) if self.scroll_x < 0 or self.scroll_x > 1: rv = False else: touch.ud['sv.handled']['x'] = True if not touch.ud['sv.handled']['y'] and self.do_scroll_y \ and self.effect_y: height = self.height if touch.ud.get('in_bar_y', False): dy = touch.dy / float(height - height * self.vbar[1]) self.scroll_y = min(max(self.scroll_y + dy, 0.), 1.) self._trigger_update_from_scroll() else: if self.scroll_type != ['bars']: self.effect_y.update(touch.y) if self.scroll_y < 0 or self.scroll_y > 1: rv = False else: touch.ud['sv.handled']['y'] = True if mode == 'unknown': ud['dx'] += abs(touch.dx) ud['dy'] += abs(touch.dy) if ud['dx'] > self.scroll_distance: if not self.do_scroll_x: # touch is in parent, but _change expects window coords touch.push() touch.apply_transform_2d(self.to_local) touch.apply_transform_2d(self.to_window) self._change_touch_mode() touch.pop() return mode = 'scroll' if ud['dy'] > self.scroll_distance: if not self.do_scroll_y: # touch is in parent, but _change expects window coords touch.push() touch.apply_transform_2d(self.to_local) touch.apply_transform_2d(self.to_window) self._change_touch_mode() touch.pop() return mode = 'scroll' ud['mode'] = mode if mode == 'scroll': ud['dt'] = touch.time_update - ud['time'] ud['time'] = touch.time_update ud['user_stopped'] = True return rv def on_touch_up(self, touch): if self._touch is not touch and self.uid not in touch.ud: # touch is in parents touch.push() touch.apply_transform_2d(self.to_local) if super(ScrollView, self).on_touch_up(touch): return True touch.pop() return False if self.dispatch('on_scroll_stop', touch): touch.ungrab(self) return True def on_scroll_stop(self, touch, check_children=True): self._touch = None if check_children: touch.push() touch.apply_transform_2d(self.to_local) if self.dispatch_children('on_scroll_stop', touch): return True touch.pop() if self._get_uid('svavoid') in touch.ud: return if self._get_uid() not in touch.ud: return False self._touch = None uid = self._get_uid() ud = touch.ud[uid] if self.do_scroll_x and self.effect_x: if not touch.ud.get('in_bar_x', False) and\ self.scroll_type != ['bars']: self.effect_x.stop(touch.x) if self.do_scroll_y and self.effect_y and\ self.scroll_type != ['bars']: if not touch.ud.get('in_bar_y', False): self.effect_y.stop(touch.y) if ud['mode'] == 'unknown': # we must do the click at least.. # only send the click if it was not a click to stop # autoscrolling if not ud['user_stopped']: self.simulate_touch_down(touch) Clock.schedule_once(partial(self._do_touch_up, touch), .2) Clock.unschedule(self._update_effect_bounds) Clock.schedule_once(self._update_effect_bounds) # if we do mouse scrolling, always accept it if 'button' in touch.profile and touch.button.startswith('scroll'): return True return self._get_uid() in touch.ud def convert_distance_to_scroll(self, dx, dy): '''Convert a distance in pixels to a scroll distance, depending on the content size and the scrollview size. The result will be a tuple of scroll distance that can be added to :data:`scroll_x` and :data:`scroll_y` ''' if not self._viewport: return 0, 0 vp = self._viewport if vp.width > self.width: sw = vp.width - self.width sx = dx / float(sw) else: sx = 0 if vp.height > self.height: sh = vp.height - self.height sy = dy / float(sh) else: sy = 1 return sx, sy def update_from_scroll(self, *largs): '''Force the reposition of the content, according to current value of :attr:`scroll_x` and :attr:`scroll_y`. This method is automatically called when one of the :attr:`scroll_x`, :attr:`scroll_y`, :attr:`pos` or :attr:`size` properties change, or if the size of the content changes. ''' if not self._viewport: return vp = self._viewport # update from size_hint if vp.size_hint_x is not None: vp.width = vp.size_hint_x * self.width if vp.size_hint_y is not None: vp.height = vp.size_hint_y * self.height if vp.width > self.width: sw = vp.width - self.width x = self.x - self.scroll_x * sw else: x = self.x if vp.height > self.height: sh = vp.height - self.height y = self.y - self.scroll_y * sh else: y = self.top - vp.height # from 1.8.0, we now use a matrix by default, instead of moving the # widget position behind. We set it here, but it will be a no-op most of # the time. vp.pos = 0, 0 self.g_translate.xy = x, y # New in 1.2.0, show bar when scrolling happens and (changed in 1.9.0) # fade to bar_inactive_color when no scroll is happening. Clock.unschedule(self._bind_inactive_bar_color) self.unbind(bar_inactive_color=self._change_bar_color) Animation.stop_all(self, '_bar_color') self.bind(bar_color=self._change_bar_color) self._bar_color = self.bar_color Clock.schedule_once(self._bind_inactive_bar_color, .5) def _bind_inactive_bar_color(self, *l): self.unbind(bar_color=self._change_bar_color) self.bind(bar_inactive_color=self._change_bar_color) Animation( _bar_color=self.bar_inactive_color, d=.5, t='out_quart').start(self) def _change_bar_color(self, inst, value): self._bar_color = value # # Private # def add_widget(self, widget, index=0): if self._viewport: raise Exception('ScrollView accept only one widget') canvas = self.canvas self.canvas = self.canvas_viewport super(ScrollView, self).add_widget(widget, index) self.canvas = canvas self._viewport = widget widget.bind(size=self._trigger_update_from_scroll) self._trigger_update_from_scroll() def remove_widget(self, widget): canvas = self.canvas self.canvas = self.canvas_viewport super(ScrollView, self).remove_widget(widget) self.canvas = canvas if widget is self._viewport: self._viewport = None def _get_uid(self, prefix='sv'): return '{0}.{1}'.format(prefix, self.uid) def _change_touch_mode(self, *largs): if not self._touch: return uid = self._get_uid() touch = self._touch ud = touch.ud[uid] if ud['mode'] != 'unknown' or ud['user_stopped']: return diff_frames = Clock.frames - ud['frames'] # in order to be able to scroll on very slow devices, let at least 3 # frames displayed to accumulate some velocity. And then, change the # touch mode. Otherwise, we might never be able to compute velocity, and # no way to scroll it. See #1464 and #1499 if diff_frames < 3: Clock.schedule_once(self._change_touch_mode, 0) return if self.do_scroll_x and self.effect_x: self.effect_x.cancel() if self.do_scroll_y and self.effect_y: self.effect_y.cancel() # XXX the next line was in the condition. But this stop # the possibily to "drag" an object out of the scrollview in the # non-used direction: if you have an horizontal scrollview, a # vertical gesture will not "stop" the scroll view to look for an # horizontal gesture, until the timeout is done. # and touch.dx + touch.dy == 0: touch.ungrab(self) self._touch = None # touch is in window coords touch.push() touch.apply_transform_2d(self.to_widget) touch.apply_transform_2d(self.to_parent) self.simulate_touch_down(touch) touch.pop() return def _do_touch_up(self, touch, *largs): # touch is in window coords touch.push() touch.apply_transform_2d(self.to_widget) super(ScrollView, self).on_touch_up(touch) touch.pop() # don't forget about grab event! for x in touch.grab_list[:]: touch.grab_list.remove(x) x = x() if not x: continue touch.grab_current = x # touch is in window coords touch.push() touch.apply_transform_2d(self.to_widget) super(ScrollView, self).on_touch_up(touch) touch.pop() touch.grab_current = None
class ScrollView(StencilView): '''ScrollView class. See module documentation for more information. :Events: `on_scroll_start` Generic event fired when scrolling starts from touch. `on_scroll_move` Generic event fired when scrolling move from touch. `on_scroll_stop` Generic event fired when scrolling stops from touch. .. versionchanged:: 1.9.0 `on_scroll_start`, `on_scroll_move` and `on_scroll_stop` events are now dispatched when scrolling to handle nested ScrollViews. .. versionchanged:: 1.7.0 `auto_scroll`, `scroll_friction`, `scroll_moves`, `scroll_stoptime' has been deprecated, use :attr:`effect_cls` instead. ''' scroll_distance = NumericProperty(_scroll_distance) '''Distance to move before scrolling the :class:`ScrollView`, in pixels. As soon as the distance has been traveled, the :class:`ScrollView` will start to scroll, and no touch event will go to children. It is advisable that you base this value on the dpi of your target device's screen. :attr:`scroll_distance` is a :class:`~kivy.properties.NumericProperty` and defaults to 20 (pixels), according to the default value in user configuration. ''' scroll_wheel_distance = NumericProperty(20) '''Distance to move when scrolling with a mouse wheel. It is advisable that you base this value on the dpi of your target device's screen. .. versionadded:: 1.8.0 :attr:`scroll_wheel_distance` is a :class:`~kivy.properties.NumericProperty` , defaults to 20 pixels. ''' scroll_timeout = NumericProperty(_scroll_timeout) '''Timeout allowed to trigger the :attr:`scroll_distance`, in milliseconds. If the user has not moved :attr:`scroll_distance` within the timeout, the scrolling will be disabled, and the touch event will go to the children. :attr:`scroll_timeout` is a :class:`~kivy.properties.NumericProperty` and defaults to 55 (milliseconds) according to the default value in user configuration. .. versionchanged:: 1.5.0 Default value changed from 250 to 55. ''' scroll_x = NumericProperty(0.) '''X scrolling value, between 0 and 1. If 0, the content's left side will touch the left side of the ScrollView. If 1, the content's right side will touch the right side. This property is controled by :class:`ScrollView` only if :attr:`do_scroll_x` is True. :attr:`scroll_x` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' scroll_y = NumericProperty(1.) '''Y scrolling value, between 0 and 1. If 0, the content's bottom side will touch the bottom side of the ScrollView. If 1, the content's top side will touch the top side. This property is controled by :class:`ScrollView` only if :attr:`do_scroll_y` is True. :attr:`scroll_y` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. ''' do_scroll_x = BooleanProperty(True) '''Allow scroll on X axis. :attr:`do_scroll_x` is a :class:`~kivy.properties.BooleanProperty` and defaults to True. ''' do_scroll_y = BooleanProperty(True) '''Allow scroll on Y axis. :attr:`do_scroll_y` is a :class:`~kivy.properties.BooleanProperty` and defaults to True. ''' def _get_do_scroll(self): return (self.do_scroll_x, self.do_scroll_y) def _set_do_scroll(self, value): if type(value) in (list, tuple): self.do_scroll_x, self.do_scroll_y = value else: self.do_scroll_x = self.do_scroll_y = bool(value) do_scroll = AliasProperty(_get_do_scroll, _set_do_scroll, bind=('do_scroll_x', 'do_scroll_y')) '''Allow scroll on X or Y axis. :attr:`do_scroll` is a :class:`~kivy.properties.AliasProperty` of (:attr:`do_scroll_x` + :attr:`do_scroll_y`) ''' def _get_vbar(self): # must return (y, height) in % # calculate the viewport size / scrollview size % if self._viewport is None: return 0, 1. vh = self._viewport.height h = self.height if vh < h or vh == 0: return 0, 1. ph = max(0.01, h / float(vh)) sy = min(1.0, max(0.0, self.scroll_y)) py = (1. - ph) * sy return (py, ph) vbar = AliasProperty(_get_vbar, None, bind=('scroll_y', '_viewport', 'viewport_size')) '''Return a tuple of (position, size) of the vertical scrolling bar. .. versionadded:: 1.2.0 The position and size are normalized between 0-1, and represent a percentage of the current scrollview height. This property is used internally for drawing the little vertical bar when you're scrolling. :attr:`vbar` is a :class:`~kivy.properties.AliasProperty`, readonly. ''' def _get_hbar(self): # must return (x, width) in % # calculate the viewport size / scrollview size % if self._viewport is None: return 0, 1. vw = self._viewport.width w = self.width if vw < w or vw == 0: return 0, 1. pw = max(0.01, w / float(vw)) sx = min(1.0, max(0.0, self.scroll_x)) px = (1. - pw) * sx return (px, pw) hbar = AliasProperty(_get_hbar, None, bind=('scroll_x', '_viewport', 'viewport_size')) '''Return a tuple of (position, size) of the horizontal scrolling bar. .. versionadded:: 1.2.0 The position and size are normalized between 0-1, and represent a percentage of the current scrollview height. This property is used internally for drawing the little horizontal bar when you're scrolling. :attr:`vbar` is a :class:`~kivy.properties.AliasProperty`, readonly. ''' bar_color = ListProperty([.7, .7, .7, .9]) '''Color of horizontal / vertical scroll bar, in RGBA format. .. versionadded:: 1.2.0 :attr:`bar_color` is a :class:`~kivy.properties.ListProperty` and defaults to [.7, .7, .7, .9]. ''' bar_inactive_color = ListProperty([.7, .7, .7, .2]) '''Color of horizontal / vertical scroll bar (in RGBA format), when no scroll is happening. .. versionadded:: 1.9.0 :attr:`bar_inactive_color` is a :class:`~kivy.properties.ListProperty` and defaults to [.7, .7, .7, .2]. ''' bar_width = NumericProperty('2dp') '''Width of the horizontal / vertical scroll bar. The width is interpreted as a height for the horizontal bar. .. versionadded:: 1.2.0 :attr:`bar_width` is a :class:`~kivy.properties.NumericProperty` and defaults to 2. ''' bar_pos_x = OptionProperty('bottom', options=('top', 'bottom')) '''Which side of the ScrollView the horizontal scroll bar should go on. Possible values are 'top' and 'bottom'. .. versionadded:: 1.8.0 :attr:`bar_pos_x` is an :class:`~kivy.properties.OptionProperty`, defaults to 'bottom'. ''' bar_pos_y = OptionProperty('right', options=('left', 'right')) '''Which side of the ScrollView the vertical scroll bar should go on. Possible values are 'left' and 'right'. .. versionadded:: 1.8.0 :attr:`bar_pos_y` is an :class:`~kivy.properties.OptionProperty` and defaults to 'right'. ''' bar_pos = ReferenceListProperty(bar_pos_x, bar_pos_y) '''Which side of the scroll view to place each of the bars on. :attr:`bar_pos` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`bar_pos_x`, :attr:`bar_pos_y`) ''' bar_margin = NumericProperty(0) '''Margin between the bottom / right side of the scrollview when drawing the horizontal / vertical scroll bar. .. versionadded:: 1.2.0 :attr:`bar_margin` is a :class:`~kivy.properties.NumericProperty`, default to 0 ''' effect_cls = ObjectProperty(DampedScrollEffect, allownone=True) '''Class effect to instanciate for X and Y axis. .. versionadded:: 1.7.0 :attr:`effect_cls` is an :class:`~kivy.properties.ObjectProperty` and defaults to :class:`DampedScrollEffect`. .. versionchanged:: 1.8.0 If you set a string, the :class:`~kivy.factory.Factory` will be used to resolve the class. ''' effect_x = ObjectProperty(None, allownone=True) '''Effect to apply for the X axis. If None is set, an instance of :attr:`effect_cls` will be created. .. versionadded:: 1.7.0 :attr:`effect_x` is an :class:`~kivy.properties.ObjectProperty` and defaults to None. ''' effect_y = ObjectProperty(None, allownone=True) '''Effect to apply for the Y axis. If None is set, an instance of :attr:`effect_cls` will be created. .. versionadded:: 1.7.0 :attr:`effect_y` is an :class:`~kivy.properties.ObjectProperty` and defaults to None, read-only. ''' viewport_size = ListProperty([0, 0]) '''(internal) Size of the internal viewport. This is the size of your only child in the scrollview. ''' scroll_type = OptionProperty(['content'], options=(['content'], ['bars'], ['bars', 'content'], ['content', 'bars'])) '''Sets the type of scrolling to use for the content of the scrollview. Available options are: ['content'], ['bars'], ['bars', 'content']. .. versionadded:: 1.8.0 :attr:`scroll_type` is a :class:`~kivy.properties.OptionProperty`, defaults to ['content']. ''' # private, for internal use only _viewport = ObjectProperty(None, allownone=True) _bar_color = ListProperty([0, 0, 0, 0]) def _set_viewport_size(self, instance, value): self.viewport_size = value def on__viewport(self, instance, value): if value: value.bind(size=self._set_viewport_size) self.viewport_size = value.size __events__ = ('on_scroll_start', 'on_scroll_move', 'on_scroll_stop') def __init__(self, **kwargs): self._touch = None self._trigger_update_from_scroll = Clock.create_trigger( self.update_from_scroll, -1) # create a specific canvas for the viewport from kivy.graphics import PushMatrix, Translate, PopMatrix, Canvas self.canvas_viewport = Canvas() self.canvas = Canvas() with self.canvas_viewport.before: PushMatrix() self.g_translate = Translate(0, 0) with self.canvas_viewport.after: PopMatrix() super(ScrollView, self).__init__(**kwargs) self.register_event_type('on_scroll_start') self.register_event_type('on_scroll_move') self.register_event_type('on_scroll_stop') # now add the viewport canvas to our canvas self.canvas.add(self.canvas_viewport) effect_cls = self.effect_cls if isinstance(effect_cls, string_types): effect_cls = Factory.get(effect_cls) if self.effect_x is None and effect_cls is not None: self.effect_x = effect_cls(target_widget=self._viewport) if self.effect_y is None and effect_cls is not None: self.effect_y = effect_cls(target_widget=self._viewport) self.bind(width=self._update_effect_x_bounds, height=self._update_effect_y_bounds, viewport_size=self._update_effect_bounds, _viewport=self._update_effect_widget, scroll_x=self._trigger_update_from_scroll, scroll_y=self._trigger_update_from_scroll, pos=self._trigger_update_from_scroll, size=self._trigger_update_from_scroll) self._update_effect_widget() self._update_effect_x_bounds() self._update_effect_y_bounds() def on_effect_x(self, instance, value): if value: value.bind(scroll=self._update_effect_x) value.target_widget = self._viewport def on_effect_y(self, instance, value): if value: value.bind(scroll=self._update_effect_y) value.target_widget = self._viewport def on_effect_cls(self, instance, cls): if isinstance(cls, string_types): cls = Factory.get(cls) self.effect_x = cls(target_widget=self._viewport) self.effect_x.bind(scroll=self._update_effect_x) self.effect_y = cls(target_widget=self._viewport) self.effect_y.bind(scroll=self._update_effect_y) def _update_effect_widget(self, *args): if self.effect_x: self.effect_x.target_widget = self._viewport if self.effect_y: self.effect_y.target_widget = self._viewport def _update_effect_x_bounds(self, *args): if not self._viewport or not self.effect_x: return self.effect_x.min = -(self.viewport_size[0] - self.width) self.effect_x.max = 0 self.effect_x.value = self.effect_x.min * self.scroll_x def _update_effect_y_bounds(self, *args): if not self._viewport or not self.effect_y: return self.effect_y.min = -(self.viewport_size[1] - self.height) self.effect_y.max = 0 self.effect_y.value = self.effect_y.min * self.scroll_y def _update_effect_bounds(self, *args): if not self._viewport: return if self.effect_x: self._update_effect_x_bounds() if self.effect_y: self._update_effect_y_bounds() def _update_effect_x(self, *args): vp = self._viewport if not vp or not self.effect_x: return sw = vp.width - self.width if sw < 1: return sx = self.effect_x.scroll / float(sw) self.scroll_x = -sx self._trigger_update_from_scroll() def _update_effect_y(self, *args): vp = self._viewport if not vp or not self.effect_y: return sh = vp.height - self.height if sh < 1: return sy = self.effect_y.scroll / float(sh) self.scroll_y = -sy self._trigger_update_from_scroll() def to_local(self, x, y, **k): tx, ty = self.g_translate.xy return x - tx, y - ty def to_parent(self, x, y, **k): tx, ty = self.g_translate.xy return x + tx, y + ty def _apply_transform(self, m, pos=None): tx, ty = self.g_translate.xy m.translate(tx, ty, 0) return super(ScrollView, self)._apply_transform(m, (0, 0)) def simulate_touch_down(self, touch): # at this point the touch is in parent coords touch.push() touch.apply_transform_2d(self.to_local) ret = super(ScrollView, self).on_touch_down(touch) touch.pop() return ret def on_touch_down(self, touch): if self.dispatch('on_scroll_start', touch): self._touch = touch touch.grab(self) return True def on_scroll_start(self, touch, check_children=True): if check_children: touch.push() touch.apply_transform_2d(self.to_local) if self.dispatch_children('on_scroll_start', touch): return True touch.pop() if not self.collide_point(*touch.pos): touch.ud[self._get_uid('svavoid')] = True return if self.disabled: return True if self._touch or (not (self.do_scroll_x or self.do_scroll_y)): return self.simulate_touch_down(touch) # handle mouse scrolling, only if the viewport size is bigger than the # scrollview size, and if the user allowed to do it vp = self._viewport if not vp: return True scroll_type = self.scroll_type ud = touch.ud scroll_bar = 'bars' in scroll_type # check if touch is in bar_x(horizontal) or bay_y(bertical) ud['in_bar_x'] = ud['in_bar_y'] = False width_scrollable = vp.width > self.width height_scrollable = vp.height > self.height bar_pos_x = self.bar_pos_x[0] bar_pos_y = self.bar_pos_y[0] d = { 'b': True if touch.y < self.y + self.bar_width else False, 't': True if touch.y > self.top - self.bar_width else False, 'l': True if touch.x < self.x + self.bar_width else False, 'r': True if touch.x > self.right - self.bar_width else False } if scroll_bar: if (width_scrollable and d[bar_pos_x]): ud['in_bar_x'] = True if (height_scrollable and d[bar_pos_y]): ud['in_bar_y'] = True if vp and 'button' in touch.profile and \ touch.button.startswith('scroll'): btn = touch.button m = sp(self.scroll_wheel_distance) e = None if ((btn == 'scrolldown' and self.scroll_y >= 1) or (btn == 'scrollup' and self.scroll_y <= 0) or (btn == 'scrollleft' and self.scroll_x >= 1) or (btn == 'scrollright' and self.scroll_x <= 0)): return False if (self.effect_x and self.do_scroll_y and height_scrollable and btn in ('scrolldown', 'scrollup')): e = self.effect_x if ud['in_bar_x'] else self.effect_y elif (self.effect_y and self.do_scroll_x and width_scrollable and btn in ('scrollleft', 'scrollright')): e = self.effect_y if ud['in_bar_y'] else self.effect_x if e: if btn in ('scrolldown', 'scrollleft'): e.value = max(e.value - m, e.min) e.velocity = 0 elif btn in ('scrollup', 'scrollright'): e.value = min(e.value + m, e.max) e.velocity = 0 touch.ud[self._get_uid('svavoid')] = True e.trigger_velocity_update() return True # no mouse scrolling, so the user is going to drag the scrollview with # this touch. self._touch = touch uid = self._get_uid() ud[uid] = { 'mode': 'unknown', 'dx': 0, 'dy': 0, 'user_stopped': False, 'frames': Clock.frames, 'time': touch.time_start } if self.do_scroll_x and self.effect_x and not ud['in_bar_x']: self.effect_x.start(touch.x) self._scroll_x_mouse = self.scroll_x if self.do_scroll_y and self.effect_y and not ud['in_bar_y']: self.effect_y.start(touch.y) self._scroll_y_mouse = self.scroll_y if (ud.get('in_bar_x', False) or ud.get('in_bar_y', False)): return True Clock.schedule_once(self._change_touch_mode, self.scroll_timeout / 1000.) if scroll_type == ['bars']: return False else: return True def on_touch_move(self, touch): if self._touch is not touch: # touch is in parent touch.push() touch.apply_transform_2d(self.to_local) super(ScrollView, self).on_touch_move(touch) touch.pop() return self._get_uid() in touch.ud if touch.grab_current is not self: return True if not (self.do_scroll_y or self.do_scroll_x): return super(ScrollView, self).on_touch_move(touch) touch.ud['sv.handled'] = {'x': False, 'y': False} if self.dispatch('on_scroll_move', touch): return True def on_scroll_move(self, touch): if self._get_uid('svavoid') in touch.ud: return False touch.push() touch.apply_transform_2d(self.to_local) if self.dispatch_children('on_scroll_move', touch): return True touch.pop() rv = True uid = self._get_uid() if not uid in touch.ud: self._touch = False return self.on_scroll_start(touch, False) ud = touch.ud[uid] mode = ud['mode'] # check if the minimum distance has been travelled if mode == 'unknown' or mode == 'scroll': if not touch.ud['sv.handled']['x'] and self.do_scroll_x \ and self.effect_x: width = self.width if touch.ud.get('in_bar_x', False): dx = touch.dx / float(width - width * self.hbar[1]) self.scroll_x = min(max(self.scroll_x + dx, 0.), 1.) self._trigger_update_from_scroll() else: if self.scroll_type != ['bars']: self.effect_x.update(touch.x) if self.scroll_x < 0 or self.scroll_x > 1: rv = False else: touch.ud['sv.handled']['x'] = True if not touch.ud['sv.handled']['y'] and self.do_scroll_y \ and self.effect_y: height = self.height if touch.ud.get('in_bar_y', False): dy = touch.dy / float(height - height * self.vbar[1]) self.scroll_y = min(max(self.scroll_y + dy, 0.), 1.) self._trigger_update_from_scroll() else: if self.scroll_type != ['bars']: self.effect_y.update(touch.y) if self.scroll_y < 0 or self.scroll_y > 1: rv = False else: touch.ud['sv.handled']['y'] = True if mode == 'unknown': ud['dx'] += abs(touch.dx) ud['dy'] += abs(touch.dy) if ud['dx'] > self.scroll_distance: if not self.do_scroll_x: # touch is in parent, but _change expects window coords touch.push() touch.apply_transform_2d(self.to_local) touch.apply_transform_2d(self.to_window) self._change_touch_mode() touch.pop() return mode = 'scroll' if ud['dy'] > self.scroll_distance: if not self.do_scroll_y: # touch is in parent, but _change expects window coords touch.push() touch.apply_transform_2d(self.to_local) touch.apply_transform_2d(self.to_window) self._change_touch_mode() touch.pop() return mode = 'scroll' ud['mode'] = mode if mode == 'scroll': ud['dt'] = touch.time_update - ud['time'] ud['time'] = touch.time_update ud['user_stopped'] = True return rv def on_touch_up(self, touch): if self._touch is not touch and self.uid not in touch.ud: # touch is in parents touch.push() touch.apply_transform_2d(self.to_local) if super(ScrollView, self).on_touch_up(touch): return True touch.pop() return False if self.dispatch('on_scroll_stop', touch): touch.ungrab(self) return True def on_scroll_stop(self, touch, check_children=True): self._touch = None if check_children: touch.push() touch.apply_transform_2d(self.to_local) if self.dispatch_children('on_scroll_stop', touch): return True touch.pop() if self._get_uid('svavoid') in touch.ud: return if self._get_uid() not in touch.ud: return False self._touch = None uid = self._get_uid() ud = touch.ud[uid] if self.do_scroll_x and self.effect_x: if not touch.ud.get('in_bar_x', False) and\ self.scroll_type != ['bars']: self.effect_x.stop(touch.x) if self.do_scroll_y and self.effect_y and\ self.scroll_type != ['bars']: if not touch.ud.get('in_bar_y', False): self.effect_y.stop(touch.y) if ud['mode'] == 'unknown': # we must do the click at least.. # only send the click if it was not a click to stop # autoscrolling if not ud['user_stopped']: self.simulate_touch_down(touch) Clock.schedule_once(partial(self._do_touch_up, touch), .2) Clock.unschedule(self._update_effect_bounds) Clock.schedule_once(self._update_effect_bounds) # if we do mouse scrolling, always accept it if 'button' in touch.profile and touch.button.startswith('scroll'): return True return self._get_uid() in touch.ud def scroll_to(self, widget, padding=10, animate=True): '''Scrolls the viewport to ensure that the given widget is visible, optionally with padding and animation. If animate is True (the default), then the default animation parameters will be used. Otherwise, it should be a dict containing arguments to pass to :class:`~kivy.animation.Animation` constructor. .. versionadded:: 1.9.1 ''' if not self.parent: return if isinstance(padding, (int, float)): padding = (padding, padding) pos = self.parent.to_widget(*widget.to_window(*widget.pos)) cor = self.parent.to_widget( *widget.to_window(widget.right, widget.top)) dx = dy = 0 if pos[1] < self.y: dy = self.y - pos[1] + dp(padding[1]) elif cor[1] > self.top: dy = self.top - cor[1] - dp(padding[1]) if pos[0] < self.x: dx = self.x - pos[0] + dp(padding[0]) elif cor[0] > self.right: dx = self.right - cor[0] - dp(padding[0]) dsx, dsy = self.convert_distance_to_scroll(dx, dy) sxp = min(1, max(0, self.scroll_x - dsx)) syp = min(1, max(0, self.scroll_y - dsy)) if animate: if animate is True: animate = {'d': 0.2, 't': 'out_quad'} Animation.stop_all(self, 'scroll_x', 'scroll_y') Animation(scroll_x=sxp, scroll_y=syp, **animate).start(self) else: self.scroll_x = sxp self.scroll_y = syp def convert_distance_to_scroll(self, dx, dy): '''Convert a distance in pixels to a scroll distance, depending on the content size and the scrollview size. The result will be a tuple of scroll distance that can be added to :data:`scroll_x` and :data:`scroll_y` ''' if not self._viewport: return 0, 0 vp = self._viewport if vp.width > self.width: sw = vp.width - self.width sx = dx / float(sw) else: sx = 0 if vp.height > self.height: sh = vp.height - self.height sy = dy / float(sh) else: sy = 1 return sx, sy def update_from_scroll(self, *largs): '''Force the reposition of the content, according to current value of :attr:`scroll_x` and :attr:`scroll_y`. This method is automatically called when one of the :attr:`scroll_x`, :attr:`scroll_y`, :attr:`pos` or :attr:`size` properties change, or if the size of the content changes. ''' if not self._viewport: return vp = self._viewport # update from size_hint if vp.size_hint_x is not None: vp.width = vp.size_hint_x * self.width if vp.size_hint_y is not None: vp.height = vp.size_hint_y * self.height if vp.width > self.width: sw = vp.width - self.width x = self.x - self.scroll_x * sw else: x = self.x if vp.height > self.height: sh = vp.height - self.height y = self.y - self.scroll_y * sh else: y = self.top - vp.height # from 1.8.0, we now use a matrix by default, instead of moving the # widget position behind. We set it here, but it will be a no-op most of # the time. vp.pos = 0, 0 self.g_translate.xy = x, y # New in 1.2.0, show bar when scrolling happens and (changed in 1.9.0) # fade to bar_inactive_color when no scroll is happening. Clock.unschedule(self._bind_inactive_bar_color) self.unbind(bar_inactive_color=self._change_bar_color) Animation.stop_all(self, '_bar_color') self.bind(bar_color=self._change_bar_color) self._bar_color = self.bar_color Clock.schedule_once(self._bind_inactive_bar_color, .5) def _bind_inactive_bar_color(self, *l): self.unbind(bar_color=self._change_bar_color) self.bind(bar_inactive_color=self._change_bar_color) Animation(_bar_color=self.bar_inactive_color, d=.5, t='out_quart').start(self) def _change_bar_color(self, inst, value): self._bar_color = value # # Private # def add_widget(self, widget, index=0): if self._viewport: raise Exception('ScrollView accept only one widget') canvas = self.canvas self.canvas = self.canvas_viewport super(ScrollView, self).add_widget(widget, index) self.canvas = canvas self._viewport = widget widget.bind(size=self._trigger_update_from_scroll) self._trigger_update_from_scroll() def remove_widget(self, widget): canvas = self.canvas self.canvas = self.canvas_viewport super(ScrollView, self).remove_widget(widget) self.canvas = canvas if widget is self._viewport: self._viewport = None def _get_uid(self, prefix='sv'): return '{0}.{1}'.format(prefix, self.uid) def _change_touch_mode(self, *largs): if not self._touch: return uid = self._get_uid() touch = self._touch if uid not in touch.ud: self._touch = False return ud = touch.ud[uid] if ud['mode'] != 'unknown' or ud['user_stopped']: return diff_frames = Clock.frames - ud['frames'] # in order to be able to scroll on very slow devices, let at least 3 # frames displayed to accumulate some velocity. And then, change the # touch mode. Otherwise, we might never be able to compute velocity, and # no way to scroll it. See #1464 and #1499 if diff_frames < 3: Clock.schedule_once(self._change_touch_mode, 0) return if self.do_scroll_x and self.effect_x: self.effect_x.cancel() if self.do_scroll_y and self.effect_y: self.effect_y.cancel() # XXX the next line was in the condition. But this stop # the possibily to "drag" an object out of the scrollview in the # non-used direction: if you have an horizontal scrollview, a # vertical gesture will not "stop" the scroll view to look for an # horizontal gesture, until the timeout is done. # and touch.dx + touch.dy == 0: touch.ungrab(self) self._touch = None # touch is in window coords touch.push() touch.apply_transform_2d(self.to_widget) touch.apply_transform_2d(self.to_parent) self.simulate_touch_down(touch) touch.pop() return def _do_touch_up(self, touch, *largs): # touch is in window coords touch.push() touch.apply_transform_2d(self.to_widget) super(ScrollView, self).on_touch_up(touch) touch.pop() # don't forget about grab event! for x in touch.grab_list[:]: touch.grab_list.remove(x) x = x() if not x: continue touch.grab_current = x # touch is in window coords touch.push() touch.apply_transform_2d(self.to_widget) super(ScrollView, self).on_touch_up(touch) touch.pop() touch.grab_current = None
class PageCurlTransition(TransitionBase): cy_x = NumericProperty(520.) cy_y = NumericProperty(-50) cy_dir = NumericProperty(1.18) cy_radius = NumericProperty(150.) def __init__(self, **kwargs): super(PageCurlTransition, self).__init__(**kwargs) self.fbo_in = None self.fbo_out = None def make_screen_fbo(self, screen, mode=None): assert(mode is not None) attr = 'fbo_' + mode fbo = getattr(self, attr) w, h = screen.size w = (w - w % TILE) + TILE h = (h - h % TILE) + TILE size = w, h if not fbo: fbo = Fbo(size=size) setattr(self, attr, fbo) fbo.clear() with fbo: ClearColor(0, 0, 0, 1) ClearBuffers() fbo.add(screen.canvas) fbo.draw() return fbo def add_screen(self, screen): self.screen_in.pos = self.screen_out.pos self.screen_in.size = self.screen_out.size self.manager.real_remove_widget(self.screen_out) print '-----------' print 'add_screen', screen, screen.canvas print 'screen_in', self.screen_in, self.screen_in.parent print 'screen_out', self.screen_out, self.screen_out.parent print '-----------' self.fbo_in = self.make_screen_fbo(self.screen_in, mode='in') self.fbo_out = self.make_screen_fbo(self.screen_out, mode='out') self.manager.canvas.add(self.fbo_in) self.manager.canvas.add(self.fbo_out) self.canvas = Canvas() self.c_front = RenderContext() self.c_front.shader.source = join(curdir, 'front.glsl') self.c_back = RenderContext() self.c_back.shader.source = join(curdir, 'back.glsl') self.c_backshadow = RenderContext() self.c_backshadow.shader.source = join(curdir, 'backshadow.glsl') self.canvas.add(self.c_front) self.canvas.add(self.c_back) self.canvas.add(self.c_backshadow) with self.canvas.before: Color(1, 1, 1) Rectangle( size=self.fbo_in.size, texture=self.fbo_in.texture) Callback(self._enter_3d) self._build_mesh(self.fbo_in.size) with self.canvas.after: Callback(self._leave_3d) self.manager.canvas.add(self.canvas) self.on_progress(0) def remove_screen(self, screen): self.manager.canvas.remove(self.fbo_in) self.manager.canvas.remove(self.fbo_out) self.manager.canvas.remove(self.canvas) self.manager.real_add_widget(self.screen_in) def on_progress(self, t): d = 0.8 if t < d: dt = t / d self.cy_dir = funcLinear(AnimationTransition.out_circ(dt), 0, 1.55) else: self.cy_dir = 1.5 self.cy_x = funcLinear(t, self.screen_in.width, -self.screen_in.width / 2.0) self.update_glsl() def update_glsl(self, *largs): size = self.manager.size proj = Matrix().view_clip(0, size[0], 0, size[1], -1000, 1000, 0) self.c_front['projection_mat'] = proj self.c_front['cylinder_position'] = map(float, (self.cy_x, self.cy_y)) self.c_front['cylinder_direction'] = (cos(self.cy_dir), sin(self.cy_dir)) self.c_front['cylinder_radius'] = float(self.cy_radius) self.c_front['texture1'] = 1 for key in ('projection_mat', 'cylinder_position', 'cylinder_radius', 'cylinder_direction', 'texture1'): self.c_back[key] = self.c_front[key] self.c_backshadow[key] = self.c_front[key] def _enter_3d(self, *args): glEnable(GL_DEPTH_TEST) glEnable(GL_CULL_FACE) def _leave_3d(self, *args): glDisable(GL_DEPTH_TEST) glDisable(GL_CULL_FACE) def _build_mesh(self, size): m = TILE width, height = map(int, size) step_width = int(width / (width / m)) step_height = int(height / (height / m)) vertices = [] indices = [] indices_back = [] fw = float(width) fh = float(height) # create all the vertices for y in xrange(0, height + step_height, step_height): for x in xrange(0, width + step_width, step_width): vertices += [x, y, 0, x / fw, y / fh] # trace a triangles mesh mx = 1 + width / step_width my = 1 + height / step_height self.vertex_format = [ ('vPosition', 3, 'float'), ('vTexCoords0', 2, 'float')] mode = 'line_loop' if DEBUG else 'triangles' for x in xrange(mx - 1): for y in xrange(my - 1): i = y * mx + x indices += [i, i + 1, i + 1 + mx, i, i + 1 + mx, i + mx] indices_back += [i, i + 1 + mx, i + 1, i, i + mx, i + 1 + mx] fbo_out_texture = None if DEBUG else self.fbo_out.texture self.g_mesh = Mesh(vertices=vertices, indices=indices, mode=mode, texture=fbo_out_texture, fmt=self.vertex_format) self.g_mesh_back = Mesh(vertices=vertices, indices=indices_back, mode=mode, texture=fbo_out_texture, fmt=self.vertex_format) self.o_vertices = vertices print 'vertices', len(vertices) print 'indices', len(indices) print 'indices_back', len(indices_back) self.c_front.add(BindTexture(source=join(curdir, 'frontshadow.png'), index=1)) self.c_front.add(self.g_mesh) self.c_backshadow.add(Rectangle(size=size)) self.c_back.add(BindTexture(source=join(curdir, 'backshadow.png'), index=1)) self.c_back.add(self.g_mesh_back)
class WindowBase(EventDispatcher): '''WindowBase is an abstract window widget for any window implementation. :Parameters: `borderless`: str, one of ('0', '1') Set the window border state. Check the :mod:`~kivy.config` documentation for a more detailed explanation on the values. `fullscreen`: str, one of ('0', '1', 'auto', 'fake') Make the window fullscreen. Check the :mod:`~kivy.config` documentation for a more detailed explanation on the values. `width`: int Width of the window. `height`: int Height of the window. :Events: `on_motion`: etype, motionevent Fired when a new :class:`~kivy.input.motionevent.MotionEvent` is dispatched `on_touch_down`: Fired when a new touch event is initiated. `on_touch_move`: Fired when an existing touch event changes location. `on_touch_up`: Fired when an existing touch event is terminated. `on_draw`: Fired when the :class:`Window` is being drawn. `on_flip`: Fired when the :class:`Window` GL surface is being flipped. `on_rotate`: rotation Fired when the :class:`Window` is being rotated. `on_close`: Fired when the :class:`Window` is closed. `on_request_close`: Fired when the event loop wants to close the window, or if the escape key is pressed and `exit_on_escape` is `True`. If a function bound to this event returns `True`, the window will not be closed. If the the event is triggered because of the keyboard escape key, the keyword argument `source` is dispatched along with a value of `keyboard` to the bound functions. .. versionadded:: 1.9.0 `on_keyboard`: key, scancode, codepoint, modifier Fired when the keyboard is used for input. .. versionchanged:: 1.3.0 The *unicode* parameter has been deprecated in favor of codepoint, and will be removed completely in future versions. `on_key_down`: key, scancode, codepoint Fired when a key pressed. .. versionchanged:: 1.3.0 The *unicode* parameter has been deprecated in favor of codepoint, and will be removed completely in future versions. `on_key_up`: key, scancode, codepoint Fired when a key is released. .. versionchanged:: 1.3.0 The *unicode* parameter has be deprecated in favor of codepoint, and will be removed completely in future versions. `on_dropfile`: str Fired when a file is dropped on the application. ''' __instance = None __initialized = False _fake_fullscreen = False # private properties _size = ListProperty([0, 0]) _modifiers = ListProperty([]) _rotation = NumericProperty(0) _clearcolor = ObjectProperty([0, 0, 0, 1]) children = ListProperty([]) '''List of the children of this window. :attr:`children` is a :class:`~kivy.properties.ListProperty` instance and defaults to an empty list. Use :meth:`add_widget` and :meth:`remove_widget` to manipulate the list of children. Don't manipulate the list directly unless you know what you are doing. ''' parent = ObjectProperty(None, allownone=True) '''Parent of this window. :attr:`parent` is a :class:`~kivy.properties.ObjectProperty` instance and defaults to None. When created, the parent is set to the window itself. You must take care of it if you are doing a recursive check. ''' icon = StringProperty() def _get_modifiers(self): return self._modifiers modifiers = AliasProperty(_get_modifiers, None) '''List of keyboard modifiers currently active. ''' def _get_size(self): r = self._rotation w, h = self._size if self.softinput_mode == 'resize': h -= self.keyboard_height if r in (0, 180): return w, h return h, w def _set_size(self, size): if self._size != size: r = self._rotation if r in (0, 180): self._size = size else: self._size = size[1], size[0] self.dispatch('on_resize', *size) return True else: return False size = AliasProperty(_get_size, _set_size, bind=('_size', )) '''Get the rotated size of the window. If :attr:`rotation` is set, then the size will change to reflect the rotation. ''' def _get_clearcolor(self): return self._clearcolor def _set_clearcolor(self, value): if value is not None: if type(value) not in (list, tuple): raise Exception('Clearcolor must be a list or tuple') if len(value) != 4: raise Exception('Clearcolor must contain 4 values') self._clearcolor = value clearcolor = AliasProperty(_get_clearcolor, _set_clearcolor, bind=('_clearcolor', )) '''Color used to clear the window. :: from kivy.core.window import Window # red background color Window.clearcolor = (1, 0, 0, 1) # don't clear background at all Window.clearcolor = None .. versionchanged:: 1.7.2 The clearcolor default value is now: (0, 0, 0, 1). ''' # make some property read-only def _get_width(self): r = self._rotation if r == 0 or r == 180: return self._size[0] return self._size[1] width = AliasProperty(_get_width, None, bind=('_rotation', '_size')) '''Rotated window width. :attr:`width` is a read-only :class:`~kivy.properties.AliasProperty`. ''' def _get_height(self): '''Rotated window height''' r = self._rotation kb = self.keyboard_height if self.softinput_mode == 'resize' else 0 if r == 0 or r == 180: return self._size[1] - kb return self._size[0] - kb height = AliasProperty(_get_height, None, bind=('_rotation', '_size')) '''Rotated window height. :attr:`height` is a read-only :class:`~kivy.properties.AliasProperty`. ''' def _get_center(self): return self.width / 2., self.height / 2. center = AliasProperty(_get_center, None, bind=('width', 'height')) '''Center of the rotated window. :attr:`center` is a :class:`~kivy.properties.AliasProperty`. ''' def _get_rotation(self): return self._rotation def _set_rotation(self, x): x = int(x % 360) if x == self._rotation: return if x not in (0, 90, 180, 270): raise ValueError('can rotate only 0, 90, 180, 270 degrees') self._rotation = x if self.initialized is False: return self.dispatch('on_resize', *self.size) self.dispatch('on_rotate', x) rotation = AliasProperty(_get_rotation, _set_rotation, bind=('_rotation', )) '''Get/set the window content rotation. Can be one of 0, 90, 180, 270 degrees. ''' softinput_mode = OptionProperty('', options=('', 'pan', 'scale', 'resize')) '''This specifies the behavior of window contents on display of soft keyboard on mobile platform. Can be one of '', 'pan', 'scale', 'resize'. When '' The main window is left as it is allowing the user to use :attr:`keyboard_height` to manage the window contents the way they want. when 'pan' The main window pans moving the bottom part of the window to be always on top of the keyboard. when 'resize' The window is resized and the contents scaled to fit the remaining space. ..versionadded::1.9.0 :attr:`softinput_mode` is a :class:`OptionProperty` defaults to None. ''' _keyboard_changed = BooleanProperty(False) def _upd_kbd_height(self, *kargs): self._keyboard_changed = not self._keyboard_changed def _get_ios_kheight(self): return 0 def _get_android_kheight(self): global android if not android: import android return android.get_keyboard_height() def _get_kheight(self): if platform == 'android': return self._get_android_kheight() if platform == 'ios': return self._get_ios_kheight() return 0 keyboard_height = AliasProperty(_get_kheight, None, bind=('_keyboard_changed',)) '''Rerturns the height of the softkeyboard/IME on mobile platforms. Will return 0 if not on mobile platform or if IME is not active. ..versionadded:: 1.9.0 :attr:`keyboard_height` is a read-only :class:`AliasProperty` defaults to 0. ''' def _set_system_size(self, size): self._size = size def _get_system_size(self): if self.softinput_mode == 'resize': return self._size[0], self._size[1] - self.keyboard_height return self._size system_size = AliasProperty( _get_system_size, _set_system_size, bind=('_size', )) '''Real size of the window ignoring rotation. ''' borderless = BooleanProperty(False) '''When set to True, this property removes the window border/decoration. .. versionadded:: 1.9.0 :attr:`borderless` is a :class:`BooleanProperty`, defaults to False. ''' fullscreen = OptionProperty(False, options=(True, False, 'auto', 'fake')) '''This property sets the fullscreen mode of the window. Available options are: True, False, 'auto', 'fake'. Check the :mod:`~kivy.config` documentation for a more detailed explanation on the values. .. versionadded:: 1.2.0 .. note:: The 'fake' option has been deprecated, use the :attr:`borderless` property instead. ''' mouse_pos = ObjectProperty([0, 0]) '''2d position of the mouse within the window. .. versionadded:: 1.2.0 ''' @property def __self__(self): return self top = NumericProperty(None, allownone=True) left = NumericProperty(None, allownone=True) position = OptionProperty('auto', options=['auto', 'custom']) render_context = ObjectProperty(None) canvas = ObjectProperty(None) title = StringProperty('Kivy') __events__ = ('on_draw', 'on_flip', 'on_rotate', 'on_resize', 'on_close', 'on_motion', 'on_touch_down', 'on_touch_move', 'on_touch_up', 'on_mouse_down', 'on_mouse_move', 'on_mouse_up', 'on_keyboard', 'on_key_down', 'on_key_up', 'on_dropfile', 'on_request_close', 'on_joy_axis', 'on_joy_hat', 'on_joy_ball', 'on_joy_button_down', "on_joy_button_up") def __new__(cls, **kwargs): if cls.__instance is None: cls.__instance = EventDispatcher.__new__(cls) return cls.__instance def __init__(self, **kwargs): kwargs.setdefault('force', False) # don't init window 2 times, # except if force is specified if WindowBase.__instance is not None and not kwargs.get('force'): return self.initialized = False self._is_desktop = Config.getboolean('kivy', 'desktop') # create a trigger for update/create the window when one of window # property changes self.trigger_create_window = Clock.create_trigger( self.create_window, -1) # Create a trigger for updating the keyboard height self.trigger_keyboard_height = Clock.create_trigger( self._upd_kbd_height, .5) # set the default window parameter according to the configuration if 'borderless' not in kwargs: kwargs['borderless'] = Config.getboolean('graphics', 'borderless') if 'fullscreen' not in kwargs: fullscreen = Config.get('graphics', 'fullscreen') if fullscreen not in ('auto', 'fake'): fullscreen = fullscreen.lower() in ('true', '1', 'yes', 'yup') kwargs['fullscreen'] = fullscreen if 'width' not in kwargs: kwargs['width'] = Config.getint('graphics', 'width') if 'height' not in kwargs: kwargs['height'] = Config.getint('graphics', 'height') if 'rotation' not in kwargs: kwargs['rotation'] = Config.getint('graphics', 'rotation') if 'position' not in kwargs: kwargs['position'] = Config.getdefault('graphics', 'position', 'auto') if 'top' in kwargs: kwargs['position'] = 'custom' kwargs['top'] = kwargs['top'] else: kwargs['top'] = Config.getint('graphics', 'top') if 'left' in kwargs: kwargs['position'] = 'custom' kwargs['left'] = kwargs['left'] else: kwargs['left'] = Config.getint('graphics', 'left') kwargs['_size'] = (kwargs.pop('width'), kwargs.pop('height')) super(WindowBase, self).__init__(**kwargs) # bind all the properties that need to recreate the window self._bind_create_window() self.bind(size=self.trigger_keyboard_height, rotation=self.trigger_keyboard_height) self.bind(softinput_mode=lambda *dt: self.update_viewport(), keyboard_height=lambda *dt: self.update_viewport()) # init privates self._system_keyboard = Keyboard(window=self) self._keyboards = {'system': self._system_keyboard} self._vkeyboard_cls = None self.children = [] self.parent = self # before creating the window import kivy.core.gl # NOQA # configure the window self.create_window() # attach modules + listener event EventLoop.set_window(self) Modules.register_window(self) EventLoop.add_event_listener(self) # manage keyboard(s) self.configure_keyboards() # assign the default context of the widget creation if not hasattr(self, '_context'): self._context = get_current_context() # mark as initialized self.initialized = True def _bind_create_window(self): for prop in ( 'fullscreen', 'borderless', 'position', 'top', 'left', '_size', 'system_size'): self.bind(**{prop: self.trigger_create_window}) def _unbind_create_window(self): for prop in ( 'fullscreen', 'borderless', 'position', 'top', 'left', '_size', 'system_size'): self.unbind(**{prop: self.trigger_create_window}) def toggle_fullscreen(self): '''Toggle between fullscreen and windowed mode. .. deprecated:: 1.9.0 Use :attr:`fullscreen` instead. ''' pass def maximize(self): '''Maximizes the window. This method should be used on desktop platforms only. .. versionadded:: 1.9.0 .. note:: This feature works with the SDL2 window provider only. .. warning:: This code is still experimental, and its API may be subject to change in a future version. ''' Logger.warning('Window: maximize() is not implemented in the current ' 'window provider.') def minimize(self): '''Minimizes the window. This method should be used on desktop platforms only. .. versionadded:: 1.9.0 .. note:: This feature works with the SDL2 window provider only. .. warning:: This code is still experimental, and its API may be subject to change in a future version. ''' Logger.warning('Window: minimize() is not implemented in the current ' 'window provider.') def restore(self): '''Restores the size and position of a maximized or minimized window. This method should be used on desktop platforms only. .. versionadded:: 1.9.0 .. note:: This feature works with the SDL2 window provider only. .. warning:: This code is still experimental, and its API may be subject to change in a future version. ''' Logger.warning('Window: restore() is not implemented in the current ' 'window provider.') def hide(self): '''Hides the window. This method should be used on desktop platforms only. .. versionadded:: 1.9.0 .. note:: This feature works with the SDL2 window provider only. .. warning:: This code is still experimental, and its API may be subject to change in a future version. ''' Logger.warning('Window: hide() is not implemented in the current ' 'window provider.') def show(self): '''Shows the window. This method should be used on desktop platforms only. .. versionadded:: 1.9.0 .. note:: This feature works with the SDL2 window provider only. .. warning:: This code is still experimental, and its API may be subject to change in a future version. ''' Logger.warning('Window: show() is not implemented in the current ' 'window provider.') def close(self): '''Close the window''' pass def create_window(self, *largs): '''Will create the main window and configure it. .. warning:: This method is called automatically at runtime. If you call it, it will recreate a RenderContext and Canvas. This means you'll have a new graphics tree, and the old one will be unusable. This method exist to permit the creation of a new OpenGL context AFTER closing the first one. (Like using runTouchApp() and stopTouchApp()). This method has only been tested in a unittest environment and is not suitable for Applications. Again, don't use this method unless you know exactly what you are doing! ''' # just to be sure, if the trigger is set, and if this method is # manually called, unset the trigger Clock.unschedule(self.create_window) # ensure the window creation will not be called twice if platform in ('android', 'ios'): self._unbind_create_window() if not self.initialized: from kivy.core.gl import init_gl init_gl() # create the render context and canvas, only the first time. from kivy.graphics import RenderContext, Canvas self.render_context = RenderContext() self.canvas = Canvas() self.render_context.add(self.canvas) else: # if we get initialized more than once, then reload opengl state # after the second time. # XXX check how it's working on embed platform. if platform == 'linux' or Window.__class__.__name__ == 'WindowSDL': # on linux, it's safe for just sending a resize. self.dispatch('on_resize', *self.system_size) else: # on other platform, window are recreated, we need to reload. from kivy.graphics.context import get_context get_context().reload() Clock.schedule_once(lambda x: self.canvas.ask_update(), 0) self.dispatch('on_resize', *self.system_size) # ensure the gl viewport is correct self.update_viewport() def on_flip(self): '''Flip between buffers (event)''' self.flip() def flip(self): '''Flip between buffers''' pass def _update_childsize(self, instance, value): self.update_childsize([instance]) def add_widget(self, widget): '''Add a widget to a window''' widget.parent = self self.children.insert(0, widget) self.canvas.add(widget.canvas) self.update_childsize([widget]) widget.bind( pos_hint=self._update_childsize, size_hint=self._update_childsize, size=self._update_childsize, pos=self._update_childsize) def remove_widget(self, widget): '''Remove a widget from a window ''' if not widget in self.children: return self.children.remove(widget) self.canvas.remove(widget.canvas) widget.parent = None widget.unbind( pos_hint=self._update_childsize, size_hint=self._update_childsize, size=self._update_childsize, pos=self._update_childsize) def clear(self): '''Clear the window with the background color''' # XXX FIXME use late binding from kivy.graphics.opengl import glClearColor, glClear, \ GL_COLOR_BUFFER_BIT, GL_DEPTH_BUFFER_BIT, GL_STENCIL_BUFFER_BIT cc = self._clearcolor if cc is not None: glClearColor(*cc) glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT) def set_title(self, title): '''Set the window title. .. versionadded:: 1.0.5 ''' self.title = title def set_icon(self, filename): '''Set the icon of the window. .. versionadded:: 1.0.5 ''' self.icon = filename def to_widget(self, x, y, initial=True, relative=False): return (x, y) def to_window(self, x, y, initial=True, relative=False): return (x, y) def get_root_window(self): return self def get_parent_window(self): return self def get_parent_layout(self): return None def on_draw(self): self.clear() self.render_context.draw() def on_motion(self, etype, me): '''Event called when a Motion Event is received. :Parameters: `etype`: str One of 'begin', 'update', 'end' `me`: :class:`~kivy.input.motionevent.MotionEvent` The Motion Event currently dispatched. ''' if me.is_touch: w, h = self.system_size me.scale_for_screen(w, h, rotation=self._rotation, smode=self.softinput_mode, kheight=self.keyboard_height) if etype == 'begin': self.dispatch('on_touch_down', me) elif etype == 'update': self.dispatch('on_touch_move', me) elif etype == 'end': self.dispatch('on_touch_up', me) FocusBehavior._handle_post_on_touch_up(me) def on_touch_down(self, touch): '''Event called when a touch down event is initiated. .. versionchanged:: 1.9.0 The touch `pos` is now transformed to window coordinates before this method is called. Before, the touch `pos` coordinate would be `(0, 0)` when this method was called. ''' for w in self.children[:]: if w.dispatch('on_touch_down', touch): return True def on_touch_move(self, touch): '''Event called when a touch event moves (changes location). .. versionchanged:: 1.9.0 The touch `pos` is now transformed to window coordinates before this method is called. Before, the touch `pos` coordinate would be `(0, 0)` when this method was called. ''' for w in self.children[:]: if w.dispatch('on_touch_move', touch): return True def on_touch_up(self, touch): '''Event called when a touch event is released (terminated). .. versionchanged:: 1.9.0 The touch `pos` is now transformed to window coordinates before this method is called. Before, the touch `pos` coordinate would be `(0, 0)` when this method was called. ''' for w in self.children[:]: if w.dispatch('on_touch_up', touch): return True def on_resize(self, width, height): '''Event called when the window is resized.''' self.update_viewport() def update_viewport(self): from kivy.graphics.opengl import glViewport from kivy.graphics.transformation import Matrix from math import radians w, h = self.system_size smode = self.softinput_mode kheight = self.keyboard_height w2, h2 = w / 2., h / 2. r = radians(self.rotation) x, y = 0, 0 _h = h if smode: y = kheight if smode == 'scale': _h -= kheight # prepare the viewport glViewport(x, y, w, _h) # do projection matrix projection_mat = Matrix() projection_mat.view_clip(0.0, w, 0.0, h, -1.0, 1.0, 0) self.render_context['projection_mat'] = projection_mat # do modelview matrix modelview_mat = Matrix().translate(w2, h2, 0) modelview_mat = modelview_mat.multiply(Matrix().rotate(r, 0, 0, 1)) w, h = self.size w2, h2 = w / 2., h / 2. modelview_mat = modelview_mat.multiply(Matrix().translate(-w2, -h2, 0)) self.render_context['modelview_mat'] = modelview_mat # redraw canvas self.canvas.ask_update() # and update childs self.update_childsize() def update_childsize(self, childs=None): width, height = self.size if childs is None: childs = self.children for w in childs: shw, shh = w.size_hint if shw and shh: w.size = shw * width, shh * height elif shw: w.width = shw * width elif shh: w.height = shh * height for key, value in w.pos_hint.items(): if key == 'x': w.x = value * width elif key == 'right': w.right = value * width elif key == 'y': w.y = value * height elif key == 'top': w.top = value * height elif key == 'center_x': w.center_x = value * width elif key == 'center_y': w.center_y = value * height def screenshot(self, name='screenshot{:04d}.png'): '''Save the actual displayed image in a file ''' i = 0 path = None if name != 'screenshot{:04d}.png': _ext = name.split('.')[-1] name = ''.join((name[:-(len(_ext) + 1)], '{:04d}.', _ext)) while True: i += 1 path = join(getcwd(), name.format(i)) if not exists(path): break return path def on_rotate(self, rotation): '''Event called when the screen has been rotated. ''' pass def on_close(self, *largs): '''Event called when the window is closed''' Modules.unregister_window(self) EventLoop.remove_event_listener(self) def on_request_close(self, *largs, **kwargs): '''Event called before we close the window. If a bound function returns `True`, the window will not be closed. If the the event is triggered because of the keyboard escape key, the keyword argument `source` is dispatched along with a value of `keyboard` to the bound functions. .. warning:: When the bound function returns True the window will not be closed, so use with care because the user would not be able to close the program, even if the red X is clicked. ''' pass def on_mouse_down(self, x, y, button, modifiers): '''Event called when the mouse is used (pressed/released)''' pass def on_mouse_move(self, x, y, modifiers): '''Event called when the mouse is moved with buttons pressed''' pass def on_mouse_up(self, x, y, button, modifiers): '''Event called when the mouse is moved with buttons pressed''' pass def on_joy_axis(self, stickid, axisid, value): '''Event called when a joystick has a stick or other axis moved .. versionadded:: 1.9.0''' pass def on_joy_hat(self, stickid, hatid, value): '''Event called when a joystick has a hat/dpad moved .. versionadded:: 1.9.0''' pass def on_joy_ball(self, stickid, ballid, value): '''Event called when a joystick has a ball moved .. versionadded:: 1.9.0''' pass def on_joy_button_down(self, stickid, buttonid): '''Event called when a joystick has a button pressed .. versionadded:: 1.9.0''' pass def on_joy_button_up(self, stickid, buttonid): '''Event called when a joystick has a button released .. versionadded:: 1.9.0''' pass def on_keyboard(self, key, scancode=None, codepoint=None, modifier=None, **kwargs): '''Event called when keyboard is used. .. warning:: Some providers may omit `scancode`, `codepoint` and/or `modifier`! ''' if 'unicode' in kwargs: Logger.warning("The use of the unicode parameter is deprecated, " "and will be removed in future versions. Use " "codepoint instead, which has identical " "semantics.") # Quit if user presses ESC or the typical OSX shortcuts CMD+q or CMD+w # TODO If just CMD+w is pressed, only the window should be closed. is_osx = platform == 'darwin' if WindowBase.on_keyboard.exit_on_escape: if key == 27 or all([is_osx, key in [113, 119], modifier == 1024]): if not self.dispatch('on_request_close', source='keyboard'): stopTouchApp() self.close() return True if Config: on_keyboard.exit_on_escape = Config.getboolean('kivy', 'exit_on_escape') def __exit(section, name, value): WindowBase.__dict__['on_keyboard'].exit_on_escape = \ Config.getboolean('kivy', 'exit_on_escape') Config.add_callback(__exit, 'kivy', 'exit_on_escape') def on_key_down(self, key, scancode=None, codepoint=None, modifier=None, **kwargs): '''Event called when a key is down (same arguments as on_keyboard)''' if 'unicode' in kwargs: Logger.warning("The use of the unicode parameter is deprecated, " "and will be removed in future versions. Use " "codepoint instead, which has identical " "semantics.") def on_key_up(self, key, scancode=None, codepoint=None, modifier=None, **kwargs): '''Event called when a key is released (same arguments as on_keyboard) ''' if 'unicode' in kwargs: Logger.warning("The use of the unicode parameter is deprecated, " "and will be removed in future versions. Use " "codepoint instead, which has identical " "semantics.") def on_dropfile(self, filename): '''Event called when a file is dropped on the application. .. warning:: This event currently works with sdl2 window provider, on pygame window provider and MacOSX with a patched version of pygame. This event is left in place for further evolution (ios, android etc.) .. versionadded:: 1.2.0 ''' pass @reify def dpi(self): '''Return the DPI of the screen. If the implementation doesn't support any DPI lookup, it will just return 96. .. warning:: This value is not cross-platform. Use :attr:`kivy.base.EventLoop.dpi` instead. ''' return 96. def configure_keyboards(self): # Configure how to provide keyboards (virtual or not) # register system keyboard to listening keys from window sk = self._system_keyboard self.bind( on_key_down=sk._on_window_key_down, on_key_up=sk._on_window_key_up) # use the device's real keyboard self.use_syskeyboard = True # use the device's real keyboard self.allow_vkeyboard = False # one single vkeyboard shared between all widgets self.single_vkeyboard = True # the single vkeyboard is always sitting at the same position self.docked_vkeyboard = False # now read the configuration mode = Config.get('kivy', 'keyboard_mode') if mode not in ('', 'system', 'dock', 'multi', 'systemanddock', 'systemandmulti'): Logger.critical('Window: unknown keyboard mode %r' % mode) # adapt mode according to the configuration if mode == 'system': self.use_syskeyboard = True self.allow_vkeyboard = False self.single_vkeyboard = True self.docked_vkeyboard = False elif mode == 'dock': self.use_syskeyboard = False self.allow_vkeyboard = True self.single_vkeyboard = True self.docked_vkeyboard = True elif mode == 'multi': self.use_syskeyboard = False self.allow_vkeyboard = True self.single_vkeyboard = False self.docked_vkeyboard = False elif mode == 'systemanddock': self.use_syskeyboard = True self.allow_vkeyboard = True self.single_vkeyboard = True self.docked_vkeyboard = True elif mode == 'systemandmulti': self.use_syskeyboard = True self.allow_vkeyboard = True self.single_vkeyboard = False self.docked_vkeyboard = False Logger.info( 'Window: virtual keyboard %sallowed, %s, %s' % ( '' if self.allow_vkeyboard else 'not ', 'single mode' if self.single_vkeyboard else 'multiuser mode', 'docked' if self.docked_vkeyboard else 'not docked')) def set_vkeyboard_class(self, cls): '''.. versionadded:: 1.0.8 Set the VKeyboard class to use. If set to None, it will use the :class:`kivy.uix.vkeyboard.VKeyboard`. ''' self._vkeyboard_cls = cls def release_all_keyboards(self): '''.. versionadded:: 1.0.8 This will ensure that no virtual keyboard / system keyboard is requested. All instances will be closed. ''' for key in list(self._keyboards.keys())[:]: keyboard = self._keyboards[key] if keyboard: keyboard.release() def request_keyboard(self, callback, target, input_type='text'): '''.. versionadded:: 1.0.4 Internal widget method to request the keyboard. This method is rarely required by the end-user as it is handled automatically by the :class:`~kivy.uix.textinput.TextInput`. We expose it in case you want to handle the keyboard manually for unique input scenarios. A widget can request the keyboard, indicating a callback to call when the keyboard is released (or taken by another widget). :Parameters: `callback`: func Callback that will be called when the keyboard is closed. This can be because somebody else requested the keyboard or the user closed it. `target`: Widget Attach the keyboard to the specified `target`. This should be the widget that requested the keyboard. Ensure you have a different target attached to each keyboard if you're working in a multi user mode. .. versionadded:: 1.0.8 `input_type`: string Choose the type of soft keyboard to request. Can be one of 'text', 'number', 'url', 'mail', 'datetime', 'tel', 'address'. .. note:: `input_type` is currently only honored on mobile devices. .. versionadded:: 1.8.0 :Return: An instance of :class:`Keyboard` containing the callback, target, and if the configuration allows it, a :class:`~kivy.uix.vkeyboard.VKeyboard` instance attached as a *.widget* property. .. note:: The behavior of this function is heavily influenced by the current `keyboard_mode`. Please see the Config's :ref:`configuration tokens <configuration-tokens>` section for more information. ''' # release any previous keyboard attached. self.release_keyboard(target) # if we can use virtual vkeyboard, activate it. if self.allow_vkeyboard: keyboard = None # late import global VKeyboard if VKeyboard is None and self._vkeyboard_cls is None: from kivy.uix.vkeyboard import VKeyboard self._vkeyboard_cls = VKeyboard # if the keyboard doesn't exist, create it. key = 'single' if self.single_vkeyboard else target if key not in self._keyboards: vkeyboard = self._vkeyboard_cls() keyboard = Keyboard(widget=vkeyboard, window=self) vkeyboard.bind( on_key_down=keyboard._on_vkeyboard_key_down, on_key_up=keyboard._on_vkeyboard_key_up) self._keyboards[key] = keyboard else: keyboard = self._keyboards[key] # configure vkeyboard keyboard.target = keyboard.widget.target = target keyboard.callback = keyboard.widget.callback = callback # add to the window self.add_widget(keyboard.widget) # only after add, do dock mode keyboard.widget.docked = self.docked_vkeyboard keyboard.widget.setup_mode() else: # system keyboard, just register the callback. keyboard = self._system_keyboard keyboard.callback = callback keyboard.target = target # use system (hardware) keyboard according to flag if self.allow_vkeyboard and self.use_syskeyboard: self.unbind( on_key_down=keyboard._on_window_key_down, on_key_up=keyboard._on_window_key_up) self.bind( on_key_down=keyboard._on_window_key_down, on_key_up=keyboard._on_window_key_up) return keyboard def release_keyboard(self, target=None): '''.. versionadded:: 1.0.4 Internal method for the widget to release the real-keyboard. Check :meth:`request_keyboard` to understand how it works. ''' if self.allow_vkeyboard: key = 'single' if self.single_vkeyboard else target if key not in self._keyboards: return keyboard = self._keyboards[key] callback = keyboard.callback if callback: keyboard.callback = None callback() keyboard.target = None self.remove_widget(keyboard.widget) if key != 'single' and key in self._keyboards: del self._keyboards[key] elif self._system_keyboard.callback: # this way will prevent possible recursion. callback = self._system_keyboard.callback self._system_keyboard.callback = None callback() return True
class WindowBase(EventDispatcher): '''WindowBase is a abstract window widget, for any window implementation. :Parameters: `fullscreen`: str, one of ('0', '1', 'auto', 'fake') Make window as fullscreen, check config documentation for more explaination about the values. `width`: int Width of window `height`: int Height of window :Events: `on_motion`: etype, motionevent Fired when a new :class:`~kivy.input.motionevent.MotionEvent` is dispatched `on_touch_down`: Fired when a new touch appear `on_touch_move`: Fired when an existing touch is moved `on_touch_up`: Fired when an existing touch disapear `on_draw`: Fired when the :class:`Window` is beeing drawed `on_flip`: Fired when the :class:`Window` GL surface is beeing flipped `on_rotate`: rotation Fired when the :class:`Window` is beeing rotated `on_close`: Fired when the :class:`Window` is closed `on_keyboard`: key, scancode, codepoint, modifier Fired when the keyboard is in action .. versionchanged:: 1.3.0 The *unicode* parameter has be deprecated in favor of codepoint, and will be removed completely in future versions `on_key_down`: key, scancode, codepoint Fired when a key is down .. versionchanged:: 1.3.0 The *unicode* parameter has be deprecated in favor of codepoint, and will be removed completely in future versions `on_key_up`: key, scancode, codepoint Fired when a key is up .. versionchanged:: 1.3.0 The *unicode* parameter has be deprecated in favor of codepoint, and will be removed completely in future versions `on_dropfile`: str Fired when a file is dropped on the application ''' __instance = None __initialized = False # private properties _size = ListProperty([0, 0]) _modifiers = ListProperty([]) _rotation = NumericProperty(0) _clearcolor = ObjectProperty([0, 0, 0, 1]) children = ListProperty([]) '''List of children of this window. :data:`children` is a :class:`~kivy.properties.ListProperty` instance, default to an empty list. Use :func:`add_widget` and :func:`remove_widget` for manipulate children list. Don't manipulate children list directly until you know what you are doing. ''' parent = ObjectProperty(None, allownone=True) '''Parent of this window :data:`parent` is a :class:`~kivy.properties.ObjectProperty` instance, default to None. When created, the parent is set to the window itself. You must take care of it if you are doing recursive check. ''' icon = StringProperty() def _get_modifiers(self): return self._modifiers modifiers = AliasProperty(_get_modifiers, None) '''List of keyboard modifiers currently in action ''' def _get_size(self): r = self._rotation w, h = self._size if r in (0, 180): return w, h return h, w def _set_size(self, size): if self._size != size: r = self._rotation if r in (0, 180): self._size = size else: self._size = size[1], size[0] self.dispatch('on_resize', *size) return True else: return False size = AliasProperty(_get_size, _set_size, bind=('_size', )) '''Get the rotated size of the window. If :data:`rotation` is set, then the size will change to reflect the rotation. ''' def _get_clearcolor(self): return self._clearcolor def _set_clearcolor(self, value): if value is not None: if type(value) not in (list, tuple): raise Exception('Clearcolor must be a list or tuple') if len(value) != 4: raise Exception('Clearcolor must contain 4 values') self._clearcolor = value clearcolor = AliasProperty(_get_clearcolor, _set_clearcolor, bind=('_clearcolor', )) '''Color used to clear window. :: from kivy.core.window import Window # red background color Window.clearcolor = (1, 0, 0, 1) # don't clear background at all Window.clearcolor = None .. versionchanged:: 1.7.2 Clear color default value is now: (0, 0, 0, 1). ''' # make some property read-only def _get_width(self): r = self._rotation if r == 0 or r == 180: return self._size[0] return self._size[1] width = AliasProperty(_get_width, None, bind=('_rotation', '_size')) '''Rotated window width. :data:`width` is a :class:`~kivy.properties.AliasProperty`. ''' def _get_height(self): '''Rotated window height''' r = self._rotation if r == 0 or r == 180: return self._size[1] return self._size[0] height = AliasProperty(_get_height, None, bind=('_rotation', '_size')) '''Rotated window height. :data:`height` is a :class:`~kivy.properties.AliasProperty`. ''' def _get_center(self): return self.width / 2., self.height / 2. center = AliasProperty(_get_center, None, bind=('width', 'height')) '''Center of the rotated window. :data:`center` is a :class:`~kivy.properties.AliasProperty`. ''' def _get_rotation(self): return self._rotation def _set_rotation(self, x): x = int(x % 360) if x == self._rotation: return if x not in (0, 90, 180, 270): raise ValueError('can rotate only 0, 90, 180, 270 degrees') self._rotation = x if self.initialized is False: return self.dispatch('on_resize', *self.size) self.dispatch('on_rotate', x) rotation = AliasProperty(_get_rotation, _set_rotation, bind=('_rotation', )) '''Get/set the window content rotation. Can be one of 0, 90, 180, 270 degrees. ''' def _set_system_size(self, size): self._size = size def _get_system_size(self): return self._size system_size = AliasProperty( _get_system_size, _set_system_size, bind=('_size', )) '''Real size of the window, without taking care of the rotation. ''' fullscreen = OptionProperty(False, options=(True, False, 'auto', 'fake')) '''If true, the window will be put in fullscreen mode, "auto". That's mean the screen size will not change, and use the current one to set the app fullscreen .. versionadded:: 1.2.0 ''' mouse_pos = ObjectProperty([0, 0]) '''2d position of the mouse within the window. .. versionadded:: 1.2.0 ''' top = NumericProperty(None, allownone=True) left = NumericProperty(None, allownone=True) position = OptionProperty('auto', options=['auto', 'custom']) render_context = ObjectProperty(None) canvas = ObjectProperty(None) title = StringProperty('Kivy') __events__ = ('on_draw', 'on_flip', 'on_rotate', 'on_resize', 'on_close', 'on_motion', 'on_touch_down', 'on_touch_move', 'on_touch_up', 'on_mouse_down', 'on_mouse_move', 'on_mouse_up', 'on_keyboard', 'on_key_down', 'on_key_up', 'on_dropfile') def __new__(cls, **kwargs): if cls.__instance is None: cls.__instance = EventDispatcher.__new__(cls) return cls.__instance def __init__(self, **kwargs): kwargs.setdefault('force', False) # don't init window 2 times, # except if force is specified if WindowBase.__instance is not None and not kwargs.get('force'): return self.initialized = False # create a trigger for update/create the window when one of window # property changes self.trigger_create_window = Clock.create_trigger( self.create_window, -1) # set the default window parameter according to the configuration if 'fullscreen' not in kwargs: fullscreen = Config.get('graphics', 'fullscreen') if fullscreen not in ('auto', 'fake'): fullscreen = fullscreen.lower() in ('true', '1', 'yes', 'yup') kwargs['fullscreen'] = fullscreen if 'width' not in kwargs: kwargs['width'] = Config.getint('graphics', 'width') if 'height' not in kwargs: kwargs['height'] = Config.getint('graphics', 'height') if 'rotation' not in kwargs: kwargs['rotation'] = Config.getint('graphics', 'rotation') if 'position' not in kwargs: kwargs['position'] = Config.getdefault('graphics', 'position', 'auto') if 'top' in kwargs: kwargs['position'] = 'custom' kwargs['top'] = kwargs['top'] else: kwargs['top'] = Config.getint('graphics', 'top') if 'left' in kwargs: kwargs['position'] = 'custom' kwargs['left'] = kwargs['left'] else: kwargs['left'] = Config.getint('graphics', 'left') kwargs['_size'] = (kwargs.pop('width'), kwargs.pop('height')) super(WindowBase, self).__init__(**kwargs) # bind all the properties that need to recreate the window for prop in ( 'fullscreen', 'position', 'top', 'left', '_size', 'system_size'): self.bind(**{prop: self.trigger_create_window}) # init privates self._system_keyboard = Keyboard(window=self) self._keyboards = {'system': self._system_keyboard} self._vkeyboard_cls = None self.children = [] self.parent = self # before creating the window import kivy.core.gl # configure the window self.create_window() # attach modules + listener event EventLoop.set_window(self) Modules.register_window(self) EventLoop.add_event_listener(self) # manage keyboard(s) self.configure_keyboards() # mark as initialized self.initialized = True def toggle_fullscreen(self): '''Toggle fullscreen on window''' pass def close(self): '''Close the window''' pass def create_window(self, *largs): '''Will create the main window and configure it. .. warning:: This method is called automatically at runtime. If you call it, it will recreate a RenderContext and Canvas. This mean you'll have a new graphics tree, and the old one will be unusable. This method exist to permit the creation of a new OpenGL context AFTER closing the first one. (Like using runTouchApp() and stopTouchApp()). This method have been only tested in unittest environment, and will be not suitable for Applications. Again, don't use this method unless you know exactly what you are doing ! ''' # just to be sure, if the trigger is set, and if this method is manually # called, unset the trigger Clock.unschedule(self.create_window) if not self.initialized: from kivy.core.gl import init_gl init_gl() # create the render context and canvas, only the first time. from kivy.graphics import RenderContext, Canvas self.render_context = RenderContext() self.canvas = Canvas() self.render_context.add(self.canvas) else: # if we get initialized more than once, then reload opengl state # after the second time. # XXX check how it's working on embed platform. if platform() == 'linux': # on linux, it's safe for just sending a resize. self.dispatch('on_resize', *self.system_size) else: # on other platform, window are recreated, we need to reload. from kivy.graphics.context import get_context get_context().reload() Clock.schedule_once(lambda x: self.canvas.ask_update(), 0) self.dispatch('on_resize', *self.system_size) # ensure the gl viewport is correct self.update_viewport() def on_flip(self): '''Flip between buffers (event)''' self.flip() def flip(self): '''Flip between buffers''' pass def _update_childsize(self, instance, value): self.update_childsize([instance]) def add_widget(self, widget): '''Add a widget on window''' widget.parent = self self.children.insert(0, widget) self.canvas.add(widget.canvas) self.update_childsize([widget]) widget.bind( pos_hint=self._update_childsize, size_hint=self._update_childsize, size=self._update_childsize, pos=self._update_childsize) def remove_widget(self, widget): '''Remove a widget from window ''' if not widget in self.children: return self.children.remove(widget) self.canvas.remove(widget.canvas) widget.parent = None widget.unbind( pos_hint=self._update_childsize, size_hint=self._update_childsize, size=self._update_childsize, pos=self._update_childsize) def clear(self): '''Clear the window with background color''' # XXX FIXME use late binding from kivy.graphics.opengl import glClearColor, glClear, \ GL_COLOR_BUFFER_BIT, GL_DEPTH_BUFFER_BIT cc = self._clearcolor if cc is not None: glClearColor(*cc) glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) def set_title(self, title): '''Set the window title. .. versionadded:: 1.0.5 ''' self.title = title def set_icon(self, filename): '''Set the icon of the window .. versionadded:: 1.0.5 ''' self.icon = filename def to_widget(self, x, y, initial=True, relative=False): return (x, y) def to_window(self, x, y, initial=True, relative=False): return (x, y) def get_root_window(self): return self def get_parent_window(self): return self def get_parent_layout(self): return None def on_draw(self): self.clear() self.render_context.draw() def on_motion(self, etype, me): '''Event called when a Motion Event is received. :Parameters: `etype`: str One of 'begin', 'update', 'end' `me`: :class:`~kivy.input.motionevent.MotionEvent` Motion Event currently dispatched ''' if me.is_touch: if etype == 'begin': self.dispatch('on_touch_down', me) elif etype == 'update': self.dispatch('on_touch_move', me) elif etype == 'end': self.dispatch('on_touch_up', me) def on_touch_down(self, touch): '''Event called when a touch is down ''' w, h = self.system_size touch.scale_for_screen(w, h, rotation=self._rotation) for w in self.children[:]: if w.dispatch('on_touch_down', touch): return True def on_touch_move(self, touch): '''Event called when a touch move ''' w, h = self.system_size touch.scale_for_screen(w, h, rotation=self._rotation) for w in self.children[:]: if w.dispatch('on_touch_move', touch): return True def on_touch_up(self, touch): '''Event called when a touch up ''' w, h = self.system_size touch.scale_for_screen(w, h, rotation=self._rotation) for w in self.children[:]: if w.dispatch('on_touch_up', touch): return True def on_resize(self, width, height): '''Event called when the window is resized''' self.update_viewport() def update_viewport(self): from kivy.graphics.opengl import glViewport from kivy.graphics.transformation import Matrix from math import radians w, h = self.system_size w2, h2 = w / 2., h / 2. r = radians(self.rotation) # prepare the viewport glViewport(0, 0, w, h) # do projection matrix projection_mat = Matrix() projection_mat.view_clip(0.0, w, 0.0, h, -1.0, 1.0, 0) self.render_context['projection_mat'] = projection_mat # do modelview matrix modelview_mat = Matrix().translate(w2, h2, 0) modelview_mat = modelview_mat.multiply(Matrix().rotate(r, 0, 0, 1)) w, h = self.size w2, h2 = w / 2., h / 2. modelview_mat = modelview_mat.multiply(Matrix().translate(-w2, -h2, 0)) self.render_context['modelview_mat'] = modelview_mat # redraw canvas self.canvas.ask_update() # and update childs self.update_childsize() def update_childsize(self, childs=None): width, height = self.size if childs is None: childs = self.children for w in childs: shw, shh = w.size_hint if shw and shh: w.size = shw * width, shh * height elif shw: w.width = shw * width elif shh: w.height = shh * height for key, value in w.pos_hint.items(): if key == 'x': w.x = value * width elif key == 'right': w.right = value * width elif key == 'y': w.y = value * height elif key == 'top': w.top = value * height elif key == 'center_x': w.center_x = value * width elif key == 'center_y': w.center_y = value * height def screenshot(self, name='screenshot%(counter)04d.png'): '''Save the actual displayed image in a file ''' i = 0 path = None while True: i += 1 path = join(getcwd(), name % {'counter': i}) if not exists(path): break return path def on_rotate(self, rotation): '''Event called when the screen have been rotated ''' pass def on_close(self, *largs): '''Event called when the window is closed''' Modules.unregister_window(self) EventLoop.remove_event_listener(self) def on_mouse_down(self, x, y, button, modifiers): '''Event called when mouse is in action (press/release)''' pass def on_mouse_move(self, x, y, modifiers): '''Event called when mouse is moving, with buttons pressed''' pass def on_mouse_up(self, x, y, button, modifiers): '''Event called when mouse is moving, with buttons pressed''' pass def on_keyboard(self, key, scancode=None, codepoint=None, modifier=None, **kwargs): '''Event called when keyboard is in action .. warning:: Some providers may omit `scancode`, `codepoint` and/or `modifier`! ''' if 'unicode' in kwargs: Logger.warning("The use of the unicode parameter is deprecated, " "and will be removed in future versions. Use codepoint " "instead, which has identical semantics.") def on_key_down(self, key, scancode=None, codepoint=None, modifier=None, **kwargs): '''Event called when a key is down (same arguments as on_keyboard)''' if 'unicode' in kwargs: Logger.warning("The use of the unicode parameter is deprecated, " "and will be removed in future versions. Use codepoint " "instead, which has identical semantics.") def on_key_up(self, key, scancode=None, codepoint=None, modifier=None, **kwargs): '''Event called when a key is up (same arguments as on_keyboard)''' if 'unicode' in kwargs: Logger.warning("The use of the unicode parameter is deprecated, " "and will be removed in future versions. Use codepoint " "instead, which has identical semantics.") def on_dropfile(self, filename): '''Event called when a file is dropped on the application. .. warning:: This event is actually used only on MacOSX with a patched version of pygame. But this will be a place for a further evolution (ios, android etc.) .. versionadded:: 1.2.0 ''' pass @reify def dpi(self): '''Return the DPI of the screen. If the implementation doesn't support any DPI lookup, it will just return 96. .. warning:: This value is not cross-platform. Use :data:`kivy.base.EventLoop.dpi` instead. ''' return 96. def configure_keyboards(self): # Configure how to provide keyboards (virtual or not) # register system keyboard to listening keys from window sk = self._system_keyboard self.bind( on_key_down=sk._on_window_key_down, on_key_up=sk._on_window_key_up) # use the device's real keyboard self.allow_vkeyboard = False # one single vkeyboard shared between all widgets self.single_vkeyboard = True # the single vkeyboard is always sitting at the same position self.docked_vkeyboard = False # now read the configuration mode = Config.get('kivy', 'keyboard_mode') if mode not in ('', 'system', 'dock', 'multi'): Logger.critical('Window: unknown keyboard mode %r' % mode) # adapt mode according to the configuration if mode == 'system': self.allow_vkeyboard = False self.single_vkeyboard = True self.docked_vkeyboard = False elif mode == 'dock': self.allow_vkeyboard = True self.single_vkeyboard = True self.docked_vkeyboard = True elif mode == 'multi': self.allow_vkeyboard = True self.single_vkeyboard = False self.docked_vkeyboard = False Logger.info('Window: virtual keyboard %sallowed, %s, %s' % ('' if self.allow_vkeyboard else 'not ', 'single mode' if self.single_vkeyboard else 'multiuser mode', 'docked' if self.docked_vkeyboard else 'not docked')) def set_vkeyboard_class(self, cls): '''.. versionadded:: 1.0.8 Set the VKeyboard class to use. If None set, it will use the :class:`kivy.uix.vkeyboard.VKeyboard`. ''' self._vkeyboard_cls = cls def release_all_keyboards(self): '''.. versionadded:: 1.0.8 This will ensure that no virtual keyboard / system keyboard are actually requested. All will be closed. ''' for key in list(self._keyboards.keys())[:]: keyboard = self._keyboards[key] if keyboard: keyboard.release() def request_keyboard(self, callback, target): '''.. versionadded:: 1.0.4 Internal method for widget, to request the keyboard. This method is not intented to be used by end-user, however, if you want to use the real-keyboard (not virtual keyboard), you don't want to share it with another widget. A widget can request the keyboard, indicating a callback to call when the keyboard will be released (or taken by another widget). :Parameters: `callback`: func Callback that will be called when the keyboard is closed. It can be because somebody else requested the keyboard, or if the user itself closed it. `target`: Widget Attach the keyboard to the specified target. Ensure you have a target attached if you're using the keyboard in a multi users mode. :Return: An instance of :class:`Keyboard`, containing the callback, target, and if configuration allowed it, a VKeyboard instance. .. versionchanged:: 1.0.8 `target` have been added, and must be the widget source that request the keyboard. If set, the widget must have one method named `on_keyboard_text`, that will be called from the vkeyboard. ''' # release any previous keyboard attached. self.release_keyboard(target) # if we can use virtual vkeyboard, activate it. if self.allow_vkeyboard: keyboard = None # late import global VKeyboard if VKeyboard is None and self._vkeyboard_cls is None: from kivy.uix.vkeyboard import VKeyboard self._vkeyboard_cls = VKeyboard # if the keyboard doesn't exist, create it. key = 'single' if self.single_vkeyboard else target if key not in self._keyboards: vkeyboard = self._vkeyboard_cls() keyboard = Keyboard(widget=vkeyboard, window=self) vkeyboard.bind( on_key_down=keyboard._on_vkeyboard_key_down, on_key_up=keyboard._on_vkeyboard_key_up) self._keyboards[key] = keyboard else: keyboard = self._keyboards[key] # configure vkeyboard keyboard.target = keyboard.widget.target = target keyboard.callback = keyboard.widget.callback = callback # add to the window self.add_widget(keyboard.widget) # only after add, do dock mode keyboard.widget.docked = self.docked_vkeyboard keyboard.widget.setup_mode() # return it. return keyboard else: # system keyboard, just register the callback. self._system_keyboard.callback = callback self._system_keyboard.target = target return self._system_keyboard def release_keyboard(self, target=None): '''.. versionadded:: 1.0.4 Internal method for widget, to release the real-keyboard. Check :func:`request_keyboard` to understand how it works. ''' if self.allow_vkeyboard: key = 'single' if self.single_vkeyboard else target if key not in self._keyboards: return keyboard = self._keyboards[key] callback = keyboard.callback if callback: keyboard.callback = None callback() keyboard.target = None self.remove_widget(keyboard.widget) if key != 'single' and key in self._keyboards: del self._keyboards[key] elif self._system_keyboard.callback: # this way will prevent possible recursion. callback = self._system_keyboard.callback self._system_keyboard.callback = None callback() return True
class WindowBase(EventDispatcher): """WindowBase is an abstract window widget for any window implementation. :Parameters: `fullscreen`: str, one of ('0', '1', 'auto', 'fake') Make the window fullscreen. Check the :mod:`~kivy.config` documentation for a more detailed explanation on the values. `width`: int Width of the window. `height`: int Height of the window. :Events: `on_motion`: etype, motionevent Fired when a new :class:`~kivy.input.motionevent.MotionEvent` is dispatched `on_touch_down`: Fired when a new touch event is initiated. `on_touch_move`: Fired when an existing touch event changes location. `on_touch_up`: Fired when an existing touch event is terminated. `on_draw`: Fired when the :class:`Window` is being drawn. `on_flip`: Fired when the :class:`Window` GL surface is being flipped. `on_rotate`: rotation Fired when the :class:`Window` is being rotated. `on_close`: Fired when the :class:`Window` is closed. `on_request_close`: Fired when the event loop wants to close the window, or if the escape key is pressed and `exit_on_escape` is `True`. If a function bound to this event returns `True`, the window will not be closed. If the the event is triggered because of the keyboard escape key, the keyword argument `source` is dispatched along with a value of `keyboard` to the bound functions. `on_keyboard`: key, scancode, codepoint, modifier Fired when the keyboard is used for input. .. versionchanged:: 1.3.0 The *unicode* parameter has been deprecated in favor of codepoint, and will be removed completely in future versions. `on_key_down`: key, scancode, codepoint Fired when a key pressed. .. versionchanged:: 1.3.0 The *unicode* parameter has been deprecated in favor of codepoint, and will be removed completely in future versions. `on_key_up`: key, scancode, codepoint Fired when a key is released. .. versionchanged:: 1.3.0 The *unicode* parameter has be deprecated in favor of codepoint, and will be removed completely in future versions. `on_dropfile`: str Fired when a file is dropped on the application. .. versionchanged:: 1.8.1 `on_request_close` has been added. """ __instance = None __initialized = False # private properties _size = ListProperty([0, 0]) _modifiers = ListProperty([]) _rotation = NumericProperty(0) _clearcolor = ObjectProperty([0, 0, 0, 1]) children = ListProperty([]) """List of the children of this window. :attr:`children` is a :class:`~kivy.properties.ListProperty` instance and defaults to an empty list. Use :meth:`add_widget` and :meth:`remove_widget` to manipulate the list of children. Don't manipulate the list directly unless you know what you are doing. """ parent = ObjectProperty(None, allownone=True) """Parent of this window. :attr:`parent` is a :class:`~kivy.properties.ObjectProperty` instance and defaults to None. When created, the parent is set to the window itself. You must take care of it if you are doing a recursive check. """ icon = StringProperty() def _get_modifiers(self): return self._modifiers modifiers = AliasProperty(_get_modifiers, None) """List of keyboard modifiers currently active. """ def _get_size(self): r = self._rotation w, h = self._size if self.softinput_mode == "resize": h -= self.keyboard_height if r in (0, 180): return w, h return h, w def _set_size(self, size): if self._size != size: r = self._rotation if r in (0, 180): self._size = size else: self._size = size[1], size[0] self.dispatch("on_resize", *size) return True else: return False size = AliasProperty(_get_size, _set_size, bind=("_size",)) """Get the rotated size of the window. If :attr:`rotation` is set, then the size will change to reflect the rotation. """ def _get_clearcolor(self): return self._clearcolor def _set_clearcolor(self, value): if value is not None: if type(value) not in (list, tuple): raise Exception("Clearcolor must be a list or tuple") if len(value) != 4: raise Exception("Clearcolor must contain 4 values") self._clearcolor = value clearcolor = AliasProperty(_get_clearcolor, _set_clearcolor, bind=("_clearcolor",)) """Color used to clear the window. :: from kivy.core.window import Window # red background color Window.clearcolor = (1, 0, 0, 1) # don't clear background at all Window.clearcolor = None .. versionchanged:: 1.7.2 The clearcolor default value is now: (0, 0, 0, 1). """ # make some property read-only def _get_width(self): r = self._rotation if r == 0 or r == 180: return self._size[0] return self._size[1] width = AliasProperty(_get_width, None, bind=("_rotation", "_size")) """Rotated window width. :attr:`width` is a read-only :class:`~kivy.properties.AliasProperty`. """ def _get_height(self): """Rotated window height""" r = self._rotation if r == 0 or r == 180: return self._size[1] return self._size[0] height = AliasProperty(_get_height, None, bind=("_rotation", "_size")) """Rotated window height. :attr:`height` is a read-only :class:`~kivy.properties.AliasProperty`. """ def _get_center(self): return self.width / 2.0, self.height / 2.0 center = AliasProperty(_get_center, None, bind=("width", "height")) """Center of the rotated window. :attr:`center` is a :class:`~kivy.properties.AliasProperty`. """ def _get_rotation(self): return self._rotation def _set_rotation(self, x): x = int(x % 360) if x == self._rotation: return if x not in (0, 90, 180, 270): raise ValueError("can rotate only 0, 90, 180, 270 degrees") self._rotation = x if self.initialized is False: return self.dispatch("on_resize", *self.size) self.dispatch("on_rotate", x) rotation = AliasProperty(_get_rotation, _set_rotation, bind=("_rotation",)) """Get/set the window content rotation. Can be one of 0, 90, 180, 270 degrees. """ softinput_mode = OptionProperty("", options=("", "pan", "scale", "resize")) """This specifies the behavior of window contents on display of soft keyboard on mobile platform. Can be one of '', 'pan', 'scale', 'resize'. When '' The main window is left as it is allowing the user to use :attr:`keyboard_height` to manage the window contents the way they want. when 'pan' The main window pans moving the bottom part of the window to be always on top of the keyboard. when 'resize' The window is resized and the contents scaled to fit the remaining space. ..versionadded::1.8.1 :attr:`softinput_mode` is a :class:`OptionProperty` defaults to None. """ _keyboard_changed = BooleanProperty(False) def _upd_kbd_height(self, *kargs): self._keyboard_changed = not self._keyboard_changed def _get_ios_kheight(self): return 0 def _get_android_kheight(self): global android if not android: import android return android.get_keyboard_height() def _get_kheight(self): if platform == "android": return self._get_android_kheight() if platform == "ios": return self._get_ios_kheight() return 0 keyboard_height = AliasProperty(_get_kheight, None, bind=("_keyboard_changed",)) """Rerturns the height of the softkeyboard/IME on mobile platforms. Will return 0 if not on mobile platform or if IME is not active. ..versionadded:: 1.8.1 :attr:`keyboard_height` is a read-only :class:`AliasProperty` defaults to 0. """ def _set_system_size(self, size): self._size = size def _get_system_size(self): if self.softinput_mode == "resize": return self._size[0], self._size[1] - self.keyboard_height return self._size system_size = AliasProperty(_get_system_size, _set_system_size, bind=("_size",)) """Real size of the window ignoring rotation. """ fullscreen = OptionProperty(False, options=(True, False, "auto", "fake")) """This property sets the fullscreen mode of the window. Available options are: True, False, 'auto', 'fake'. Check the :mod:`~kivy.config` documentation for a more detailed explanation on the values. .. versionadded:: 1.2.0 """ mouse_pos = ObjectProperty([0, 0]) """2d position of the mouse within the window. .. versionadded:: 1.2.0 """ top = NumericProperty(None, allownone=True) left = NumericProperty(None, allownone=True) position = OptionProperty("auto", options=["auto", "custom"]) render_context = ObjectProperty(None) canvas = ObjectProperty(None) title = StringProperty("Kivy") __events__ = ( "on_draw", "on_flip", "on_rotate", "on_resize", "on_close", "on_motion", "on_touch_down", "on_touch_move", "on_touch_up", "on_mouse_down", "on_mouse_move", "on_mouse_up", "on_keyboard", "on_key_down", "on_key_up", "on_dropfile", "on_request_close", ) def __new__(cls, **kwargs): if cls.__instance is None: cls.__instance = EventDispatcher.__new__(cls) return cls.__instance def __init__(self, **kwargs): kwargs.setdefault("force", False) # don't init window 2 times, # except if force is specified if WindowBase.__instance is not None and not kwargs.get("force"): return self.initialized = False # create a trigger for update/create the window when one of window # property changes self.trigger_create_window = Clock.create_trigger(self.create_window, -1) # Create a trigger for updating the keyboard height self.trigger_keyboard_height = Clock.create_trigger(self._upd_kbd_height, 0.5) # set the default window parameter according to the configuration if "fullscreen" not in kwargs: fullscreen = Config.get("graphics", "fullscreen") if fullscreen not in ("auto", "fake"): fullscreen = fullscreen.lower() in ("true", "1", "yes", "yup") kwargs["fullscreen"] = fullscreen if "width" not in kwargs: kwargs["width"] = Config.getint("graphics", "width") if "height" not in kwargs: kwargs["height"] = Config.getint("graphics", "height") if "rotation" not in kwargs: kwargs["rotation"] = Config.getint("graphics", "rotation") if "position" not in kwargs: kwargs["position"] = Config.getdefault("graphics", "position", "auto") if "top" in kwargs: kwargs["position"] = "custom" kwargs["top"] = kwargs["top"] else: kwargs["top"] = Config.getint("graphics", "top") if "left" in kwargs: kwargs["position"] = "custom" kwargs["left"] = kwargs["left"] else: kwargs["left"] = Config.getint("graphics", "left") kwargs["_size"] = (kwargs.pop("width"), kwargs.pop("height")) super(WindowBase, self).__init__(**kwargs) # bind all the properties that need to recreate the window for prop in ("fullscreen", "position", "top", "left", "_size", "system_size"): self.bind(**{prop: self.trigger_create_window}) self.bind(size=self.trigger_keyboard_height, rotation=self.trigger_keyboard_height) self.bind(softinput_mode=lambda *dt: self.update_viewport(), keyboard_height=lambda *dt: self.update_viewport()) # init privates self._system_keyboard = Keyboard(window=self) self._keyboards = {"system": self._system_keyboard} self._vkeyboard_cls = None self.children = [] self.parent = self # before creating the window import kivy.core.gl # NOQA # configure the window self.create_window() # attach modules + listener event EventLoop.set_window(self) Modules.register_window(self) EventLoop.add_event_listener(self) # manage keyboard(s) self.configure_keyboards() # assign the default context of the widget creation if not hasattr(self, "_context"): self._context = get_current_context() # mark as initialized self.initialized = True def toggle_fullscreen(self): """Toggle fullscreen on window""" pass def close(self): """Close the window""" pass def create_window(self, *largs): """Will create the main window and configure it. .. warning:: This method is called automatically at runtime. If you call it, it will recreate a RenderContext and Canvas. This means you'll have a new graphics tree, and the old one will be unusable. This method exist to permit the creation of a new OpenGL context AFTER closing the first one. (Like using runTouchApp() and stopTouchApp()). This method has only been tested in a unittest environment and is not suitable for Applications. Again, don't use this method unless you know exactly what you are doing! """ # just to be sure, if the trigger is set, and if this method is # manually called, unset the trigger Clock.unschedule(self.create_window) if not self.initialized: from kivy.core.gl import init_gl init_gl() # create the render context and canvas, only the first time. from kivy.graphics import RenderContext, Canvas self.render_context = RenderContext() self.canvas = Canvas() self.render_context.add(self.canvas) else: # if we get initialized more than once, then reload opengl state # after the second time. # XXX check how it's working on embed platform. if platform == "linux": # on linux, it's safe for just sending a resize. self.dispatch("on_resize", *self.system_size) else: # on other platform, window are recreated, we need to reload. from kivy.graphics.context import get_context get_context().reload() Clock.schedule_once(lambda x: self.canvas.ask_update(), 0) self.dispatch("on_resize", *self.system_size) # ensure the gl viewport is correct self.update_viewport() def on_flip(self): """Flip between buffers (event)""" self.flip() def flip(self): """Flip between buffers""" pass def _update_childsize(self, instance, value): self.update_childsize([instance]) def add_widget(self, widget): """Add a widget to a window""" widget.parent = self self.children.insert(0, widget) self.canvas.add(widget.canvas) self.update_childsize([widget]) widget.bind( pos_hint=self._update_childsize, size_hint=self._update_childsize, size=self._update_childsize, pos=self._update_childsize, ) def remove_widget(self, widget): """Remove a widget from a window """ if not widget in self.children: return self.children.remove(widget) self.canvas.remove(widget.canvas) widget.parent = None widget.unbind( pos_hint=self._update_childsize, size_hint=self._update_childsize, size=self._update_childsize, pos=self._update_childsize, ) def clear(self): """Clear the window with the background color""" # XXX FIXME use late binding from kivy.graphics.opengl import ( glClearColor, glClear, GL_COLOR_BUFFER_BIT, GL_DEPTH_BUFFER_BIT, GL_STENCIL_BUFFER_BIT, ) cc = self._clearcolor if cc is not None: glClearColor(*cc) glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT) def set_title(self, title): """Set the window title. .. versionadded:: 1.0.5 """ self.title = title def set_icon(self, filename): """Set the icon of the window. .. versionadded:: 1.0.5 """ self.icon = filename def to_widget(self, x, y, initial=True, relative=False): return (x, y) def to_window(self, x, y, initial=True, relative=False): return (x, y) def get_root_window(self): return self def get_parent_window(self): return self def get_parent_layout(self): return None def on_draw(self): self.clear() self.render_context.draw() def on_motion(self, etype, me): """Event called when a Motion Event is received. :Parameters: `etype`: str One of 'begin', 'update', 'end' `me`: :class:`~kivy.input.motionevent.MotionEvent` The Motion Event currently dispatched. """ if me.is_touch: w, h = self.system_size me.scale_for_screen(w, h, rotation=self._rotation, smode=self.softinput_mode, kheight=self.keyboard_height) if etype == "begin": self.dispatch("on_touch_down", me) elif etype == "update": self.dispatch("on_touch_move", me) elif etype == "end": self.dispatch("on_touch_up", me) def on_touch_down(self, touch): """Event called when a touch down event is initiated. .. versionchanged:: 1.8.1 The touch `pos` is now transformed to window coordinates before this method is called. Before, the touch `pos` coordinate would be `(0, 0)` when this method was called. """ for w in self.children[:]: if w.dispatch("on_touch_down", touch): return True def on_touch_move(self, touch): """Event called when a touch event moves (changes location). .. versionchanged:: 1.8.1 The touch `pos` is now transformed to window coordinates before this method is called. Before, the touch `pos` coordinate would be `(0, 0)` when this method was called. """ for w in self.children[:]: if w.dispatch("on_touch_move", touch): return True def on_touch_up(self, touch): """Event called when a touch event is released (terminated). .. versionchanged:: 1.8.1 The touch `pos` is now transformed to window coordinates before this method is called. Before, the touch `pos` coordinate would be `(0, 0)` when this method was called. """ for w in self.children[:]: if w.dispatch("on_touch_up", touch): return True def on_resize(self, width, height): """Event called when the window is resized.""" self.update_viewport() def update_viewport(self): from kivy.graphics.opengl import glViewport from kivy.graphics.transformation import Matrix from math import radians w, h = self.system_size smode = self.softinput_mode kheight = self.keyboard_height w2, h2 = w / 2.0, h / 2.0 r = radians(self.rotation) x, y = 0, 0 _h = h if smode: y = kheight if smode == "scale": _h -= kheight # prepare the viewport glViewport(x, y, w, _h) # do projection matrix projection_mat = Matrix() projection_mat.view_clip(0.0, w, 0.0, h, -1.0, 1.0, 0) self.render_context["projection_mat"] = projection_mat # do modelview matrix modelview_mat = Matrix().translate(w2, h2, 0) modelview_mat = modelview_mat.multiply(Matrix().rotate(r, 0, 0, 1)) w, h = self.size w2, h2 = w / 2.0, h / 2.0 modelview_mat = modelview_mat.multiply(Matrix().translate(-w2, -h2, 0)) self.render_context["modelview_mat"] = modelview_mat # redraw canvas self.canvas.ask_update() # and update childs self.update_childsize() def update_childsize(self, childs=None): width, height = self.size if childs is None: childs = self.children for w in childs: shw, shh = w.size_hint if shw and shh: w.size = shw * width, shh * height elif shw: w.width = shw * width elif shh: w.height = shh * height for key, value in w.pos_hint.items(): if key == "x": w.x = value * width elif key == "right": w.right = value * width elif key == "y": w.y = value * height elif key == "top": w.top = value * height elif key == "center_x": w.center_x = value * width elif key == "center_y": w.center_y = value * height def screenshot(self, name="screenshot{:04d}.png"): """Save the actual displayed image in a file """ i = 0 path = None if name != "screenshot{:04d}.png": _ext = name.split(".")[-1] name = "".join((name[: -(len(_ext) + 1)], "{:04d}.", _ext)) while True: i += 1 path = join(getcwd(), name.format(i)) if not exists(path): break return path def on_rotate(self, rotation): """Event called when the screen has been rotated. """ pass def on_close(self, *largs): """Event called when the window is closed""" Modules.unregister_window(self) EventLoop.remove_event_listener(self) def on_request_close(self, *largs, **kwargs): """Event called before we close the window. If a bound function returns `True`, the window will not be closed. If the the event is triggered because of the keyboard escape key, the keyword argument `source` is dispatched along with a value of `keyboard` to the bound functions. .. warning:: When the bound function returns True the window will not be closed, so use with care because the user would not be able to close the program, even if the red X is clicked. """ pass def on_mouse_down(self, x, y, button, modifiers): """Event called when the mouse is used (pressed/released)""" pass def on_mouse_move(self, x, y, modifiers): """Event called when the mouse is moved with buttons pressed""" pass def on_mouse_up(self, x, y, button, modifiers): """Event called when the mouse is moved with buttons pressed""" pass def on_keyboard(self, key, scancode=None, codepoint=None, modifier=None, **kwargs): """Event called when keyboard is used. .. warning:: Some providers may omit `scancode`, `codepoint` and/or `modifier`! """ if "unicode" in kwargs: Logger.warning( "The use of the unicode parameter is deprecated, " "and will be removed in future versions. Use " "codepoint instead, which has identical " "semantics." ) # Quit if user presses ESC or the typical OSX shortcuts CMD+q or CMD+w # TODO If just CMD+w is pressed, only the window should be closed. is_osx = platform == "darwin" if self.on_keyboard.exit_on_escape: if key == 27 or all([is_osx, key in [113, 119], modifier == 1024]): if not self.dispatch("on_request_close", source="keyboard"): stopTouchApp() self.close() return True if Config: on_keyboard.exit_on_escape = Config.getboolean("kivy", "exit_on_escape") def __exit(section, name, value): WindowBase.__dict__["on_keyboard"].exit_on_escape = Config.getboolean("kivy", "exit_on_escape") Config.add_callback(__exit, "kivy", "exit_on_escape") def on_key_down(self, key, scancode=None, codepoint=None, modifier=None, **kwargs): """Event called when a key is down (same arguments as on_keyboard)""" if "unicode" in kwargs: Logger.warning( "The use of the unicode parameter is deprecated, " "and will be removed in future versions. Use " "codepoint instead, which has identical " "semantics." ) def on_key_up(self, key, scancode=None, codepoint=None, modifier=None, **kwargs): """Event called when a key is released (same arguments as on_keyboard) """ if "unicode" in kwargs: Logger.warning( "The use of the unicode parameter is deprecated, " "and will be removed in future versions. Use " "codepoint instead, which has identical " "semantics." ) def on_dropfile(self, filename): """Event called when a file is dropped on the application. .. warning:: This event is currently used only on MacOSX with a patched version of pygame, but is left in place for further evolution (ios, android etc.) .. versionadded:: 1.2.0 """ pass @reify def dpi(self): """Return the DPI of the screen. If the implementation doesn't support any DPI lookup, it will just return 96. .. warning:: This value is not cross-platform. Use :attr:`kivy.base.EventLoop.dpi` instead. """ return 96.0 def configure_keyboards(self): # Configure how to provide keyboards (virtual or not) # register system keyboard to listening keys from window sk = self._system_keyboard self.bind(on_key_down=sk._on_window_key_down, on_key_up=sk._on_window_key_up) # use the device's real keyboard self.use_syskeyboard = True # use the device's real keyboard self.allow_vkeyboard = False # one single vkeyboard shared between all widgets self.single_vkeyboard = True # the single vkeyboard is always sitting at the same position self.docked_vkeyboard = False # now read the configuration mode = Config.get("kivy", "keyboard_mode") if mode not in ("", "system", "dock", "multi", "systemanddock", "systemandmulti"): Logger.critical("Window: unknown keyboard mode %r" % mode) # adapt mode according to the configuration if mode == "system": self.use_syskeyboard = True self.allow_vkeyboard = False self.single_vkeyboard = True self.docked_vkeyboard = False elif mode == "dock": self.use_syskeyboard = False self.allow_vkeyboard = True self.single_vkeyboard = True self.docked_vkeyboard = True elif mode == "multi": self.use_syskeyboard = False self.allow_vkeyboard = True self.single_vkeyboard = False self.docked_vkeyboard = False elif mode == "systemanddock": self.use_syskeyboard = True self.allow_vkeyboard = True self.single_vkeyboard = True self.docked_vkeyboard = True elif mode == "systemandmulti": self.use_syskeyboard = True self.allow_vkeyboard = True self.single_vkeyboard = False self.docked_vkeyboard = False Logger.info( "Window: virtual keyboard %sallowed, %s, %s" % ( "" if self.allow_vkeyboard else "not ", "single mode" if self.single_vkeyboard else "multiuser mode", "docked" if self.docked_vkeyboard else "not docked", ) ) def set_vkeyboard_class(self, cls): """.. versionadded:: 1.0.8 Set the VKeyboard class to use. If set to None, it will use the :class:`kivy.uix.vkeyboard.VKeyboard`. """ self._vkeyboard_cls = cls def release_all_keyboards(self): """.. versionadded:: 1.0.8 This will ensure that no virtual keyboard / system keyboard is requested. All instances will be closed. """ for key in list(self._keyboards.keys())[:]: keyboard = self._keyboards[key] if keyboard: keyboard.release() def request_keyboard(self, callback, target, input_type="text"): """.. versionadded:: 1.0.4 Internal widget method to request the keyboard. This method is rarely required by the end-user as it is handled automatically by the :class:`~kivy.uix.textinput.TextInput`. We expose it in case you want to handle the keyboard manually for unique input scenarios. A widget can request the keyboard, indicating a callback to call when the keyboard is released (or taken by another widget). :Parameters: `callback`: func Callback that will be called when the keyboard is closed. This can be because somebody else requested the keyboard or the user closed it. `target`: Widget Attach the keyboard to the specified `target`. This should be the widget that requested the keyboard. Ensure you have a different target attached to each keyboard if you're working in a multi user mode. .. versionadded:: 1.0.8 `input_type`: string Choose the type of soft keyboard to request. Can be one of 'text', 'number', 'url', 'mail', 'datetime', 'tel', 'address'. .. note:: `input_type` is currently only honored on mobile devices. .. versionadded:: 1.8.0 :Return: An instance of :class:`Keyboard` containing the callback, target, and if the configuration allows it, a :class:`~kivy.uix.vkeyboard.VKeyboard` instance attached as a *.widget* property. """ # release any previous keyboard attached. self.release_keyboard(target) # if we can use virtual vkeyboard, activate it. if self.allow_vkeyboard: keyboard = None # late import global VKeyboard if VKeyboard is None and self._vkeyboard_cls is None: from kivy.uix.vkeyboard import VKeyboard self._vkeyboard_cls = VKeyboard # if the keyboard doesn't exist, create it. key = "single" if self.single_vkeyboard else target if key not in self._keyboards: vkeyboard = self._vkeyboard_cls() keyboard = Keyboard(widget=vkeyboard, window=self) vkeyboard.bind(on_key_down=keyboard._on_vkeyboard_key_down, on_key_up=keyboard._on_vkeyboard_key_up) self._keyboards[key] = keyboard else: keyboard = self._keyboards[key] # configure vkeyboard keyboard.target = keyboard.widget.target = target keyboard.callback = keyboard.widget.callback = callback # add to the window self.add_widget(keyboard.widget) # only after add, do dock mode keyboard.widget.docked = self.docked_vkeyboard keyboard.widget.setup_mode() else: # system keyboard, just register the callback. keyboard = self._system_keyboard keyboard.callback = callback keyboard.target = target # use system (hardware) keyboard according to flag if self.allow_vkeyboard and self.use_syskeyboard: self.unbind(on_key_down=keyboard._on_window_key_down, on_key_up=keyboard._on_window_key_up) self.bind(on_key_down=keyboard._on_window_key_down, on_key_up=keyboard._on_window_key_up) return keyboard def release_keyboard(self, target=None): """.. versionadded:: 1.0.4 Internal method for the widget to release the real-keyboard. Check :meth:`request_keyboard` to understand how it works. """ if self.allow_vkeyboard: key = "single" if self.single_vkeyboard else target if key not in self._keyboards: return keyboard = self._keyboards[key] callback = keyboard.callback if callback: keyboard.callback = None callback() keyboard.target = None self.remove_widget(keyboard.widget) if key != "single" and key in self._keyboards: del self._keyboards[key] elif self._system_keyboard.callback: # this way will prevent possible recursion. callback = self._system_keyboard.callback self._system_keyboard.callback = None callback() return True
class Widget(WidgetBase): '''Widget class. See module documentation for more information. :Events: `on_touch_down`: Fired when a new touch event occurs `on_touch_move`: Fired when an existing touch moves `on_touch_up`: Fired when an existing touch disappears .. versionchanged:: 1.0.9 Everything related to event properties has been moved to the :class:`~kivy.event.EventDispatcher`. Event properties can now be used when contructing a simple class without subclassing :class:`Widget`. .. versionchanged:: 1.5.0 The constructor now accepts on_* arguments to automatically bind callbacks to properties or events, as in the Kv language. ''' __metaclass__ = WidgetMetaclass __events__ = ('on_touch_down', 'on_touch_move', 'on_touch_up') def __init__(self, **kwargs): # Before doing anything, ensure the windows exist. EventLoop.ensure_window() # assign the default context of the widget creation if not hasattr(self, '_context'): self._context = get_current_context() super(Widget, self).__init__(**kwargs) # Create the default canvas if not exist if self.canvas is None: self.canvas = Canvas(opacity=self.opacity) # Apply all the styles if '__no_builder' not in kwargs: #current_root = Builder.idmap.get('root') #Builder.idmap['root'] = self Builder.apply(self) #if current_root is not None: # Builder.idmap['root'] = current_root #else: # Builder.idmap.pop('root') # Bind all the events for argument in kwargs: if argument[:3] == 'on_': self.bind(**{argument: kwargs[argument]}) @property def proxy_ref(self): '''Return a proxy reference to the widget, i.e. without creating a reference to the widget. See `weakref.proxy <http://docs.python.org/2/library/weakref.html?highlight\ =proxy#weakref.proxy>`_ for more information. .. versionadded:: 1.7.2 ''' if hasattr(self, '_proxy_ref'): return self._proxy_ref f = partial(_widget_destructor, self.uid) self._proxy_ref = _proxy_ref = proxy(self, f) # only f should be enough here, but it appears that is a very # specific case, the proxy destructor is not called if both f and # _proxy_ref are not together in a tuple _widget_destructors[self.uid] = (f, _proxy_ref) return _proxy_ref def __eq__(self, other): if not isinstance(other, Widget): return False return self.proxy_ref is other.proxy_ref def __hash__(self): return id(self) @property def __self__(self): return self # # Collision # def collide_point(self, x, y): '''Check if a point (x, y) is inside the widget's axis aligned bounding box. :Parameters: `x`: numeric X position of the point (in window coordinates) `y`: numeric Y position of the point (in window coordinates) :Returns: bool, True if the point is inside the bounding box. >>> Widget(pos=(10, 10), size=(50, 50)).collide_point(40, 40) True ''' return self.x <= x <= self.right and self.y <= y <= self.top def collide_widget(self, wid): '''Check if the other widget collides with this widget. Performs an axis-aligned bounding box intersection test by default. :Parameters: `wid`: :class:`Widget` class Widget to collide with. :Returns: bool, True if the other widget collides with this widget. >>> wid = Widget(size=(50, 50)) >>> wid2 = Widget(size=(50, 50), pos=(25, 25)) >>> wid.collide_widget(wid2) True >>> wid2.pos = (55, 55) >>> wid.collide_widget(wid2) False ''' if self.right < wid.x: return False if self.x > wid.right: return False if self.top < wid.y: return False if self.y > wid.top: return False return True # # Default event handlers # def on_touch_down(self, touch): '''Receive a touch down event. :Parameters: `touch`: :class:`~kivy.input.motionevent.MotionEvent` class Touch received. The touch is in parent coordinates. See :mod:`~kivy.uix.relativelayout` for a discussion on coordinate systems. :Returns: bool. If True, the dispatching of the touch event will stop. ''' if self.disabled and self.collide_point(*touch.pos): return True for child in self.children[:]: if child.dispatch('on_touch_down', touch): return True def on_touch_move(self, touch): '''Receive a touch move event. The touch is in parent coordinates. See :meth:`on_touch_down` for more information. ''' if self.disabled: return for child in self.children[:]: if child.dispatch('on_touch_move', touch): return True def on_touch_up(self, touch): '''Receive a touch up event. The touch is in parent coordinates. See :meth:`on_touch_down` for more information. ''' if self.disabled: return for child in self.children[:]: if child.dispatch('on_touch_up', touch): return True def on_disabled(self, instance, value): for child in self.children: child.disabled = value # # Tree management # def add_widget(self, widget, index=0): '''Add a new widget as a child of this widget. :Parameters: `widget`: :class:`Widget` Widget to add to our list of children. `index`: int, defaults to 0 Index to insert the widget in the list .. versionadded:: 1.0.5 >>> from kivy.uix.button import Button >>> from kivy.uix.slider import Slider >>> root = Widget() >>> root.add_widget(Button()) >>> slider = Slider() >>> root.add_widget(slider) ''' if not isinstance(widget, Widget): raise WidgetException( 'add_widget() can be used only with Widget classes.') widget = widget.__self__ if widget is self: raise WidgetException('You cannot add yourself in a Widget') parent = widget.parent # check if widget is already a child of another widget if parent: raise WidgetException('Cannot add %r, it already has a parent %r' % (widget, parent)) widget.parent = parent = self # child will be disabled if added to a disabled parent if parent.disabled: widget.disabled = True if index == 0 or len(self.children) == 0: self.children.insert(0, widget) self.canvas.add(widget.canvas) else: canvas = self.canvas children = self.children if index >= len(children): index = len(children) next_index = 0 else: next_child = children[index] next_index = canvas.indexof(next_child.canvas) if next_index == -1: next_index = canvas.length() else: next_index += 1 children.insert(index, widget) # we never want to insert widget _before_ canvas.before. if next_index == 0 and canvas.has_before: next_index = 1 canvas.insert(next_index, widget.canvas) def remove_widget(self, widget): '''Remove a widget from the children of this widget. :Parameters: `widget`: :class:`Widget` Widget to remove from our children list. >>> from kivy.uix.button import Button >>> root = Widget() >>> button = Button() >>> root.add_widget(button) >>> root.remove_widget(button) ''' if widget not in self.children: return self.children.remove(widget) self.canvas.remove(widget.canvas) widget.parent = None def clear_widgets(self, children=None): '''Remove all widgets added to this widget. .. versionchanged:: 1.8.0 `children` argument can be used to select the children we want to remove. It should be a list of children (or filtered list) of the current widget. ''' if not children: children = self.children remove_widget = self.remove_widget for child in children[:]: remove_widget(child) def export_to_png(self, filename, *args): '''Saves an image of the widget and its children in png format at the specified filename. Works by removing the widget canvas from its parent, rendering to an :class:`~kivy.graphics.fbo.Fbo`, and calling :meth:`~kivy.graphics.texture.Texture.save`. .. note:: The image includes only this widget and its children. If you want to include widgets elsewhere in the tree, you must call :meth:`~Widget.export_to_png` from their common parent, or use :meth:`~kivy.core.window.Window.screenshot` to capture the whole window. .. note:: The image will be saved in png format, you should include the extension in your filename. .. versionadded:: 1.8.1 ''' if self.parent is not None: canvas_parent_index = self.parent.canvas.indexof(self.canvas) self.parent.canvas.remove(self.canvas) fbo = Fbo(size=self.size) with fbo: ClearColor(0, 0, 0, 1) ClearBuffers() Translate(-self.x, -self.y, 0) fbo.add(self.canvas) fbo.draw() fbo.texture.save(filename) fbo.remove(self.canvas) if self.parent is not None: self.parent.canvas.insert(canvas_parent_index, self.canvas) return True def get_root_window(self): '''Return the root window. :Returns: Instance of the root window. Can be a :class:`~kivy.core.window.WindowBase` or :class:`Widget`. ''' if self.parent: return self.parent.get_root_window() def get_parent_window(self): '''Return the parent window. :Returns: Instance of the parent window. Can be a :class:`~kivy.core.window.WindowBase` or :class:`Widget`. ''' if self.parent: return self.parent.get_parent_window() def _walk(self, restrict=False, loopback=False, index=None): # we pass index only when we are going on the parent. # so don't yield the parent as well. if index is None: index = len(self.children) yield self for child in reversed(self.children[:index]): for walk_child in child._walk(restrict=True): yield walk_child # if we want to continue with our parent, just do it if not restrict: parent = self.parent try: if parent is None or not isinstance(parent, Widget): raise ValueError index = parent.children.index(self) except ValueError: # self is root, if wanted to loopback from first element then -> if not loopback: return # if we started with root (i.e. index==None), then we have to # start from root again, so we return self again. Otherwise, we # never returned it, so return it now starting with it parent = self index = None for walk_child in parent._walk(loopback=loopback, index=index): yield walk_child def walk(self, restrict=False, loopback=False): ''' Iterator that walks the widget tree starting with this widget and goes forward returning widgets in the order in which layouts display them. :Parameters: `restrict`: If True, it will only iterate through the widget and its children (or children of its children etc.). Defaults to False. `loopback`: If True, when the last widget in the tree is reached, it'll loop back to the uppermost root and start walking until we hit this widget again. Naturally, it can only loop back when `restrict` is False. Defaults to False. :return: A generator that walks the tree, returning widgets in the forward layout order. For example, given a tree with the following structure:: GridLayout: Button BoxLayout: id: box Widget Button Widget walking this tree:: >>> # Call walk on box with loopback True, and restrict False >>> [type(widget) for widget in box.walk(loopback=True)] [<class 'BoxLayout'>, <class 'Widget'>, <class 'Button'>, <class 'Widget'>, <class 'GridLayout'>, <class 'Button'>] >>> # Now with loopback False, and restrict False >>> [type(widget) for widget in box.walk()] [<class 'BoxLayout'>, <class 'Widget'>, <class 'Button'>, <class 'Widget'>] >>> # Now with restrict True >>> [type(widget) for widget in box.walk(restrict=True)] [<class 'BoxLayout'>, <class 'Widget'>, <class 'Button'>] .. versionadded:: 1.8.1 ''' gen = self._walk(restrict, loopback) yield next(gen) for node in gen: if node is self: return yield node def _walk_reverse(self, loopback=False, go_up=False): # process is walk up level, walk down its children tree, then walk up # next level etc. # default just walk down the children tree root = self index = 0 # we need to go up a level before walking tree if go_up: root = self.parent try: if root is None or not isinstance(root, Widget): raise ValueError index = root.children.index(self) + 1 except ValueError: if not loopback: return index = 0 go_up = False root = self # now walk children tree starting with last-most child for child in islice(root.children, index, None): for walk_child in child._walk_reverse(loopback=loopback): yield walk_child # we need to return ourself last, in all cases yield root # if going up, continue walking up the parent tree if go_up: for walk_child in root._walk_reverse(loopback=loopback, go_up=go_up): yield walk_child def walk_reverse(self, loopback=False): ''' Iterator that walks the widget tree backwards starting with the widget before this, and going backwards returning widgets in the reverse order in which layouts display them. This walks in the opposite direction of :meth:`walk`, so a list of the tree generated with :meth:`walk` will be in reverse order compared to the list generated with this, provided `loopback` is True. :Parameters: `loopback`: If True, when the uppermost root in the tree is reached, it'll loop back to the last widget and start walking back until after we hit widget again. Defaults to False :return: A generator that walks the tree, returning widgets in the reverse layout order. For example, given a tree with the following structure:: GridLayout: Button BoxLayout: id: box Widget Button Widget walking this tree:: >>> # Call walk on box with loopback True >>> [type(widget) for widget in box.walk_reverse(loopback=True)] [<class 'Button'>, <class 'GridLayout'>, <class 'Widget'>, <class 'Button'>, <class 'Widget'>, <class 'BoxLayout'>] >>> # Now with loopback False >>> [type(widget) for widget in box.walk_reverse()] [<class 'Button'>, <class 'GridLayout'>] >>> forward = [w for w in box.walk(loopback=True)] >>> backward = [w for w in box.walk_reverse(loopback=True)] >>> forward == backward[::-1] True .. versionadded:: 1.8.1 ''' for node in self._walk_reverse(loopback=loopback, go_up=True): yield node if node is self: return def to_widget(self, x, y, relative=False): '''Convert the given coordinate from window to local widget coordinates. See :mod:`~kivy.uix.relativelayout` for details on the coordinate systems. ''' if self.parent: x, y = self.parent.to_widget(x, y) return self.to_local(x, y, relative=relative) def to_window(self, x, y, initial=True, relative=False): '''Transform local coordinates to window coordinates. See :mod:`~kivy.uix.relativelayout` for details on the coordinate systems. ''' if not initial: x, y = self.to_parent(x, y, relative=relative) if self.parent: return self.parent.to_window(x, y, initial=False, relative=relative) return (x, y) def to_parent(self, x, y, relative=False): '''Transform local coordinates to parent coordinates. See :mod:`~kivy.uix.relativelayout` for details on the coordinate systems. :Parameters: `relative`: bool, defaults to False Change to True if you want to translate relative positions from a widget to its parent coordinates. ''' if relative: return (x + self.x, y + self.y) return (x, y) def to_local(self, x, y, relative=False): '''Transform parent coordinates to local coordinates. See :mod:`~kivy.uix.relativelayout` for details on the coordinate systems. :Parameters: `relative`: bool, defaults to False Change to True if you want to translate coordinates to relative widget coordinates. ''' if relative: return (x - self.x, y - self.y) return (x, y) x = NumericProperty(0) '''X position of the widget. :attr:`x` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' y = NumericProperty(0) '''Y position of the widget. :attr:`y` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' width = NumericProperty(100) '''Width of the widget. :attr:`width` is a :class:`~kivy.properties.NumericProperty` ans defaults to 100. .. warning:: Keep in mind that the `width` property is subject to layout logic and that this has not yet happened at the time of the widget's `__init__` method. ''' height = NumericProperty(100) '''Height of the widget. :attr:`height` is a :class:`~kivy.properties.NumericProperty` and defaults to 100. .. warning:: Keep in mind that the `height` property is subject to layout logic and that this has not yet happened at the time of the widget's `__init__` method. ''' pos = ReferenceListProperty(x, y) '''Position of the widget. :attr:`pos` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`x`, :attr:`y`) properties. ''' size = ReferenceListProperty(width, height) '''Size of the widget. :attr:`size` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`width`, :attr:`height`) properties. ''' def get_right(self): return self.x + self.width def set_right(self, value): self.x = value - self.width right = AliasProperty(get_right, set_right, bind=('x', 'width')) '''Right position of the widget. :attr:`right` is an :class:`~kivy.properties.AliasProperty` of (:attr:`x` + :attr:`width`), ''' def get_top(self): return self.y + self.height def set_top(self, value): self.y = value - self.height top = AliasProperty(get_top, set_top, bind=('y', 'height')) '''Top position of the widget. :attr:`top` is an :class:`~kivy.properties.AliasProperty` of (:attr:`y` + :attr:`height`), ''' def get_center_x(self): return self.x + self.width / 2. def set_center_x(self, value): self.x = value - self.width / 2. center_x = AliasProperty(get_center_x, set_center_x, bind=('x', 'width')) '''X center position of the widget. :attr:`center_x` is an :class:`~kivy.properties.AliasProperty` of (:attr:`x` + :attr:`width` / 2.), ''' def get_center_y(self): return self.y + self.height / 2. def set_center_y(self, value): self.y = value - self.height / 2. center_y = AliasProperty(get_center_y, set_center_y, bind=('y', 'height')) '''Y center position of the widget. :attr:`center_y` is an :class:`~kivy.properties.AliasProperty` of (:attr:`y` + :attr:`height` / 2.) ''' center = ReferenceListProperty(center_x, center_y) '''Center position of the widget. :attr:`center` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`center_x`, :attr:`center_y`) ''' cls = ListProperty([]) '''Class of the widget, used for styling. ''' id = StringProperty(None, allownone=True) '''Unique identifier of the widget in the tree. :attr:`id` is a :class:`~kivy.properties.StringProperty` and defaults to None. .. warning:: If the :attr:`id` is already used in the tree, an exception will be raised. ''' children = ListProperty([]) '''List of children of this widget. :attr:`children` is a :class:`~kivy.properties.ListProperty` and defaults to an empty list. Use :meth:`add_widget` and :meth:`remove_widget` for manipulating the children list. Don't manipulate the children list directly unless you know what you are doing. ''' parent = ObjectProperty(None, allownone=True) '''Parent of this widget. :attr:`parent` is an :class:`~kivy.properties.ObjectProperty` and defaults to None. The parent of a widget is set when the widget is added to another widget and unset when the widget is removed from its parent. ''' size_hint_x = NumericProperty(1, allownone=True) '''X size hint. Represents how much space the widget should use in the direction of the X axis relative to its parent's width. Only the :class:`~kivy.uix.layout.Layout` and :class:`~kivy.core.window.Window` classes make use of the hint. The value is in percent as a float from 0. to 1., where 1. means the full size of his parent. 0.5 represents 50%. :attr:`size_hint_x` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. ''' size_hint_y = NumericProperty(1, allownone=True) '''Y size hint. :attr:`size_hint_y` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. See :attr:`size_hint_x` for more information ''' size_hint = ReferenceListProperty(size_hint_x, size_hint_y) '''Size hint. :attr:`size_hint` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`size_hint_x`, :attr:`size_hint_y`). See :attr:`size_hint_x` for more information ''' pos_hint = ObjectProperty({}) '''Position hint. This property allows you to set the position of the widget inside its parent layout, in percent (similar to size_hint). For example, if you want to set the top of the widget to be at 90% height of its parent layout, you can write:: widget = Widget(pos_hint={'top': 0.9}) The keys 'x', 'right' and 'center_x' will use the parent width. The keys 'y', 'top' and 'center_y' will use the parent height. See :doc:`api-kivy.uix.floatlayout` for further reference. Position hint is only used by the :class:`~kivy.uix.floatlayout.FloatLayout` and :class:`~kivy.core.window.Window`. :attr:`pos_hint` is an :class:`~kivy.properties.ObjectProperty` containing a dict. ''' ids = DictProperty({}) '''This is a Dictionary of id's defined in your kv language. This will only be populated if you use id's in your kv language code. .. versionadded:: 1.7.0 :attr:`ids` is a :class:`~kivy.properties.DictProperty` and defaults to a empty dict {}. ''' opacity = NumericProperty(1.0) '''Opacity of the widget and all the children. .. versionadded:: 1.4.1 The opacity attribute controls the opacity of the widget and its children. Be careful, it's a cumulative attribute: the value is multiplied by the current global opacity and the result is applied to the current context color. For example, if the parent has an opacity of 0.5 and a child has an opacity of 0.2, the real opacity of the child will be 0.5 * 0.2 = 0.1. Then, the opacity is applied by the shader as:: frag_color = color * vec4(1.0, 1.0, 1.0, opacity); :attr:`opacity` is a :class:`~kivy.properties.NumericProperty` and defaults to 1.0. ''' def on_opacity(self, instance, value): canvas = self.canvas if canvas is not None: canvas.opacity = value canvas = None '''Canvas of the widget. The canvas is a graphics object that contains all the drawing instructions for the graphical representation of the widget. There are no general properties for the Widget class, such as background color, to keep the design simple and lean. Some derived classes, such as Button, do add such convenience properties but generally the developer is responsible for implementing the graphics representation for a custom widget from the ground up. See the derived widget classes for patterns to follow and extend. See :class:`~kivy.graphics.Canvas` for more information about the usage. ''' disabled = BooleanProperty(False) '''Indicates whether this widget can interact with input or not.
class WindowBase(EventDispatcher): '''WindowBase is an abstract window widget for any window implementation. :Parameters: `fullscreen`: str, one of ('0', '1', 'auto', 'fake') Make the window fullscreen. Check the :mod:`~kivy.config` documentation for a more detailed explanation on the values. `width`: int Width of the window. `height`: int Height of the window. :Events: `on_motion`: etype, motionevent Fired when a new :class:`~kivy.input.motionevent.MotionEvent` is dispatched `on_touch_down`: Fired when a new touch event is initiated. `on_touch_move`: Fired when an existing touch event changes location. `on_touch_up`: Fired when an existing touch event is terminated. `on_draw`: Fired when the :class:`Window` is being drawn. `on_flip`: Fired when the :class:`Window` GL surface is being flipped. `on_rotate`: rotation Fired when the :class:`Window` is being rotated. `on_close`: Fired when the :class:`Window` is closed. `on_request_close`: Fired when the event loop wants to close the window, or if the escape key is pressed and `exit_on_escape` is `True`. If a function bound to this event returns `True`, the window will not be closed. If the the event is triggered because of the keyboard escape key, the keyword argument `source` is dispatched along with a value of `keyboard` to the bound functions. `on_keyboard`: key, scancode, codepoint, modifier Fired when the keyboard is used for input. .. versionchanged:: 1.3.0 The *unicode* parameter has been deprecated in favor of codepoint, and will be removed completely in future versions. `on_key_down`: key, scancode, codepoint Fired when a key pressed. .. versionchanged:: 1.3.0 The *unicode* parameter has been deprecated in favor of codepoint, and will be removed completely in future versions. `on_key_up`: key, scancode, codepoint Fired when a key is released. .. versionchanged:: 1.3.0 The *unicode* parameter has be deprecated in favor of codepoint, and will be removed completely in future versions. `on_dropfile`: str Fired when a file is dropped on the application. .. versionchanged:: 1.9.0 `on_request_close` has been added. ''' __instance = None __initialized = False # private properties _size = ListProperty([0, 0]) _modifiers = ListProperty([]) _rotation = NumericProperty(0) _clearcolor = ObjectProperty([0, 0, 0, 1]) children = ListProperty([]) '''List of the children of this window. :attr:`children` is a :class:`~kivy.properties.ListProperty` instance and defaults to an empty list. Use :meth:`add_widget` and :meth:`remove_widget` to manipulate the list of children. Don't manipulate the list directly unless you know what you are doing. ''' parent = ObjectProperty(None, allownone=True) '''Parent of this window. :attr:`parent` is a :class:`~kivy.properties.ObjectProperty` instance and defaults to None. When created, the parent is set to the window itself. You must take care of it if you are doing a recursive check. ''' icon = StringProperty() def _get_modifiers(self): return self._modifiers modifiers = AliasProperty(_get_modifiers, None) '''List of keyboard modifiers currently active. ''' def _get_size(self): r = self._rotation w, h = self._size if self.softinput_mode == 'resize': h -= self.keyboard_height if r in (0, 180): return w, h return h, w def _set_size(self, size): if self._size != size: r = self._rotation if r in (0, 180): self._size = size else: self._size = size[1], size[0] self.dispatch('on_resize', *size) return True else: return False size = AliasProperty(_get_size, _set_size, bind=('_size', )) '''Get the rotated size of the window. If :attr:`rotation` is set, then the size will change to reflect the rotation. ''' def _get_clearcolor(self): return self._clearcolor def _set_clearcolor(self, value): if value is not None: if type(value) not in (list, tuple): raise Exception('Clearcolor must be a list or tuple') if len(value) != 4: raise Exception('Clearcolor must contain 4 values') self._clearcolor = value clearcolor = AliasProperty(_get_clearcolor, _set_clearcolor, bind=('_clearcolor', )) '''Color used to clear the window. :: from kivy.core.window import Window # red background color Window.clearcolor = (1, 0, 0, 1) # don't clear background at all Window.clearcolor = None .. versionchanged:: 1.7.2 The clearcolor default value is now: (0, 0, 0, 1). ''' # make some property read-only def _get_width(self): r = self._rotation if r == 0 or r == 180: return self._size[0] return self._size[1] width = AliasProperty(_get_width, None, bind=('_rotation', '_size')) '''Rotated window width. :attr:`width` is a read-only :class:`~kivy.properties.AliasProperty`. ''' def _get_height(self): '''Rotated window height''' r = self._rotation kb = self.keyboard_height if self.softinput_mode == 'resize' else 0 if r == 0 or r == 180: return self._size[1] - kb return self._size[0] - kb height = AliasProperty(_get_height, None, bind=('_rotation', '_size')) '''Rotated window height. :attr:`height` is a read-only :class:`~kivy.properties.AliasProperty`. ''' def _get_center(self): return self.width / 2., self.height / 2. center = AliasProperty(_get_center, None, bind=('width', 'height')) '''Center of the rotated window. :attr:`center` is a :class:`~kivy.properties.AliasProperty`. ''' def _get_rotation(self): return self._rotation def _set_rotation(self, x): x = int(x % 360) if x == self._rotation: return if x not in (0, 90, 180, 270): raise ValueError('can rotate only 0, 90, 180, 270 degrees') self._rotation = x if self.initialized is False: return self.dispatch('on_resize', *self.size) self.dispatch('on_rotate', x) rotation = AliasProperty(_get_rotation, _set_rotation, bind=('_rotation', )) '''Get/set the window content rotation. Can be one of 0, 90, 180, 270 degrees. ''' softinput_mode = OptionProperty('', options=('', 'pan', 'scale', 'resize')) '''This specifies the behavior of window contents on display of soft keyboard on mobile platform. Can be one of '', 'pan', 'scale', 'resize'. When '' The main window is left as it is allowing the user to use :attr:`keyboard_height` to manage the window contents the way they want. when 'pan' The main window pans moving the bottom part of the window to be always on top of the keyboard. when 'resize' The window is resized and the contents scaled to fit the remaining space. ..versionadded::1.9.0 :attr:`softinput_mode` is a :class:`OptionProperty` defaults to None. ''' _keyboard_changed = BooleanProperty(False) def _upd_kbd_height(self, *kargs): self._keyboard_changed = not self._keyboard_changed def _get_ios_kheight(self): return 0 def _get_android_kheight(self): global android if not android: import android return android.get_keyboard_height() def _get_kheight(self): if platform == 'android': return self._get_android_kheight() if platform == 'ios': return self._get_ios_kheight() return 0 keyboard_height = AliasProperty(_get_kheight, None, bind=('_keyboard_changed',)) '''Rerturns the height of the softkeyboard/IME on mobile platforms. Will return 0 if not on mobile platform or if IME is not active. ..versionadded:: 1.9.0 :attr:`keyboard_height` is a read-only :class:`AliasProperty` defaults to 0. ''' def _set_system_size(self, size): self._size = size def _get_system_size(self): if self.softinput_mode == 'resize': return self._size[0], self._size[1] - self.keyboard_height return self._size system_size = AliasProperty( _get_system_size, _set_system_size, bind=('_size', )) '''Real size of the window ignoring rotation. ''' fullscreen = OptionProperty(False, options=(True, False, 'auto', 'fake')) '''This property sets the fullscreen mode of the window. Available options are: True, False, 'auto', 'fake'. Check the :mod:`~kivy.config` documentation for a more detailed explanation on the values. .. versionadded:: 1.2.0 ''' mouse_pos = ObjectProperty([0, 0]) '''2d position of the mouse within the window. .. versionadded:: 1.2.0 ''' @property def __self__(self): return self top = NumericProperty(None, allownone=True) left = NumericProperty(None, allownone=True) position = OptionProperty('auto', options=['auto', 'custom']) render_context = ObjectProperty(None) canvas = ObjectProperty(None) title = StringProperty('Kivy') __events__ = ('on_draw', 'on_flip', 'on_rotate', 'on_resize', 'on_close', 'on_motion', 'on_touch_down', 'on_touch_move', 'on_touch_up', 'on_mouse_down', 'on_mouse_move', 'on_mouse_up', 'on_keyboard', 'on_key_down', 'on_key_up', 'on_dropfile', 'on_request_close') def __new__(cls, **kwargs): if cls.__instance is None: cls.__instance = EventDispatcher.__new__(cls) return cls.__instance def __init__(self, **kwargs): kwargs.setdefault('force', False) # don't init window 2 times, # except if force is specified if WindowBase.__instance is not None and not kwargs.get('force'): return self.initialized = False # create a trigger for update/create the window when one of window # property changes self.trigger_create_window = Clock.create_trigger( self.create_window, -1) # Create a trigger for updating the keyboard height self.trigger_keyboard_height = Clock.create_trigger( self._upd_kbd_height, .5) # set the default window parameter according to the configuration if 'fullscreen' not in kwargs: fullscreen = Config.get('graphics', 'fullscreen') if fullscreen not in ('auto', 'fake'): fullscreen = fullscreen.lower() in ('true', '1', 'yes', 'yup') kwargs['fullscreen'] = fullscreen if 'width' not in kwargs: kwargs['width'] = Config.getint('graphics', 'width') if 'height' not in kwargs: kwargs['height'] = Config.getint('graphics', 'height') if 'rotation' not in kwargs: kwargs['rotation'] = Config.getint('graphics', 'rotation') if 'position' not in kwargs: kwargs['position'] = Config.getdefault('graphics', 'position', 'auto') if 'top' in kwargs: kwargs['position'] = 'custom' kwargs['top'] = kwargs['top'] else: kwargs['top'] = Config.getint('graphics', 'top') if 'left' in kwargs: kwargs['position'] = 'custom' kwargs['left'] = kwargs['left'] else: kwargs['left'] = Config.getint('graphics', 'left') kwargs['_size'] = (kwargs.pop('width'), kwargs.pop('height')) super(WindowBase, self).__init__(**kwargs) # bind all the properties that need to recreate the window for prop in ( 'fullscreen', 'position', 'top', 'left', '_size', 'system_size'): self.bind(**{prop: self.trigger_create_window}) self.bind(size=self.trigger_keyboard_height, rotation=self.trigger_keyboard_height) self.bind(softinput_mode=lambda *dt: self.update_viewport(), keyboard_height=lambda *dt: self.update_viewport()) # init privates self._system_keyboard = Keyboard(window=self) self._keyboards = {'system': self._system_keyboard} self._vkeyboard_cls = None self.children = [] self.parent = self # before creating the window import kivy.core.gl # NOQA # configure the window self.create_window() # attach modules + listener event EventLoop.set_window(self) Modules.register_window(self) EventLoop.add_event_listener(self) # manage keyboard(s) self.configure_keyboards() # assign the default context of the widget creation if not hasattr(self, '_context'): self._context = get_current_context() # mark as initialized self.initialized = True def toggle_fullscreen(self): '''Toggle fullscreen on window''' pass def close(self): '''Close the window''' pass def create_window(self, *largs): '''Will create the main window and configure it. .. warning:: This method is called automatically at runtime. If you call it, it will recreate a RenderContext and Canvas. This means you'll have a new graphics tree, and the old one will be unusable. This method exist to permit the creation of a new OpenGL context AFTER closing the first one. (Like using runTouchApp() and stopTouchApp()). This method has only been tested in a unittest environment and is not suitable for Applications. Again, don't use this method unless you know exactly what you are doing! ''' # just to be sure, if the trigger is set, and if this method is # manually called, unset the trigger Clock.unschedule(self.create_window) if not self.initialized: from kivy.core.gl import init_gl init_gl() # create the render context and canvas, only the first time. from kivy.graphics import RenderContext, Canvas self.render_context = RenderContext() self.canvas = Canvas() self.render_context.add(self.canvas) else: # if we get initialized more than once, then reload opengl state # after the second time. # XXX check how it's working on embed platform. if platform == 'linux' or Window.__class__.__name__ == 'WindowSDL': # on linux, it's safe for just sending a resize. self.dispatch('on_resize', *self.system_size) else: # on other platform, window are recreated, we need to reload. from kivy.graphics.context import get_context get_context().reload() Clock.schedule_once(lambda x: self.canvas.ask_update(), 0) self.dispatch('on_resize', *self.system_size) # ensure the gl viewport is correct self.update_viewport() def on_flip(self): '''Flip between buffers (event)''' self.flip() def flip(self): '''Flip between buffers''' pass def _update_childsize(self, instance, value): self.update_childsize([instance]) def add_widget(self, widget): '''Add a widget to a window''' widget.parent = self self.children.insert(0, widget) self.canvas.add(widget.canvas) self.update_childsize([widget]) widget.bind( pos_hint=self._update_childsize, size_hint=self._update_childsize, size=self._update_childsize, pos=self._update_childsize) def remove_widget(self, widget): '''Remove a widget from a window ''' if not widget in self.children: return self.children.remove(widget) self.canvas.remove(widget.canvas) widget.parent = None widget.unbind( pos_hint=self._update_childsize, size_hint=self._update_childsize, size=self._update_childsize, pos=self._update_childsize) def clear(self): '''Clear the window with the background color''' # XXX FIXME use late binding from kivy.graphics.opengl import glClearColor, glClear, \ GL_COLOR_BUFFER_BIT, GL_DEPTH_BUFFER_BIT, GL_STENCIL_BUFFER_BIT cc = self._clearcolor if cc is not None: glClearColor(*cc) glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT) def set_title(self, title): '''Set the window title. .. versionadded:: 1.0.5 ''' self.title = title def set_icon(self, filename): '''Set the icon of the window. .. versionadded:: 1.0.5 ''' self.icon = filename def to_widget(self, x, y, initial=True, relative=False): return (x, y) def to_window(self, x, y, initial=True, relative=False): return (x, y) def get_root_window(self): return self def get_parent_window(self): return self def get_parent_layout(self): return None def on_draw(self): self.clear() self.render_context.draw() def on_motion(self, etype, me): '''Event called when a Motion Event is received. :Parameters: `etype`: str One of 'begin', 'update', 'end' `me`: :class:`~kivy.input.motionevent.MotionEvent` The Motion Event currently dispatched. ''' if me.is_touch: w, h = self.system_size me.scale_for_screen(w, h, rotation=self._rotation, smode=self.softinput_mode, kheight=self.keyboard_height) if etype == 'begin': self.dispatch('on_touch_down', me) elif etype == 'update': self.dispatch('on_touch_move', me) elif etype == 'end': self.dispatch('on_touch_up', me) def on_touch_down(self, touch): '''Event called when a touch down event is initiated. .. versionchanged:: 1.9.0 The touch `pos` is now transformed to window coordinates before this method is called. Before, the touch `pos` coordinate would be `(0, 0)` when this method was called. ''' for w in self.children[:]: if w.dispatch('on_touch_down', touch): return True def on_touch_move(self, touch): '''Event called when a touch event moves (changes location). .. versionchanged:: 1.9.0 The touch `pos` is now transformed to window coordinates before this method is called. Before, the touch `pos` coordinate would be `(0, 0)` when this method was called. ''' for w in self.children[:]: if w.dispatch('on_touch_move', touch): return True def on_touch_up(self, touch): '''Event called when a touch event is released (terminated). .. versionchanged:: 1.9.0 The touch `pos` is now transformed to window coordinates before this method is called. Before, the touch `pos` coordinate would be `(0, 0)` when this method was called. ''' for w in self.children[:]: if w.dispatch('on_touch_up', touch): return True def on_resize(self, width, height): '''Event called when the window is resized.''' self.update_viewport() def update_viewport(self): from kivy.graphics.opengl import glViewport from kivy.graphics.transformation import Matrix from math import radians w, h = self.system_size smode = self.softinput_mode kheight = self.keyboard_height w2, h2 = w / 2., h / 2. r = radians(self.rotation) x, y = 0, 0 _h = h if smode: y = kheight if smode == 'scale': _h -= kheight # prepare the viewport glViewport(x, y, w, _h) # do projection matrix projection_mat = Matrix() projection_mat.view_clip(0.0, w, 0.0, h, -1.0, 1.0, 0) self.render_context['projection_mat'] = projection_mat # do modelview matrix modelview_mat = Matrix().translate(w2, h2, 0) modelview_mat = modelview_mat.multiply(Matrix().rotate(r, 0, 0, 1)) w, h = self.size w2, h2 = w / 2., h / 2. modelview_mat = modelview_mat.multiply(Matrix().translate(-w2, -h2, 0)) self.render_context['modelview_mat'] = modelview_mat # redraw canvas self.canvas.ask_update() # and update childs self.update_childsize() def update_childsize(self, childs=None): width, height = self.size if childs is None: childs = self.children for w in childs: shw, shh = w.size_hint if shw and shh: w.size = shw * width, shh * height elif shw: w.width = shw * width elif shh: w.height = shh * height for key, value in w.pos_hint.items(): if key == 'x': w.x = value * width elif key == 'right': w.right = value * width elif key == 'y': w.y = value * height elif key == 'top': w.top = value * height elif key == 'center_x': w.center_x = value * width elif key == 'center_y': w.center_y = value * height def screenshot(self, name='screenshot{:04d}.png'): '''Save the actual displayed image in a file ''' i = 0 path = None if name != 'screenshot{:04d}.png': _ext = name.split('.')[-1] name = ''.join((name[:-(len(_ext) + 1)], '{:04d}.', _ext)) while True: i += 1 path = join(getcwd(), name.format(i)) if not exists(path): break return path def on_rotate(self, rotation): '''Event called when the screen has been rotated. ''' pass def on_close(self, *largs): '''Event called when the window is closed''' Modules.unregister_window(self) EventLoop.remove_event_listener(self) def on_request_close(self, *largs, **kwargs): '''Event called before we close the window. If a bound function returns `True`, the window will not be closed. If the the event is triggered because of the keyboard escape key, the keyword argument `source` is dispatched along with a value of `keyboard` to the bound functions. .. warning:: When the bound function returns True the window will not be closed, so use with care because the user would not be able to close the program, even if the red X is clicked. ''' pass def on_mouse_down(self, x, y, button, modifiers): '''Event called when the mouse is used (pressed/released)''' pass def on_mouse_move(self, x, y, modifiers): '''Event called when the mouse is moved with buttons pressed''' pass def on_mouse_up(self, x, y, button, modifiers): '''Event called when the mouse is moved with buttons pressed''' pass def on_keyboard(self, key, scancode=None, codepoint=None, modifier=None, **kwargs): '''Event called when keyboard is used. .. warning:: Some providers may omit `scancode`, `codepoint` and/or `modifier`! ''' if 'unicode' in kwargs: Logger.warning("The use of the unicode parameter is deprecated, " "and will be removed in future versions. Use " "codepoint instead, which has identical " "semantics.") # Quit if user presses ESC or the typical OSX shortcuts CMD+q or CMD+w # TODO If just CMD+w is pressed, only the window should be closed. is_osx = platform == 'darwin' if WindowBase.on_keyboard.exit_on_escape: if key == 27 or all([is_osx, key in [113, 119], modifier == 1024]): if not self.dispatch('on_request_close', source='keyboard'): stopTouchApp() self.close() return True if Config: on_keyboard.exit_on_escape = Config.getboolean('kivy', 'exit_on_escape') def __exit(section, name, value): WindowBase.__dict__['on_keyboard'].exit_on_escape = \ Config.getboolean('kivy', 'exit_on_escape') Config.add_callback(__exit, 'kivy', 'exit_on_escape') def on_key_down(self, key, scancode=None, codepoint=None, modifier=None, **kwargs): '''Event called when a key is down (same arguments as on_keyboard)''' if 'unicode' in kwargs: Logger.warning("The use of the unicode parameter is deprecated, " "and will be removed in future versions. Use " "codepoint instead, which has identical " "semantics.") def on_key_up(self, key, scancode=None, codepoint=None, modifier=None, **kwargs): '''Event called when a key is released (same arguments as on_keyboard) ''' if 'unicode' in kwargs: Logger.warning("The use of the unicode parameter is deprecated, " "and will be removed in future versions. Use " "codepoint instead, which has identical " "semantics.") def on_dropfile(self, filename): '''Event called when a file is dropped on the application. .. warning:: This event is currently used only on MacOSX with a patched version of pygame, but is left in place for further evolution (ios, android etc.) .. versionadded:: 1.2.0 ''' pass @reify def dpi(self): '''Return the DPI of the screen. If the implementation doesn't support any DPI lookup, it will just return 96. .. warning:: This value is not cross-platform. Use :attr:`kivy.base.EventLoop.dpi` instead. ''' return 96. def configure_keyboards(self): # Configure how to provide keyboards (virtual or not) # register system keyboard to listening keys from window sk = self._system_keyboard self.bind( on_key_down=sk._on_window_key_down, on_key_up=sk._on_window_key_up) # use the device's real keyboard self.use_syskeyboard = True # use the device's real keyboard self.allow_vkeyboard = False # one single vkeyboard shared between all widgets self.single_vkeyboard = True # the single vkeyboard is always sitting at the same position self.docked_vkeyboard = False # now read the configuration mode = Config.get('kivy', 'keyboard_mode') if mode not in ('', 'system', 'dock', 'multi', 'systemanddock', 'systemandmulti'): Logger.critical('Window: unknown keyboard mode %r' % mode) # adapt mode according to the configuration if mode == 'system': self.use_syskeyboard = True self.allow_vkeyboard = False self.single_vkeyboard = True self.docked_vkeyboard = False elif mode == 'dock': self.use_syskeyboard = False self.allow_vkeyboard = True self.single_vkeyboard = True self.docked_vkeyboard = True elif mode == 'multi': self.use_syskeyboard = False self.allow_vkeyboard = True self.single_vkeyboard = False self.docked_vkeyboard = False elif mode == 'systemanddock': self.use_syskeyboard = True self.allow_vkeyboard = True self.single_vkeyboard = True self.docked_vkeyboard = True elif mode == 'systemandmulti': self.use_syskeyboard = True self.allow_vkeyboard = True self.single_vkeyboard = False self.docked_vkeyboard = False Logger.info( 'Window: virtual keyboard %sallowed, %s, %s' % ( '' if self.allow_vkeyboard else 'not ', 'single mode' if self.single_vkeyboard else 'multiuser mode', 'docked' if self.docked_vkeyboard else 'not docked')) def set_vkeyboard_class(self, cls): '''.. versionadded:: 1.0.8 Set the VKeyboard class to use. If set to None, it will use the :class:`kivy.uix.vkeyboard.VKeyboard`. ''' self._vkeyboard_cls = cls def release_all_keyboards(self): '''.. versionadded:: 1.0.8 This will ensure that no virtual keyboard / system keyboard is requested. All instances will be closed. ''' for key in list(self._keyboards.keys())[:]: keyboard = self._keyboards[key] if keyboard: keyboard.release() def request_keyboard(self, callback, target, input_type='text'): '''.. versionadded:: 1.0.4 Internal widget method to request the keyboard. This method is rarely required by the end-user as it is handled automatically by the :class:`~kivy.uix.textinput.TextInput`. We expose it in case you want to handle the keyboard manually for unique input scenarios. A widget can request the keyboard, indicating a callback to call when the keyboard is released (or taken by another widget). :Parameters: `callback`: func Callback that will be called when the keyboard is closed. This can be because somebody else requested the keyboard or the user closed it. `target`: Widget Attach the keyboard to the specified `target`. This should be the widget that requested the keyboard. Ensure you have a different target attached to each keyboard if you're working in a multi user mode. .. versionadded:: 1.0.8 `input_type`: string Choose the type of soft keyboard to request. Can be one of 'text', 'number', 'url', 'mail', 'datetime', 'tel', 'address'. .. note:: `input_type` is currently only honored on mobile devices. .. versionadded:: 1.8.0 :Return: An instance of :class:`Keyboard` containing the callback, target, and if the configuration allows it, a :class:`~kivy.uix.vkeyboard.VKeyboard` instance attached as a *.widget* property. .. note:: The behavior of this function is heavily influenced by the current `keyboard_mode`. Please see the Config's :ref:`configuration tokens <configuration-tokens>` section for more information. ''' # release any previous keyboard attached. self.release_keyboard(target) # if we can use virtual vkeyboard, activate it. if self.allow_vkeyboard: keyboard = None # late import global VKeyboard if VKeyboard is None and self._vkeyboard_cls is None: from kivy.uix.vkeyboard import VKeyboard self._vkeyboard_cls = VKeyboard # if the keyboard doesn't exist, create it. key = 'single' if self.single_vkeyboard else target if key not in self._keyboards: vkeyboard = self._vkeyboard_cls() keyboard = Keyboard(widget=vkeyboard, window=self) vkeyboard.bind( on_key_down=keyboard._on_vkeyboard_key_down, on_key_up=keyboard._on_vkeyboard_key_up) self._keyboards[key] = keyboard else: keyboard = self._keyboards[key] # configure vkeyboard keyboard.target = keyboard.widget.target = target keyboard.callback = keyboard.widget.callback = callback # add to the window self.add_widget(keyboard.widget) # only after add, do dock mode keyboard.widget.docked = self.docked_vkeyboard keyboard.widget.setup_mode() else: # system keyboard, just register the callback. keyboard = self._system_keyboard keyboard.callback = callback keyboard.target = target # use system (hardware) keyboard according to flag if self.allow_vkeyboard and self.use_syskeyboard: self.unbind( on_key_down=keyboard._on_window_key_down, on_key_up=keyboard._on_window_key_up) self.bind( on_key_down=keyboard._on_window_key_down, on_key_up=keyboard._on_window_key_up) return keyboard def release_keyboard(self, target=None): '''.. versionadded:: 1.0.4 Internal method for the widget to release the real-keyboard. Check :meth:`request_keyboard` to understand how it works. ''' if self.allow_vkeyboard: key = 'single' if self.single_vkeyboard else target if key not in self._keyboards: return keyboard = self._keyboards[key] callback = keyboard.callback if callback: keyboard.callback = None callback() keyboard.target = None self.remove_widget(keyboard.widget) if key != 'single' and key in self._keyboards: del self._keyboards[key] elif self._system_keyboard.callback: # this way will prevent possible recursion. callback = self._system_keyboard.callback self._system_keyboard.callback = None callback() return True
class PageCurlTransition(TransitionBase): cy_x = NumericProperty(520.) cy_y = NumericProperty(-50) cy_dir = NumericProperty(1.18) cy_radius = NumericProperty(150.) def make_screen_fbo(self, screen): fbo = Fbo(size=screen.size) with fbo: ClearColor(0, 1, 0, 1) ClearBuffers() fbo.add(screen.canvas) return fbo def add_screen(self, screen): self.screen_in.pos = self.screen_out.pos self.screen_in.size = self.screen_out.size self.manager.real_remove_widget(self.screen_out) self.fbo_in = self.make_screen_fbo(self.screen_in) self.fbo_out = self.make_screen_fbo(self.screen_out) self.manager.canvas.add(self.fbo_in) self.manager.canvas.add(self.fbo_out) self.canvas = Canvas() self.c_front = RenderContext() self.c_front.shader.source = resource_find('front.glsl') self.c_back = RenderContext() self.c_back.shader.source = resource_find('back.glsl') self.c_backshadow = RenderContext() self.c_backshadow.shader.source = resource_find('backshadow.glsl') self.canvas.add(self.c_front) self.canvas.add(self.c_back) self.canvas.add(self.c_backshadow) with self.canvas.before: Color(1, 1, 1) Rectangle( size=self.fbo_in.size, texture=self.fbo_in.texture) Callback(self._enter_3d) self._build_mesh(self.fbo_in.size) with self.canvas.after: Callback(self._leave_3d) self.manager.canvas.add(self.canvas) def remove_screen(self, screen): self.manager.canvas.remove(self.fbo_in) self.manager.canvas.remove(self.fbo_out) self.manager.canvas.remove(self.canvas) self.manager.real_add_widget(self.screen_in) def on_progress(self, t): d = 0.8 if t < d: dt = t / d self.cy_dir = funcLinear(AnimationTransition.out_circ(dt), 0, 1.55) else: self.cy_dir = 1.5 self.cy_x = funcLinear(t, self.screen_in.width, -self.screen_in.width / 2.0) self.update_glsl() def update_glsl(self, *largs): size = self.manager.size proj = Matrix().view_clip(0, size[0], 0, size[1], -1000, 1000, 0) self.c_front['projection_mat'] = proj self.c_front['cylinder_position'] = map(float, (self.cy_x, self.cy_y)) self.c_front['cylinder_direction'] = (cos(self.cy_dir), sin(self.cy_dir)) self.c_front['cylinder_radius'] = float(self.cy_radius) for key in ('projection_mat', 'cylinder_position', 'cylinder_radius', 'cylinder_direction'): self.c_back[key] = self.c_front[key] self.c_backshadow[key] = self.c_front[key] self.c_front['texture1'] = 1 self.c_backshadow['texture1'] = 1 self.c_back['texture1'] = 1 def _enter_3d(self, *args): glEnable(GL_DEPTH_TEST) glEnable(GL_CULL_FACE) def _leave_3d(self, *args): glDisable(GL_DEPTH_TEST) glDisable(GL_CULL_FACE) def _build_mesh(self, size): m = 20 width, height = map(int, size) step_width = int(width / (width / m)) step_height = int(height / (height / m)) vertices = [] indices = [] indices_back = [] fw = float(width) fh = float(height) # create all the vertices for y in xrange(0, height + step_height, step_height): for x in xrange(0, width + step_width, step_width): vertices += [x, y, 0, x / fw, y / fh] # trace a triangles mesh mx = 1 + width / step_width my = 1 + height / step_height mode = 'triangles' self.vertex_format = [ ('vPosition', 3, 'float'), ('vTexCoords0', 2, 'float')] for x in xrange(mx - 1): for y in xrange(my - 1): i = y * mx + x indices += [i, i + 1, i + 1 + mx, i, i + 1 + mx, i + mx] indices_back += [i, i + 1 + mx, i + 1, i, i + mx, i + 1 + mx] self.g_mesh = Mesh(vertices=vertices, indices=indices, mode=mode, texture=self.fbo_out.texture, fmt=self.vertex_format) self.g_mesh_back = Mesh(vertices=vertices, indices=indices_back, mode=mode, texture=self.fbo_out.texture, fmt=self.vertex_format) self.o_vertices = vertices self.c_front.add(BindTexture(source='frontshadow.png', index=1)) self.c_front.add(self.g_mesh) self.c_backshadow.add(Rectangle(size=size)) self.c_back.add(BindTexture(source='backshadow.png', index=1)) self.c_back.add(self.g_mesh_back)
class WindowBase(EventDispatcher): '''WindowBase is a abstract window widget, for any window implementation. .. warning:: The parameters are not working in normal case. Because at import, Kivy create a default OpenGL window, to add the ability to use OpenGL directives, texture creation.. before creating Window. If you don't like this behavior, you can include before the very first import of Kivy :: import os os.environ['KIVY_SHADOW'] = '0' This will forbid Kivy to create the default window ! :Parameters: `fullscreen`: bool Make window as fullscreen `width`: int Width of window `height`: int Height of window :Events: `on_motion`: etype, motionevent Fired when a new :class:`~kivy.input.motionevent.MotionEvent` is dispatched `on_touch_down`: Fired when a new touch appear `on_touch_move`: Fired when an existing touch is moved `on_touch_down`: Fired when an existing touch disapear `on_draw`: Fired when the :class:`Window` is beeing drawed `on_flip`: Fired when the :class:`Window` GL surface is beeing flipped `on_rotate`: rotation Fired when the :class:`Window` is beeing rotated `on_close`: Fired when the :class:`Window` is closed `on_keyboard`: key, scancode, unicode, modifier Fired when the keyboard is in action `on_key_down`: key, scancode, unicode Fired when a key is down `on_key_up`: key, scancode, unicode Fired when a key is up ''' __instance = None __initialized = False def __new__(cls, **kwargs): if cls.__instance is None: cls.__instance = EventDispatcher.__new__(cls) return cls.__instance def __init__(self, **kwargs): kwargs.setdefault('force', False) kwargs.setdefault('config', None) # don't init window 2 times, # except if force is specified if self.__initialized and not kwargs.get('force'): return super(WindowBase, self).__init__() # init privates self._keyboard_callback = None self._modifiers = [] self._size = (0, 0) self._rotation = 0 self._clearcolor = [0, 0, 0, 0] # event subsystem self.register_event_type('on_draw') self.register_event_type('on_flip') self.register_event_type('on_rotate') self.register_event_type('on_resize') self.register_event_type('on_close') self.register_event_type('on_motion') self.register_event_type('on_touch_down') self.register_event_type('on_touch_move') self.register_event_type('on_touch_up') self.register_event_type('on_mouse_down') self.register_event_type('on_mouse_move') self.register_event_type('on_mouse_up') self.register_event_type('on_keyboard') self.register_event_type('on_key_down') self.register_event_type('on_key_up') self.children = [] self.parent = self #self.visible = True # add view if 'view' in kwargs: self.add_widget(kwargs.get('view')) # get window params, user options before config option params = {} if 'fullscreen' in kwargs: params['fullscreen'] = kwargs.get('fullscreen') else: params['fullscreen'] = Config.get('graphics', 'fullscreen') if params['fullscreen'] not in ('auto', 'fake'): params['fullscreen'] = params['fullscreen'].lower() in \ ('true', '1', 'yes', 'yup') if 'width' in kwargs: params['width'] = kwargs.get('width') else: params['width'] = Config.getint('graphics', 'width') if 'height' in kwargs: params['height'] = kwargs.get('height') else: params['height'] = Config.getint('graphics', 'height') if 'rotation' in kwargs: params['rotation'] = kwargs.get('rotation') else: params['rotation'] = Config.getint('graphics', 'rotation') params['position'] = Config.get( 'graphics', 'position', 'auto') if 'top' in kwargs: params['position'] = 'custom' params['top'] = kwargs.get('top') else: params['top'] = Config.getint('graphics', 'top') if 'left' in kwargs: params['position'] = 'custom' params['left'] = kwargs.get('left') else: params['left'] = Config.getint('graphics', 'left') # before creating the window import kivy.core.gl # configure the window self.params = params self.create_window() # attach modules + listener event Modules.register_window(self) EventLoop.set_window(self) EventLoop.add_event_listener(self) # mark as initialized self.__initialized = True def toggle_fullscreen(self): '''Toggle fullscreen on window''' pass def close(self): '''Close the window''' pass def create_window(self): '''Will create the main window and configure it. .. warning:: This method is called automatically at runtime. If you call it, it will recreate a RenderContext and Canvas. This mean you'll have a new graphics tree, and the old one will be unusable. This method exist to permit the creation of a new OpenGL context AFTER closing the first one. (Like using runTouchApp() and stopTouchApp()). This method have been only tested in unittest environment, and will be not suitable for Applications. Again, don't use this method unless you know exactly what you are doing ! ''' from kivy.core.gl import init_gl init_gl() # create the render context and canvas from kivy.graphics import RenderContext, Canvas self.render_context = RenderContext() self.canvas = Canvas() self.render_context.add(self.canvas) def on_flip(self): '''Flip between buffers (event)''' self.flip() def flip(self): '''Flip between buffers''' pass def _get_modifiers(self): return self._modifiers modifiers = property(_get_modifiers) def _get_size(self): r = self._rotation w, h = self._size if r == 0 or r == 180: return w, h return h, w def _set_size(self, size): if super(WindowBase, self)._set_size(size): Logger.debug('Window: Resize window to %s' % str(self.size)) self.dispatch('on_resize', *size) return True return False size = property(_get_size, _set_size, doc='''Rotated size of the window''') def _get_clearcolor(self): return self._clearcolor def _set_clearcolor(self, value): if value is not None: if type(value) not in (list, tuple): raise Exception('Clearcolor must be a list or tuple') if len(value) != 4: raise Exception('Clearcolor must contain 4 values') self._clearcolor = value clearcolor = property(_get_clearcolor, _set_clearcolor, doc='''Color used to clear window:: from kivy.core.window import Window # red background color Window.clearcolor = (1, 0, 0, 1) # don't clear background at all Window.clearcolor = None ''') # make some property read-only @property def width(self): '''Rotated window width''' r = self._rotation if r == 0 or r == 180: return self._size[0] return self._size[1] @property def height(self): '''Rotated window height''' r = self._rotation if r == 0 or r == 180: return self._size[1] return self._size[0] @property def center(self): '''Rotated window center''' return self.width / 2., self.height / 2. def add_widget(self, widget): '''Add a widget on window''' self.children.append(widget) widget.parent = self self.canvas.add(widget.canvas) self.update_childsize([widget]) def remove_widget(self, widget): '''Remove a widget from window ''' if not widget in self.children: return self.children.remove(widget) self.canvas.remove(widget.canvas) widget.parent = None def clear(self): '''Clear the window with background color''' # XXX FIXME use late binding from kivy.graphics.opengl import glClearColor, glClear, \ GL_COLOR_BUFFER_BIT, GL_DEPTH_BUFFER_BIT cc = self._clearcolor if cc is not None: glClearColor(*cc) glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) def to_widget(self, x, y, initial=True, relative=False): return (x, y) def to_window(self, x, y, initial=True, relative=False): return (x, y) def get_root_window(self): return self def get_parent_window(self): return self def get_parent_layout(self): return None def on_draw(self): self.clear() self.render_context.draw() def on_motion(self, etype, me): '''Event called when a Motion Event is received. :Parameters: `etype`: str One of 'begin', 'update', 'end' `me`: :class:`~kivy.input.motionevent.MotionEvent` Motion Event currently dispatched ''' if me.is_touch: if etype == 'begin': self.dispatch('on_touch_down', me) elif etype == 'update': self.dispatch('on_touch_move', me) elif etype == 'end': self.dispatch('on_touch_up', me) def on_touch_down(self, touch): '''Event called when a touch is down ''' w, h = self.system_size touch.scale_for_screen(w, h, rotation=self._rotation) for w in self.children[:]: if w.dispatch('on_touch_down', touch): return True def on_touch_move(self, touch): '''Event called when a touch move ''' w, h = self.system_size touch.scale_for_screen(w, h, rotation=self._rotation) for w in self.children[:]: if w.dispatch('on_touch_move', touch): return True def on_touch_up(self, touch): '''Event called when a touch up ''' w, h = self.system_size touch.scale_for_screen(w, h, rotation=self._rotation) for w in self.children[:]: if w.dispatch('on_touch_up', touch): return True def on_resize(self, width, height): '''Event called when the window is resized''' self.update_viewport() def update_viewport(self): from kivy.graphics.opengl import glViewport from kivy.graphics.transformation import Matrix width, height = self.system_size w2 = width / 2. h2 = height / 2. # prepare the viewport glViewport(0, 0, width, height) projection_mat = Matrix() projection_mat.view_clip(0.0, width, 0.0, height, -1.0, 1.0, 0) self.render_context['projection_mat'] = projection_mat # use the rotated size. # XXX FIXME fix rotation ''' width, height = self.size w2 = width / 2. h2 = height / 2. glTranslatef(-w2, -h2, -500) # set the model view glMatrixMode(GL_MODELVIEW) glLoadIdentity() glTranslatef(w2, h2, 0) glRotatef(self._rotation, 0, 0, 1) glTranslatef(-w2, -h2, 0) ''' self.update_childsize() def update_childsize(self, childs=None): width, height = self.system_size if childs is None: childs = self.children for w in childs: shw, shh = w.size_hint if shw and shh: w.size = shw * width, shh * height elif shw: w.width = shw * width elif shh: w.height = shh * height for key, value in w.pos_hint.iteritems(): if key == 'x': w.x = value * width elif key == 'right': w.right = value * width elif key == 'y': w.y = value * height elif key == 'top': w.top = value * height elif key == 'center_x': w.center_x = value * width elif key == 'center_y': w.center_y = value * height def _get_rotation(self): return self._rotation def _set_rotation(self, x): x = int(x % 360) if x == self._rotation: return if x not in (0, 90, 180, 270): raise ValueError('can rotate only 0,90,180,270 degrees') self._rotation = x self.dispatch('on_resize', *self.size) self.dispatch('on_rotate', x) rotation = property(_get_rotation, _set_rotation, 'Get/set the window content rotation. Can be one of ' '0, 90, 180, 270 degrees.') @property def system_size(self): '''Real size of the window, without taking care of the rotation ''' return self._size def screenshot(self, name='screenshot%(counter)04d.jpg'): '''Save the actual displayed image in a file ''' from os.path import join, exists from os import getcwd i = 0 path = None while True: i += 1 path = join(getcwd(), name % {'counter': i}) if not exists(path): break return path def on_rotate(self, rotation): '''Event called when the screen have been rotated ''' pass def on_close(self, *largs): '''Event called when the window is closed''' Modules.unregister_window(self) EventLoop.remove_event_listener(self) def on_mouse_down(self, x, y, button, modifiers): '''Event called when mouse is in action (press/release)''' pass def on_mouse_move(self, x, y, modifiers): '''Event called when mouse is moving, with buttons pressed''' pass def on_mouse_up(self, x, y, button, modifiers): '''Event called when mouse is moving, with buttons pressed''' pass def on_keyboard(self, key, scancode=None, unicode=None, modifier=None): '''Event called when keyboard is in action .. warning:: Some providers may omit `scancode`, `unicode` and/or `modifier`! ''' pass def on_key_down(self, key, scancode=None, unicode=None, modifier=None): '''Event called when a key is down (same arguments as on_keyboard)''' pass def on_key_up(self, key, scancode=None, unicode=None): '''Event called when a key is up (same arguments as on_keyboard)''' pass def request_keyboard(self, callback): '''.. versionadded:: 1.0.4 Internal method for widget, to request the keyboard. This method is not intented to be used by end-user, however, if you want to use the real-keyboard (not virtual keyboard), you don't want to share it with another widget. A widget can request the keyboard, indicating a callback to call when the keyboard will be released (or taken by another widget). ''' self.release_keyboard() self._keyboard_callback = callback return True def release_keyboard(self): '''.. versionadded:: 1.0.4 Internal method for widget, to release the real-keyboard. Check :func:`request_keyboard` to understand how it works. ''' if self._keyboard_callback: # this way will prevent possible recursion. callback = self._keyboard_callback self._keyboard_callback = None callback() return True
class ScrollView(StencilView): """ScrollView class. See module documentation for more information. .. versionchanged:: 1.7.0 `auto_scroll`, `scroll_friction`, `scroll_moves`, `scroll_stoptime' has been deprecated, use :attr:`effect_cls` instead. """ scroll_distance = NumericProperty(_scroll_distance) """Distance to move before scrolling the :class:`ScrollView`, in pixels. As soon as the distance has been traveled, the :class:`ScrollView` will start to scroll, and no touch event will go to children. It is advisable that you base this value on the dpi of your target device's screen. :attr:`scroll_distance` is a :class:`~kivy.properties.NumericProperty` and defaults to 20 (pixels), according to the default value in user configuration. """ scroll_wheel_distance = NumericProperty(20) """Distance to move when scrolling with a mouse wheel. It is advisable that you base this value on the dpi of your target device's screen. .. versionadded:: 1.8.0 :attr:`scroll_wheel_distance` is a :class:`~kivy.properties.NumericProperty` , defaults to 20 pixels. """ scroll_timeout = NumericProperty(_scroll_timeout) """Timeout allowed to trigger the :attr:`scroll_distance`, in milliseconds. If the user has not moved :attr:`scroll_distance` within the timeout, the scrolling will be disabled, and the touch event will go to the children. :attr:`scroll_timeout` is a :class:`~kivy.properties.NumericProperty` and defaults to 55 (milliseconds) according to the default value in user configuration. .. versionchanged:: 1.5.0 Default value changed from 250 to 55. """ scroll_x = NumericProperty(0.0) """X scrolling value, between 0 and 1. If 0, the content's left side will touch the left side of the ScrollView. If 1, the content's right side will touch the right side. This property is controled by :class:`ScrollView` only if :attr:`do_scroll_x` is True. :attr:`scroll_x` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. """ scroll_y = NumericProperty(1.0) """Y scrolling value, between 0 and 1. If 0, the content's bottom side will touch the bottom side of the ScrollView. If 1, the content's top side will touch the top side. This property is controled by :class:`ScrollView` only if :attr:`do_scroll_y` is True. :attr:`scroll_y` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. """ do_scroll_x = BooleanProperty(True) """Allow scroll on X axis. :attr:`do_scroll_x` is a :class:`~kivy.properties.BooleanProperty` and defaults to True. """ do_scroll_y = BooleanProperty(True) """Allow scroll on Y axis. :attr:`do_scroll_y` is a :class:`~kivy.properties.BooleanProperty` and defaults to True. """ def _get_do_scroll(self): return (self.do_scroll_x, self.do_scroll_y) def _set_do_scroll(self, value): if type(value) in (list, tuple): self.do_scroll_x, self.do_scroll_y = value else: self.do_scroll_x = self.do_scroll_y = bool(value) do_scroll = AliasProperty(_get_do_scroll, _set_do_scroll, bind=("do_scroll_x", "do_scroll_y")) """Allow scroll on X or Y axis. :attr:`do_scroll` is a :class:`~kivy.properties.AliasProperty` of (:attr:`do_scroll_x` + :attr:`do_scroll_y`) """ def _get_vbar(self): # must return (y, height) in % # calculate the viewport size / scrollview size % if self._viewport is None: return 0, 1.0 vh = self._viewport.height h = self.height if vh < h or vh == 0: return 0, 1.0 ph = max(0.01, h / float(vh)) sy = min(1.0, max(0.0, self.scroll_y)) py = (1.0 - ph) * sy return (py, ph) vbar = AliasProperty(_get_vbar, None, bind=("scroll_y", "_viewport", "viewport_size")) """Return a tuple of (position, size) of the vertical scrolling bar. .. versionadded:: 1.2.0 The position and size are normalized between 0-1, and represent a percentage of the current scrollview height. This property is used internally for drawing the little vertical bar when you're scrolling. :attr:`vbar` is a :class:`~kivy.properties.AliasProperty`, readonly. """ def _get_hbar(self): # must return (x, width) in % # calculate the viewport size / scrollview size % if self._viewport is None: return 0, 1.0 vw = self._viewport.width w = self.width if vw < w or vw == 0: return 0, 1.0 pw = max(0.01, w / float(vw)) sx = min(1.0, max(0.0, self.scroll_x)) px = (1.0 - pw) * sx return (px, pw) hbar = AliasProperty(_get_hbar, None, bind=("scroll_x", "_viewport", "viewport_size")) """Return a tuple of (position, size) of the horizontal scrolling bar. .. versionadded:: 1.2.0 The position and size are normalized between 0-1, and represent a percentage of the current scrollview height. This property is used internally for drawing the little horizontal bar when you're scrolling. :attr:`vbar` is a :class:`~kivy.properties.AliasProperty`, readonly. """ bar_color = ListProperty([0.7, 0.7, 0.7, 0.9]) """Color of horizontal / vertical scroll bar, in RGBA format. .. versionadded:: 1.2.0 :attr:`bar_color` is a :class:`~kivy.properties.ListProperty` and defaults to [.7, .7, .7, .9]. """ bar_width = NumericProperty("2dp") """Width of the horizontal / vertical scroll bar. The width is interpreted as a height for the horizontal bar. .. versionadded:: 1.2.0 :attr:`bar_width` is a :class:`~kivy.properties.NumericProperty` and defaults to 2. """ bar_pos_x = OptionProperty("bottom", options=("top", "bottom")) """Which side of the ScrollView the horizontal scroll bar should go on. Possible values are 'top' and 'bottom'. .. versionadded:: 1.8.0 :attr:`bar_pos_x` is an :class:`~kivy.properties.OptionProperty`, default to 'bottom' """ bar_pos_y = OptionProperty("right", options=("left", "right")) """Which side of the ScrollView the vertical scroll bar should go on. Possible values are 'left' and 'right'. .. versionadded:: 1.8.0 :attr:`bar_pos_y` is an :class:`~kivy.properties.OptionProperty`, default to 'right' """ bar_pos = ReferenceListProperty(bar_pos_x, bar_pos_y) """Which side of the scroll view to place each of the bars on. :attr:`bar_pos` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`bar_pos_x`, :attr:`bar_pos_y`) """ bar_margin = NumericProperty(0) """Margin between the bottom / right side of the scrollview when drawing the horizontal / vertical scroll bar. .. versionadded:: 1.2.0 :attr:`bar_margin` is a :class:`~kivy.properties.NumericProperty`, default to 0 """ effect_cls = ObjectProperty(DampedScrollEffect, allownone=True) """Class effect to instanciate for X and Y axis. .. versionadded:: 1.7.0 :attr:`effect_cls` is an :class:`~kivy.properties.ObjectProperty` and defaults to :class:`DampedScrollEffect`. .. versionchanged:: 1.8.0 If you set a string, the :class:`~kivy.factory.Factory` will be used to resolve the class. """ effect_x = ObjectProperty(None, allownone=True) """Effect to apply for the X axis. If None is set, an instance of :attr:`effect_cls` will be created. .. versionadded:: 1.7.0 :attr:`effect_x` is an :class:`~kivy.properties.ObjectProperty` and defaults to None. """ effect_y = ObjectProperty(None, allownone=True) """Effect to apply for the Y axis. If None is set, an instance of :attr:`effect_cls` will be created. .. versionadded:: 1.7.0 :attr:`effect_y` is an :class:`~kivy.properties.ObjectProperty` and defaults to None, read-only. """ viewport_size = ListProperty([0, 0]) """(internal) Size of the internal viewport. This is the size of your only child in the scrollview. """ scroll_type = OptionProperty(["content"], options=(["content"], ["bars"], ["bars", "content"], ["content", "bars"])) """Sets the type of scrolling to use for the content of the scrollview. Available options are: ['content'], ['bars'], ['bars', 'content']. .. versionadded:: 1.8.0 :attr:`scroll_type` is a :class:`~kivy.properties.OptionProperty`, defaults to ['content']. """ # private, for internal use only _viewport = ObjectProperty(None, allownone=True) bar_alpha = NumericProperty(1.0) def _set_viewport_size(self, instance, value): self.viewport_size = value def on__viewport(self, instance, value): if value: value.bind(size=self._set_viewport_size) self.viewport_size = value.size def __init__(self, **kwargs): self._touch = None self._trigger_update_from_scroll = Clock.create_trigger(self.update_from_scroll, -1) # create a specific canvas for the viewport from kivy.graphics import PushMatrix, Translate, PopMatrix, Canvas self.canvas_viewport = Canvas() self.canvas = Canvas() with self.canvas_viewport.before: PushMatrix() self.g_translate = Translate(0, 0) with self.canvas_viewport.after: PopMatrix() super(ScrollView, self).__init__(**kwargs) # now add the viewport canvas to our canvas self.canvas.add(self.canvas_viewport) effect_cls = self.effect_cls if isinstance(effect_cls, string_types): effect_cls = Factory.get(effect_cls) if self.effect_x is None and effect_cls is not None: self.effect_x = effect_cls(target_widget=self._viewport) if self.effect_y is None and effect_cls is not None: self.effect_y = effect_cls(target_widget=self._viewport) self.bind( width=self._update_effect_x_bounds, height=self._update_effect_y_bounds, viewport_size=self._update_effect_bounds, _viewport=self._update_effect_widget, scroll_x=self._trigger_update_from_scroll, scroll_y=self._trigger_update_from_scroll, pos=self._trigger_update_from_scroll, size=self._trigger_update_from_scroll, ) self._update_effect_widget() self._update_effect_x_bounds() self._update_effect_y_bounds() def on_effect_x(self, instance, value): if value: value.bind(scroll=self._update_effect_x) value.target_widget = self._viewport def on_effect_y(self, instance, value): if value: value.bind(scroll=self._update_effect_y) value.target_widget = self._viewport def on_effect_cls(self, instance, cls): if isinstance(cls, string_types): cls = Factory.get(cls) self.effect_x = cls(target_widget=self._viewport) self.effect_x.bind(scroll=self._update_effect_x) self.effect_y = cls(target_widget=self._viewport) self.effect_y.bind(scroll=self._update_effect_y) def _update_effect_widget(self, *args): if self.effect_x: self.effect_x.target_widget = self._viewport if self.effect_y: self.effect_y.target_widget = self._viewport def _update_effect_x_bounds(self, *args): if not self._viewport or not self.effect_x: return self.effect_x.min = -(self.viewport_size[0] - self.width) self.effect_x.max = 0 self.effect_x.value = self.effect_x.min * self.scroll_x def _update_effect_y_bounds(self, *args): if not self._viewport or not self.effect_y: return self.effect_y.min = -(self.viewport_size[1] - self.height) self.effect_y.max = 0 self.effect_y.value = self.effect_y.min * self.scroll_y def _update_effect_bounds(self, *args): if not self._viewport: return if self.effect_x: self._update_effect_x_bounds() if self.effect_y: self._update_effect_y_bounds() def _update_effect_x(self, *args): vp = self._viewport if not vp or not self.effect_x: return sw = vp.width - self.width if sw < 1: return sx = self.effect_x.scroll / float(sw) self.scroll_x = -sx self._trigger_update_from_scroll() def _update_effect_y(self, *args): vp = self._viewport if not vp or not self.effect_y: return sh = vp.height - self.height if sh < 1: return sy = self.effect_y.scroll / float(sh) self.scroll_y = -sy self._trigger_update_from_scroll() def to_local(self, x, y, **k): tx, ty = self.g_translate.xy return x - tx, y - ty def to_parent(self, x, y, **k): tx, ty = self.g_translate.xy return x + tx, y + ty def simulate_touch_down(self, touch): # at this point the touch is in parent coords touch.push() touch.apply_transform_2d(self.to_local) ret = super(ScrollView, self).on_touch_down(touch) touch.pop() return ret def on_touch_down(self, touch): if not self.collide_point(*touch.pos): touch.ud[self._get_uid("svavoid")] = True return if self.disabled: return True if self._touch or (not (self.do_scroll_x or self.do_scroll_y)): return self.simulate_touch_down(touch) # handle mouse scrolling, only if the viewport size is bigger than the # scrollview size, and if the user allowed to do it vp = self._viewport if not vp: return True scroll_type = self.scroll_type ud = touch.ud scroll_bar = "bars" in scroll_type # check if touch is in bar_x(horizontal) or bay_y(bertical) ud["in_bar_x"] = ud["in_bar_y"] = False width_scrollable = vp.width > self.width height_scrollable = vp.height > self.height bar_pos_x = self.bar_pos_x[0] bar_pos_y = self.bar_pos_y[0] d = { "b": True if touch.y < self.y + self.bar_width else False, "t": True if touch.y > self.top - self.bar_width else False, "l": True if touch.x < self.x + self.bar_width else False, "r": True if touch.x > self.right - self.bar_width else False, } if scroll_bar: if width_scrollable and d[bar_pos_x]: ud["in_bar_x"] = True if height_scrollable and d[bar_pos_y]: ud["in_bar_y"] = True if vp and "button" in touch.profile and touch.button.startswith("scroll"): btn = touch.button m = sp(self.scroll_wheel_distance) e = None if self.effect_x and self.do_scroll_y and height_scrollable and btn in ("scrolldown", "scrollup"): e = self.effect_x if ud["in_bar_x"] else self.effect_y elif self.effect_y and self.do_scroll_x and width_scrollable and btn in ("scrollleft", "scrollright"): e = self.effect_y if ud["in_bar_y"] else self.effect_x if e: if btn in ("scrolldown", "scrollleft"): e.value = max(e.value - m, e.min) e.velocity = 0 elif btn in ("scrollup", "scrollright"): e.value = min(e.value + m, e.max) e.velocity = 0 touch.ud[self._get_uid("svavoid")] = True e.trigger_velocity_update() return True # no mouse scrolling, so the user is going to drag the scrollview with # this touch. self._touch = touch uid = self._get_uid() touch.grab(self) ud[uid] = { "mode": "unknown", "dx": 0, "dy": 0, "user_stopped": False, "frames": Clock.frames, "time": touch.time_start, } if self.do_scroll_x and self.effect_x and not ud["in_bar_x"]: self.effect_x.start(touch.x) if self.do_scroll_y and self.effect_y and not ud["in_bar_y"]: self.effect_y.start(touch.y) if ud.get("in_bar_x", False) or ud.get("in_bar_y", False): return if scroll_type == ["bars"]: # touch is in parent, but _change_touch_mode expects window coords touch.push() touch.apply_transform_2d(self.to_local) touch.apply_transform_2d(self.to_window) self._change_touch_mode() touch.pop() return False else: Clock.schedule_once(self._change_touch_mode, self.scroll_timeout / 1000.0) return True def on_touch_move(self, touch): if self._get_uid("svavoid") in touch.ud: return if self._touch is not touch: # touch is in parent touch.push() touch.apply_transform_2d(self.to_local) super(ScrollView, self).on_touch_move(touch) touch.pop() return self._get_uid() in touch.ud if touch.grab_current is not self: return True uid = self._get_uid() ud = touch.ud[uid] mode = ud["mode"] # check if the minimum distance has been travelled if mode == "unknown" or mode == "scroll": if self.do_scroll_x and self.effect_x: width = self.width if touch.ud.get("in_bar_x", False): dx = touch.dx / float(width - width * self.hbar[1]) self.scroll_x = min(max(self.scroll_x + dx, 0.0), 1.0) self._trigger_update_from_scroll() else: if self.scroll_type != ["bars"]: self.effect_x.update(touch.x) if self.do_scroll_y and self.effect_y: height = self.height if touch.ud.get("in_bar_y", False): dy = touch.dy / float(height - height * self.vbar[1]) self.scroll_y = min(max(self.scroll_y + dy, 0.0), 1.0) self._trigger_update_from_scroll() else: if self.scroll_type != ["bars"]: self.effect_y.update(touch.y) if mode == "unknown": ud["dx"] += abs(touch.dx) ud["dy"] += abs(touch.dy) if ud["dx"] > self.scroll_distance: if not self.do_scroll_x: # touch is in parent, but _change expects window coords touch.push() touch.apply_transform_2d(self.to_local) touch.apply_transform_2d(self.to_window) self._change_touch_mode() touch.pop() return mode = "scroll" if ud["dy"] > self.scroll_distance: if not self.do_scroll_y: # touch is in parent, but _change expects window coords touch.push() touch.apply_transform_2d(self.to_local) touch.apply_transform_2d(self.to_window) self._change_touch_mode() touch.pop() return mode = "scroll" ud["mode"] = mode if mode == "scroll": ud["dt"] = touch.time_update - ud["time"] ud["time"] = touch.time_update ud["user_stopped"] = True return True def on_touch_up(self, touch): if self._get_uid("svavoid") in touch.ud: return if self in [x() for x in touch.grab_list]: touch.ungrab(self) self._touch = None uid = self._get_uid() ud = touch.ud[uid] if self.do_scroll_x and self.effect_x: if not touch.ud.get("in_bar_x", False) and self.scroll_type != ["bars"]: self.effect_x.stop(touch.x) if self.do_scroll_y and self.effect_y and self.scroll_type != ["bars"]: if not touch.ud.get("in_bar_y", False): self.effect_y.stop(touch.y) if ud["mode"] == "unknown": # we must do the click at least.. # only send the click if it was not a click to stop # autoscrolling if not ud["user_stopped"]: self.simulate_touch_down(touch) Clock.schedule_once(partial(self._do_touch_up, touch), 0.2) Clock.unschedule(self._update_effect_bounds) Clock.schedule_once(self._update_effect_bounds) else: if self._touch is not touch and self.uid not in touch.ud: # touch is in parents touch.push() touch.apply_transform_2d(self.to_local) super(ScrollView, self).on_touch_up(touch) touch.pop() # if we do mouse scrolling, always accept it if "button" in touch.profile and touch.button.startswith("scroll"): return True return self._get_uid() in touch.ud def convert_distance_to_scroll(self, dx, dy): """Convert a distance in pixels to a scroll distance, depending on the content size and the scrollview size. The result will be a tuple of scroll distance that can be added to :data:`scroll_x` and :data:`scroll_y` """ if not self._viewport: return 0, 0 vp = self._viewport if vp.width > self.width: sw = vp.width - self.width sx = dx / float(sw) else: sx = 0 if vp.height > self.height: sh = vp.height - self.height sy = dy / float(sh) else: sy = 1 return sx, sy def update_from_scroll(self, *largs): """Force the reposition of the content, according to current value of :attr:`scroll_x` and :attr:`scroll_y`. This method is automatically called when one of the :attr:`scroll_x`, :attr:`scroll_y`, :attr:`pos` or :attr:`size` properties change, or if the size of the content changes. """ if not self._viewport: return vp = self._viewport # update from size_hint if vp.size_hint_x is not None: vp.width = vp.size_hint_x * self.width if vp.size_hint_y is not None: vp.height = vp.size_hint_y * self.height if vp.width > self.width: sw = vp.width - self.width x = self.x - self.scroll_x * sw else: x = self.x if vp.height > self.height: sh = vp.height - self.height y = self.y - self.scroll_y * sh else: y = self.top - vp.height # from 1.8.0, we now use a matrix by default, instead of moving the # widget position behind. We set it here, but it will be a no-op most of # the time. vp.pos = 0, 0 self.g_translate.xy = x, y # new in 1.2.0, show bar when scrolling happen # and slowly remove them when no scroll is happening. self.bar_alpha = 1.0 Animation.stop_all(self, "bar_alpha") Clock.unschedule(self._start_decrease_alpha) Clock.schedule_once(self._start_decrease_alpha, 0.5) def _start_decrease_alpha(self, *l): self.bar_alpha = 1.0 # show bars if scroll_type != content bar_alpha = 0.2 if self.scroll_type != ["content"] else 0 Animation(bar_alpha=bar_alpha, d=0.5, t="out_quart").start(self) # # Private # def add_widget(self, widget, index=0): if self._viewport: raise Exception("ScrollView accept only one widget") canvas = self.canvas self.canvas = self.canvas_viewport super(ScrollView, self).add_widget(widget, index) self.canvas = canvas self._viewport = widget widget.bind(size=self._trigger_update_from_scroll) self._trigger_update_from_scroll() def remove_widget(self, widget): canvas = self.canvas self.canvas = self.canvas_viewport super(ScrollView, self).remove_widget(widget) self.canvas = canvas if widget is self._viewport: self._viewport = None def _get_uid(self, prefix="sv"): return "{0}.{1}".format(prefix, self.uid) def _change_touch_mode(self, *largs): if not self._touch: return uid = self._get_uid() touch = self._touch ud = touch.ud[uid] if ud["mode"] != "unknown" or ud["user_stopped"]: return diff_frames = Clock.frames - ud["frames"] # in order to be able to scroll on very slow devices, let at least 3 # frames displayed to accumulate some velocity. And then, change the # touch mode. Otherwise, we might never be able to compute velocity, and # no way to scroll it. See #1464 and #1499 if diff_frames < 3: Clock.schedule_once(self._change_touch_mode, 0) return if self.do_scroll_x and self.effect_x: self.effect_x.cancel() if self.do_scroll_y and self.effect_y: self.effect_y.cancel() # XXX the next line was in the condition. But this stop # the possibily to "drag" an object out of the scrollview in the # non-used direction: if you have an horizontal scrollview, a # vertical gesture will not "stop" the scroll view to look for an # horizontal gesture, until the timeout is done. # and touch.dx + touch.dy == 0: touch.ungrab(self) self._touch = None # touch is in window coords touch.push() touch.apply_transform_2d(self.to_widget) touch.apply_transform_2d(self.to_parent) self.simulate_touch_down(touch) touch.pop() return def _do_touch_up(self, touch, *largs): # touch is in window coords touch.push() touch.apply_transform_2d(self.to_widget) super(ScrollView, self).on_touch_up(touch) touch.pop() # don't forget about grab event! for x in touch.grab_list[:]: touch.grab_list.remove(x) x = x() if not x: continue touch.grab_current = x # touch is in window coords touch.push() touch.apply_transform_2d(self.to_widget) super(ScrollView, self).on_touch_up(touch) touch.pop() touch.grab_current = None
class Widget(WidgetBase): '''Widget class. See module documentation for more information. :Events: `on_touch_down`: Fired when a new touch event occurs `on_touch_move`: Fired when an existing touch moves `on_touch_up`: Fired when an existing touch disappears .. versionchanged:: 1.0.9 Everything related to event properties has been moved to the :class:`~kivy.event.EventDispatcher`. Event properties can now be used when contructing a simple class without subclassing :class:`Widget`. .. versionchanged:: 1.5.0 Constructor now accept on_* arguments to automatically bind callbacks to properties or events, as the Kv language. ''' __metaclass__ = WidgetMetaclass __events__ = ('on_touch_down', 'on_touch_move', 'on_touch_up') def __init__(self, **kwargs): # Before doing anything, ensure the windows exist. EventLoop.ensure_window() # assign the default context of the widget creation if not hasattr(self, '_context'): self._context = get_current_context() super(Widget, self).__init__(**kwargs) # Create the default canvas if not exist if self.canvas is None: self.canvas = Canvas(opacity=self.opacity) # Apply all the styles if '__no_builder' not in kwargs: #current_root = Builder.idmap.get('root') #Builder.idmap['root'] = self Builder.apply(self) #if current_root is not None: # Builder.idmap['root'] = current_root #else: # Builder.idmap.pop('root') # Bind all the events for argument in kwargs: if argument[:3] == 'on_': self.bind(**{argument: kwargs[argument]}) @property def proxy_ref(self): '''Return a proxy reference to the widget, i.e. without creating a reference to the widget. See `weakref.proxy <http://docs.python.org/2/library/weakref.html?highlight\ =proxy#weakref.proxy>`_ for more information. .. versionadded:: 1.7.2 ''' if hasattr(self, '_proxy_ref'): return self._proxy_ref f = partial(_widget_destructor, self.uid) self._proxy_ref = _proxy_ref = proxy(self, f) # only f should be enough here, but it appears that is a very # specific case, the proxy destructor is not called if both f and # _proxy_ref are not together in a tuple _widget_destructors[self.uid] = (f, _proxy_ref) return _proxy_ref def __eq__(self, other): if not isinstance(other, Widget): return False return self.proxy_ref is other.proxy_ref def __hash__(self): return id(self) @property def __self__(self): return self # # Collision # def collide_point(self, x, y): '''Check if a point (x, y) is inside the widget's axis aligned bounding box. :Parameters: `x`: numeric X position of the point (in window coordinates) `y`: numeric Y position of the point (in window coordinates) :Returns: bool, True if the point is inside the bounding box. >>> Widget(pos=(10, 10), size=(50, 50)).collide_point(40, 40) True ''' return self.x <= x <= self.right and self.y <= y <= self.top def collide_widget(self, wid): '''Check if the other widget collides with this widget. Performs an axis-aligned bounding box intersection test by default. :Parameters: `wid`: :class:`Widget` class Widget to collide with. :Returns: bool, True if the other widget collides with this widget. >>> wid = Widget(size=(50, 50)) >>> wid2 = Widget(size=(50, 50), pos=(25, 25)) >>> wid.collide_widget(wid2) True >>> wid2.pos = (55, 55) >>> wid.collide_widget(wid2) False ''' if self.right < wid.x: return False if self.x > wid.right: return False if self.top < wid.y: return False if self.y > wid.top: return False return True # # Default event handlers # def on_touch_down(self, touch): '''Receive a touch down event. :Parameters: `touch`: :class:`~kivy.input.motionevent.MotionEvent` class Touch received :Returns: bool. If True, the dispatching of the touch event will stop. ''' if self.disabled and self.collide_point(*touch.pos): return True for child in self.children[:]: if child.dispatch('on_touch_down', touch): return True def on_touch_move(self, touch): '''Receive a touch move event. See :meth:`on_touch_down` for more information. ''' if self.disabled: return for child in self.children[:]: if child.dispatch('on_touch_move', touch): return True def on_touch_up(self, touch): '''Receive a touch up event. See :meth:`on_touch_down` for more information. ''' if self.disabled: return for child in self.children[:]: if child.dispatch('on_touch_up', touch): return True def on_disabled(self, instance, value): for child in self.children: child.disabled = value # # Tree management # def add_widget(self, widget, index=0): '''Add a new widget as a child of this widget. :Parameters: `widget`: :class:`Widget` Widget to add to our list of children. `index`: int, defaults to 0 *(this attribute was added in 1.0.5)* Index to insert the widget in the list >>> root = Widget() >>> root.add_widget(Button()) >>> slider = Slider() >>> root.add_widget(slider) ''' if not isinstance(widget, Widget): raise WidgetException( 'add_widget() can be used only with Widget classes.') widget = widget.__self__ if widget is self: raise WidgetException('You cannot add yourself in a Widget') parent = widget.parent # check if widget is already a child of another widget if parent: raise WidgetException('Cannot add %r, it already has a parent %r' % (widget, parent)) widget.parent = parent = self # child will be disabled if added to a disabled parent if parent.disabled: widget.disabled = True if index == 0 or len(self.children) == 0: self.children.insert(0, widget) self.canvas.add(widget.canvas) else: canvas = self.canvas children = self.children if index >= len(children): index = len(children) next_index = 0 else: next_child = children[index] next_index = canvas.indexof(next_child.canvas) if next_index == -1: next_index = canvas.length() else: next_index += 1 children.insert(index, widget) # we never want to insert widget _before_ canvas.before. if next_index == 0 and canvas.has_before: next_index = 1 canvas.insert(next_index, widget.canvas) def remove_widget(self, widget): '''Remove a widget from the children of this widget. :Parameters: `widget`: :class:`Widget` Widget to remove from our children list. >>> root = Widget() >>> button = Button() >>> root.add_widget(button) >>> root.remove_widget(button) ''' if widget not in self.children: return parent = widget.parent self.children.remove(widget) self.canvas.remove(widget.canvas) widget.parent = None def clear_widgets(self, children=None): '''Remove all widgets added to this widget. .. versionchanged:: 1.8.0 `children` argument can be used to select the children we want to remove. It should be a list of children (or filtered list) of the current widget. ''' if not children: children = self.children remove_widget = self.remove_widget for child in children[:]: remove_widget(child) def get_root_window(self): '''Return the root window. :Returns: Instance of the root window. Can be a :class:`~kivy.core.window.WindowBase` or :class:`Widget`. ''' if self.parent: return self.parent.get_root_window() def get_parent_window(self): '''Return the parent window. :Returns: Instance of the parent window. Can be a :class:`~kivy.core.window.WindowBase` or :class:`Widget`. ''' if self.parent: return self.parent.get_parent_window() def to_widget(self, x, y, relative=False): '''Convert the given coordinate from window to local widget coordinates. ''' if self.parent: x, y = self.parent.to_widget(x, y) return self.to_local(x, y, relative=relative) def to_window(self, x, y, initial=True, relative=False): '''Transform local coordinates to window coordinates.''' if not initial: x, y = self.to_parent(x, y, relative=relative) if self.parent: return self.parent.to_window(x, y, initial=False, relative=relative) return (x, y) def to_parent(self, x, y, relative=False): '''Transform local coordinates to parent coordinates. :Parameters: `relative`: bool, defaults to False Change to True if you want to translate relative positions from a widget to its parent coordinates. ''' if relative: return (x + self.x, y + self.y) return (x, y) def to_local(self, x, y, relative=False): '''Transform parent coordinates to local coordinates. :Parameters: `relative`: bool, defaults to False Change to True if you want to translate coordinates to relative widget coordinates. ''' if relative: return (x - self.x, y - self.y) return (x, y) x = NumericProperty(0) '''X position of the widget. :data:`x` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' y = NumericProperty(0) '''Y position of the widget. :data:`y` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' width = NumericProperty(100) '''Width of the widget. :data:`width` is a :class:`~kivy.properties.NumericProperty` ans defaults to 100. .. warning:: Keep in mind that the `width` property is subject to layout logic and that this has not yet happened at the time of the widget's `__init__` method. ''' height = NumericProperty(100) '''Height of the widget. :data:`height` is a :class:`~kivy.properties.NumericProperty` and defaults to 100. .. warning:: Keep in mind that the `height` property is subject to layout logic and that this has not yet happened at the time of the widget's `__init__` method. ''' pos = ReferenceListProperty(x, y) '''Position of the widget. :data:`pos` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`x`, :data:`y`) properties. ''' size = ReferenceListProperty(width, height) '''Size of the widget. :data:`size` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`width`, :data:`height`) properties. ''' def get_right(self): return self.x + self.width def set_right(self, value): self.x = value - self.width right = AliasProperty(get_right, set_right, bind=('x', 'width')) '''Right position of the widget. :data:`right` is an :class:`~kivy.properties.AliasProperty` of (:data:`x` + :data:`width`), ''' def get_top(self): return self.y + self.height def set_top(self, value): self.y = value - self.height top = AliasProperty(get_top, set_top, bind=('y', 'height')) '''Top position of the widget. :data:`top` is an :class:`~kivy.properties.AliasProperty` of (:data:`y` + :data:`height`), ''' def get_center_x(self): return self.x + self.width / 2. def set_center_x(self, value): self.x = value - self.width / 2. center_x = AliasProperty(get_center_x, set_center_x, bind=('x', 'width')) '''X center position of the widget. :data:`center_x` is an :class:`~kivy.properties.AliasProperty` of (:data:`x` + :data:`width` / 2.), ''' def get_center_y(self): return self.y + self.height / 2. def set_center_y(self, value): self.y = value - self.height / 2. center_y = AliasProperty(get_center_y, set_center_y, bind=('y', 'height')) '''Y center position of the widget. :data:`center_y` is an :class:`~kivy.properties.AliasProperty` of (:data:`y` + :data:`height` / 2.) ''' center = ReferenceListProperty(center_x, center_y) '''Center position of the widget. :data:`center` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`center_x`, :data:`center_y`) ''' cls = ListProperty([]) '''Class of the widget, used for styling. ''' id = StringProperty(None, allownone=True) '''Unique identifier of the widget in the tree. :data:`id` is a :class:`~kivy.properties.StringProperty` and defaults to None. .. warning:: If the :data:`id` is already used in the tree, an exception will be raised. ''' children = ListProperty([]) '''List of children of this widget. :data:`children` is a :class:`~kivy.properties.ListProperty` and defaults to an empty list. Use :meth:`add_widget` and :meth:`remove_widget` for manipulating the children list. Don't manipulate the children list directly unless you know what you are doing. ''' parent = ObjectProperty(None, allownone=True) '''Parent of this widget. :data:`parent` is an :class:`~kivy.properties.ObjectProperty` and defaults to None. The parent of a widget is set when the widget is added to another widget and unset when the widget is removed from its parent. ''' size_hint_x = NumericProperty(1, allownone=True) '''X size hint. Represents how much space the widget should use in the direction of the X axis relative to its parent's width. Only the :class:`~kivy.uix.layout.Layout` and :class:`~kivy.core.window.Window` classes make use of the hint. The value is in percent as a float from 0. to 1., where 1. means the full size of his parent. 0.5 represents 50%. :data:`size_hint_x` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. ''' size_hint_y = NumericProperty(1, allownone=True) '''Y size hint. :data:`size_hint_y` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. See :data:`size_hint_x` for more information ''' size_hint = ReferenceListProperty(size_hint_x, size_hint_y) '''Size hint. :data:`size_hint` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`size_hint_x`, :data:`size_hint_y`). See :data:`size_hint_x` for more information ''' pos_hint = ObjectProperty({}) '''Position hint. This property allows you to set the position of the widget inside its parent layout, in percent (similar to size_hint). For example, if you want to set the top of the widget to be at 90% height of its parent layout, you can write: widget = Widget(pos_hint={'top': 0.9}) The keys 'x', 'right' and 'center_x' will use the parent width. The keys 'y', 'top' and 'center_y' will use the parent height. See :doc:`api-kivy.uix.floatlayout` for further reference. Position hint is only used by the :class:`~kivy.uix.floatlayout.FloatLayout` and :class:`~kivy.core.window.Window`. :data:`pos_hint` is an :class:`~kivy.properties.ObjectProperty` containing a dict. ''' ids = DictProperty({}) '''This is a Dictionary of id's defined in your kv language. This will only be populated if you use id's in your kv language code. .. versionadded:: 1.7.0 :data:`ids` is a :class:`~kivy.properties.DictProperty` and defaults to a empty dict {}. ''' opacity = NumericProperty(1.0) '''Opacity of the widget and all the children. .. versionadded:: 1.4.1 The opacity attribute controls the opacity of the widget and its children. Be careful, it's a cumulative attribute: the value is multiplied by the current global opacity and the result is applied to the current context color. For example, if the parent has an opacity of 0.5 and a child has an opacity of 0.2, the real opacity of the child will be 0.5 * 0.2 = 0.1. Then, the opacity is applied by the shader as:: frag_color = color * vec4(1.0, 1.0, 1.0, opacity); :data:`opacity` is a :class:`~kivy.properties.NumericProperty` and defaults to 1.0. ''' def on_opacity(self, instance, value): canvas = self.canvas if canvas is not None: canvas.opacity = value canvas = None '''Canvas of the widget. The canvas is a graphics object that contains all the drawing instructions for the graphical representation of the widget. There are no general properties for the Widget class, such as background color, to keep the design simple and lean. Some derived classes, such as Button, do add such convenience properties but generally the developer is responsible for implementing the graphics representation for a custom widget from the ground up. See the derived widget classes for patterns to follow and extend. See :class:`~kivy.graphics.Canvas` for more information about the usage. ''' disabled = BooleanProperty(False) '''Indicates whether this widget can interact with input or not.
class GeoJsonMapLayer(MapLayer): source = StringProperty() geojson = ObjectProperty() cache_dir = StringProperty(CACHE_DIR) def __init__(self, **kwargs): self.first_time = True self.initial_zoom = None super(GeoJsonMapLayer, self).__init__(**kwargs) with self.canvas: self.canvas_polygon = Canvas() with self.canvas_polygon.before: PushMatrix() self.g_matrix = MatrixInstruction() self.g_scale = Scale() self.g_translate = Translate() with self.canvas_polygon: self.g_canvas_polygon = Canvas() with self.canvas_polygon.after: PopMatrix() def reposition(self): vx, vy = self.parent.delta_x, self.parent.delta_y pzoom = self.parent.zoom zoom = self.initial_zoom if zoom is None: self.initial_zoom = zoom = pzoom if zoom != pzoom: diff = 2**(pzoom - zoom) vx /= diff vy /= diff self.g_scale.x = self.g_scale.y = diff else: self.g_scale.x = self.g_scale.y = 1. self.g_translate.xy = vx, vy self.g_matrix.matrix = self.parent._scatter.transform if self.geojson: update = not self.first_time self.on_geojson(self, self.geojson, update=update) self.first_time = False def traverse_feature(self, func, part=None): """Traverse the whole geojson and call the func with every element found. """ if part is None: part = self.geojson if not part: return tp = part["type"] if tp == "FeatureCollection": for feature in part["features"]: func(feature) elif tp == "Feature": func(part) @property def bounds(self): # return the min lon, max lon, min lat, max lat bounds = [float("inf"), float("-inf"), float("inf"), float("-inf")] def _submit_coordinate(coord): lon, lat = coord bounds[0] = min(bounds[0], lon) bounds[1] = max(bounds[1], lon) bounds[2] = min(bounds[2], lat) bounds[3] = max(bounds[3], lat) def _get_bounds(feature): geometry = feature["geometry"] tp = geometry["type"] if tp == "Point": _submit_coordinate(geometry["coordinates"]) elif tp == "Polygon": for coordinate in geometry["coordinates"][0]: _submit_coordinate(coordinate) elif tp == "MultiPolygon": for polygon in geometry["coordinates"]: for coordinate in polygon[0]: _submit_coordinate(coordinate) self.traverse_feature(_get_bounds) return bounds @property def center(self): min_lon, max_lon, min_lat, max_lat = self.bounds cx = (max_lon - min_lon) / 2. cy = (max_lat - min_lat) / 2. return min_lon + cx, min_lat + cy def on_geojson(self, instance, geojson, update=False): if self.parent is None: return if not update: # print "Reload geojson (polygon)" self.g_canvas_polygon.clear() self._geojson_part(geojson, geotype="Polygon") # print "Reload geojson (LineString)" self._geojson_part(geojson, geotype="LineString") def on_source(self, instance, value): if value.startswith("http://") or value.startswith("https://"): Downloader.instance(cache_dir=self.cache_dir).download( value, self._load_geojson_url) else: with open(value, "rb") as fd: geojson = json.load(fd) self.geojson = geojson def _load_geojson_url(self, url, r): self.geojson = r.json() def _geojson_part(self, part, geotype=None): tp = part["type"] if tp == "FeatureCollection": for feature in part["features"]: if geotype and feature["geometry"]["type"] != geotype: continue self._geojson_part_f(feature) elif tp == "Feature": if geotype and part["geometry"]["type"] == geotype: self._geojson_part_f(part) else: # unhandled geojson part pass def _geojson_part_f(self, feature): properties = feature["properties"] geometry = feature["geometry"] graphics = self._geojson_part_geometry(geometry, properties) for g in graphics: self.g_canvas_polygon.add(g) def _geojson_part_geometry(self, geometry, properties): tp = geometry["type"] graphics = [] if tp == "Polygon": tess = Tesselator() for c in geometry["coordinates"]: xy = list(self._lonlat_to_xy(c)) xy = flatten(xy) tess.add_contour(xy) tess.tesselate(WINDING_ODD, TYPE_POLYGONS) color = self._get_color_from( properties.get("style").get("stroke", "FF000088")) graphics.append(Color(*color)) for vertices, indices in tess.meshes: graphics.append( Mesh(vertices=vertices, indices=indices, mode="triangle_fan")) elif tp == "LineString": stroke = get_color_from_hex( properties.get("style").get("stroke", "#ffffff")) print "stroke: " + ` stroke ` #storke = [0.0, 0.0, 0.0, 1] print 'properties.get("width") :' + ` properties.get("style").get( "width") ` stroke_width = dp(properties.get("style").get("width")) print "stroke_width: " + ` stroke_width ` xy = list(self._lonlat_to_xy(geometry["coordinates"])) xy = flatten(xy) graphics.append(Color(*stroke)) graphics.append(Line(points=xy, width=stroke_width)) return graphics def _lonlat_to_xy(self, lonlats): view = self.parent zoom = view.zoom for lon, lat in lonlats: p = view.get_window_xy_from(lat, lon, zoom) p = p[0] - self.parent.delta_x, p[1] - self.parent.delta_y p = self.parent._scatter.to_local(*p) yield p def _get_color_from(self, value): color = COLORS.get(value.lower(), value) color = get_color_from_hex(color) return color
class Widget(WidgetBase): '''Widget class. See module documentation for more information. :Events: `on_touch_down`: Fired when a new touch happens `on_touch_move`: Fired when an existing touch is moved `on_touch_up`: Fired when an existing touch disappears .. versionchanged:: 1.0.9 Everything related to event properties has been moved to :class:`~kivy.event.EventDispatcher`. Event properties can now be used in contructing a simple class, without subclassing :class:`Widget`. .. versionchanged:: 1.5.0 Constructor now accept on_* arguments to automatically bind callbacks to properties or events, as the Kv language. ''' __metaclass__ = WidgetMetaclass __events__ = ('on_touch_down', 'on_touch_move', 'on_touch_up') def __init__(self, **kwargs): # Before doing anything, ensure the windows exist. EventLoop.ensure_window() # assign the default context of the widget creation if not hasattr(self, '_context'): self._context = get_current_context() super(Widget, self).__init__(**kwargs) # Create the default canvas if not exist if self.canvas is None: self.canvas = Canvas(opacity=self.opacity) # Apply all the styles if '__no_builder' not in kwargs: #current_root = Builder.idmap.get('root') #Builder.idmap['root'] = self Builder.apply(self) #if current_root is not None: # Builder.idmap['root'] = current_root #else: # Builder.idmap.pop('root') # Bind all the events for argument in kwargs: if argument[:3] == 'on_': self.bind(**{argument: kwargs[argument]}) @property def proxy_ref(self): '''Return a proxy reference to the widget, ie, without taking a reference of the widget. See `weakref.proxy <http://docs.python.org/2/library/weakref.html?highlight\ =proxy#weakref.proxy>`_ for more information about it. .. versionadded:: 1.7.2 ''' if hasattr(self, '_proxy_ref'): return self._proxy_ref f = partial(_widget_destructor, self.uid) self._proxy_ref = _proxy_ref = proxy(self, f) # only f should be enough here, but it appears that is a very # specific case, the proxy destructor is not called if both f and # _proxy_ref are not together in a tuple _widget_destructors[self.uid] = (f, _proxy_ref) return _proxy_ref def __eq__(self, other): if not isinstance(other, Widget): return False return self.proxy_ref is other.proxy_ref def __hash__(self): return id(self) @property def __self__(self): return self # # Collision # def collide_point(self, x, y): '''Check if a point (x, y) is inside the widget's axis aligned bounding box. :Parameters: `x`: numeric X position of the point (in window coordinates) `y`: numeric Y position of the point (in window coordinates) :Returns: bool, True if the point is inside the bounding box. >>> Widget(pos=(10, 10), size=(50, 50)).collide_point(40, 40) True ''' return self.x <= x <= self.right and self.y <= y <= self.top def collide_widget(self, wid): '''Check if the other widget collides with this widget. Performs an axis-aligned bounding box intersection test by default. :Parameters: `wid`: :class:`Widget` class Widget to collide with. :Returns: bool, True if the other widget collides with this widget. >>> wid = Widget(size=(50, 50)) >>> wid2 = Widget(size=(50, 50), pos=(25, 25)) >>> wid.collide_widget(wid2) True >>> wid2.pos = (55, 55) >>> wid.collide_widget(wid2) False ''' if self.right < wid.x: return False if self.x > wid.right: return False if self.top < wid.y: return False if self.y > wid.top: return False return True # # Default event handlers # def on_touch_down(self, touch): '''Receive a touch down event. :Parameters: `touch`: :class:`~kivy.input.motionevent.MotionEvent` class Touch received :Returns: bool. If True, the dispatching of the touch will stop. ''' if self.disabled and self.collide_point(*touch.pos): return True for child in self.children[:]: if child.dispatch('on_touch_down', touch): return True def on_touch_move(self, touch): '''Receive a touch move event. See :meth:`on_touch_down` for more information ''' if self.disabled: return for child in self.children[:]: if child.dispatch('on_touch_move', touch): return True def on_touch_up(self, touch): '''Receive a touch up event. See :meth:`on_touch_down` for more information ''' if self.disabled: return for child in self.children[:]: if child.dispatch('on_touch_up', touch): return True def on_disabled(self, instance, value): for child in self.children: child.disabled = value # # Tree management # def add_widget(self, widget, index=0): '''Add a new widget as a child of this widget. :Parameters: `widget`: :class:`Widget` Widget to add to our list of children. `index`: int, default to 0 *(this attribute have been added in 1.0.5)* Index to insert the widget in the list >>> root = Widget() >>> root.add_widget(Button()) >>> slider = Slider() >>> root.add_widget(slider) ''' if not isinstance(widget, Widget): raise WidgetException( 'add_widget() can be used only with Widget classes.') widget = widget.__self__ if widget is self: raise WidgetException('You cannot add yourself in a Widget') parent = widget.parent # check if widget is already a child of another widget if parent: raise WidgetException('Cannot add %r, it already has a parent %r' % (widget, parent)) widget.parent = parent = self # child will be disabled if added to a disabled parent if parent.disabled: widget.disabled = True if index == 0 or len(self.children) == 0: self.children.insert(0, widget) self.canvas.add(widget.canvas) else: canvas = self.canvas children = self.children if index >= len(children): index = len(children) next_index = 0 else: next_child = children[index] next_index = canvas.indexof(next_child.canvas) if next_index == -1: next_index = canvas.length() else: next_index += 1 children.insert(index, widget) # we never want to insert widget _before_ canvas.before. if next_index == 0 and canvas.has_before: next_index = 1 canvas.insert(next_index, widget.canvas) def remove_widget(self, widget): '''Remove a widget from the children of this widget. :Parameters: `widget`: :class:`Widget` Widget to remove from our children list. >>> root = Widget() >>> button = Button() >>> root.add_widget(button) >>> root.remove_widget(button) ''' if widget not in self.children: return parent = widget.parent self.children.remove(widget) self.canvas.remove(widget.canvas) widget.parent = None def clear_widgets(self, children=None): '''Remove all widgets added to this widget. .. versionchanged:: 1.8.0 `children` argument can be used to select the children we want to remove. It should be a children list (or filtered list) of the current widget. ''' if not children: children = self.children remove_widget = self.remove_widget for child in children[:]: remove_widget(child) def get_root_window(self): '''Return the root window. :Returns: Instance of the root window. Can be :class:`~kivy.core.window.WindowBase` or :class:`Widget` ''' if self.parent: return self.parent.get_root_window() def get_parent_window(self): '''Return the parent window. :Returns: Instance of the parent window. Can be :class:`~kivy.core.window.WindowBase` or :class:`Widget` ''' if self.parent: return self.parent.get_parent_window() def to_widget(self, x, y, relative=False): '''Convert the given coordinate from window to local widget coordinates. ''' if self.parent: x, y = self.parent.to_widget(x, y) return self.to_local(x, y, relative=relative) def to_window(self, x, y, initial=True, relative=False): '''Transform local coordinates to window coordinates.''' if not initial: x, y = self.to_parent(x, y, relative=relative) if self.parent: return self.parent.to_window(x, y, initial=False, relative=relative) return (x, y) def to_parent(self, x, y, relative=False): '''Transform local coordinates to parent coordinates. :Parameters: `relative`: bool, default to False Change to True if you want to translate relative positions from widget to its parent. ''' if relative: return (x + self.x, y + self.y) return (x, y) def to_local(self, x, y, relative=False): '''Transform parent coordinates to local coordinates. :Parameters: `relative`: bool, default to False Change to True if you want to translate coordinates to relative widget coordinates. ''' if relative: return (x - self.x, y - self.y) return (x, y) x = NumericProperty(0) '''X position of the widget. :data:`x` is a :class:`~kivy.properties.NumericProperty`, default to 0. ''' y = NumericProperty(0) '''Y position of the widget. :data:`y` is a :class:`~kivy.properties.NumericProperty`, default to 0. ''' width = NumericProperty(100) '''Width of the widget. :data:`width` is a :class:`~kivy.properties.NumericProperty`, default to 100. .. warning:: Keep in mind that the `width` property is subject to layout logic, and that this has not happened yet at the time of the widget's `__init__` method. ''' height = NumericProperty(100) '''Height of the widget. :data:`height` is a :class:`~kivy.properties.NumericProperty`, default to 100. .. warning:: Keep in mind that the `height` property is subject to layout logic, and that this has not happened yet at the time of the widget's `__init__` method. ''' pos = ReferenceListProperty(x, y) '''Position of the widget. :data:`pos` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`x`, :data:`y`) properties. ''' size = ReferenceListProperty(width, height) '''Size of the widget. :data:`size` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`width`, :data:`height`) properties. ''' def get_right(self): return self.x + self.width def set_right(self, value): self.x = value - self.width right = AliasProperty(get_right, set_right, bind=('x', 'width')) '''Right position of the widget. :data:`right` is a :class:`~kivy.properties.AliasProperty` of (:data:`x` + :data:`width`) ''' def get_top(self): return self.y + self.height def set_top(self, value): self.y = value - self.height top = AliasProperty(get_top, set_top, bind=('y', 'height')) '''Top position of the widget. :data:`top` is a :class:`~kivy.properties.AliasProperty` of (:data:`y` + :data:`height`) ''' def get_center_x(self): return self.x + self.width / 2. def set_center_x(self, value): self.x = value - self.width / 2. center_x = AliasProperty(get_center_x, set_center_x, bind=('x', 'width')) '''X center position of the widget. :data:`center_x` is a :class:`~kivy.properties.AliasProperty` of (:data:`x` + :data:`width` / 2.) ''' def get_center_y(self): return self.y + self.height / 2. def set_center_y(self, value): self.y = value - self.height / 2. center_y = AliasProperty(get_center_y, set_center_y, bind=('y', 'height')) '''Y center position of the widget. :data:`center_y` is a :class:`~kivy.properties.AliasProperty` of (:data:`y` + :data:`height` / 2.) ''' center = ReferenceListProperty(center_x, center_y) '''Center position of the widget. :data:`center` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`center_x`, :data:`center_y`) ''' cls = ListProperty([]) '''Class of the widget, used for styling. ''' id = StringProperty(None, allownone=True) '''Unique identifier of the widget in the tree. :data:`id` is a :class:`~kivy.properties.StringProperty`, default to None. .. warning:: If the :data:`id` is already used in the tree, an exception will be raised. ''' children = ListProperty([]) '''List of children of this widget. :data:`children` is a :class:`~kivy.properties.ListProperty` instance, default to an empty list. Use :meth:`add_widget` and :meth:`remove_widget` for manipulating the children list. Don't manipulate the children list directly until you know what you are doing. ''' parent = ObjectProperty(None, allownone=True) '''Parent of this widget. :data:`parent` is a :class:`~kivy.properties.ObjectProperty` instance, default to None. The parent of a widget is set when the widget is added to another one, and unset when the widget is removed from its parent. ''' size_hint_x = NumericProperty(1, allownone=True) '''X size hint. Represents how much space the widget should use in the direction of the X axis, relative to its parent's width. Only :class:`~kivy.uix.layout.Layout` and :class:`~kivy.core.window.Window` make use of the hint. The value is in percent as a float from 0. to 1., where 1. means the full size of his parent. 0.5 represents 50%. :data:`size_hint_x` is a :class:`~kivy.properties.NumericProperty`, default to 1. ''' size_hint_y = NumericProperty(1, allownone=True) '''Y size hint. :data:`size_hint_y` is a :class:`~kivy.properties.NumericProperty`, default to 1. See :data:`size_hint_x` for more information ''' size_hint = ReferenceListProperty(size_hint_x, size_hint_y) '''Size hint. :data:`size_hint` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`size_hint_x`, :data:`size_hint_y`) See :data:`size_hint_x` for more information ''' pos_hint = ObjectProperty({}) '''Position hint. This property allows you to set the position of the widget inside its parent layout, in percent (similar to size_hint). For example, if you want to set the top of the widget to be at 90% height of its parent layout, you can write: widget = Widget(pos_hint={'top': 0.9}) The keys 'x', 'right', 'center_x', will use the parent width. The keys 'y', 'top', 'center_y', will use the parent height. See :doc:`api-kivy.uix.floatlayout` for further reference. Position hint is only used in :class:`~kivy.uix.floatlayout.FloatLayout` and :class:`~kivy.core.window.Window`. :data:`pos_hint` is a :class:`~kivy.properties.ObjectProperty` containing a dict. ''' ids = DictProperty({}) '''This is a Dictionary of id's defined in your kv language. This will only be populated if you use id's in your kv language code. .. versionadded:: 1.7.0 :data:`ids` is a :class:`~kivy.properties.DictProperty`, defaults to a empty dict {}. ''' opacity = NumericProperty(1.0) '''Opacity of the widget and all the children. .. versionadded:: 1.4.1 The opacity attribute controls the opacity of the widget and its children. Be careful, it's a cumulative attribute: the value is multiplied to the current global opacity, and the result is applied to the current context color. For example: if your parent have an opacity of 0.5, and one children have an opacity of 0.2, the real opacity of the children will be 0.5 * 0.2 = 0.1. Then, the opacity is applied on the shader as:: frag_color = color * vec4(1.0, 1.0, 1.0, opacity); :data:`opacity` is a :class:`~kivy.properties.NumericProperty`, default to 1.0. ''' def on_opacity(self, instance, value): canvas = self.canvas if canvas is not None: canvas.opacity = value canvas = None '''Canvas of the widget. The canvas is a graphics object that contains all the drawing instructions for the graphical representation of the widget. There are no general properties for the Widget class, such as background color, to keep the design simple and lean. Some derived classes, such as Button, do add such convenience properties, but generally the developer is responsible for implementing the graphics representation for a custom widget from the ground up. See the derived widget classes for patterns to follow and extend. See :class:`~kivy.graphics.Canvas` for more information about the usage. ''' disabled = BooleanProperty(False) '''Indicates whether this widget can interact with input or not.
class WindowBase(EventDispatcher): '''WindowBase is a abstract window widget, for any window implementation. :Parameters: `fullscreen`: str, one of ('0', '1', 'auto', 'fake') Make window as fullscreen, check config documentation for more explaination about the values. `width`: int Width of window `height`: int Height of window :Events: `on_motion`: etype, motionevent Fired when a new :class:`~kivy.input.motionevent.MotionEvent` is dispatched `on_touch_down`: Fired when a new touch appear `on_touch_move`: Fired when an existing touch is moved `on_touch_up`: Fired when an existing touch disapear `on_draw`: Fired when the :class:`Window` is beeing drawed `on_flip`: Fired when the :class:`Window` GL surface is beeing flipped `on_rotate`: rotation Fired when the :class:`Window` is beeing rotated `on_close`: Fired when the :class:`Window` is closed `on_keyboard`: key, scancode, unicode, modifier Fired when the keyboard is in action `on_key_down`: key, scancode, unicode Fired when a key is down `on_key_up`: key, scancode, unicode Fired when a key is up ''' __instance = None __initialized = False def __new__(cls, **kwargs): if cls.__instance is None: cls.__instance = EventDispatcher.__new__(cls) return cls.__instance def __init__(self, **kwargs): kwargs.setdefault('force', False) kwargs.setdefault('config', None) # don't init window 2 times, # except if force is specified if self.__initialized and not kwargs.get('force'): return # event subsystem self.register_event_type('on_draw') self.register_event_type('on_flip') self.register_event_type('on_rotate') self.register_event_type('on_resize') self.register_event_type('on_close') self.register_event_type('on_motion') self.register_event_type('on_touch_down') self.register_event_type('on_touch_move') self.register_event_type('on_touch_up') self.register_event_type('on_mouse_down') self.register_event_type('on_mouse_move') self.register_event_type('on_mouse_up') self.register_event_type('on_keyboard') self.register_event_type('on_key_down') self.register_event_type('on_key_up') super(WindowBase, self).__init__() # init privates self._system_keyboard = Keyboard(window=self) self._keyboards = {'system': self._system_keyboard} self._modifiers = [] self._size = (0, 0) self._rotation = 0 self._clearcolor = [0, 0, 0, 0] self._vkeyboard_cls = None self.children = [] self.parent = self # add view if 'view' in kwargs: self.add_widget(kwargs.get('view')) # get window params, user options before config option params = {} if 'fullscreen' in kwargs: params['fullscreen'] = kwargs.get('fullscreen') else: params['fullscreen'] = Config.get('graphics', 'fullscreen') if params['fullscreen'] not in ('auto', 'fake'): params['fullscreen'] = params['fullscreen'].lower() in \ ('true', '1', 'yes', 'yup') if 'width' in kwargs: params['width'] = kwargs.get('width') else: params['width'] = Config.getint('graphics', 'width') if 'height' in kwargs: params['height'] = kwargs.get('height') else: params['height'] = Config.getint('graphics', 'height') if 'rotation' in kwargs: params['rotation'] = kwargs.get('rotation') else: params['rotation'] = Config.getint('graphics', 'rotation') params['position'] = Config.get( 'graphics', 'position', 'auto') if 'top' in kwargs: params['position'] = 'custom' params['top'] = kwargs.get('top') else: params['top'] = Config.getint('graphics', 'top') if 'left' in kwargs: params['position'] = 'custom' params['left'] = kwargs.get('left') else: params['left'] = Config.getint('graphics', 'left') # before creating the window import kivy.core.gl # configure the window self.params = params self.create_window() # attach modules + listener event Modules.register_window(self) EventLoop.set_window(self) EventLoop.add_event_listener(self) # manage keyboard(s) self.configure_keyboards() # mark as initialized self.__initialized = True def toggle_fullscreen(self): '''Toggle fullscreen on window''' pass def close(self): '''Close the window''' pass def create_window(self): '''Will create the main window and configure it. .. warning:: This method is called automatically at runtime. If you call it, it will recreate a RenderContext and Canvas. This mean you'll have a new graphics tree, and the old one will be unusable. This method exist to permit the creation of a new OpenGL context AFTER closing the first one. (Like using runTouchApp() and stopTouchApp()). This method have been only tested in unittest environment, and will be not suitable for Applications. Again, don't use this method unless you know exactly what you are doing ! ''' from kivy.core.gl import init_gl init_gl() # create the render context and canvas from kivy.graphics import RenderContext, Canvas self.render_context = RenderContext() self.canvas = Canvas() self.render_context.add(self.canvas) def on_flip(self): '''Flip between buffers (event)''' self.flip() def flip(self): '''Flip between buffers''' pass def _get_modifiers(self): return self._modifiers modifiers = property(_get_modifiers) def _get_size(self): r = self._rotation w, h = self._size if r == 0 or r == 180: return w, h return h, w def _set_size(self, size): if super(WindowBase, self)._set_size(size): Logger.debug('Window: Resize window to %s' % str(self.size)) self.dispatch('on_resize', *size) return True return False size = property(_get_size, _set_size, doc='''Rotated size of the window''') def _get_clearcolor(self): return self._clearcolor def _set_clearcolor(self, value): if value is not None: if type(value) not in (list, tuple): raise Exception('Clearcolor must be a list or tuple') if len(value) != 4: raise Exception('Clearcolor must contain 4 values') self._clearcolor = value clearcolor = property(_get_clearcolor, _set_clearcolor, doc=''' Color used to clear window:: from kivy.core.window import Window # red background color Window.clearcolor = (1, 0, 0, 1) # don't clear background at all Window.clearcolor = None ''') # make some property read-only @property def width(self): '''Rotated window width''' r = self._rotation if r == 0 or r == 180: return self._size[0] return self._size[1] @property def height(self): '''Rotated window height''' r = self._rotation if r == 0 or r == 180: return self._size[1] return self._size[0] @property def center(self): '''Rotated window center''' return self.width / 2., self.height / 2. def _update_childsize(self, instance, value): self.update_childsize([instance]) def add_widget(self, widget): '''Add a widget on window''' widget.parent = self self.children.insert(0, widget) self.canvas.add(widget.canvas) self.update_childsize([widget]) widget.bind( pos_hint=self._update_childsize, size_hint=self._update_childsize, size=self._update_childsize, pos=self._update_childsize) def remove_widget(self, widget): '''Remove a widget from window ''' if not widget in self.children: return self.children.remove(widget) self.canvas.remove(widget.canvas) widget.parent = None widget.unbind( pos_hint=self._update_childsize, size_hint=self._update_childsize, size=self._update_childsize, pos=self._update_childsize) def clear(self): '''Clear the window with background color''' # XXX FIXME use late binding from kivy.graphics.opengl import glClearColor, glClear, \ GL_COLOR_BUFFER_BIT, GL_DEPTH_BUFFER_BIT cc = self._clearcolor if cc is not None: glClearColor(*cc) glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) def set_title(self, title): '''Set the window title. .. versionadded:: 1.0.5 ''' pass def set_icon(self, filename): '''Set the icon of the window .. versionadded:: 1.0.5 ''' pass def to_widget(self, x, y, initial=True, relative=False): return (x, y) def to_window(self, x, y, initial=True, relative=False): return (x, y) def get_root_window(self): return self def get_parent_window(self): return self def get_parent_layout(self): return None def on_draw(self): self.clear() self.render_context.draw() def on_motion(self, etype, me): '''Event called when a Motion Event is received. :Parameters: `etype`: str One of 'begin', 'update', 'end' `me`: :class:`~kivy.input.motionevent.MotionEvent` Motion Event currently dispatched ''' if me.is_touch: if etype == 'begin': self.dispatch('on_touch_down', me) elif etype == 'update': self.dispatch('on_touch_move', me) elif etype == 'end': self.dispatch('on_touch_up', me) def on_touch_down(self, touch): '''Event called when a touch is down ''' w, h = self.system_size touch.scale_for_screen(w, h, rotation=self._rotation) for w in self.children[:]: if w.dispatch('on_touch_down', touch): return True def on_touch_move(self, touch): '''Event called when a touch move ''' w, h = self.system_size touch.scale_for_screen(w, h, rotation=self._rotation) for w in self.children[:]: if w.dispatch('on_touch_move', touch): return True def on_touch_up(self, touch): '''Event called when a touch up ''' w, h = self.system_size touch.scale_for_screen(w, h, rotation=self._rotation) for w in self.children[:]: if w.dispatch('on_touch_up', touch): return True def on_resize(self, width, height): '''Event called when the window is resized''' self.update_viewport() def update_viewport(self): from kivy.graphics.opengl import glViewport from kivy.graphics.transformation import Matrix from math import radians w, h = self.system_size w2, h2 = w / 2., h / 2. r = radians(self.rotation) # prepare the viewport glViewport(0, 0, w, h) # do projection matrix projection_mat = Matrix() projection_mat.view_clip(0.0, w, 0.0, h, -1.0, 1.0, 0) self.render_context['projection_mat'] = projection_mat # do modelview matrix modelview_mat = Matrix().translate(w2, h2, 0) modelview_mat = modelview_mat.multiply(Matrix().rotate(r, 0, 0, 1)) w, h = self.size w2, h2 = w / 2., h / 2. modelview_mat = modelview_mat.multiply(Matrix().translate(-w2, -h2, 0)) self.render_context['modelview_mat'] = modelview_mat # redraw canvas self.canvas.ask_update() # and update childs self.update_childsize() def update_childsize(self, childs=None): width, height = self.size if childs is None: childs = self.children for w in childs: shw, shh = w.size_hint if shw and shh: w.size = shw * width, shh * height elif shw: w.width = shw * width elif shh: w.height = shh * height for key, value in w.pos_hint.iteritems(): if key == 'x': w.x = value * width elif key == 'right': w.right = value * width elif key == 'y': w.y = value * height elif key == 'top': w.top = value * height elif key == 'center_x': w.center_x = value * width elif key == 'center_y': w.center_y = value * height def _get_rotation(self): return self._rotation def _set_rotation(self, x): x = int(x % 360) if x == self._rotation: return if x not in (0, 90, 180, 270): raise ValueError('can rotate only 0,90,180,270 degrees') self._rotation = x self.dispatch('on_resize', *self.size) self.dispatch('on_rotate', x) rotation = property(_get_rotation, _set_rotation, 'Get/set the window content rotation. Can be one of ' '0, 90, 180, 270 degrees.') @property def system_size(self): '''Real size of the window, without taking care of the rotation ''' return self._size def screenshot(self, name='screenshot%(counter)04d.jpg'): '''Save the actual displayed image in a file ''' i = 0 path = None while True: i += 1 path = join(getcwd(), name % {'counter': i}) if not exists(path): break return path def on_rotate(self, rotation): '''Event called when the screen have been rotated ''' pass def on_close(self, *largs): '''Event called when the window is closed''' Modules.unregister_window(self) EventLoop.remove_event_listener(self) def on_mouse_down(self, x, y, button, modifiers): '''Event called when mouse is in action (press/release)''' pass def on_mouse_move(self, x, y, modifiers): '''Event called when mouse is moving, with buttons pressed''' pass def on_mouse_up(self, x, y, button, modifiers): '''Event called when mouse is moving, with buttons pressed''' pass def on_keyboard(self, key, scancode=None, unicode=None, modifier=None): '''Event called when keyboard is in action .. warning:: Some providers may omit `scancode`, `unicode` and/or `modifier`! ''' pass def on_key_down(self, key, scancode=None, unicode=None, modifier=None): '''Event called when a key is down (same arguments as on_keyboard)''' pass def on_key_up(self, key, scancode=None, unicode=None, modifier=None): '''Event called when a key is up (same arguments as on_keyboard)''' pass def configure_keyboards(self): # Configure how to provide keyboards (virtual or not) # register system keyboard to listening keys from window sk = self._system_keyboard self.bind( on_key_down=sk._on_window_key_down, on_key_up=sk._on_window_key_up) # use the device's real keyboard self.allow_vkeyboard = False # one single vkeyboard shared between all widgets self.single_vkeyboard = True # the single vkeyboard is always sitting at the same position self.docked_vkeyboard = False # now read the configuration mode = Config.get('kivy', 'keyboard_mode') if mode not in ('', 'system', 'dock', 'multi'): Logger.critical('Window: unknown keyboard mode %r' % mode) # adapt mode according to the configuration if mode == 'system': self.allow_vkeyboard = False self.single_vkeyboard = True self.docked_vkeyboard = False elif mode == 'dock': self.allow_vkeyboard = True self.single_vkeyboard = True self.docked_vkeyboard = True elif mode == 'multi': self.allow_vkeyboard = True self.single_vkeyboard = False self.docked_vkeyboard = False Logger.info('Window: virtual keyboard %sallowed, %s, %s' % ('' if self.allow_vkeyboard else 'not ', 'single mode' if self.single_vkeyboard else 'multiuser mode', 'docked' if self.docked_vkeyboard else 'not docked')) def set_vkeyboard_class(self, cls): '''.. versionadded:: 1.0.8 Set the VKeyboard class to use. If None set, it will use the :class:`kivy.uix.vkeyboard.VKeyboard`. ''' self._vkeyboard_cls = cls def release_all_keyboards(self): '''.. versionadded:: 1.0.8 This will ensure that no virtual keyboard / system keyboard are actually requested. All will be closed. ''' for key in self._keyboards.keys()[:]: keyboard = self._keyboards[key] if keyboard: keyboard.release() def request_keyboard(self, callback, target): '''.. versionadded:: 1.0.4 Internal method for widget, to request the keyboard. This method is not intented to be used by end-user, however, if you want to use the real-keyboard (not virtual keyboard), you don't want to share it with another widget. A widget can request the keyboard, indicating a callback to call when the keyboard will be released (or taken by another widget). :Parameters: `callback`: func Callback that will be called when the keyboard is closed. It can be because somebody else requested the keyboard, or if the user itself closed it. `target`: Widget Attach the keyboard to the specified target. Ensure you have a target attached if you're using the keyboard in a multi users mode. :Return: An instance of :class:`Keyboard`, containing the callback, target, and if configuration allowed it, a VKeyboard instance. .. versionchanged:: 1.0.8 `target` have been added, and must be the widget source that request the keyboard. If set, the widget must have one method named `on_keyboard_text`, that will be called from the vkeyboard. ''' # release any previous keyboard attached. self.release_keyboard(target) # if we can use virtual vkeyboard, activate it. if self.allow_vkeyboard: keyboard = None # late import global VKeyboard if VKeyboard is None and self._vkeyboard_cls is None: from kivy.uix.vkeyboard import VKeyboard self._vkeyboard_cls = VKeyboard # if the keyboard doesn't exist, create it. key = 'single' if self.single_vkeyboard else target if key not in self._keyboards: vkeyboard = self._vkeyboard_cls() keyboard = Keyboard(widget=vkeyboard, window=self) vkeyboard.bind( on_key_down=keyboard._on_vkeyboard_key_down, on_key_up=keyboard._on_vkeyboard_key_up) self._keyboards[key] = keyboard else: keyboard = self._keyboards[key] # configure vkeyboard keyboard.target = keyboard.widget.target = target keyboard.callback = keyboard.widget.callback = callback # add to the window self.add_widget(keyboard.widget) # only after add, do dock mode keyboard.widget.docked = self.docked_vkeyboard keyboard.widget.setup_mode() # return it. return keyboard else: # system keyboard, just register the callback. self._system_keyboard.callback = callback self._system_keyboard.target = target return self._system_keyboard def release_keyboard(self, target=None): '''.. versionadded:: 1.0.4 Internal method for widget, to release the real-keyboard. Check :func:`request_keyboard` to understand how it works. ''' if self.allow_vkeyboard: key = 'single' if self.single_vkeyboard else target if key not in self._keyboards: return keyboard = self._keyboards[key] callback = keyboard.callback if callback: keyboard.callback = None callback() keyboard.target = None self.remove_widget(keyboard.widget) if key != 'single' and key in self._keyboards: del self._keyboards[key] elif self._system_keyboard.callback: # this way will prevent possible recursion. callback = self._system_keyboard.callback self._system_keyboard.callback = None callback() return True
class GeoJsonMapLayer(MapLayer): source = StringProperty() geojson = ObjectProperty() initial_zoom = None first_time = True def __init__(self, **kwargs): super(GeoJsonMapLayer, self).__init__(**kwargs) with self.canvas: self.canvas_polygon = Canvas() self.canvas_line = Canvas() with self.canvas_polygon.before: PushMatrix() self.g_matrix = MatrixInstruction() self.g_scale = Scale() self.g_translate = Translate() with self.canvas_polygon: self.g_canvas_polygon = Canvas() with self.canvas_polygon.after: PopMatrix() def reposition(self): vx, vy = self.parent.delta_x, self.parent.delta_y pzoom = self.parent.zoom zoom = self.initial_zoom if zoom is None: self.initial_zoom = zoom = pzoom if zoom != pzoom: diff = 2**(pzoom - zoom) vx /= diff vy /= diff self.g_scale.x = self.g_scale.y = diff else: self.g_scale.x = self.g_scale.y = 1. self.g_translate.xy = vx, vy self.g_matrix.matrix = self.parent._scatter.transform if self.geojson: update = not self.first_time self.on_geojson(self, self.geojson, update=update) self.first_time = False def on_geojson(self, instance, geojson, update=False): if self.parent is None: return if not update: # print "Reload geojson (polygon)" self.g_canvas_polygon.clear() self._geojson_part(geojson, geotype="Polygon") # print "Reload geojson (LineString)" self.canvas_line.clear() self._geojson_part(geojson, geotype="LineString") def on_source(self, instance, value): if value.startswith("http://") or value.startswith("https://"): Downloader.instance().download(value, self._load_geojson_url) else: with open(value, "rb") as fd: geojson = json.load(fd) self.geojson = geojson def _load_geojson_url(self, url, r): self.geojson = r.json() def _geojson_part(self, part, geotype=None): tp = part["type"] if tp == "FeatureCollection": for feature in part["features"]: if geotype and feature["geometry"]["type"] != geotype: continue self._geojson_part_f(feature) elif tp == "Feature": if geotype and part["geometry"]["type"] == geotype: self._geojson_part_f(part) else: # unhandled geojson part pass def _geojson_part_f(self, feature): properties = feature["properties"] geometry = feature["geometry"] graphics = self._geojson_part_geometry(geometry, properties) for g in graphics: tp = geometry["type"] if tp == "Polygon": self.g_canvas_polygon.add(g) else: self.canvas_line.add(g) def _geojson_part_geometry(self, geometry, properties): from kivy.graphics import Mesh, Line, Color from kivy.graphics.tesselator import Tesselator, WINDING_ODD, TYPE_POLYGONS from kivy.utils import get_color_from_hex from kivy.metrics import dp tp = geometry["type"] graphics = [] if tp == "Polygon": tess = Tesselator() for c in geometry["coordinates"]: xy = list(self._lonlat_to_xy(c)) xy = flatten(xy) tess.add_contour(xy) tess.tesselate(WINDING_ODD, TYPE_POLYGONS) color = self._get_color_from(properties.get("color", "FF000088")) graphics.append(Color(*color)) for vertices, indices in tess.meshes: graphics.append( Mesh(vertices=vertices, indices=indices, mode="triangle_fan")) elif tp == "LineString": stroke = get_color_from_hex(properties.get("stroke", "#ffffff")) stroke_width = dp(properties.get("stroke-width")) xy = list(self._lonlat_to_xy(geometry["coordinates"])) xy = flatten(xy) graphics.append(Color(*stroke)) graphics.append(Line(points=xy, width=stroke_width)) return graphics def _lonlat_to_xy(self, lonlats): view = self.parent zoom = view.zoom for lon, lat in lonlats: p = view.get_window_xy_from(lat, lon, zoom) p = p[0] - self.parent.delta_x, p[1] - self.parent.delta_y p = self.parent._scatter.to_local(*p) yield p def _get_color_from(self, value): color = COLORS.get(value.lower(), value) color = get_color_from_hex(color) return color
class WindowBase(EventDispatcher): '''WindowBase is a abstract window widget, for any window implementation. .. warning:: The parameters are not working in normal case. Because at import, Kivy create a default OpenGL window, to add the ability to use OpenGL directives, texture creation.. before creating Window. If you don't like this behavior, you can include before the very first import of Kivy :: import os os.environ['KIVY_SHADOW'] = '0' This will forbid Kivy to create the default window ! :Parameters: `fullscreen`: bool Make window as fullscreen `width`: int Width of window `height`: int Height of window :Events: `on_motion`: etype, motionevent Fired when a new :class:`~kivy.input.motionevent.MotionEvent` is dispatched `on_touch_down`: Fired when a new touch appear `on_touch_move`: Fired when an existing touch is moved `on_touch_down`: Fired when an existing touch disapear `on_draw`: Fired when the :class:`Window` is beeing drawed `on_flip`: Fired when the :class:`Window` GL surface is beeing flipped `on_rotate`: rotation Fired when the :class:`Window` is beeing rotated `on_close`: Fired when the :class:`Window` is closed `on_keyboard`: key, scancode, unicode Fired when the keyboard is in action `on_key_down`: key, scancode, unicode Fired when a key is down `on_key_up`: key, scancode, unicode Fired when a key is up ''' __instance = None __initialized = False def __new__(cls, **kwargs): if cls.__instance is None: cls.__instance = EventDispatcher.__new__(cls) return cls.__instance def __init__(self, **kwargs): kwargs.setdefault('force', False) kwargs.setdefault('config', None) # don't init window 2 times, # except if force is specified if self.__initialized and not kwargs.get('force'): return super(WindowBase, self).__init__() # init privates self._modifiers = [] self._size = (0, 0) self._rotation = 0 self._clearcolor = [0, 0, 0, 0] # event subsystem self.register_event_type('on_draw') self.register_event_type('on_flip') self.register_event_type('on_rotate') self.register_event_type('on_resize') self.register_event_type('on_close') self.register_event_type('on_motion') self.register_event_type('on_touch_down') self.register_event_type('on_touch_move') self.register_event_type('on_touch_up') self.register_event_type('on_mouse_down') self.register_event_type('on_mouse_move') self.register_event_type('on_mouse_up') self.register_event_type('on_keyboard') self.register_event_type('on_key_down') self.register_event_type('on_key_up') self.children = [] self.parent = self #self.visible = True # add view if 'view' in kwargs: self.add_widget(kwargs.get('view')) # get window params, user options before config option params = {} if 'fullscreen' in kwargs: params['fullscreen'] = kwargs.get('fullscreen') else: params['fullscreen'] = Config.get('graphics', 'fullscreen') if params['fullscreen'] not in ('auto', 'fake'): params['fullscreen'] = params['fullscreen'].lower() in \ ('true', '1', 'yes', 'yup') if 'width' in kwargs: params['width'] = kwargs.get('width') else: params['width'] = Config.getint('graphics', 'width') if 'height' in kwargs: params['height'] = kwargs.get('height') else: params['height'] = Config.getint('graphics', 'height') if 'rotation' in kwargs: params['rotation'] = kwargs.get('rotation') else: params['rotation'] = Config.getint('graphics', 'rotation') params['position'] = Config.get( 'graphics', 'position', 'auto') if 'top' in kwargs: params['position'] = 'custom' params['top'] = kwargs.get('top') else: params['top'] = Config.getint('graphics', 'top') if 'left' in kwargs: params['position'] = 'custom' params['left'] = kwargs.get('left') else: params['left'] = Config.getint('graphics', 'left') # before creating the window import kivy.core.gl # configure the window self.params = params self.create_window() # attach modules + listener event Modules.register_window(self) EventLoop.set_window(self) EventLoop.add_event_listener(self) # mark as initialized self.__initialized = True def toggle_fullscreen(self): '''Toggle fullscreen on window''' pass def close(self): '''Close the window''' pass def create_window(self): '''Will create the main window and configure it. .. warning:: This method is called automatically at runtime. If you call it, it will recreate a RenderContext and Canvas. This mean you'll have a new graphics tree, and the old one will be unusable. This method exist to permit the creation of a new OpenGL context AFTER closing the first one. (Like using runTouchApp() and stopTouchApp()). This method have been only tested in unittest environment, and will be not suitable for Applications. Again, don't use this method unless you know exactly what you are doing ! ''' from kivy.core.gl import init_gl init_gl() # create the render context and canvas from kivy.graphics import RenderContext, Canvas self.render_context = RenderContext() self.canvas = Canvas() self.render_context.add(self.canvas) def on_flip(self): '''Flip between buffers (event)''' self.flip() def flip(self): '''Flip between buffers''' pass def _get_modifiers(self): return self._modifiers modifiers = property(_get_modifiers) def _get_size(self): r = self._rotation w, h = self._size if r == 0 or r == 180: return w, h return h, w def _set_size(self, size): if super(WindowBase, self)._set_size(size): Logger.debug('Window: Resize window to %s' % str(self.size)) self.dispatch('on_resize', *size) return True return False size = property(_get_size, _set_size, doc='''Rotated size of the window''') def _get_clearcolor(self): return self._clearcolor def _set_clearcolor(self, value): if value is not None: if type(value) not in (list, tuple): raise Exception('Clearcolor must be a list or tuple') if len(value) != 4: raise Exception('Clearcolor must contain 4 values') self._clearcolor = value clearcolor = property(_get_clearcolor, _set_clearcolor, doc='''Color used to clear window:: from kivy.core.window import Window # red background color Window.clearcolor = (1, 0, 0, 1) # don't clear background at all Window.clearcolor = None ''') # make some property read-only @property def width(self): '''Rotated window width''' r = self._rotation if r == 0 or r == 180: return self._size[0] return self._size[1] @property def height(self): '''Rotated window height''' r = self._rotation if r == 0 or r == 180: return self._size[1] return self._size[0] @property def center(self): '''Rotated window center''' return self.width / 2., self.height / 2. def add_widget(self, widget): '''Add a widget on window''' self.children.append(widget) widget.parent = self self.canvas.add(widget.canvas) self.update_childsize([widget]) def remove_widget(self, widget): '''Remove a widget from window ''' if not widget in self.children: return self.children.remove(widget) self.canvas.remove(widget.canvas) widget.parent = None def clear(self): '''Clear the window with background color''' # XXX FIXME use late binding from kivy.graphics.opengl import glClearColor, glClear, \ GL_COLOR_BUFFER_BIT, GL_DEPTH_BUFFER_BIT cc = self._clearcolor if cc is not None: glClearColor(*cc) glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) def to_widget(self, x, y, initial=True, relative=False): return (x, y) def to_window(self, x, y, initial=True, relative=False): return (x, y) def get_root_window(self): return self def get_parent_window(self): return self def get_parent_layout(self): return None def on_draw(self): self.clear() self.render_context.draw() def on_motion(self, etype, me): '''Event called when a Motion Event is received. :Parameters: `etype`: str One of 'begin', 'update', 'end' `me`: :class:`~kivy.input.motionevent.MotionEvent` Motion Event currently dispatched ''' if me.is_touch: if etype == 'begin': self.dispatch('on_touch_down', me) elif etype == 'update': self.dispatch('on_touch_move', me) elif etype == 'end': self.dispatch('on_touch_up', me) def on_touch_down(self, touch): '''Event called when a touch is down ''' w, h = self.system_size touch.scale_for_screen(w, h, rotation=self._rotation) for w in self.children[:]: if w.dispatch('on_touch_down', touch): return True def on_touch_move(self, touch): '''Event called when a touch move ''' w, h = self.system_size touch.scale_for_screen(w, h, rotation=self._rotation) for w in self.children[:]: if w.dispatch('on_touch_move', touch): return True def on_touch_up(self, touch): '''Event called when a touch up ''' w, h = self.system_size touch.scale_for_screen(w, h, rotation=self._rotation) for w in self.children[:]: if w.dispatch('on_touch_up', touch): return True def on_resize(self, width, height): '''Event called when the window is resized''' self.update_viewport() def update_viewport(self): from kivy.graphics.opengl import glViewport from kivy.graphics.transformation import Matrix width, height = self.system_size w2 = width / 2. h2 = height / 2. # prepare the viewport glViewport(0, 0, width, height) projection_mat = Matrix() projection_mat.view_clip(0.0, width, 0.0, height, -1.0, 1.0, 0) self.render_context['projection_mat'] = projection_mat # use the rotated size. # XXX FIXME fix rotation ''' width, height = self.size w2 = width / 2. h2 = height / 2. glTranslatef(-w2, -h2, -500) # set the model view glMatrixMode(GL_MODELVIEW) glLoadIdentity() glTranslatef(w2, h2, 0) glRotatef(self._rotation, 0, 0, 1) glTranslatef(-w2, -h2, 0) ''' self.update_childsize() def update_childsize(self, childs=None): width, height = self.system_size if childs is None: childs = self.children for w in childs: shw, shh = w.size_hint if shw and shh: w.size = shw * width, shh * height elif shw: w.width = shw * width elif shh: w.height = shh * height for key, value in w.pos_hint.iteritems(): if key == 'x': w.x = value * width elif key == 'right': w.right = value * width elif key == 'y': w.y = value * height elif key == 'top': w.top = value * height def _get_rotation(self): return self._rotation def _set_rotation(self, x): x = int(x % 360) if x == self._rotation: return if x not in (0, 90, 180, 270): raise ValueError('can rotate only 0,90,180,270 degrees') self._rotation = x self.dispatch('on_resize', *self.size) self.dispatch('on_rotate', x) rotation = property(_get_rotation, _set_rotation, 'Get/set the window content rotation. Can be one of ' '0, 90, 180, 270 degrees.') @property def system_size(self): '''Real size of the window, without taking care of the rotation ''' return self._size def screenshot(self, name='screenshot%(counter)04d.jpg'): '''Save the actual displayed image in a file ''' from os.path import join, exists from os import getcwd i = 0 path = None while True: i += 1 path = join(getcwd(), name % {'counter': i}) if not exists(path): break return path def on_rotate(self, rotation): '''Event called when the screen have been rotated ''' pass def on_close(self, *largs): '''Event called when the window is closed''' Modules.unregister_window(self) EventLoop.remove_event_listener(self) def on_mouse_down(self, x, y, button, modifiers): '''Event called when mouse is in action (press/release)''' pass def on_mouse_move(self, x, y, modifiers): '''Event called when mouse is moving, with buttons pressed''' pass def on_mouse_up(self, x, y, button, modifiers): '''Event called when mouse is moving, with buttons pressed''' pass def on_keyboard(self, key, scancode=None, unicode=None): '''Event called when keyboard is in action .. warning:: Some providers can skip `scancode` or `unicode` !! ''' pass def on_key_down(self, key, scancode=None, unicode=None): '''Event called when a key is down (same arguments as on_keyboard)''' pass def on_key_up(self, key, scancode=None, unicode=None): '''Event called when a key is up (same arguments as on_keyboard)''' pass
class Widget(EventDispatcher): '''Widget class. See module documentation for more information. :Events: `on_touch_down`: Fired when a new touch happens `on_touch_move`: Fired when an existing touch is moved `on_touch_up`: Fired when an existing touch disappears .. versionchanged:: 1.0.9 Everything related to event properties has been moved to :class:`~kivy.event.EventDispatcher`. Event properties can now be used in contructing a simple class, without subclassing :class:`Widget`. ''' def __init__(self, **kwargs): # Before doing anything, ensure the windows exist. EventLoop.ensure_window() # Register touch events self.register_event_type('on_touch_down') self.register_event_type('on_touch_move') self.register_event_type('on_touch_up') super(Widget, self).__init__(**kwargs) # Create the default canvas if not exist if self.canvas is None: self.canvas = Canvas() # Apply all the styles if '__no_builder' not in kwargs: #current_root = Builder.idmap.get('root') #Builder.idmap['root'] = self Builder.apply(self) #if current_root is not None: # Builder.idmap['root'] = current_root #else: # Builder.idmap.pop('root') # # Collision # def collide_point(self, x, y): '''Check if a point (x, y) is inside the widget's axis aligned bounding box. :Parameters: `x`: numeric X position of the point (in window coordinates) `y`: numeric Y position of the point (in window coordinates) :Returns: bool, True if the point is inside the bounding box. >>> Widget(pos=(10, 10), size=(50, 50)).collide_point(40, 40) True ''' return self.x <= x <= self.right and self.y <= y <= self.top def collide_widget(self, wid): '''Check if the other widget collides with this widget. Performs an axis-aligned bounding box intersection test by default. :Parameters: `wid`: :class:`Widget` class Widget to collide with. :Returns: bool, True if the other widget collides with this widget. >>> wid = Widget(size=(50, 50)) >>> wid2 = Widget(size=(50, 50), pos=(25, 25)) >>> wid.collide_widget(wid2) True >>> wid2.pos = (55, 55) >>> wid.collide_widget(wid2) False ''' if self.right < wid.x: return False if self.x > wid.right: return False if self.top < wid.y: return False if self.y > wid.top: return False return True # # Default event handlers # def on_touch_down(self, touch): '''Receive a touch down event. :Parameters: `touch`: :class:`~kivy.input.motionevent.MotionEvent` class Touch received :Returns: bool. If True, the dispatching of the touch will stop. ''' for child in self.children[:]: if child.dispatch('on_touch_down', touch): return True def on_touch_move(self, touch): '''Receive a touch move event. See :func:`on_touch_down` for more information ''' for child in self.children[:]: if child.dispatch('on_touch_move', touch): return True def on_touch_up(self, touch): '''Receive a touch up event. See :func:`on_touch_down` for more information ''' for child in self.children[:]: if child.dispatch('on_touch_up', touch): return True # # Tree management # def add_widget(self, widget, index=0): '''Add a new widget as a child of this widget. :Parameters: `widget`: :class:`Widget` Widget to add to our list of children. `index`: int, default to 0 *(this attribute have been added in 1.0.5)* Index to insert the widget in the list >>> root = Widget() >>> root.add_widget(Button()) >>> slider = Slider() >>> root.add_widget(slider) ''' if widget is self: raise WidgetException('You cannot add yourself in a Widget') if not isinstance(widget, Widget): raise WidgetException( 'add_widget() can be used only with Widget classes.') widget.parent = self if index == 0 or len(self.children) == 0: self.children.insert(0, widget) self.canvas.add(widget.canvas) else: canvas = self.canvas children = self.children if index >= len(children): index = len(children) next_index = 0 else: next_child = children[index] next_index = canvas.indexof(next_child.canvas) if next_index == -1: next_index = canvas.length() else: next_index += 1 children.insert(index, widget) canvas.insert(next_index, widget.canvas) def remove_widget(self, widget): '''Remove a widget from the children of this widget. :Parameters: `widget`: :class:`Widget` Widget to remove from our children list. >>> root = Widget() >>> button = Button() >>> root.add_widget(button) >>> root.remove_widget(button) ''' if widget not in self.children: return self.children.remove(widget) self.canvas.remove(widget.canvas) widget.parent = None def clear_widgets(self): '''Remove all widgets added to this widget. ''' remove_widget = self.remove_widget for child in self.children[:]: remove_widget(child) def get_root_window(self): '''Return the root window. :Returns: Instance of the root window. Can be :class:`~kivy.core.window.WindowBase` or :class:`Widget` ''' if self.parent: return self.parent.get_root_window() def get_parent_window(self): '''Return the parent window. :Returns: Instance of the parent window. Can be :class:`~kivy.core.window.WindowBase` or :class:`Widget` ''' if self.parent: return self.parent.get_parent_window() def to_widget(self, x, y, relative=False): '''Convert the given coordinate from window to local widget coordinates. ''' if self.parent: x, y = self.parent.to_widget(x, y) return self.to_local(x, y, relative=relative) def to_window(self, x, y, initial=True, relative=False): '''Transform local coordinates to window coordinates.''' if not initial: x, y = self.to_parent(x, y, relative=relative) if self.parent: return self.parent.to_window(x, y, initial=False, relative=relative) return (x, y) def to_parent(self, x, y, relative=False): '''Transform local coordinates to parent coordinates. :Parameters: `relative`: bool, default to False Change to True if you want to translate relative positions from widget to its parent. ''' if relative: return (x + self.x, y + self.y) return (x, y) def to_local(self, x, y, relative=False): '''Transform parent coordinates to local coordinates. :Parameters: `relative`: bool, default to False Change to True if you want to translate coordinates to relative widget coordinates. ''' if relative: return (x - self.x, y - self.y) return (x, y) x = NumericProperty(0) '''X position of the widget. :data:`x` is a :class:`~kivy.properties.NumericProperty`, default to 0. ''' y = NumericProperty(0) '''Y position of the widget. :data:`y` is a :class:`~kivy.properties.NumericProperty`, default to 0. ''' width = NumericProperty(100) '''Width of the widget. :data:`width` is a :class:`~kivy.properties.NumericProperty`, default to 100. ''' height = NumericProperty(100) '''Height of the widget. :data:`height` is a :class:`~kivy.properties.NumericProperty`, default to 100. ''' pos = ReferenceListProperty(x, y) '''Position of the widget. :data:`pos` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`x`, :data:`y`) properties. ''' size = ReferenceListProperty(width, height) '''Size of the widget. :data:`size` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`width`, :data:`height`) properties. ''' def get_right(self): return self.x + self.width def set_right(self, value): self.x = value - self.width right = AliasProperty(get_right, set_right, bind=('x', 'width')) '''Right position of the widget. :data:`right` is a :class:`~kivy.properties.AliasProperty` of (:data:`x` + :data:`width`) ''' def get_top(self): return self.y + self.height def set_top(self, value): self.y = value - self.height top = AliasProperty(get_top, set_top, bind=('y', 'height')) '''Top position of the widget. :data:`top` is a :class:`~kivy.properties.AliasProperty` of (:data:`y` + :data:`height`) ''' def get_center_x(self): return self.x + self.width / 2. def set_center_x(self, value): self.x = value - self.width / 2. center_x = AliasProperty(get_center_x, set_center_x, bind=('x', 'width')) '''X center position of the widget. :data:`center_x` is a :class:`~kivy.properties.AliasProperty` of (:data:`x` + :data:`width` / 2.) ''' def get_center_y(self): return self.y + self.height / 2. def set_center_y(self, value): self.y = value - self.height / 2. center_y = AliasProperty(get_center_y, set_center_y, bind=('y', 'height')) '''Y center position of the widget. :data:`center_y` is a :class:`~kivy.properties.AliasProperty` of (:data:`y` + :data:`height` / 2.) ''' center = ReferenceListProperty(center_x, center_y) '''Center position of the widget. :data:`center` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`center_x`, :data:`center_y`) ''' cls = ListProperty([]) '''Class of the widget, used for styling. ''' def get_uid(self): return self.__dict__['__uid'] uid = AliasProperty(get_uid, None) '''Unique identifier of the widget in the whole Kivy instance. .. versionadded:: 1.0.7 :data:`uid` is a :class:`~kivy.properties.AliasProperty`, read-only. ''' id = StringProperty(None, allownone=True) '''Unique identifier of the widget in the tree. :data:`id` is a :class:`~kivy.properties.StringProperty`, default to None. .. warning:: If the :data:`id` is already used in the tree, an exception will be raised. ''' children = ListProperty([]) '''List of children of this widget. :data:`children` is a :class:`~kivy.properties.ListProperty` instance, default to an empty list. Use :func:`add_widget` and :func:`remove_widget` for manipulating the children list. Don't manipulate the children list directly until you know what you are doing. ''' parent = ObjectProperty(None, allownone=True) '''Parent of this widget. :data:`parent` is a :class:`~kivy.properties.ObjectProperty` instance, default to None. The parent of a widget is set when the widget is added to another one, and unset when the widget is removed from its parent. ''' size_hint_x = NumericProperty(1, allownone=True) '''X size hint. Represents how much space the widget should use in the direction of the X axis, relative to its parent's width. Only :class:`~kivy.uix.layout.Layout` and :class:`~kivy.core.window.Window` make use of the hint. The value is in percent as a float from 0. to 1., where 1. means the full size of his parent. 0.5 represents 50%. :data:`size_hint_x` is a :class:`~kivy.properties.NumericProperty`, default to 1. ''' size_hint_y = NumericProperty(1, allownone=True) '''Y size hint. :data:`size_hint_y` is a :class:`~kivy.properties.NumericProperty`, default to 1. See :data:`size_hint_x` for more information ''' size_hint = ReferenceListProperty(size_hint_x, size_hint_y) '''Size hint. :data:`size_hint` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`size_hint_x`, :data:`size_hint_y`) See :data:`size_hint_x` for more information ''' pos_hint = ObjectProperty({}) '''Position hint. This property allows you to set the position of the widget inside its parent layout, in percent (similar to size_hint). For example, if you want to set the top of the widget to be at 90% height of its parent layout, you can write: widget = Widget(pos_hint={'top': 0.9}) The keys 'x', 'right', 'center_x', will use the parent width. The keys 'y', 'top', 'center_y', will use the parent height. See :doc:`api-kivy.uix.floatlayout` for further reference. Position hint is only used in :class:`~kivy.uix.floatlayout.FloatLayout` and :class:`~kivy.core.window.Window`. :data:`pos_hint` is a :class:`~kivy.properties.ObjectProperty` containing a dict. ''' canvas = None '''Canvas of the widget.
class Widget(EventDispatcher): '''Widget class. See module documentation for more information. :Events: `on_touch_down`: Fired when a new touch appear `on_touch_move`: Fired when an existing touch is moved `on_touch_up`: Fired when an existing touch disappears ''' # UID counter __widget_uid = 0 __cache_properties = {} def __new__(__cls__, *largs, **kwargs): self = super(Widget, __cls__).__new__(__cls__) # XXX for the moment, we need to create a uniq id for properties. # Properties need a identifier to the class instance. hash() and id() # are longer than using a custom __uid. I hope we can figure out a way # of doing that without require any python code. :) Widget.__widget_uid += 1 self.__dict__['__uid'] = Widget.__widget_uid cp = Widget.__cache_properties if __cls__ not in cp: attrs_found = cp[__cls__] = {} attrs = dir(__cls__) for k in attrs: attr = getattr(__cls__, k) if isinstance(attr, Property): if k in Widget_forbidden_properties: raise Exception( 'The property <%s> have a forbidden name' % k) attrs_found[k] = attr else: attrs_found = cp[__cls__] # First loop, link all the properties storage to our instance for k, attr in attrs_found.iteritems(): attr.link(self, k) # Second loop, resolve all the reference for k, attr in attrs_found.iteritems(): attr.link_deps(self, k) self.__properties = attrs_found # Then, return the class instance return self def __del__(self): # The thing here, since the storage of the property is inside the # Property class, we must remove ourself from the storage of each # Property. The usage is faster, the creation / deletion is longer. for attr in self.__properties.itervalues(): attr.unlink(self) def __init__(self, **kwargs): super(Widget, self).__init__() # Register touch events self.register_event_type('on_touch_down') self.register_event_type('on_touch_move') self.register_event_type('on_touch_up') # Before doing anything, ensure the windows exist. EventLoop.ensure_window() # Auto bind on own handler if exist properties = self.__properties.keys() for func in dir(self): if not func.startswith('on_'): continue name = func[3:] if name in properties: self.bind(**{name: getattr(self, func)}) # Create the default canvas if not exist if self.canvas is None: self.canvas = Canvas() # Apply the existing arguments to our widget for key, value in kwargs.iteritems(): if hasattr(self, key): setattr(self, key, value) # Apply all the styles if '__no_builder' not in kwargs: Builder.apply(self) def create_property(self, name): '''Create a new property at runtime. .. warning:: This function is designed for the Kivy language, don't use it in your code. You should declare the property in your class instead of using this method. :Parameters: `name`: string Name of the property The class of the property cannot be specified, it will always be an :class:`~kivy.properties.ObjectProperty` class. The default value of the property will be None, until you set a new value. >>> mywidget = Widget() >>> mywidget.create_property('custom') >>> mywidget.custom = True >>> print mywidget.custom True ''' prop = ObjectProperty(None) prop.link(self, name) prop.link_deps(self, name) self.__properties[name] = prop setattr(self.__class__, name, prop) # # Collision # def collide_point(self, x, y): '''Check if a point (x, y) is inside the widget's axis aligned bounding box. :Parameters: `x`: numeric X position of the point (in window coordinates) `y`: numeric Y position of the point (in window coordinates) :Returns: bool, True if the point is inside the bounding box. >>> Widget(pos=(10, 10), size=(50, 50)).collide_point(40, 40) True ''' return self.x <= x <= self.right and self.y <= y <= self.top def collide_widget(self, wid): '''Check if the other widget collides with this widget. Performs an axis-aligned bounding box intersection test by default. :Parameters: `wid`: :class:`Widget` class Widget to collide with. :Returns: bool, True if the other widget collides with this widget. >>> wid = Widget(size=(50, 50)) >>> wid2 = Widget(size=(50, 50), pos=(25, 25)) >>> wid.collide_widget(wid2) True >>> wid2.pos = (55, 55) >>> wid.collide_widget(wid2) False ''' if self.right < wid.x: return False if self.x > wid.right: return False if self.top < wid.y: return False if self.y > wid.top: return False return True # # Default event handlers # def on_touch_down(self, touch): '''Receive a touch down event. :Parameters: `touch`: :class:`~kivy.input.motionevent.MotionEvent` class Touch received :Returns: bool. If True, the dispatching of the touch will stop. ''' for child in self.children[:]: if child.dispatch('on_touch_down', touch): return True def on_touch_move(self, touch): '''Receive a touch move event. See :func:`on_touch_down` for more information ''' for child in self.children[:]: if child.dispatch('on_touch_move', touch): return True def on_touch_up(self, touch): '''Receive a touch up event. See :func:`on_touch_down` for more information ''' for child in self.children[:]: if child.dispatch('on_touch_up', touch): return True # # Events # def bind(self, **kwargs): '''Bind properties or events to a handler. Example usage:: def my_x_callback(obj, value): print 'on object', obj, 'x changed to', value def my_width_callback(obj, value): print 'on object', obj, 'width changed to', value self.bind(x=my_x_callback, width=my_width_callback) ''' super(Widget, self).bind(**kwargs) for key, value in kwargs.iteritems(): if key.startswith('on_'): continue self.__properties[key].bind(self, value) def unbind(self, **kwargs): '''Unbind properties or events from their handler. See :func:`bind()` for more information. ''' super(Widget, self).unbind(**kwargs) for key, value in kwargs.iteritems(): if key.startswith('on_'): continue self.__properties[key].unbind(self, value) # # Tree management # def add_widget(self, widget, index=0): '''Add a new widget as a child of this widget. :Parameters: `widget`: :class:`Widget` Widget to add to our list of children. `index`: int, default to 0 *(this attribute have been added in 1.0.5)* Index to insert the widget in the list >>> root = Widget() >>> root.add_widget(Button()) >>> slider = Slider() >>> root.add_widget(slider) ''' if not isinstance(widget, Widget): raise WidgetException( 'add_widget() can be used only with Widget classes.') widget.parent = self if index == 0 or len(self.children) == 0: self.children.insert(0, widget) self.canvas.add(widget.canvas) else: canvas = self.canvas children = self.children if index >= len(children): index = len(children) next_index = 0 else: next_child = children[index] next_index = canvas.indexof(next_child.canvas) if next_index == -1: next_index = canvas.length() else: next_index += 1 children.insert(index, widget) canvas.insert(next_index, widget.canvas) def remove_widget(self, widget): '''Remove a widget from the children of this widget. :Parameters: `widget`: :class:`Widget` Widget to remove from our children list. >>> root = Widget() >>> button = Button() >>> root.add_widget(button) >>> root.remove_widget(button) ''' if widget not in self.children: return self.children.remove(widget) self.canvas.remove(widget.canvas) widget.parent = None def clear_widgets(self): '''Remove all widgets added to this widget. ''' remove_widget = self.remove_widget for child in self.children[:]: remove_widget(child) def get_root_window(self): '''Return the root window. :Returns: Instance of the root window. Can be :class:`~kivy.core.window.WindowBase` or :class:`Widget` ''' if self.parent: return self.parent.get_root_window() def get_parent_window(self): '''Return the parent window. :Returns: Instance of the parent window. Can be :class:`~kivy.core.window.WindowBase` or :class:`Widget` ''' if self.parent: return self.parent.get_parent_window() def to_widget(self, x, y, relative=False): '''Convert the given coordinate from window to local widget coordinates. ''' if self.parent: x, y = self.parent.to_widget(x, y) return self.to_local(x, y, relative=relative) def to_window(self, x, y, initial=True, relative=False): '''Transform local coordinates to window coordinates.''' if not initial: x, y = self.to_parent(x, y, relative=relative) if self.parent: return self.parent.to_window(x, y, initial=False, relative=relative) return (x, y) def to_parent(self, x, y, relative=False): '''Transform local coordinates to parent coordinates. :Parameters: `relative`: bool, default to False Change to True if you want to translate relative positions from widget to its parent. ''' if relative: return (x + self.x, y + self.y) return (x, y) def to_local(self, x, y, relative=False): '''Transform parent coordinates to local coordinates. :Parameters: `relative`: bool, default to False Change to True if you want to translate coordinates to relative widget coordinates. ''' if relative: return (x - self.x, y - self.y) return (x, y) # # Properties # def setter(self, name): '''Return the setter of a property. Useful if you want to directly bind a property to another. For example, if you want to position one widget next to you :: self.bind(right=nextchild.setter('x')) ''' return self.__properties[name].__set__ def getter(self, name): '''Return the getter of a property. ''' return self.__properties[name].__get__ x = NumericProperty(0) '''X position of the widget. :data:`x` is a :class:`~kivy.properties.NumericProperty`, default to 0. ''' y = NumericProperty(0) '''Y position of the widget. :data:`y` is a :class:`~kivy.properties.NumericProperty`, default to 0. ''' width = NumericProperty(100) '''Width of the widget. :data:`width` is a :class:`~kivy.properties.NumericProperty`, default to 100. ''' height = NumericProperty(100) '''Height of the widget. :data:`height` is a :class:`~kivy.properties.NumericProperty`, default to 100. ''' pos = ReferenceListProperty(x, y) '''Position of the widget. :data:`pos` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`x`, :data:`y`) properties. ''' size = ReferenceListProperty(width, height) '''Size of the widget. :data:`size` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`width`, :data:`height`) properties. ''' def get_right(self): return self.x + self.width def set_right(self, value): self.x = value - self.width right = AliasProperty(get_right, set_right, bind=('x', 'width')) '''Right position of the widget :data:`right` is a :class:`~kivy.properties.AliasProperty` of (:data:`x` + :data:`width`) ''' def get_top(self): return self.y + self.height def set_top(self, value): self.y = value - self.height top = AliasProperty(get_top, set_top, bind=('y', 'height')) '''Top position of the widget :data:`top` is a :class:`~kivy.properties.AliasProperty` of (:data:`y` + :data:`height`) ''' def get_center_x(self): return self.x + self.width / 2. def set_center_x(self, value): self.x = value - self.width / 2. center_x = AliasProperty(get_center_x, set_center_x, bind=('x', 'width')) '''X center position of the widget :data:`center_x` is a :class:`~kivy.properties.AliasProperty` of (:data:`x` + :data:`width` / 2.) ''' def get_center_y(self): return self.y + self.height / 2. def set_center_y(self, value): self.y = value - self.height / 2. center_y = AliasProperty(get_center_y, set_center_y, bind=('y', 'height')) '''Y center position of the widget :data:`center_y` is a :class:`~kivy.properties.AliasProperty` of (:data:`y` + :data:`height` / 2.) ''' center = ReferenceListProperty(center_x, center_y) '''Center position of the widget :data:`center` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`center_x`, :data:`center_y`) ''' cls = ListProperty([]) '''Class of the widget, used for styling. ''' id = StringProperty(None, allownone=True) '''Unique identifier of the widget in the tree. :data:`id` is a :class:`~kivy.properties.StringProperty`, default to None. .. warning:: If the :data:`id` is already used in the tree, an exception will be raised. ''' children = ListProperty([]) '''List of children of this widget :data:`children` is a :class:`~kivy.properties.ListProperty` instance, default to an empty list. Use :func:`add_widget` and :func:`remove_widget` for manipulate children list. Don't manipulate children list directly until you know what you are doing. ''' parent = ObjectProperty(None, allownone=True) '''Parent of this widget :data:`parent` is a :class:`~kivy.properties.ObjectProperty` instance, default to None. The parent of a widget is set when the widget is added to another one, and unset when the widget is removed from his parent. ''' size_hint_x = NumericProperty(1, allownone=True) '''X size hint. It represents how much space the widget should use in the direction of the X axis, relative to its parent's width. Only :class:`~kivy.uix.layout.Layout` and :class:`~kivy.core.window.Window` make use of the hint. The value is in percent as a float from 0. to 1., where 1. means the full size of his parent, i.e. 100%. 0.5 represents 50%. :data:`size_hint_x` is a :class:`~kivy.properties.NumericProperty`, default to 1. ''' size_hint_y = NumericProperty(1, allownone=True) '''Y size hint. :data:`size_hint_y` is a :class:`~kivy.properties.NumericProperty`, default to 1. See :data:`size_hint_x` for more information ''' size_hint = ReferenceListProperty(size_hint_x, size_hint_y) '''Size hint. :data:`size_hint` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`size_hint_x`, :data:`size_hint_y`) See :data:`size_hint_x` for more information ''' pos_hint = ObjectProperty({}) '''Position hint. This property allows you to set the position of the widget inside its parent layout, in percent (similar to size_hint). For example, if you want to set the top of the widget to be at 90% height of its parent layout, you can write: widget = Widget(pos_hint={'top': 0.9}) The keys 'x', 'right', 'center_x', will use the parent width. The keys 'y', 'top', 'center_y', will use the parent height. Check :doc:`api-kivy.uix.floatlayout` for further reference. Position hint is only used in :class:`~kivy.uix.floatlayout.FloatLayout` and :class:`~kivy.core.window.Window`. :data:`pos_hint` is a :class:`~kivy.properties.ObjectProperty` containing a dict. ''' canvas = None '''Canvas of the widget.
class Widget(EventDispatcher): """Widget class. See module documentation for more information. :Events: `on_touch_down`: Fired when a new touch happens `on_touch_move`: Fired when an existing touch is moved `on_touch_up`: Fired when an existing touch disappears .. versionchanged:: 1.0.9 Everything related to event properties has been moved to :class:`~kivy.event.EventDispatcher`. Event properties can now be used in contructing a simple class, without subclassing :class:`Widget`. .. versionchanged:: 1.5.0 Constructor now accept on_* arguments to automatically bind callbacks to properties or events, as the Kv language. """ __metaclass__ = WidgetMetaclass def __init__(self, **kwargs): # Before doing anything, ensure the windows exist. EventLoop.ensure_window() # Register touch events self.register_event_type("on_touch_down") self.register_event_type("on_touch_move") self.register_event_type("on_touch_up") super(Widget, self).__init__(**kwargs) # Create the default canvas if not exist if self.canvas is None: self.canvas = Canvas(opacity=self.opacity) # Apply all the styles if "__no_builder" not in kwargs: # current_root = Builder.idmap.get('root') # Builder.idmap['root'] = self Builder.apply(self) # if current_root is not None: # Builder.idmap['root'] = current_root # else: # Builder.idmap.pop('root') # Bind all the events for argument in kwargs: if argument[:3] == "on_": self.bind(**{argument: kwargs[argument]}) # # Collision # def collide_point(self, x, y): """Check if a point (x, y) is inside the widget's axis aligned bounding box. :Parameters: `x`: numeric X position of the point (in window coordinates) `y`: numeric Y position of the point (in window coordinates) :Returns: bool, True if the point is inside the bounding box. >>> Widget(pos=(10, 10), size=(50, 50)).collide_point(40, 40) True """ return self.x <= x <= self.right and self.y <= y <= self.top def collide_widget(self, wid): """Check if the other widget collides with this widget. Performs an axis-aligned bounding box intersection test by default. :Parameters: `wid`: :class:`Widget` class Widget to collide with. :Returns: bool, True if the other widget collides with this widget. >>> wid = Widget(size=(50, 50)) >>> wid2 = Widget(size=(50, 50), pos=(25, 25)) >>> wid.collide_widget(wid2) True >>> wid2.pos = (55, 55) >>> wid.collide_widget(wid2) False """ if self.right < wid.x: return False if self.x > wid.right: return False if self.top < wid.y: return False if self.y > wid.top: return False return True # # Default event handlers # def on_touch_down(self, touch): """Receive a touch down event. :Parameters: `touch`: :class:`~kivy.input.motionevent.MotionEvent` class Touch received :Returns: bool. If True, the dispatching of the touch will stop. """ for child in self.children[:]: if child.dispatch("on_touch_down", touch): return True def on_touch_move(self, touch): """Receive a touch move event. See :meth:`on_touch_down` for more information """ for child in self.children[:]: if child.dispatch("on_touch_move", touch): return True def on_touch_up(self, touch): """Receive a touch up event. See :meth:`on_touch_down` for more information """ for child in self.children[:]: if child.dispatch("on_touch_up", touch): return True # # Tree management # def add_widget(self, widget, index=0): """Add a new widget as a child of this widget. :Parameters: `widget`: :class:`Widget` Widget to add to our list of children. `index`: int, default to 0 *(this attribute have been added in 1.0.5)* Index to insert the widget in the list >>> root = Widget() >>> root.add_widget(Button()) >>> slider = Slider() >>> root.add_widget(slider) """ if widget is self: raise WidgetException("You cannot add yourself in a Widget") if not isinstance(widget, Widget): raise WidgetException("add_widget() can be used only with Widget classes.") parent = widget.parent # check if widget is already a child of another widget if parent: raise WidgetException("Cannot add %r, it already has a parent %r" % (widget, parent)) widget.parent = self if index == 0 or len(self.children) == 0: self.children.insert(0, widget) self.canvas.add(widget.canvas) else: canvas = self.canvas children = self.children if index >= len(children): index = len(children) next_index = -1 else: next_child = children[index] next_index = canvas.indexof(next_child.canvas) if next_index == -1: next_index = canvas.length() else: next_index += 1 children.insert(index, widget) canvas.insert(next_index, widget.canvas) def remove_widget(self, widget): """Remove a widget from the children of this widget. :Parameters: `widget`: :class:`Widget` Widget to remove from our children list. >>> root = Widget() >>> button = Button() >>> root.add_widget(button) >>> root.remove_widget(button) """ if widget not in self.children: return self.children.remove(widget) self.canvas.remove(widget.canvas) widget.parent = None def clear_widgets(self): """Remove all widgets added to this widget. """ remove_widget = self.remove_widget for child in self.children[:]: remove_widget(child) def get_root_window(self): """Return the root window. :Returns: Instance of the root window. Can be :class:`~kivy.core.window.WindowBase` or :class:`Widget` """ if self.parent: return self.parent.get_root_window() def get_parent_window(self): """Return the parent window. :Returns: Instance of the parent window. Can be :class:`~kivy.core.window.WindowBase` or :class:`Widget` """ if self.parent: return self.parent.get_parent_window() def to_widget(self, x, y, relative=False): """Convert the given coordinate from window to local widget coordinates. """ if self.parent: x, y = self.parent.to_widget(x, y) return self.to_local(x, y, relative=relative) def to_window(self, x, y, initial=True, relative=False): """Transform local coordinates to window coordinates.""" if not initial: x, y = self.to_parent(x, y, relative=relative) if self.parent: return self.parent.to_window(x, y, initial=False, relative=relative) return (x, y) def to_parent(self, x, y, relative=False): """Transform local coordinates to parent coordinates. :Parameters: `relative`: bool, default to False Change to True if you want to translate relative positions from widget to its parent. """ if relative: return (x + self.x, y + self.y) return (x, y) def to_local(self, x, y, relative=False): """Transform parent coordinates to local coordinates. :Parameters: `relative`: bool, default to False Change to True if you want to translate coordinates to relative widget coordinates. """ if relative: return (x - self.x, y - self.y) return (x, y) x = NumericProperty(0) """X position of the widget. :data:`x` is a :class:`~kivy.properties.NumericProperty`, default to 0. """ y = NumericProperty(0) """Y position of the widget. :data:`y` is a :class:`~kivy.properties.NumericProperty`, default to 0. """ width = NumericProperty(100) """Width of the widget. :data:`width` is a :class:`~kivy.properties.NumericProperty`, default to 100. """ height = NumericProperty(100) """Height of the widget. :data:`height` is a :class:`~kivy.properties.NumericProperty`, default to 100. """ pos = ReferenceListProperty(x, y) """Position of the widget. :data:`pos` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`x`, :data:`y`) properties. """ size = ReferenceListProperty(width, height) """Size of the widget. :data:`size` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`width`, :data:`height`) properties. """ def get_right(self): return self.x + self.width def set_right(self, value): self.x = value - self.width right = AliasProperty(get_right, set_right, bind=("x", "width")) """Right position of the widget. :data:`right` is a :class:`~kivy.properties.AliasProperty` of (:data:`x` + :data:`width`) """ def get_top(self): return self.y + self.height def set_top(self, value): self.y = value - self.height top = AliasProperty(get_top, set_top, bind=("y", "height")) """Top position of the widget. :data:`top` is a :class:`~kivy.properties.AliasProperty` of (:data:`y` + :data:`height`) """ def get_center_x(self): return self.x + self.width / 2.0 def set_center_x(self, value): self.x = value - self.width / 2.0 center_x = AliasProperty(get_center_x, set_center_x, bind=("x", "width")) """X center position of the widget. :data:`center_x` is a :class:`~kivy.properties.AliasProperty` of (:data:`x` + :data:`width` / 2.) """ def get_center_y(self): return self.y + self.height / 2.0 def set_center_y(self, value): self.y = value - self.height / 2.0 center_y = AliasProperty(get_center_y, set_center_y, bind=("y", "height")) """Y center position of the widget. :data:`center_y` is a :class:`~kivy.properties.AliasProperty` of (:data:`y` + :data:`height` / 2.) """ center = ReferenceListProperty(center_x, center_y) """Center position of the widget. :data:`center` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`center_x`, :data:`center_y`) """ cls = ListProperty([]) """Class of the widget, used for styling. """ id = StringProperty(None, allownone=True) """Unique identifier of the widget in the tree. :data:`id` is a :class:`~kivy.properties.StringProperty`, default to None. .. warning:: If the :data:`id` is already used in the tree, an exception will be raised. """ children = ListProperty([]) """List of children of this widget. :data:`children` is a :class:`~kivy.properties.ListProperty` instance, default to an empty list. Use :meth:`add_widget` and :meth:`remove_widget` for manipulating the children list. Don't manipulate the children list directly until you know what you are doing. """ parent = ObjectProperty(None, allownone=True) """Parent of this widget. :data:`parent` is a :class:`~kivy.properties.ObjectProperty` instance, default to None. The parent of a widget is set when the widget is added to another one, and unset when the widget is removed from its parent. """ size_hint_x = NumericProperty(1, allownone=True) """X size hint. Represents how much space the widget should use in the direction of the X axis, relative to its parent's width. Only :class:`~kivy.uix.layout.Layout` and :class:`~kivy.core.window.Window` make use of the hint. The value is in percent as a float from 0. to 1., where 1. means the full size of his parent. 0.5 represents 50%. :data:`size_hint_x` is a :class:`~kivy.properties.NumericProperty`, default to 1. """ size_hint_y = NumericProperty(1, allownone=True) """Y size hint. :data:`size_hint_y` is a :class:`~kivy.properties.NumericProperty`, default to 1. See :data:`size_hint_x` for more information """ size_hint = ReferenceListProperty(size_hint_x, size_hint_y) """Size hint. :data:`size_hint` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`size_hint_x`, :data:`size_hint_y`) See :data:`size_hint_x` for more information """ pos_hint = ObjectProperty({}) """Position hint. This property allows you to set the position of the widget inside its parent layout, in percent (similar to size_hint). For example, if you want to set the top of the widget to be at 90% height of its parent layout, you can write: widget = Widget(pos_hint={'top': 0.9}) The keys 'x', 'right', 'center_x', will use the parent width. The keys 'y', 'top', 'center_y', will use the parent height. See :doc:`api-kivy.uix.floatlayout` for further reference. Position hint is only used in :class:`~kivy.uix.floatlayout.FloatLayout` and :class:`~kivy.core.window.Window`. :data:`pos_hint` is a :class:`~kivy.properties.ObjectProperty` containing a dict. """ opacity = NumericProperty(1.0) """Opacity of the widget and all the children. .. versionadded:: 1.4.1 The opacity attribute controls the opacity of the widget and its children. Be careful, it's a cumulative attribute: the value is multiplied to the current global opacity, and the result is applied to the current context color. For example: if your parent have an opacity of 0.5, and one children have an opacity of 0.2, the real opacity of the children will be 0.5 * 0.2 = 0.1. Then, the opacity is applied on the shader as:: frag_color = color * vec4(1.0, 1.0, 1.0, opacity); :data:`opacity` is a :class:`~kivy.properties.NumericProperty`, default to 1.0. """ def on_opacity(self, instance, value): canvas = self.canvas if canvas is not None: canvas.opacity = value canvas = None """Canvas of the widget.
class Widget(EventDispatcher): '''Widget class. See module documentation for more information. :Events: `on_touch_down`: Fired when a new touch happens `on_touch_move`: Fired when an existing touch is moved `on_touch_up`: Fired when an existing touch disappears .. versionchanged:: 1.0.9 Everything related to event properties has been moved to :class:`~kivy.event.EventDispatcher`. Event properties can now be used in contructing a simple class, without subclassing :class:`Widget`. .. versionchanged:: 1.5.0 Constructor now accept on_* arguments to automatically bind callbacks to properties or events, as the Kv language. ''' __metaclass__ = WidgetMetaclass def __init__(self, **kwargs): # Before doing anything, ensure the windows exist. EventLoop.ensure_window() # Register touch events self.register_event_type('on_touch_down') self.register_event_type('on_touch_move') self.register_event_type('on_touch_up') super(Widget, self).__init__(**kwargs) # Create the default canvas if not exist if self.canvas is None: self.canvas = Canvas(opacity=self.opacity) # Apply all the styles if '__no_builder' not in kwargs: #current_root = Builder.idmap.get('root') #Builder.idmap['root'] = self Builder.apply(self) #if current_root is not None: # Builder.idmap['root'] = current_root #else: # Builder.idmap.pop('root') # Bind all the events for argument in kwargs: if argument[:3] == 'on_': self.bind(**{argument: kwargs[argument]}) # # Collision # def collide_point(self, x, y): '''Check if a point (x, y) is inside the widget's axis aligned bounding box. :Parameters: `x`: numeric X position of the point (in window coordinates) `y`: numeric Y position of the point (in window coordinates) :Returns: bool, True if the point is inside the bounding box. >>> Widget(pos=(10, 10), size=(50, 50)).collide_point(40, 40) True ''' return self.x <= x <= self.right and self.y <= y <= self.top def collide_widget(self, wid): '''Check if the other widget collides with this widget. Performs an axis-aligned bounding box intersection test by default. :Parameters: `wid`: :class:`Widget` class Widget to collide with. :Returns: bool, True if the other widget collides with this widget. >>> wid = Widget(size=(50, 50)) >>> wid2 = Widget(size=(50, 50), pos=(25, 25)) >>> wid.collide_widget(wid2) True >>> wid2.pos = (55, 55) >>> wid.collide_widget(wid2) False ''' if self.right < wid.x: return False if self.x > wid.right: return False if self.top < wid.y: return False if self.y > wid.top: return False return True # # Default event handlers # def on_touch_down(self, touch): '''Receive a touch down event. :Parameters: `touch`: :class:`~kivy.input.motionevent.MotionEvent` class Touch received :Returns: bool. If True, the dispatching of the touch will stop. ''' for child in self.children[:]: if child.dispatch('on_touch_down', touch): return True def on_touch_move(self, touch): '''Receive a touch move event. See :meth:`on_touch_down` for more information ''' for child in self.children[:]: if child.dispatch('on_touch_move', touch): return True def on_touch_up(self, touch): '''Receive a touch up event. See :meth:`on_touch_down` for more information ''' for child in self.children[:]: if child.dispatch('on_touch_up', touch): return True # # Tree management # def add_widget(self, widget, index=0): '''Add a new widget as a child of this widget. :Parameters: `widget`: :class:`Widget` Widget to add to our list of children. `index`: int, default to 0 *(this attribute have been added in 1.0.5)* Index to insert the widget in the list >>> root = Widget() >>> root.add_widget(Button()) >>> slider = Slider() >>> root.add_widget(slider) ''' if widget is self: raise WidgetException('You cannot add yourself in a Widget') if not isinstance(widget, Widget): raise WidgetException( 'add_widget() can be used only with Widget classes.') parent = widget.parent # check if widget is already a child of another widget if parent: raise WidgetException('Cannot add %r, it already has a parent %r' % (widget, parent)) widget.parent = self if index == 0 or len(self.children) == 0: self.children.insert(0, widget) self.canvas.add(widget.canvas) else: canvas = self.canvas children = self.children if index >= len(children): index = len(children) next_index = -1 else: next_child = children[index] next_index = canvas.indexof(next_child.canvas) if next_index == -1: next_index = canvas.length() else: next_index += 1 children.insert(index, widget) canvas.insert(next_index, widget.canvas) def remove_widget(self, widget): '''Remove a widget from the children of this widget. :Parameters: `widget`: :class:`Widget` Widget to remove from our children list. >>> root = Widget() >>> button = Button() >>> root.add_widget(button) >>> root.remove_widget(button) ''' if widget not in self.children: return self.children.remove(widget) self.canvas.remove(widget.canvas) widget.parent = None def clear_widgets(self): '''Remove all widgets added to this widget. ''' remove_widget = self.remove_widget for child in self.children[:]: remove_widget(child) def get_root_window(self): '''Return the root window. :Returns: Instance of the root window. Can be :class:`~kivy.core.window.WindowBase` or :class:`Widget` ''' if self.parent: return self.parent.get_root_window() def get_parent_window(self): '''Return the parent window. :Returns: Instance of the parent window. Can be :class:`~kivy.core.window.WindowBase` or :class:`Widget` ''' if self.parent: return self.parent.get_parent_window() def to_widget(self, x, y, relative=False): '''Convert the given coordinate from window to local widget coordinates. ''' if self.parent: x, y = self.parent.to_widget(x, y) return self.to_local(x, y, relative=relative) def to_window(self, x, y, initial=True, relative=False): '''Transform local coordinates to window coordinates.''' if not initial: x, y = self.to_parent(x, y, relative=relative) if self.parent: return self.parent.to_window(x, y, initial=False, relative=relative) return (x, y) def to_parent(self, x, y, relative=False): '''Transform local coordinates to parent coordinates. :Parameters: `relative`: bool, default to False Change to True if you want to translate relative positions from widget to its parent. ''' if relative: return (x + self.x, y + self.y) return (x, y) def to_local(self, x, y, relative=False): '''Transform parent coordinates to local coordinates. :Parameters: `relative`: bool, default to False Change to True if you want to translate coordinates to relative widget coordinates. ''' if relative: return (x - self.x, y - self.y) return (x, y) x = NumericProperty(0) '''X position of the widget. :data:`x` is a :class:`~kivy.properties.NumericProperty`, default to 0. ''' y = NumericProperty(0) '''Y position of the widget. :data:`y` is a :class:`~kivy.properties.NumericProperty`, default to 0. ''' width = NumericProperty(100) '''Width of the widget. :data:`width` is a :class:`~kivy.properties.NumericProperty`, default to 100. ''' height = NumericProperty(100) '''Height of the widget. :data:`height` is a :class:`~kivy.properties.NumericProperty`, default to 100. ''' pos = ReferenceListProperty(x, y) '''Position of the widget. :data:`pos` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`x`, :data:`y`) properties. ''' size = ReferenceListProperty(width, height) '''Size of the widget. :data:`size` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`width`, :data:`height`) properties. ''' def get_right(self): return self.x + self.width def set_right(self, value): self.x = value - self.width right = AliasProperty(get_right, set_right, bind=('x', 'width')) '''Right position of the widget. :data:`right` is a :class:`~kivy.properties.AliasProperty` of (:data:`x` + :data:`width`) ''' def get_top(self): return self.y + self.height def set_top(self, value): self.y = value - self.height top = AliasProperty(get_top, set_top, bind=('y', 'height')) '''Top position of the widget. :data:`top` is a :class:`~kivy.properties.AliasProperty` of (:data:`y` + :data:`height`) ''' def get_center_x(self): return self.x + self.width / 2. def set_center_x(self, value): self.x = value - self.width / 2. center_x = AliasProperty(get_center_x, set_center_x, bind=('x', 'width')) '''X center position of the widget. :data:`center_x` is a :class:`~kivy.properties.AliasProperty` of (:data:`x` + :data:`width` / 2.) ''' def get_center_y(self): return self.y + self.height / 2. def set_center_y(self, value): self.y = value - self.height / 2. center_y = AliasProperty(get_center_y, set_center_y, bind=('y', 'height')) '''Y center position of the widget. :data:`center_y` is a :class:`~kivy.properties.AliasProperty` of (:data:`y` + :data:`height` / 2.) ''' center = ReferenceListProperty(center_x, center_y) '''Center position of the widget. :data:`center` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`center_x`, :data:`center_y`) ''' cls = ListProperty([]) '''Class of the widget, used for styling. ''' id = StringProperty(None, allownone=True) '''Unique identifier of the widget in the tree. :data:`id` is a :class:`~kivy.properties.StringProperty`, default to None. .. warning:: If the :data:`id` is already used in the tree, an exception will be raised. ''' children = ListProperty([]) '''List of children of this widget. :data:`children` is a :class:`~kivy.properties.ListProperty` instance, default to an empty list. Use :meth:`add_widget` and :meth:`remove_widget` for manipulating the children list. Don't manipulate the children list directly until you know what you are doing. ''' parent = ObjectProperty(None, allownone=True) '''Parent of this widget. :data:`parent` is a :class:`~kivy.properties.ObjectProperty` instance, default to None. The parent of a widget is set when the widget is added to another one, and unset when the widget is removed from its parent. ''' size_hint_x = NumericProperty(1, allownone=True) '''X size hint. Represents how much space the widget should use in the direction of the X axis, relative to its parent's width. Only :class:`~kivy.uix.layout.Layout` and :class:`~kivy.core.window.Window` make use of the hint. The value is in percent as a float from 0. to 1., where 1. means the full size of his parent. 0.5 represents 50%. :data:`size_hint_x` is a :class:`~kivy.properties.NumericProperty`, default to 1. ''' size_hint_y = NumericProperty(1, allownone=True) '''Y size hint. :data:`size_hint_y` is a :class:`~kivy.properties.NumericProperty`, default to 1. See :data:`size_hint_x` for more information ''' size_hint = ReferenceListProperty(size_hint_x, size_hint_y) '''Size hint. :data:`size_hint` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`size_hint_x`, :data:`size_hint_y`) See :data:`size_hint_x` for more information ''' pos_hint = ObjectProperty({}) '''Position hint. This property allows you to set the position of the widget inside its parent layout, in percent (similar to size_hint). For example, if you want to set the top of the widget to be at 90% height of its parent layout, you can write: widget = Widget(pos_hint={'top': 0.9}) The keys 'x', 'right', 'center_x', will use the parent width. The keys 'y', 'top', 'center_y', will use the parent height. See :doc:`api-kivy.uix.floatlayout` for further reference. Position hint is only used in :class:`~kivy.uix.floatlayout.FloatLayout` and :class:`~kivy.core.window.Window`. :data:`pos_hint` is a :class:`~kivy.properties.ObjectProperty` containing a dict. ''' opacity = NumericProperty(1.0) '''Opacity of the widget and all the children. .. versionadded:: 1.4.1 The opacity attribute controls the opacity of the widget and its children. Be careful, it's a cumulative attribute: the value is multiplied to the current global opacity, and the result is applied to the current context color. For example: if your parent have an opacity of 0.5, and one children have an opacity of 0.2, the real opacity of the children will be 0.5 * 0.2 = 0.1. Then, the opacity is applied on the shader as:: frag_color = color * vec4(1.0, 1.0, 1.0, opacity); :data:`opacity` is a :class:`~kivy.properties.NumericProperty`, default to 1.0. ''' def on_opacity(self, instance, value): canvas = self.canvas if canvas is not None: canvas.opacity = value canvas = None '''Canvas of the widget.
class Widget(WidgetBase): '''Widget class. See module documentation for more information. :Events: `on_touch_down`: Fired when a new touch event occurs `on_touch_move`: Fired when an existing touch moves `on_touch_up`: Fired when an existing touch disappears .. versionchanged:: 1.0.9 Everything related to event properties has been moved to the :class:`~kivy.event.EventDispatcher`. Event properties can now be used when contructing a simple class without subclassing :class:`Widget`. .. versionchanged:: 1.5.0 The constructor now accepts on_* arguments to automatically bind callbacks to properties or events, as in the Kv language. ''' __metaclass__ = WidgetMetaclass __events__ = ('on_touch_down', 'on_touch_move', 'on_touch_up') def __init__(self, **kwargs): # Before doing anything, ensure the windows exist. EventLoop.ensure_window() # assign the default context of the widget creation if not hasattr(self, '_context'): self._context = get_current_context() super(Widget, self).__init__(**kwargs) # Create the default canvas if not exist if self.canvas is None: self.canvas = Canvas(opacity=self.opacity) # Apply all the styles if '__no_builder' not in kwargs: #current_root = Builder.idmap.get('root') #Builder.idmap['root'] = self Builder.apply(self) #if current_root is not None: # Builder.idmap['root'] = current_root #else: # Builder.idmap.pop('root') # Bind all the events for argument in kwargs: if argument[:3] == 'on_': self.bind(**{argument: kwargs[argument]}) @property def proxy_ref(self): '''Return a proxy reference to the widget, i.e. without creating a reference to the widget. See `weakref.proxy <http://docs.python.org/2/library/weakref.html?highlight\ =proxy#weakref.proxy>`_ for more information. .. versionadded:: 1.7.2 ''' if hasattr(self, '_proxy_ref'): return self._proxy_ref f = partial(_widget_destructor, self.uid) self._proxy_ref = _proxy_ref = proxy(self, f) # only f should be enough here, but it appears that is a very # specific case, the proxy destructor is not called if both f and # _proxy_ref are not together in a tuple _widget_destructors[self.uid] = (f, _proxy_ref) return _proxy_ref def __eq__(self, other): if not isinstance(other, Widget): return False return self.proxy_ref is other.proxy_ref def __hash__(self): return id(self) @property def __self__(self): return self # # Collision # def collide_point(self, x, y): '''Check if a point (x, y) is inside the widget's axis aligned bounding box. :Parameters: `x`: numeric X position of the point (in window coordinates) `y`: numeric Y position of the point (in window coordinates) :Returns: bool, True if the point is inside the bounding box. >>> Widget(pos=(10, 10), size=(50, 50)).collide_point(40, 40) True ''' return self.x <= x <= self.right and self.y <= y <= self.top def collide_widget(self, wid): '''Check if the other widget collides with this widget. Performs an axis-aligned bounding box intersection test by default. :Parameters: `wid`: :class:`Widget` class Widget to collide with. :Returns: bool, True if the other widget collides with this widget. >>> wid = Widget(size=(50, 50)) >>> wid2 = Widget(size=(50, 50), pos=(25, 25)) >>> wid.collide_widget(wid2) True >>> wid2.pos = (55, 55) >>> wid.collide_widget(wid2) False ''' if self.right < wid.x: return False if self.x > wid.right: return False if self.top < wid.y: return False if self.y > wid.top: return False return True # # Default event handlers # def on_touch_down(self, touch): '''Receive a touch down event. :Parameters: `touch`: :class:`~kivy.input.motionevent.MotionEvent` class Touch received. The touch is in parent coordinates. See :mod:`~kivy.uix.relativelayout` for a discussion on coordinate systems. :Returns: bool. If True, the dispatching of the touch event will stop. ''' if self.disabled and self.collide_point(*touch.pos): return True for child in self.children[:]: if child.dispatch('on_touch_down', touch): return True def on_touch_move(self, touch): '''Receive a touch move event. The touch is in parent coordinates. See :meth:`on_touch_down` for more information. ''' if self.disabled: return for child in self.children[:]: if child.dispatch('on_touch_move', touch): return True def on_touch_up(self, touch): '''Receive a touch up event. The touch is in parent coordinates. See :meth:`on_touch_down` for more information. ''' if self.disabled: return for child in self.children[:]: if child.dispatch('on_touch_up', touch): return True def on_disabled(self, instance, value): for child in self.children: child.disabled = value # # Tree management # def add_widget(self, widget, index=0): '''Add a new widget as a child of this widget. :Parameters: `widget`: :class:`Widget` Widget to add to our list of children. `index`: int, defaults to 0 Index to insert the widget in the list .. versionadded:: 1.0.5 >>> from kivy.uix.button import Button >>> from kivy.uix.slider import Slider >>> root = Widget() >>> root.add_widget(Button()) >>> slider = Slider() >>> root.add_widget(slider) ''' if not isinstance(widget, Widget): raise WidgetException( 'add_widget() can be used only with Widget classes.') widget = widget.__self__ if widget is self: raise WidgetException('You cannot add yourself in a Widget') parent = widget.parent # check if widget is already a child of another widget if parent: raise WidgetException('Cannot add %r, it already has a parent %r' % (widget, parent)) widget.parent = parent = self # child will be disabled if added to a disabled parent if parent.disabled: widget.disabled = True if index == 0 or len(self.children) == 0: self.children.insert(0, widget) self.canvas.add(widget.canvas) else: canvas = self.canvas children = self.children if index >= len(children): index = len(children) next_index = 0 else: next_child = children[index] next_index = canvas.indexof(next_child.canvas) if next_index == -1: next_index = canvas.length() else: next_index += 1 children.insert(index, widget) # we never want to insert widget _before_ canvas.before. if next_index == 0 and canvas.has_before: next_index = 1 canvas.insert(next_index, widget.canvas) def remove_widget(self, widget): '''Remove a widget from the children of this widget. :Parameters: `widget`: :class:`Widget` Widget to remove from our children list. >>> from kivy.uix.button import Button >>> root = Widget() >>> button = Button() >>> root.add_widget(button) >>> root.remove_widget(button) ''' if widget not in self.children: return self.children.remove(widget) self.canvas.remove(widget.canvas) widget.parent = None def clear_widgets(self, children=None): '''Remove all widgets added to this widget. .. versionchanged:: 1.8.0 `children` argument can be used to select the children we want to remove. It should be a list of children (or filtered list) of the current widget. ''' if not children: children = self.children remove_widget = self.remove_widget for child in children[:]: remove_widget(child) def export_to_png(self, filename, *args): '''Saves an image of the widget and its children in png format at the specified filename. Works by removing the widget canvas from its parent, rendering to an :class:`~kivy.graphics.fbo.Fbo`, and calling :meth:`~kivy.graphics.texture.Texture.save`. .. note:: The image includes only this widget and its children. If you want to include widgets elsewhere in the tree, you must call :meth:`~Widget.export_to_png` from their common parent, or use :meth:`~kivy.core.window.Window.screenshot` to capture the whole window. .. note:: The image will be saved in png format, you should include the extension in your filename. .. versionadded:: 1.8.1 ''' if self.parent is not None: canvas_parent_index = self.parent.canvas.indexof(self.canvas) self.parent.canvas.remove(self.canvas) fbo = Fbo(size=self.size) with fbo: ClearColor(0, 0, 0, 1) ClearBuffers() Translate(-self.x, -self.y, 0) fbo.add(self.canvas) fbo.draw() fbo.texture.save(filename) fbo.remove(self.canvas) if self.parent is not None: self.parent.canvas.insert(canvas_parent_index, self.canvas) return True def get_root_window(self): '''Return the root window. :Returns: Instance of the root window. Can be a :class:`~kivy.core.window.WindowBase` or :class:`Widget`. ''' if self.parent: return self.parent.get_root_window() def get_parent_window(self): '''Return the parent window. :Returns: Instance of the parent window. Can be a :class:`~kivy.core.window.WindowBase` or :class:`Widget`. ''' if self.parent: return self.parent.get_parent_window() def _walk(self, restrict=False, loopback=False, index=None): # we pass index only when we are going on the parent. # so don't yield the parent as well. if index is None: index = len(self.children) yield self for child in reversed(self.children[:index]): for walk_child in child._walk(restrict=True): yield walk_child # if we want to continue with our parent, just do it if not restrict: parent = self.parent try: if parent is None or not isinstance(parent, Widget): raise ValueError index = parent.children.index(self) except ValueError: # self is root, if wanted to loopback from first element then -> if not loopback: return # if we started with root (i.e. index==None), then we have to # start from root again, so we return self again. Otherwise, we # never returned it, so return it now starting with it parent = self index = None for walk_child in parent._walk(loopback=loopback, index=index): yield walk_child def walk(self, restrict=False, loopback=False): ''' Iterator that walks the widget tree starting with this widget and goes forward returning widgets in the order in which layouts display them. :Parameters: `restrict`: If True, it will only iterate through the widget and its children (or children of its children etc.). Defaults to False. `loopback`: If True, when the last widget in the tree is reached, it'll loop back to the uppermost root and start walking until we hit this widget again. Naturally, it can only loop back when `restrict` is False. Defaults to False. :return: A generator that walks the tree, returning widgets in the forward layout order. For example, given a tree with the following structure:: GridLayout: Button BoxLayout: id: box Widget Button Widget walking this tree:: >>> # Call walk on box with loopback True, and restrict False >>> [type(widget) for widget in box.walk(loopback=True)] [<class 'BoxLayout'>, <class 'Widget'>, <class 'Button'>, <class 'Widget'>, <class 'GridLayout'>, <class 'Button'>] >>> # Now with loopback False, and restrict False >>> [type(widget) for widget in box.walk()] [<class 'BoxLayout'>, <class 'Widget'>, <class 'Button'>, <class 'Widget'>] >>> # Now with restrict True >>> [type(widget) for widget in box.walk(restrict=True)] [<class 'BoxLayout'>, <class 'Widget'>, <class 'Button'>] .. versionadded:: 1.8.1 ''' gen = self._walk(restrict, loopback) yield next(gen) for node in gen: if node is self: return yield node def _walk_reverse(self, loopback=False, go_up=False): # process is walk up level, walk down its children tree, then walk up # next level etc. # default just walk down the children tree root = self index = 0 # we need to go up a level before walking tree if go_up: root = self.parent try: if root is None or not isinstance(root, Widget): raise ValueError index = root.children.index(self) + 1 except ValueError: if not loopback: return index = 0 go_up = False root = self # now walk children tree starting with last-most child for child in islice(root.children, index, None): for walk_child in child._walk_reverse(loopback=loopback): yield walk_child # we need to return ourself last, in all cases yield root # if going up, continue walking up the parent tree if go_up: for walk_child in root._walk_reverse(loopback=loopback, go_up=go_up): yield walk_child def walk_reverse(self, loopback=False): ''' Iterator that walks the widget tree backwards starting with the widget before this, and going backwards returning widgets in the reverse order in which layouts display them. This walks in the opposite direction of :meth:`walk`, so a list of the tree generated with :meth:`walk` will be in reverse order compared to the list generated with this, provided `loopback` is True. :Parameters: `loopback`: If True, when the uppermost root in the tree is reached, it'll loop back to the last widget and start walking back until after we hit widget again. Defaults to False :return: A generator that walks the tree, returning widgets in the reverse layout order. For example, given a tree with the following structure:: GridLayout: Button BoxLayout: id: box Widget Button Widget walking this tree:: >>> # Call walk on box with loopback True >>> [type(widget) for widget in box.walk_reverse(loopback=True)] [<class 'Button'>, <class 'GridLayout'>, <class 'Widget'>, <class 'Button'>, <class 'Widget'>, <class 'BoxLayout'>] >>> # Now with loopback False >>> [type(widget) for widget in box.walk_reverse()] [<class 'Button'>, <class 'GridLayout'>] >>> forward = [w for w in box.walk(loopback=True)] >>> backward = [w for w in box.walk_reverse(loopback=True)] >>> forward == backward[::-1] True .. versionadded:: 1.8.1 ''' for node in self._walk_reverse(loopback=loopback, go_up=True): yield node if node is self: return def to_widget(self, x, y, relative=False): '''Convert the given coordinate from window to local widget coordinates. See :mod:`~kivy.uix.relativelayout` for details on the coordinate systems. ''' if self.parent: x, y = self.parent.to_widget(x, y) return self.to_local(x, y, relative=relative) def to_window(self, x, y, initial=True, relative=False): '''Transform local coordinates to window coordinates. See :mod:`~kivy.uix.relativelayout` for details on the coordinate systems. ''' if not initial: x, y = self.to_parent(x, y, relative=relative) if self.parent: return self.parent.to_window(x, y, initial=False, relative=relative) return (x, y) def to_parent(self, x, y, relative=False): '''Transform local coordinates to parent coordinates. See :mod:`~kivy.uix.relativelayout` for details on the coordinate systems. :Parameters: `relative`: bool, defaults to False Change to True if you want to translate relative positions from a widget to its parent coordinates. ''' if relative: return (x + self.x, y + self.y) return (x, y) def to_local(self, x, y, relative=False): '''Transform parent coordinates to local coordinates. See :mod:`~kivy.uix.relativelayout` for details on the coordinate systems. :Parameters: `relative`: bool, defaults to False Change to True if you want to translate coordinates to relative widget coordinates. ''' if relative: return (x - self.x, y - self.y) return (x, y) x = NumericProperty(0) '''X position of the widget. :attr:`x` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' y = NumericProperty(0) '''Y position of the widget. :attr:`y` is a :class:`~kivy.properties.NumericProperty` and defaults to 0. ''' width = NumericProperty(100) '''Width of the widget. :attr:`width` is a :class:`~kivy.properties.NumericProperty` ans defaults to 100. .. warning:: Keep in mind that the `width` property is subject to layout logic and that this has not yet happened at the time of the widget's `__init__` method. ''' height = NumericProperty(100) '''Height of the widget. :attr:`height` is a :class:`~kivy.properties.NumericProperty` and defaults to 100. .. warning:: Keep in mind that the `height` property is subject to layout logic and that this has not yet happened at the time of the widget's `__init__` method. ''' pos = ReferenceListProperty(x, y) '''Position of the widget. :attr:`pos` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`x`, :attr:`y`) properties. ''' size = ReferenceListProperty(width, height) '''Size of the widget. :attr:`size` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`width`, :attr:`height`) properties. ''' def get_right(self): return self.x + self.width def set_right(self, value): self.x = value - self.width right = AliasProperty(get_right, set_right, bind=('x', 'width')) '''Right position of the widget. :attr:`right` is an :class:`~kivy.properties.AliasProperty` of (:attr:`x` + :attr:`width`), ''' def get_top(self): return self.y + self.height def set_top(self, value): self.y = value - self.height top = AliasProperty(get_top, set_top, bind=('y', 'height')) '''Top position of the widget. :attr:`top` is an :class:`~kivy.properties.AliasProperty` of (:attr:`y` + :attr:`height`), ''' def get_center_x(self): return self.x + self.width / 2. def set_center_x(self, value): self.x = value - self.width / 2. center_x = AliasProperty(get_center_x, set_center_x, bind=('x', 'width')) '''X center position of the widget. :attr:`center_x` is an :class:`~kivy.properties.AliasProperty` of (:attr:`x` + :attr:`width` / 2.), ''' def get_center_y(self): return self.y + self.height / 2. def set_center_y(self, value): self.y = value - self.height / 2. center_y = AliasProperty(get_center_y, set_center_y, bind=('y', 'height')) '''Y center position of the widget. :attr:`center_y` is an :class:`~kivy.properties.AliasProperty` of (:attr:`y` + :attr:`height` / 2.) ''' center = ReferenceListProperty(center_x, center_y) '''Center position of the widget. :attr:`center` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`center_x`, :attr:`center_y`) ''' cls = ListProperty([]) '''Class of the widget, used for styling. ''' id = StringProperty(None, allownone=True) '''Unique identifier of the widget in the tree. :attr:`id` is a :class:`~kivy.properties.StringProperty` and defaults to None. .. warning:: If the :attr:`id` is already used in the tree, an exception will be raised. ''' children = ListProperty([]) '''List of children of this widget. :attr:`children` is a :class:`~kivy.properties.ListProperty` and defaults to an empty list. Use :meth:`add_widget` and :meth:`remove_widget` for manipulating the children list. Don't manipulate the children list directly unless you know what you are doing. ''' parent = ObjectProperty(None, allownone=True) '''Parent of this widget. :attr:`parent` is an :class:`~kivy.properties.ObjectProperty` and defaults to None. The parent of a widget is set when the widget is added to another widget and unset when the widget is removed from its parent. ''' size_hint_x = NumericProperty(1, allownone=True) '''X size hint. Represents how much space the widget should use in the direction of the X axis relative to its parent's width. Only the :class:`~kivy.uix.layout.Layout` and :class:`~kivy.core.window.Window` classes make use of the hint. The value is in percent as a float from 0. to 1., where 1. means the full size of his parent. 0.5 represents 50%. :attr:`size_hint_x` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. ''' size_hint_y = NumericProperty(1, allownone=True) '''Y size hint. :attr:`size_hint_y` is a :class:`~kivy.properties.NumericProperty` and defaults to 1. See :attr:`size_hint_x` for more information ''' size_hint = ReferenceListProperty(size_hint_x, size_hint_y) '''Size hint. :attr:`size_hint` is a :class:`~kivy.properties.ReferenceListProperty` of (:attr:`size_hint_x`, :attr:`size_hint_y`). See :attr:`size_hint_x` for more information ''' pos_hint = ObjectProperty({}) '''Position hint. This property allows you to set the position of the widget inside its parent layout, in percent (similar to size_hint). For example, if you want to set the top of the widget to be at 90% height of its parent layout, you can write: widget = Widget(pos_hint={'top': 0.9}) The keys 'x', 'right' and 'center_x' will use the parent width. The keys 'y', 'top' and 'center_y' will use the parent height. See :doc:`api-kivy.uix.floatlayout` for further reference. Position hint is only used by the :class:`~kivy.uix.floatlayout.FloatLayout` and :class:`~kivy.core.window.Window`. :attr:`pos_hint` is an :class:`~kivy.properties.ObjectProperty` containing a dict. ''' ids = DictProperty({}) '''This is a Dictionary of id's defined in your kv language. This will only be populated if you use id's in your kv language code. .. versionadded:: 1.7.0 :attr:`ids` is a :class:`~kivy.properties.DictProperty` and defaults to a empty dict {}. ''' opacity = NumericProperty(1.0) '''Opacity of the widget and all the children. .. versionadded:: 1.4.1 The opacity attribute controls the opacity of the widget and its children. Be careful, it's a cumulative attribute: the value is multiplied by the current global opacity and the result is applied to the current context color. For example, if the parent has an opacity of 0.5 and a child has an opacity of 0.2, the real opacity of the child will be 0.5 * 0.2 = 0.1. Then, the opacity is applied by the shader as:: frag_color = color * vec4(1.0, 1.0, 1.0, opacity); :attr:`opacity` is a :class:`~kivy.properties.NumericProperty` and defaults to 1.0. ''' def on_opacity(self, instance, value): canvas = self.canvas if canvas is not None: canvas.opacity = value canvas = None '''Canvas of the widget. The canvas is a graphics object that contains all the drawing instructions for the graphical representation of the widget. There are no general properties for the Widget class, such as background color, to keep the design simple and lean. Some derived classes, such as Button, do add such convenience properties but generally the developer is responsible for implementing the graphics representation for a custom widget from the ground up. See the derived widget classes for patterns to follow and extend. See :class:`~kivy.graphics.Canvas` for more information about the usage. ''' disabled = BooleanProperty(False) '''Indicates whether this widget can interact with input or not.
class GeoJsonMapLayer(MapLayer): source = StringProperty() geojson = ObjectProperty() initial_zoom = None first_time = True def __init__(self, **kwargs): super(GeoJsonMapLayer, self).__init__(**kwargs) with self.canvas: self.scissor = ScissorPush(x=0, y=0, width=100, height=100) self.canvas_polygon = Canvas() self.canvas_line = Canvas() ScissorPop() with self.canvas_polygon.before: PushMatrix() self.g_matrix = MatrixInstruction() self.g_scale = Scale() self.g_translate = Translate() with self.canvas_polygon: self.g_canvas_polygon = Canvas() with self.canvas_polygon.after: PopMatrix() def reposition(self): self.scissor.x = self.parent.parent.x self.scissor.width = self.parent.parent.width self.scissor.y = self.parent.parent.y self.scissor.height = self.parent.parent.height vx, vy = self.parent.delta_x, self.parent.delta_y pzoom = self.parent.zoom zoom = self.initial_zoom if zoom is None: self.initial_zoom = zoom = pzoom if zoom != pzoom: diff = 2 ** (pzoom - zoom) vx /= diff vy /= diff self.g_scale.x = self.g_scale.y = diff else: self.g_scale.x = self.g_scale.y = 1. self.g_translate.xy = vx, vy self.g_matrix.matrix = self.parent._scatter.transform if self.geojson: update = not self.first_time self.on_geojson(self, self.geojson, update=update) self.first_time = False def on_geojson(self, instance, geojson, update=False): if self.parent is None: return if not update: # print "Reload geojson (polygon)" self.g_canvas_polygon.clear() self._geojson_part(geojson, geotype="Polygon") # print "Reload geojson (LineString)" self.canvas_line.clear() self._geojson_part(geojson, geotype="LineString") def on_source(self, instance, value): if value.startswith("http://") or value.startswith("https://"): Downloader.instance().download(value, self._load_geojson_url) else: # "rb" was replaced by "r" to avoid this error: # TypeError: the JSON object must be str, not 'bytes' with open(value, "r") as fd: geojson = json.load(fd) self.geojson = geojson def _load_geojson_url(self, url, r): self.geojson = r.json() def _geojson_part(self, part, geotype=None): tp = part["type"] if tp == "FeatureCollection": for feature in part["features"]: if geotype and feature["geometry"]["type"] != geotype: continue self._geojson_part_f(feature) elif tp == "Feature": if geotype and part["geometry"]["type"] == geotype: self._geojson_part_f(part) else: # unhandled geojson part pass def _geojson_part_f(self, feature): properties = feature["properties"] geometry = feature["geometry"] graphics = self._geojson_part_geometry(geometry, properties) for g in graphics: tp = geometry["type"] if tp == "Polygon": self.g_canvas_polygon.add(g) else: self.canvas_line.add(g) def _geojson_part_geometry(self, geometry, properties): from kivy.graphics import Mesh, Line, Color from kivy.graphics.tesselator import Tesselator, WINDING_ODD, TYPE_POLYGONS from kivy.utils import get_color_from_hex from kivy.metrics import dp tp = geometry["type"] graphics = [] if tp == "Polygon": tess = Tesselator() for c in geometry["coordinates"]: xy = list(self._lonlat_to_xy(c)) xy = flatten(xy) tess.add_contour(xy) tess.tesselate(WINDING_ODD, TYPE_POLYGONS) color = self._get_color_from(properties.get("color", "FF000088")) graphics.append(Color(*color)) for vertices, indices in tess.meshes: graphics.append(Mesh(vertices=vertices, indices=indices, mode="triangle_fan")) elif tp == "LineString": stroke = get_color_from_hex(properties.get("stroke", "#ff000088")) stroke_width = dp(properties.get("stroke-width", 2)) xy = list(self._lonlat_to_xy(geometry["coordinates"])) xy = flatten(xy) graphics.append(Color(*stroke)) graphics.append(Line(points=xy, width=stroke_width)) return graphics def _lonlat_to_xy(self, lonlats): view = self.parent zoom = view.zoom for lon, lat in lonlats: yield view.get_window_xy_from(lat, lon, zoom) # p = view.get_window_xy_from(lat, lon, zoom) # p = p[0] - self.parent.delta_x, p[1] - self.parent.delta_y # p = self.parent._scatter.to_local(*p) # yield p def _get_color_from(self, value): color = COLORS.get(value.lower(), value) color = get_color_from_hex(color) return color
class MapView(Widget): """MapView is the widget that control the map displaying, navigation, and layers management. """ lon = NumericProperty() """Longitude at the center of the widget """ lat = NumericProperty() """Latitude at the center of the widget """ zoom = NumericProperty(0) """Zoom of the widget. Must be between :meth:`MapSource.get_min_zoom` and :meth:`MapSource.get_max_zoom`. Default to 0. """ map_source = ObjectProperty(MapSource()) """Provider of the map, default to a empty :class:`MapSource`. """ double_tap_zoom = BooleanProperty(False) """If True, this will activate the double-tap to zoom. """ pause_on_action = BooleanProperty(True) """Pause any map loading / tiles loading when an action is done. This allow better performance on mobile, but can be safely deactivated on desktop. """ snap_to_zoom = BooleanProperty(True) """When the user initiate a zoom, it will snap to the closest zoom for better graphics. The map can be blur if the map is scaled between 2 zoom. Default to True, even if it doesn't fully working yet. """ animation_duration = NumericProperty(100) """Duration to animate Tiles alpha from 0 to 1 when it's ready to show. Default to 100 as 100ms. Use 0 to deactivate. """ delta_x = NumericProperty(0) delta_y = NumericProperty(0) background_color = ListProperty([181 / 255., 208 / 255., 208 / 255., 1]) cache_dir = StringProperty(CACHE_DIR) _zoom = NumericProperty(0) _pause = BooleanProperty(False) _scale = 1. _disabled_count = 0 __events__ = ["on_map_relocated"] # Public API @property def viewport_pos(self): vx, vy = self._scatter.to_local(self.x, self.y) return vx - self.delta_x, vy - self.delta_y @property def scale(self): if self._invalid_scale: self._invalid_scale = False self._scale = self._scatter.scale return self._scale def get_bbox(self, margin=0): """Returns the bounding box from the bottom/left (lat1, lon1) to top/right (lat2, lon2). """ x1, y1 = self.to_local(0 - margin, 0 - margin) x2, y2 = self.to_local((self.width + margin), (self.height + margin)) c1 = self.get_latlon_at(x1, y1) c2 = self.get_latlon_at(x2, y2) return Bbox((c1.lat, c1.lon, c2.lat, c2.lon)) bbox = AliasProperty(get_bbox, None, bind=["lat", "lon", "_zoom"]) def unload(self): """Unload the view and all the layers. It also cancel all the remaining downloads. """ self.remove_all_tiles() def get_window_xy_from(self, lat, lon, zoom): """Returns the x/y position in the widget absolute coordinates from a lat/lon""" scale = self.scale vx, vy = self.viewport_pos ms = self.map_source x = ms.get_x(zoom, lon) - vx y = ms.get_y(zoom, lat) - vy x *= scale y *= scale x = x + self.pos[0] y = y + self.pos[1] return x, y def center_on(self, *args): """Center the map on the coordinate :class:`Coordinate`, or a (lat, lon) """ map_source = self.map_source zoom = self._zoom if len(args) == 1 and isinstance(args[0], Coordinate): coord = args[0] lat = coord.lat lon = coord.lon elif len(args) == 2: lat, lon = args else: raise Exception("Invalid argument for center_on") lon = clamp(lon, MIN_LONGITUDE, MAX_LONGITUDE) lat = clamp(lat, MIN_LATITUDE, MAX_LATITUDE) scale = self._scatter.scale x = map_source.get_x(zoom, lon) - self.center_x / scale y = map_source.get_y(zoom, lat) - self.center_y / scale self.delta_x = -x self.delta_y = -y self.lon = lon self.lat = lat self._scatter.pos = 0, 0 self.trigger_update(True) def set_zoom_at(self, zoom, x, y, scale=None): """Sets the zoom level, leaving the (x, y) at the exact same point in the view. """ zoom = clamp(zoom, self.map_source.get_min_zoom(), self.map_source.get_max_zoom()) if int(zoom) == int(self._zoom): if scale is None: return elif scale == self.scale: return scale = scale or 1. # first, rescale the scatter scatter = self._scatter scale = clamp(scale, scatter.scale_min, scatter.scale_max) rescale = scale * 1.0 / scatter.scale scatter.apply_transform(Matrix().scale(rescale, rescale, rescale), post_multiply=True, anchor=scatter.to_local(x, y)) # adjust position if the zoom changed c1 = self.map_source.get_col_count(self._zoom) c2 = self.map_source.get_col_count(zoom) if c1 != c2: f = float(c2) / float(c1) self.delta_x = scatter.x + self.delta_x * f self.delta_y = scatter.y + self.delta_y * f # back to 0 every time scatter.apply_transform(Matrix().translate(-scatter.x, -scatter.y, 0), post_multiply=True) # avoid triggering zoom changes. self._zoom = zoom self.zoom = self._zoom def on_zoom(self, instance, zoom): if zoom == self._zoom: return x = self.map_source.get_x(zoom, self.lon) - self.delta_x y = self.map_source.get_y(zoom, self.lat) - self.delta_y self.set_zoom_at(zoom, x, y) self.center_on(self.lat, self.lon) def get_latlon_at(self, x, y, zoom=None): """Return the current :class:`Coordinate` within the (x, y) widget coordinate. """ if zoom is None: zoom = self._zoom vx, vy = self.viewport_pos scale = self._scale return Coordinate(lat=self.map_source.get_lat(zoom, y / scale + vy), lon=self.map_source.get_lon(zoom, x / scale + vx)) def add_marker(self, marker, layer=None): """Add a marker into the layer. If layer is None, it will be added in the default marker layer. If there is no default marker layer, a new one will be automatically created """ if layer is None: if not self._default_marker_layer: layer = MarkerMapLayer() self.add_layer(layer) else: layer = self._default_marker_layer layer.add_widget(marker) layer.set_marker_position(self, marker) def remove_marker(self, marker): """Remove a marker from its layer """ marker.detach() def add_layer(self, layer, mode="window"): """Add a new layer to update at the same time the base tile layer. mode can be either "scatter" or "window". If "scatter", it means the layer will be within the scatter transformation. It's perfect if you want to display path / shape, but not for text. If "window", it will have no transformation. You need to position the widget yourself: think as Z-sprite / billboard. Defaults to "window". """ assert (mode in ("scatter", "window")) if self._default_marker_layer is None and \ isinstance(layer, MarkerMapLayer): self._default_marker_layer = layer self._layers.append(layer) c = self.canvas if mode == "scatter": self.canvas = self.canvas_layers else: self.canvas = self.canvas_layers_out layer.canvas_parent = self.canvas super(MapView, self).add_widget(layer) self.canvas = c def remove_layer(self, layer): """Remove the layer """ c = self.canvas self._layers.remove(layer) self.canvas = layer.canvas_parent super(MapView, self).remove_widget(layer) self.canvas = c def sync_to(self, other): """Reflect the lat/lon/zoom of the other MapView to the current one. """ if self._zoom != other._zoom: self.set_zoom_at(other._zoom, *self.center) self.center_on(other.get_latlon_at(*self.center)) # Private API def __init__(self, **kwargs): from kivy.base import EventLoop EventLoop.ensure_window() self._invalid_scale = True self._tiles = [] self._tiles_bg = [] self._tilemap = {} self._layers = [] self._default_marker_layer = None self._need_redraw_all = False self._transform_lock = False self.trigger_update(True) self.canvas = Canvas() self._scatter = MapViewScatter() self.add_widget(self._scatter) with self._scatter.canvas: self.canvas_map = Canvas() self.canvas_layers = Canvas() with self.canvas: self.canvas_layers_out = Canvas() self._scale_target_anim = False self._scale_target = 1. self._touch_count = 0 self.map_source.cache_dir = self.cache_dir Clock.schedule_interval(self._animate_color, 1 / 60.) self.lat = kwargs.get("lat", self.lat) self.lon = kwargs.get("lon", self.lon) super(MapView, self).__init__(**kwargs) def _animate_color(self, dt): # fast path d = self.animation_duration if d == 0: for tile in self._tiles: if tile.state == "need-animation": tile.g_color.a = 1. tile.state = "animated" for tile in self._tiles_bg: if tile.state == "need-animation": tile.g_color.a = 1. tile.state = "animated" else: d = d / 1000. for tile in self._tiles: if tile.state != "need-animation": continue tile.g_color.a += dt / d if tile.g_color.a >= 1: tile.state = "animated" for tile in self._tiles_bg: if tile.state != "need-animation": continue tile.g_color.a += dt / d if tile.g_color.a >= 1: tile.state = "animated" def add_widget(self, widget): if isinstance(widget, MapMarker): self.add_marker(widget) elif isinstance(widget, MapLayer): self.add_layer(widget) else: super(MapView, self).add_widget(widget) def remove_widget(self, widget): if isinstance(widget, MapMarker): self.remove_marker(widget) elif isinstance(widget, MapLayer): self.remove_layer(widget) else: super(MapView, self).remove_widget(widget) def on_map_relocated(self, zoom, coord): pass def animated_diff_scale_at(self, d, x, y): self._scale_target_time = 1. self._scale_target_pos = x, y if self._scale_target_anim is False: self._scale_target_anim = True self._scale_target = d else: self._scale_target += d Clock.unschedule(self._animate_scale) Clock.schedule_interval(self._animate_scale, 1 / 60.) def _animate_scale(self, dt): diff = self._scale_target / 3. if abs(diff) < 0.01: diff = self._scale_target self._scale_target = 0 else: self._scale_target -= diff self._scale_target_time -= dt self.diff_scale_at(diff, *self._scale_target_pos) ret = self._scale_target != 0 if not ret: self._pause = False return ret def diff_scale_at(self, d, x, y): scatter = self._scatter scale = scatter.scale * (2**d) self.scale_at(scale, x, y) def scale_at(self, scale, x, y): scatter = self._scatter scale = clamp(scale, scatter.scale_min, scatter.scale_max) rescale = scale * 1.0 / scatter.scale scatter.apply_transform(Matrix().scale(rescale, rescale, rescale), post_multiply=True, anchor=scatter.to_local(x, y)) def on_touch_down(self, touch): if not self.collide_point(*touch.pos): return if self.pause_on_action: self._pause = True if "button" in touch.profile and touch.button in ("scrolldown", "scrollup"): d = 1 if touch.button == "scrollup" else -1 self.animated_diff_scale_at(d, *touch.pos) return True elif touch.is_double_tap and self.double_tap_zoom: self.animated_diff_scale_at(1, *touch.pos) return True touch.grab(self) self._touch_count += 1 if self._touch_count == 1: self._touch_zoom = (self.zoom, self._scale) return super(MapView, self).on_touch_down(touch) def on_touch_up(self, touch): if touch.grab_current == self: touch.ungrab(self) self._touch_count -= 1 if self._touch_count == 0: # animate to the closest zoom zoom, scale = self._touch_zoom cur_zoom = self.zoom cur_scale = self._scale if cur_zoom < zoom or cur_scale < scale: self.animated_diff_scale_at(1. - cur_scale, *touch.pos) elif cur_zoom > zoom or cur_scale > scale: self.animated_diff_scale_at(2. - cur_scale, *touch.pos) self._pause = False return True return super(MapView, self).on_touch_up(touch) def on_transform(self, *args): self._invalid_scale = True if self._transform_lock: return self._transform_lock = True # recalculate viewport map_source = self.map_source zoom = self._zoom scatter = self._scatter scale = scatter.scale if scale >= 2.01: zoom += 1 scale /= 2. elif scale < 0.99: zoom -= 1 scale *= 2. zoom = clamp(zoom, map_source.min_zoom, map_source.max_zoom) if zoom != self._zoom: self.set_zoom_at(zoom, scatter.x, scatter.y, scale=scale) self.trigger_update(True) else: if zoom == map_source.min_zoom and scatter.scale < 1.: scatter.scale = 1. self.trigger_update(True) else: self.trigger_update(False) if map_source.bounds: self._apply_bounds() self._transform_lock = False self._scale = self._scatter.scale def _apply_bounds(self): # if the map_source have any constraints, apply them here. map_source = self.map_source zoom = self._zoom min_lon, min_lat, max_lon, max_lat = map_source.bounds xmin = map_source.get_x(zoom, min_lon) xmax = map_source.get_x(zoom, max_lon) ymin = map_source.get_y(zoom, min_lat) ymax = map_source.get_y(zoom, max_lat) dx = self.delta_x dy = self.delta_y oxmin, oymin = self._scatter.to_local(self.x, self.y) oxmax, oymax = self._scatter.to_local(self.right, self.top) s = self._scale cxmin = (oxmin - dx) if cxmin < xmin: self._scatter.x += (cxmin - xmin) * s cymin = (oymin - dy) if cymin < ymin: self._scatter.y += (cymin - ymin) * s cxmax = (oxmax - dx) if cxmax > xmax: self._scatter.x -= (xmax - cxmax) * s cymax = (oymax - dy) if cymax > ymax: self._scatter.y -= (ymax - cymax) * s def on__pause(self, instance, value): if not value: self.trigger_update(True) def trigger_update(self, full): self._need_redraw_full = full or self._need_redraw_full Clock.unschedule(self.do_update) Clock.schedule_once(self.do_update, -1) def do_update(self, dt): zoom = self._zoom scale = self._scale self.lon = self.map_source.get_lon( zoom, (self.center_x - self._scatter.x) / scale - self.delta_x) self.lat = self.map_source.get_lat( zoom, (self.center_y - self._scatter.y) / scale - self.delta_y) self.dispatch("on_map_relocated", zoom, Coordinate(self.lon, self.lat)) for layer in self._layers: layer.reposition() if self._need_redraw_full: self._need_redraw_full = False self.move_tiles_to_background() self.load_visible_tiles() else: self.load_visible_tiles() def bbox_for_zoom(self, vx, vy, w, h, zoom): # return a tile-bbox for the zoom map_source = self.map_source size = map_source.dp_tile_size scale = self._scale max_x_end = map_source.get_col_count(zoom) max_y_end = map_source.get_row_count(zoom) x_count = int(ceil(w / scale / float(size))) + 1 y_count = int(ceil(h / scale / float(size))) + 1 tile_x_first = int(clamp(vx / float(size), 0, max_x_end)) tile_y_first = int(clamp(vy / float(size), 0, max_y_end)) tile_x_last = tile_x_first + x_count tile_y_last = tile_y_first + y_count tile_x_last = int(clamp(tile_x_last, tile_x_first, max_x_end)) tile_y_last = int(clamp(tile_y_last, tile_y_first, max_y_end)) x_count = tile_x_last - tile_x_first y_count = tile_y_last - tile_y_first return (tile_x_first, tile_y_first, tile_x_last, tile_y_last, x_count, y_count) def load_visible_tiles(self): map_source = self.map_source vx, vy = self.viewport_pos zoom = self._zoom dirs = [0, 1, 0, -1, 0] bbox_for_zoom = self.bbox_for_zoom size = map_source.dp_tile_size tile_x_first, tile_y_first, tile_x_last, tile_y_last, \ x_count, y_count = bbox_for_zoom(vx, vy, self.width, self.height, zoom) # print "Range {},{} to {},{}".format( # tile_x_first, tile_y_first, # tile_x_last, tile_y_last) # Adjust tiles behind us for tile in self._tiles_bg[:]: tile_x = tile.tile_x tile_y = tile.tile_y f = 2**(zoom - tile.zoom) w = self.width / f h = self.height / f btile_x_first, btile_y_first, btile_x_last, btile_y_last, \ _, _ = bbox_for_zoom(vx / f, vy / f, w, h, tile.zoom) if tile_x < btile_x_first or tile_x >= btile_x_last or \ tile_y < btile_y_first or tile_y >= btile_y_last: tile.state = "done" self._tiles_bg.remove(tile) self.canvas_map.before.remove(tile.g_color) self.canvas_map.before.remove(tile) continue tsize = size * f tile.size = tsize, tsize tile.pos = (tile_x * tsize + self.delta_x, tile_y * tsize + self.delta_y) # Get rid of old tiles first for tile in self._tiles[:]: tile_x = tile.tile_x tile_y = tile.tile_y if tile_x < tile_x_first or tile_x >= tile_x_last or \ tile_y < tile_y_first or tile_y >= tile_y_last: tile.state = "done" self.tile_map_set(tile_x, tile_y, False) self._tiles.remove(tile) self.canvas_map.remove(tile) self.canvas_map.remove(tile.g_color) else: tile.size = (size, size) tile.pos = (tile_x * size + self.delta_x, tile_y * size + self.delta_y) # Load new tiles if needed x = tile_x_first + x_count // 2 - 1 y = tile_y_first + y_count // 2 - 1 arm_max = max(x_count, y_count) + 2 arm_size = 1 turn = 0 while arm_size < arm_max: for i in range(arm_size): if not self.tile_in_tile_map(x, y) and \ y >= tile_y_first and y < tile_y_last and \ x >= tile_x_first and x < tile_x_last: self.load_tile(x, y, size, zoom) x += dirs[turn % 4 + 1] y += dirs[turn % 4] if turn % 2 == 1: arm_size += 1 turn += 1 def load_tile(self, x, y, size, zoom): if self.tile_in_tile_map(x, y) or zoom != self._zoom: return self.load_tile_for_source(self.map_source, 1., size, x, y, zoom) # XXX do overlay support self.tile_map_set(x, y, True) def load_tile_for_source(self, map_source, opacity, size, x, y, zoom): tile = Tile(size=(size, size), cache_dir=self.cache_dir) tile.g_color = Color(1, 1, 1, 0) tile.tile_x = x tile.tile_y = y tile.zoom = zoom tile.pos = (x * size + self.delta_x, y * size + self.delta_y) tile.map_source = map_source tile.state = "loading" if not self._pause: map_source.fill_tile(tile) self.canvas_map.add(tile.g_color) self.canvas_map.add(tile) self._tiles.append(tile) def move_tiles_to_background(self): # remove all the tiles of the main map to the background map # retain only the one who are on the current zoom level # for all the tile in the background, stop the download if not yet started. zoom = self._zoom tiles = self._tiles btiles = self._tiles_bg canvas_map = self.canvas_map tile_size = self.map_source.tile_size # move all tiles to background while tiles: tile = tiles.pop() if tile.state == "loading": tile.state = "done" continue btiles.append(tile) # clear the canvas canvas_map.clear() canvas_map.before.clear() self._tilemap = {} # unsure if it's really needed, i personnally didn't get issues right now # btiles.sort(key=lambda z: -z.zoom) # add all the btiles into the back canvas. # except for the tiles that are owned by the current zoom level for tile in btiles[:]: if tile.zoom == zoom: btiles.remove(tile) tiles.append(tile) tile.size = tile_size, tile_size canvas_map.add(tile.g_color) canvas_map.add(tile) self.tile_map_set(tile.tile_x, tile.tile_y, True) continue canvas_map.before.add(tile.g_color) canvas_map.before.add(tile) def remove_all_tiles(self): # clear the map of all tiles. self.canvas_map.clear() self.canvas_map.before.clear() for tile in self._tiles: tile.state = "done" del self._tiles[:] del self._tiles_bg[:] self._tilemap = {} def tile_map_set(self, tile_x, tile_y, value): key = tile_y * self.map_source.get_col_count(self._zoom) + tile_x if value: self._tilemap[key] = value else: self._tilemap.pop(key, None) def tile_in_tile_map(self, tile_x, tile_y): key = tile_y * self.map_source.get_col_count(self._zoom) + tile_x return key in self._tilemap def on_size(self, instance, size): for layer in self._layers: layer.size = size self.center_on(self.lat, self.lon) self.trigger_update(True) def on_pos(self, instance, pos): self.center_on(self.lat, self.lon) self.trigger_update(True) def on_map_source(self, instance, source): if isinstance(source, string_types): self.map_source = MapSource.from_provider(source) elif isinstance(source, (tuple, list)): cache_key, min_zoom, max_zoom, url, attribution, options = source self.map_source = MapSource(url=url, cache_key=cache_key, min_zoom=min_zoom, max_zoom=max_zoom, attribution=attribution, cache_dir=self.cache_dir, **options) elif isinstance(source, MapSource): self.map_source = source else: raise Exception("Invalid map source provider") self.zoom = clamp(self.zoom, self.map_source.min_zoom, self.map_source.max_zoom) self.remove_all_tiles() self.trigger_update(True)
class Widget(EventDispatcher): """Widget class. See module documentation for more information. :Events: `on_touch_down`: Fired when a new touch appear `on_touch_move`: Fired when an existing touch is moved `on_touch_up`: Fired when an existing touch disappears """ # UID counter __widget_uid = 0 __cache_properties = {} def __new__(__cls__, *largs, **kwargs): self = super(Widget, __cls__).__new__(__cls__) # XXX for the moment, we need to create a uniq id for properties. # Properties need a identifier to the class instance. hash() and id() # are longer than using a custom __uid. I hope we can figure out a way # of doing that without require any python code. :) Widget.__widget_uid += 1 self.__dict__["__uid"] = Widget.__widget_uid cp = Widget.__cache_properties if __cls__ not in cp: attrs_found = cp[__cls__] = {} attrs = dir(__cls__) for k in attrs: attr = getattr(__cls__, k) if isinstance(attr, Property): if k in Widget_forbidden_properties: raise Exception("The property <%s> have a forbidden name" % k) attrs_found[k] = attr else: attrs_found = cp[__cls__] # First loop, link all the properties storage to our instance for k, attr in attrs_found.iteritems(): attr.link(self, k) # Second loop, resolve all the reference for k, attr in attrs_found.iteritems(): attr.link_deps(self, k) self.__properties = attrs_found # Then, return the class instance return self def __del__(self): # The thing here, since the storage of the property is inside the # Property class, we must remove ourself from the storage of each # Property. The usage is faster, the creation / deletion is longer. for attr in self.__properties.itervalues(): attr.unlink(self) def __init__(self, **kwargs): super(Widget, self).__init__() # Register touch events self.register_event_type("on_touch_down") self.register_event_type("on_touch_move") self.register_event_type("on_touch_up") # Before doing anything, ensure the windows exist. EventLoop.ensure_window() # Auto bind on own handler if exist properties = self.__properties.keys() for func in dir(self): if not func.startswith("on_"): continue name = func[3:] if name in properties: self.bind(**{name: getattr(self, func)}) # Create the default canvas if not exist if self.canvas is None: self.canvas = Canvas() # Apply the existing arguments to our widget for key, value in kwargs.iteritems(): if hasattr(self, key): setattr(self, key, value) # Apply all the styles if "__no_builder" not in kwargs: Builder.apply(self) def create_property(self, name): """Create a new property at runtime. .. warning:: This function is designed for the Kivy language, don't use it in your code. You should declare the property in your class instead of using this method. :Parameters: `name`: string Name of the property The class of the property cannot be specified, it will always be an :class:`~kivy.properties.ObjectProperty` class. The default value of the property will be None, until you set a new value. >>> mywidget = Widget() >>> mywidget.create_property('custom') >>> mywidget.custom = True >>> print mywidget.custom True """ prop = ObjectProperty(None) prop.link(self, name) prop.link_deps(self, name) self.__properties[name] = prop setattr(self.__class__, name, prop) # # Collision # def collide_point(self, x, y): """Check if a point (x, y) is inside the widget's axis aligned bounding box. :Parameters: `x`: numeric X position of the point (in window coordinates) `y`: numeric Y position of the point (in window coordinates) :Returns: bool, True if the point is inside the bounding box. >>> Widget(pos=(10, 10), size=(50, 50)).collide_point(40, 40) True """ return self.x <= x <= self.right and self.y <= y <= self.top def collide_widget(self, wid): """Check if the other widget collides with this widget. Performs an axis-aligned bounding box intersection test by default. :Parameters: `wid`: :class:`Widget` class Widget to collide with. :Returns: bool, True if the other widget collides with this widget. >>> wid = Widget(size=(50, 50)) >>> wid2 = Widget(size=(50, 50), pos=(25, 25)) >>> wid.collide_widget(wid2) True >>> wid2.pos = (55, 55) >>> wid.collide_widget(wid2) False """ if self.right < wid.x: return False if self.x > wid.right: return False if self.top < wid.y: return False if self.y > wid.top: return False return True # # Default event handlers # def on_touch_down(self, touch): """Receive a touch down event. :Parameters: `touch`: :class:`~kivy.input.motionevent.MotionEvent` class Touch received :Returns: bool. If True, the dispatching of the touch will stop. """ for child in self.children[:]: if child.dispatch("on_touch_down", touch): return True def on_touch_move(self, touch): """Receive a touch move event. See :func:`on_touch_down` for more information """ for child in self.children[:]: if child.dispatch("on_touch_move", touch): return True def on_touch_up(self, touch): """Receive a touch up event. See :func:`on_touch_down` for more information """ for child in self.children[:]: if child.dispatch("on_touch_up", touch): return True # # Events # def bind(self, **kwargs): """Bind properties or events to a handler. Example usage:: def my_x_callback(obj, value): print 'on object', obj, 'x changed to', value def my_width_callback(obj, value): print 'on object', obj, 'width changed to', value self.bind(x=my_x_callback, width=my_width_callback) """ super(Widget, self).bind(**kwargs) for key, value in kwargs.iteritems(): if key.startswith("on_"): continue self.__properties[key].bind(self, value) def unbind(self, **kwargs): """Unbind properties or events from their handler. See :func:`bind()` for more information. """ super(Widget, self).unbind(**kwargs) for key, value in kwargs.iteritems(): if key.startswith("on_"): continue self.__properties[key].unbind(self, value) # # Tree management # def add_widget(self, widget, index=0): """Add a new widget as a child of this widget. :Parameters: `widget`: :class:`Widget` Widget to add to our list of children. `index`: int, default to 0 *(this attribute have been added in 1.0.5)* Index to insert the widget in the list >>> root = Widget() >>> root.add_widget(Button()) >>> slider = Slider() >>> root.add_widget(slider) """ if not isinstance(widget, Widget): raise WidgetException("add_widget() can be used only with Widget classes.") widget.parent = self if index == 0 or len(self.children) == 0: self.children.insert(0, widget) self.canvas.add(widget.canvas) else: canvas = self.canvas children = self.children if index >= len(children): index = len(children) next_index = 0 else: next_child = children[index] next_index = canvas.indexof(next_child.canvas) if next_index == -1: next_index = canvas.length() else: next_index += 1 children.insert(index, widget) canvas.insert(next_index, widget.canvas) def remove_widget(self, widget): """Remove a widget from the children of this widget. :Parameters: `widget`: :class:`Widget` Widget to remove from our children list. >>> root = Widget() >>> button = Button() >>> root.add_widget(button) >>> root.remove_widget(button) """ if widget not in self.children: return self.children.remove(widget) self.canvas.remove(widget.canvas) widget.parent = None def clear_widgets(self): """Remove all widgets added to this widget. """ remove_widget = self.remove_widget for child in self.children[:]: remove_widget(child) def get_root_window(self): """Return the root window. :Returns: Instance of the root window. Can be :class:`~kivy.core.window.WindowBase` or :class:`Widget` """ if self.parent: return self.parent.get_root_window() def get_parent_window(self): """Return the parent window. :Returns: Instance of the parent window. Can be :class:`~kivy.core.window.WindowBase` or :class:`Widget` """ if self.parent: return self.parent.get_parent_window() def to_widget(self, x, y, relative=False): """Convert the given coordinate from window to local widget coordinates. """ if self.parent: x, y = self.parent.to_widget(x, y) return self.to_local(x, y, relative=relative) def to_window(self, x, y, initial=True, relative=False): """Transform local coordinates to window coordinates.""" if not initial: x, y = self.to_parent(x, y, relative=relative) if self.parent: return self.parent.to_window(x, y, initial=False, relative=relative) return (x, y) def to_parent(self, x, y, relative=False): """Transform local coordinates to parent coordinates. :Parameters: `relative`: bool, default to False Change to True if you want to translate relative positions from widget to its parent. """ if relative: return (x + self.x, y + self.y) return (x, y) def to_local(self, x, y, relative=False): """Transform parent coordinates to local coordinates. :Parameters: `relative`: bool, default to False Change to True if you want to translate coordinates to relative widget coordinates. """ if relative: return (x - self.x, y - self.y) return (x, y) # # Properties # def setter(self, name): """Return the setter of a property. Useful if you want to directly bind a property to another. For example, if you want to position one widget next to you :: self.bind(right=nextchild.setter('x')) """ return self.__properties[name].__set__ def getter(self, name): """Return the getter of a property. """ return self.__properties[name].__get__ x = NumericProperty(0) """X position of the widget. :data:`x` is a :class:`~kivy.properties.NumericProperty`, default to 0. """ y = NumericProperty(0) """Y position of the widget. :data:`y` is a :class:`~kivy.properties.NumericProperty`, default to 0. """ width = NumericProperty(100) """Width of the widget. :data:`width` is a :class:`~kivy.properties.NumericProperty`, default to 100. """ height = NumericProperty(100) """Height of the widget. :data:`height` is a :class:`~kivy.properties.NumericProperty`, default to 100. """ pos = ReferenceListProperty(x, y) """Position of the widget. :data:`pos` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`x`, :data:`y`) properties. """ size = ReferenceListProperty(width, height) """Size of the widget. :data:`size` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`width`, :data:`height`) properties. """ def get_right(self): return self.x + self.width def set_right(self, value): self.x = value - self.width right = AliasProperty(get_right, set_right, bind=("x", "width")) """Right position of the widget :data:`right` is a :class:`~kivy.properties.AliasProperty` of (:data:`x` + :data:`width`) """ def get_top(self): return self.y + self.height def set_top(self, value): self.y = value - self.height top = AliasProperty(get_top, set_top, bind=("y", "height")) """Top position of the widget :data:`top` is a :class:`~kivy.properties.AliasProperty` of (:data:`y` + :data:`height`) """ def get_center_x(self): return self.x + self.width / 2.0 def set_center_x(self, value): self.x = value - self.width / 2.0 center_x = AliasProperty(get_center_x, set_center_x, bind=("x", "width")) """X center position of the widget :data:`center_x` is a :class:`~kivy.properties.AliasProperty` of (:data:`x` + :data:`width` / 2.) """ def get_center_y(self): return self.y + self.height / 2.0 def set_center_y(self, value): self.y = value - self.height / 2.0 center_y = AliasProperty(get_center_y, set_center_y, bind=("y", "height")) """Y center position of the widget :data:`center_y` is a :class:`~kivy.properties.AliasProperty` of (:data:`y` + :data:`height` / 2.) """ center = ReferenceListProperty(center_x, center_y) """Center position of the widget :data:`center` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`center_x`, :data:`center_y`) """ cls = ListProperty([]) """Class of the widget, used for styling. """ id = StringProperty(None, allownone=True) """Unique identifier of the widget in the tree. :data:`id` is a :class:`~kivy.properties.StringProperty`, default to None. .. warning:: If the :data:`id` is already used in the tree, an exception will be raised. """ children = ListProperty([]) """List of children of this widget :data:`children` is a :class:`~kivy.properties.ListProperty` instance, default to an empty list. Use :func:`add_widget` and :func:`remove_widget` for manipulate children list. Don't manipulate children list directly until you know what you are doing. """ parent = ObjectProperty(None, allownone=True) """Parent of this widget :data:`parent` is a :class:`~kivy.properties.ObjectProperty` instance, default to None. The parent of a widget is set when the widget is added to another one, and unset when the widget is removed from his parent. """ size_hint_x = NumericProperty(1, allownone=True) """X size hint. It represents how much space the widget should use in the direction of the X axis, relative to its parent's width. Only :class:`~kivy.uix.layout.Layout` and :class:`~kivy.core.window.Window` make use of the hint. The value is in percent as a float from 0. to 1., where 1. means the full size of his parent, i.e. 100%. 0.5 represents 50%. :data:`size_hint_x` is a :class:`~kivy.properties.NumericProperty`, default to 1. """ size_hint_y = NumericProperty(1, allownone=True) """Y size hint. :data:`size_hint_y` is a :class:`~kivy.properties.NumericProperty`, default to 1. See :data:`size_hint_x` for more information """ size_hint = ReferenceListProperty(size_hint_x, size_hint_y) """Size hint. :data:`size_hint` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`size_hint_x`, :data:`size_hint_y`) See :data:`size_hint_x` for more information """ pos_hint = ObjectProperty({}) """Position hint. This property allows you to set the position of the widget inside its parent layout, in percent (similar to size_hint). For example, if you want to set the top of the widget to be at 90% height of its parent layout, you can write: widget = Widget(pos_hint={'top': 0.9}) The keys 'x', 'right', 'center_x', will use the parent width. The keys 'y', 'top', 'center_y', will use the parent height. Check :doc:`api-kivy.uix.floatlayout` for further reference. Position hint is only used in :class:`~kivy.uix.floatlayout.FloatLayout` and :class:`~kivy.core.window.Window`. :data:`pos_hint` is a :class:`~kivy.properties.ObjectProperty` containing a dict. """ canvas = None """Canvas of the widget.
class Widget(EventDispatcher): '''Widget class. See module documentation for more informations. :Events: `on_touch_down`: Fired when a new touch appear `on_touch_move`: Fired when an existing touch is moved `on_touch_down`: Fired when an existing touch disapear ''' # UID counter __widget_uid = 0 def __new__(__cls__, *largs, **kwargs): self = super(Widget, __cls__).__new__(__cls__) # XXX for the moment, we need to create a uniq id for properties. # Properties need a identifier to the class instance. hash() and id() # are longer than using a custom __uid. I hope we can figure out a way # of doing that without require any python code. :) Widget.__widget_uid += 1 self.__dict__['__uid'] = Widget.__widget_uid # First loop, link all the properties storage to our instance attrs_found = {} attrs = dir(__cls__) for k in attrs: attr = getattr(__cls__, k) if isinstance(attr, Property): if k in Widget_forbidden_properties: raise Exception( 'The property <%s> have a forbidden name' % k) attr.link(self, k) attrs_found[k] = attr # Second loop, resolve all the reference for k in attrs: attr = getattr(__cls__, k) if isinstance(attr, Property): attr.link_deps(self, k) self.__properties = attrs_found # Then, return the class instance return self def __del__(self): # The thing here, since the storage of the property is inside the # Property class, we must remove ourself from the storage of each # Property. The usage is faster, the creation / deletion is longer. for attr in self.__properties.itervalues(): attr.unlink(self) def __init__(self, **kwargs): super(Widget, self).__init__() # Register touch events self.register_event_type('on_touch_down') self.register_event_type('on_touch_move') self.register_event_type('on_touch_up') # Before doing anything, ensure the windows exist. EventLoop.ensure_window() # Auto bind on own handler if exist properties = self.__properties.keys() for func in dir(self): if not func.startswith('on_'): continue name = func[3:] if name in properties: self.bind(**{name: getattr(self, func)}) # Create the default canvas self.canvas = Canvas() # Apply the existing arguments to our widget for key, value in kwargs.iteritems(): if hasattr(self, key): setattr(self, key, value) # Apply all the styles if '__no_builder' not in kwargs: Builder.apply(self) def create_property(self, name): '''Create a new property at runtime. .. warning:: This function is designed for the Kivy language, don't use it in your code. You should declare the property in your class instead of using this method. :Parameters: `name`: string Name of the property The class of the property cannot be specified, it will be always an :class:`~kivy.properties.ObjectProperty` class. The default value of the property will be None, until you set a new value. >>> mywidget = Widget() >>> mywidget.create_property('custom') >>> mywidget.custom = True >>> print mywidget.custom True ''' prop = ObjectProperty(None) prop.link(self, name) prop.link_deps(self, name) self.__properties[name] = prop setattr(self, name, prop) # # Collision # def collide_point(self, x, y): '''Check if a point (x, y) is inside the widget bounding box. :Parameters: `x`: numeric X position of the point `y`: numeric Y position of the point :Returns: bool, True if the point is inside the bounding box >>> Widget(pos=(10, 10), size=(50, 50)).collide_point(40, 40) True ''' return self.x <= x <= self.right and self.y <= y <= self.top def collide_widget(self, wid): '''Check if widget (bounding box) is colliding with our widget bounding box. :Parameters: `wid`: :class:`Widget` class Widget to collide to. :Returns: bool, True if the widget is colliding us. >>> wid = Widget(size=(50, 50)) >>> wid2 = Widget(size=(50, 50), pos=(25, 25)) >>> wid.collide_widget(wid2) True >>> wid2.pos = (55, 55) >>> wid.collide_widget(wid2) False ''' if self.right < wid.x: return False if self.x > wid.right: return False if self.top < wid.y: return False if self.y > wid.top: return False return True # # Default event handlers # def on_touch_down(self, touch): '''Receive a touch down event :Parameters: `touch`: :class:`~kivy.input.motionevent.MotionEvent` class Touch received :Returns: bool. If True, the dispatching will stop. ''' for child in self.children[:]: if child.dispatch('on_touch_down', touch): return True def on_touch_move(self, touch): '''Receive a touch move event. See :func:`on_touch_down` for more information ''' for child in self.children[:]: if child.dispatch('on_touch_move', touch): return True def on_touch_up(self, touch): '''Receive a touch up event. See :func:`on_touch_down` for more information ''' for child in self.children[:]: if child.dispatch('on_touch_up', touch): return True # # Events # def bind(self, **kwargs): '''Bind properties or event to handler. Example of usage:: def my_x_callback(obj, value): print 'on object', obj', 'x changed to', value def my_width_callback(obj, value): print 'on object', obj, 'width changed to', value self.bind(x=my_x_callback, width=my_width_callback) ''' super(Widget, self).bind(**kwargs) for key, value in kwargs.iteritems(): if key.startswith('on_'): continue self.__properties[key].bind(self, value) def unbind(self, **kwargs): '''Unbind properties or event from handler See :func:`bind()` for more information. ''' super(Widget, self).unbind(**kwargs) for key, value in kwargs.iteritems(): if key.startswith('on_'): continue self.__properties[key].unbind(self, value) # # Tree management # def add_widget(self, widget): '''Add a new widget as a child of current widget :Parameters: `widget`: :class:`Widget` Widget to add in our children list. >>> root = Widget() >>> root.add_widget(Button()) >>> slider = Slider() >>> root.add_widget(slider) ''' if not isinstance(widget, Widget): raise WidgetException( 'add_widget() can be used only with Widget classes.') widget.parent = self self.children = [widget] + self.children self.canvas.add(widget.canvas) def remove_widget(self, widget): '''Remove a widget from the children of current widget :Parameters: `widget`: :class:`Widget` Widget to add in our children list. >>> root = Widget() >>> button = Button() >>> root.add_widget(button) >>> root.remove_widget(button) ''' if widget not in self.children: return self.children.remove(widget) self.children = self.children[:] self.canvas.remove(widget.canvas) widget.parent = None def clear_widgets(self): '''Remove all widgets added to the widget. ''' remove_widget = self.remove_widget for child in self.children[:]: remove_widget(child) def get_root_window(self): '''Return the root window :Returns: Instance of the root window. Can be :class:`~kivy.core.window.WindowBase` or :class:`Widget` ''' if self.parent: return self.parent.get_root_window() def get_parent_window(self): '''Return the parent window :Returns: Instance of the root window. Can be :class:`~kivy.core.window.WindowBase` or :class:`Widget` ''' if self.parent: return self.parent.get_parent_window() def to_widget(self, x, y, relative=False): '''Return the coordinate from window to local widget''' if self.parent: x, y = self.parent.to_widget(x, y) return self.to_local(x, y, relative=relative) def to_window(self, x, y, initial=True, relative=False): '''Transform local coordinate to window coordinate''' if not initial: x, y = self.to_parent(x, y, relative=relative) if self.parent: return self.parent.to_window(x, y, initial=False, relative=relative) return (x, y) def to_parent(self, x, y, relative=False): '''Transform local coordinate to parent coordinate :Parameters: `relative`: bool, default to False Change to True is you want to translate relative position from widget to his parent. ''' if relative: return (x + self.x, y + self.y) return (x, y) def to_local(self, x, y, relative=False): '''Transform parent coordinate to local coordinate :Parameters: `relative`: bool, default to False Change to True is you want to translate a coordinate to a relative coordinate from widget. ''' if relative: return (x - self.x, y - self.y) return (x, y) # # Properties # def setter(self, name): '''Return the setter of a property. Useful if you want to directly bind a property to another. For example, if you want to position one widget next to you :: self.bind(right=nextchild.setter('x')) ''' return self.__properties[name].__set__ def getter(self, name): '''Return the getter of a property. ''' return self.__properties[name].__get__ x = NumericProperty(0) '''X position of the widget. :data:`x` is a :class:`~kivy.properties.NumericProperty`, default to 0. ''' y = NumericProperty(0) '''Y position of the widget. :data:`y` is a :class:`~kivy.properties.NumericProperty`, default to 0. ''' width = NumericProperty(100) '''Width of the widget. :data:`width` is a :class:`~kivy.properties.NumericProperty`, default to 100. ''' height = NumericProperty(100) '''Height of the widget. :data:`height` is a :class:`~kivy.properties.NumericProperty`, default to 100. ''' pos = ReferenceListProperty(x, y) '''Position of the widget. :data:`pos` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`x`, :data:`y`) properties. ''' size = ReferenceListProperty(width, height) '''Size of the widget. :data:`size` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`width`, :data:`height`) properties. ''' def get_right(self): return self.x + self.width def set_right(self, value): self.x = value - self.width right = AliasProperty(get_right, set_right, bind=('x', 'width')) '''Right position of the widget :data:`right` is a :class:`~kivy.properties.AliasProperty` of (:data:`x` + :data:`width`) ''' def get_top(self): return self.y + self.height def set_top(self, value): self.y = value - self.height top = AliasProperty(get_top, set_top, bind=('y', 'height')) '''Top position of the widget :data:`top` is a :class:`~kivy.properties.AliasProperty` of (:data:`y` + :data:`height`) ''' def get_center_x(self): return self.x + self.width / 2. def set_center_x(self, value): self.x = value - self.width / 2. center_x = AliasProperty(get_center_x, set_center_x, bind=('x', 'width')) '''X center position of the widget :data:`center_x` is a :class:`~kivy.properties.AliasProperty` of (:data:`x` + :data:`width` / 2.) ''' def get_center_y(self): return self.y + self.height / 2. def set_center_y(self, value): self.y = value - self.height / 2. center_y = AliasProperty(get_center_y, set_center_y, bind=('y', 'height')) '''Y center position of the widget :data:`center_y` is a :class:`~kivy.properties.AliasProperty` of (:data:`y` + :data:`height` / 2.) ''' center = ReferenceListProperty(center_x, center_y) '''Center position of the widget :data:`center` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`center_x`, :data:`center_y`) ''' cls = ListProperty([]) '''Class of the widget, used for styling. ''' id = StringProperty(None, allownone=True) '''Uniq identifier of the widget in the tree. :data:`id` is a :class:`~kivy.properties.StringProperty`, default to None. .. warning:: If the :data:`id` is already used in the tree, an exception will occur. ''' children = ListProperty([]) '''Children list :data:`children` is a :class:`~kivy.properties.ListProperty` instance, default to an empty list. Use :func:`add_widget` and :func:`remove_widget` for manipulate children list. Don't manipulate children list directly until you know what you are doing. ''' parent = ObjectProperty(None, allownone=True) '''Parent of the widget :data:`parent` is a :class:`~kivy.properties.ObjectProperty` instance, default to None. The parent of a widget is set when the widget is added to another one, and unset when the widget is removed from his parent. ''' size_hint_x = NumericProperty(1, allownone=True) '''X size hint. It represent how much space the widget should use in the X axis from his parent. Only :class:`~kivy.uix.layout.Layout` and :class:`~kivy.core.window.Window` are using the hint. Value is in percent, 1. will mean the full size of his parent, aka 100%. 0.5 will represent 50%. :data:`size_hint_x` is a :class:`~kivy.properties.NumericProperty`, default to 1. ''' size_hint_y = NumericProperty(1, allownone=True) '''Y size hint. :data:`size_hint_y` is a :class:`~kivy.properties.NumericProperty`, default to 1. See :data:`size_hint_x` for more information ''' size_hint = ReferenceListProperty(size_hint_x, size_hint_y) '''Size hint. :data:`size_hint` is a :class:`~kivy.properties.ReferenceListProperty` of (:data:`size_hint_x`, :data:`size_hint_y`) See :data:`size_hint_x` for more information ''' pos_hint = ObjectProperty({}) '''Position hint. This property permit you to set the position of the widget inside his parent layout, in percent. For example, if you want to set the top of the widget to be at 90% height of his parent layout, you can write: widget = Widget(pos_hint={'top': 0.9}) The keys 'x', 'right' will use the parent width. The keys 'y', 'top' will use the parent height. Check :doc:`api-kivy.uix.floatlayout` for more informations. Position hint is only used in :class:`~kivy.uix.floatlayout.FloatLayout` and :class:`~kivy.core.window.Window`. :data:`pos_hint` is a :class:`~kivy.properties.ObjectProperty` containing a dict. ''' canvas = None '''Canvas of the widget.
class QuestionButton(Button): disabled = BooleanProperty() alpha_rotation = NumericProperty(0) background_default = StringProperty() background_wrong = StringProperty() color_wrong = ListProperty([1, 1, 1, 0]) text_wrong = StringProperty() def __init__(self, **kwargs): super(QuestionButton, self).__init__(**kwargs) self._origin = {} Clock.schedule_once(self._prepare_fbo, 0) def on_text(self, *args): self._update_mesh() def _prepare_fbo(self, *args): # put all the current canvas into an FBO # then use the fbo texture into a Quad, for animating when disable # create the Fbo self.fbo = Fbo(size=(1, 1)) with self.fbo.before: self.g_translate = Translate(self.x, self.y) self.orig_canvas = self.canvas self.fbo.add(self.canvas) # create a new Canvas self.canvas = Canvas() self.canvas.add(self.fbo) with self.canvas: Color(1, 1, 1) self.g_quad = Quad(texture=self.fbo.texture) # replace the canvas from the parent with the new one self.parent.canvas.remove(self.orig_canvas) self.parent.canvas.add(self.canvas) # ensure we'll be updated when we'll change position self.bind(pos=self._update_mesh, size=self._update_mesh, alpha_rotation=self._update_mesh) self._update_mesh() def _update_mesh(self, *args): m = self.g_quad alpha = self.alpha_rotation # don't do anything if the fbo size will be messup. if 0 in self.size: return # update fbo size, and reassign the new texture to the quad if self.fbo.size != self.size: self.fbo.size = self.size self.g_quad.texture = self.fbo.texture # change the background to red, and ensure we are not seeing any # changes when clicking if alpha >= 0.5: # and self.background_normal != self.background_wrong: # self._origin = { # 'background_normal': self.background_normal, # 'background_down': self.background_down, # 'color': (1, 1, 1, 1)} self.background_normal = self.background_wrong self.background_down = self.background_wrong self.color = self.color_wrong self.text = self.text_wrong # correctly setup the positionning for the quad rendering self.g_translate.xy = -self.x, -self.y # 3d fake effect dx = sin(alpha * pi / 2.0) * self.width dy = sin(alpha * pi) * 25 if alpha > 0.5: dy = -dy dx = self.width - dx m.points = ( self.x + dx, self.y + dy, self.right - dx, self.y - dy, self.right - dx, self.top + dy, self.x + dx, self.top - dy, ) def disable(self): if self.alpha_rotation > 0: return d = 1.0 hd = 0.16 # at 0.16, the animation will be at the middle t = "out_quart" Animation(alpha_rotation=1.0, t=t, d=d).start(self) (Animation(color=self.color_wrong, t=t, d=hd) + Animation(color=self.color, t=t, d=1 - hd)).start(self) def reset(self, text): self.alpha_rotation = 0 self.disabled = False self.background_normal = "ui/screens/question/qbg.png" self.background_down = "ui/screens/question/qbg_down.png" self.color = (0.96, 0.96, 0.96, 1) self.text = text self._update_mesh()
class MapView(Widget): """MapView is the widget that control the map displaying, navigation, and layers management. """ lon = NumericProperty() """Longitude at the center of the widget """ lat = NumericProperty() """Latitude at the center of the widget """ zoom = NumericProperty(0) """Zoom of the widget. Must be between :meth:`MapSource.get_min_zoom` and :meth:`MapSource.get_max_zoom`. Default to 0. """ map_source = ObjectProperty(MapSource()) """Provider of the map, default to a empty :class:`MapSource`. """ double_tap_zoom = BooleanProperty(False) """If True, this will activate the double-tap to zoom. """ pause_on_action = BooleanProperty(True) """Pause any map loading / tiles loading when an action is done. This allow better performance on mobile, but can be safely deactivated on desktop. """ snap_to_zoom = BooleanProperty(True) """When the user initiate a zoom, it will snap to the closest zoom for better graphics. The map can be blur if the map is scaled between 2 zoom. Default to True, even if it doesn't fully working yet. """ animation_duration = NumericProperty(100) """Duration to animate Tiles alpha from 0 to 1 when it's ready to show. Default to 100 as 100ms. Use 0 to deactivate. """ delta_x = NumericProperty(0) delta_y = NumericProperty(0) background_color = ListProperty([181 / 255., 208 / 255., 208 / 255., 1]) _zoom = NumericProperty(0) _pause = BooleanProperty(False) _scale = 1. __events__ = ["on_map_relocated"] # Public API @property def viewport_pos(self): vx, vy = self._scatter.to_local(self.x, self.y) return vx - self.delta_x, vy - self.delta_y @property def scale(self): if self._invalid_scale: self._invalid_scale = False self._scale = self._scatter.scale return self._scale def get_bbox(self, margin=0): """Returns the bounding box from the bottom/left (lat1, lon1) to top/right (lat2, lon2). """ x1, y1 = self.to_local(0 - margin, 0 - margin) x2, y2 = self.to_local((self.width + margin), (self.height + margin)) c1 = self.get_latlon_at(x1, y1) c2 = self.get_latlon_at(x2, y2) return Bbox((c1.lat, c1.lon, c2.lat, c2.lon)) bbox = AliasProperty(get_bbox, None, bind=["lat", "lon", "_zoom"]) def unload(self): """Unload the view and all the layers. It also cancel all the remaining downloads. """ self.remove_all_tiles() def get_window_xy_from(self, lat, lon, zoom): """Returns the x/y position in the widget absolute coordinates from a lat/lon""" scale = self.scale vx, vy = self.viewport_pos ms = self.map_source x = ms.get_x(zoom, lon) - vx y = ms.get_y(zoom, lat) - vy x *= scale y *= scale return x, y def center_on(self, *args): """Center the map on the coordinate :class:`Coordinate`, or a (lat, lon) """ map_source = self.map_source zoom = self._zoom if len(args) == 1 and isinstance(args[0], Coordinate): coord = args[0] lat = coord.lat lon = coord.lon elif len(args) == 2: lat, lon = args else: raise Exception("Invalid argument for center_on") lon = clamp(lon, MIN_LONGITUDE, MAX_LONGITUDE) lat = clamp(lat, MIN_LATITUDE, MAX_LATITUDE) scale = self._scatter.scale x = map_source.get_x(zoom, lon) - self.center_x / scale y = map_source.get_y(zoom, lat) - self.center_y / scale self.delta_x = -x self.delta_y = -y self.lon = lon self.lat = lat self._scatter.pos = 0, 0 self.trigger_update(True) def set_zoom_at(self, zoom, x, y, scale=None): """Sets the zoom level, leaving the (x, y) at the exact same point in the view. """ zoom = clamp(zoom, self.map_source.get_min_zoom(), self.map_source.get_max_zoom()) if int(zoom) == int(self._zoom): if scale is None: return elif scale == self.scale: return scale = scale or 1. # first, rescale the scatter scatter = self._scatter scale = clamp(scale, scatter.scale_min, scatter.scale_max) rescale = scale * 1.0 / scatter.scale scatter.apply_transform(Matrix().scale(rescale, rescale, rescale), post_multiply=True, anchor=scatter.to_local(x, y)) # adjust position if the zoom changed c1 = self.map_source.get_col_count(self._zoom) c2 = self.map_source.get_col_count(zoom) if c1 != c2: f = float(c2) / float(c1) self.delta_x = scatter.x + self.delta_x * f self.delta_y = scatter.y + self.delta_y * f # back to 0 every time scatter.apply_transform(Matrix().translate( -scatter.x, -scatter.y, 0 ), post_multiply=True) # avoid triggering zoom changes. self._zoom = zoom self.zoom = self._zoom def on_zoom(self, instance, zoom): if zoom == self._zoom: return x = self.map_source.get_x(zoom, self.lon) - self.delta_x y = self.map_source.get_y(zoom, self.lat) - self.delta_y self.set_zoom_at(zoom, x, y) self.center_on(self.lat, self.lon) def get_latlon_at(self, x, y, zoom=None): """Return the current :class:`Coordinate` within the (x, y) widget coordinate. """ if zoom is None: zoom = self._zoom vx, vy = self.viewport_pos scale = self._scale return Coordinate( lat=self.map_source.get_lat(zoom, y / scale + vy), lon=self.map_source.get_lon(zoom, x / scale + vx)) def add_marker(self, marker, layer=None): """Add a marker into the layer. If layer is None, it will be added in the default marker layer. If there is no default marker layer, a new one will be automatically created """ if layer is None: if not self._default_marker_layer: layer = MarkerMapLayer() self.add_layer(layer) else: layer = self._default_marker_layer layer.add_widget(marker) layer.set_marker_position(self, marker) def remove_marker(self, marker): """Remove a marker from its layer """ marker.detach() def add_layer(self, layer, mode="window"): """Add a new layer to update at the same time the base tile layer. mode can be either "scatter" or "window". If "scatter", it means the layer will be within the scatter transformation. It's perfect if you want to display path / shape, but not for text. If "window", it will have no transformation. You need to position the widget yourself: think as Z-sprite / billboard. Defaults to "window". """ assert(mode in ("scatter", "window")) if self._default_marker_layer is None and \ isinstance(layer, MarkerMapLayer): self._default_marker_layer = layer self._layers.append(layer) c = self.canvas if mode == "scatter": self.canvas = self.canvas_layers else: self.canvas = self.canvas_layers_out layer.canvas_parent = self.canvas super(MapView, self).add_widget(layer) self.canvas = c def remove_layer(self, layer): """Remove the layer """ c = self.canvas self._layers.remove(layer) self.canvas = layer.canvas_parent super(MapView, self).remove_widget(layer) self.canvas = c def sync_to(self, other): """Reflect the lat/lon/zoom of the other MapView to the current one. """ if self._zoom != other._zoom: self.set_zoom_at(other._zoom, *self.center) self.center_on(other.get_latlon_at(*self.center)) # Private API def __init__(self, cache_dir='cache', **kwargs): from kivy.base import EventLoop EventLoop.ensure_window() CACHE['directory'] = cache_dir self._invalid_scale = True self._tiles = [] self._tiles_bg = [] self._tilemap = {} self._layers = [] self._default_marker_layer = None self._need_redraw_all = False self._transform_lock = False self.trigger_update(True) self.canvas = Canvas() self._scatter = MapViewScatter() self.add_widget(self._scatter) with self._scatter.canvas: self.canvas_map = Canvas() self.canvas_layers = Canvas() with self.canvas: self.canvas_layers_out = Canvas() self._scale_target_anim = False self._scale_target = 1. self._touch_count = 0 Clock.schedule_interval(self._animate_color, 1 / 60.) self.lat = kwargs.get("lat", self.lat) self.lon = kwargs.get("lon", self.lon) super(MapView, self).__init__(**kwargs) def _animate_color(self, dt): # fast path d = self.animation_duration if d == 0: for tile in self._tiles: if tile.state == "need-animation": tile.g_color.a = 1. tile.state = "animated" for tile in self._tiles_bg: if tile.state == "need-animation": tile.g_color.a = 1. tile.state = "animated" else: d = d / 1000. for tile in self._tiles: if tile.state != "need-animation": continue tile.g_color.a += dt / d if tile.g_color.a >= 1: tile.state = "animated" for tile in self._tiles_bg: if tile.state != "need-animation": continue tile.g_color.a += dt / d if tile.g_color.a >= 1: tile.state = "animated" def add_widget(self, widget): if isinstance(widget, MapMarker): self.add_marker(widget) elif isinstance(widget, MapLayer): self.add_layer(widget) else: super(MapView, self).add_widget(widget) def remove_widget(self, widget): if isinstance(widget, MapMarker): self.remove_marker(widget) elif isinstance(widget, MapLayer): self.remove_layer(widget) else: super(MapView, self).remove_widget(widget) def on_map_relocated(self, zoom, coord): pass def animated_diff_scale_at(self, d, x, y): self._scale_target_time = 1. self._scale_target_pos = x, y if self._scale_target_anim == False: self._scale_target_anim = True self._scale_target = d else: self._scale_target += d Clock.unschedule(self._animate_scale) Clock.schedule_interval(self._animate_scale, 1 / 60.) def _animate_scale(self, dt): diff = self._scale_target / 3. if abs(diff) < 0.01: diff = self._scale_target self._scale_target = 0 else: self._scale_target -= diff self._scale_target_time -= dt self.diff_scale_at(diff, *self._scale_target_pos) ret = self._scale_target != 0 if not ret: self._pause = False return ret def diff_scale_at(self, d, x, y): scatter = self._scatter scale = scatter.scale * (2 ** d) self.scale_at(scale, x, y) def scale_at(self, scale, x, y): scatter = self._scatter scale = clamp(scale, scatter.scale_min, scatter.scale_max) rescale = scale * 1.0 / scatter.scale scatter.apply_transform(Matrix().scale(rescale, rescale, rescale), post_multiply=True, anchor=scatter.to_local(x, y)) def on_touch_down(self, touch): if not self.collide_point(*touch.pos): return if self.pause_on_action: self._pause = True if "button" in touch.profile and touch.button in ("scrolldown", "scrollup"): d = 1 if touch.button == "scrollup" else -1 self.animated_diff_scale_at(d, *touch.pos) return True elif touch.is_double_tap and self.double_tap_zoom: self.animated_diff_scale_at(1, *touch.pos) return True touch.grab(self) self._touch_count += 1 if self._touch_count == 1: self._touch_zoom = (self.zoom, self._scale) return super(MapView, self).on_touch_down(touch) def on_touch_up(self, touch): if touch.grab_current == self: touch.ungrab(self) self._touch_count -= 1 if self._touch_count == 0: # animate to the closest zoom zoom, scale = self._touch_zoom cur_zoom = self.zoom cur_scale = self._scale if cur_zoom < zoom or cur_scale < scale: self.animated_diff_scale_at(1. - cur_scale, *touch.pos) elif cur_zoom > zoom or cur_scale > scale: self.animated_diff_scale_at(2. - cur_scale, *touch.pos) self._pause = False return True return super(MapView, self).on_touch_up(touch) def on_transform(self, *args): self._invalid_scale = True if self._transform_lock: return self._transform_lock = True # recalculate viewport map_source = self.map_source zoom = self._zoom scatter = self._scatter scale = scatter.scale if scale >= 2.: zoom += 1 scale /= 2. elif scale < 1: zoom -= 1 scale *= 2. zoom = clamp(zoom, map_source.min_zoom, map_source.max_zoom) if zoom != self._zoom: self.set_zoom_at(zoom, scatter.x, scatter.y, scale=scale) self.trigger_update(True) else: if zoom == map_source.min_zoom and scatter.scale < 1.: scatter.scale = 1. self.trigger_update(True) else: self.trigger_update(False) if map_source.bounds: self._apply_bounds() self._transform_lock = False self._scale = self._scatter.scale def _apply_bounds(self): # if the map_source have any constraints, apply them here. map_source = self.map_source zoom = self._zoom min_lon, min_lat, max_lon, max_lat = map_source.bounds xmin = map_source.get_x(zoom, min_lon) xmax = map_source.get_x(zoom, max_lon) ymin = map_source.get_y(zoom, min_lat) ymax = map_source.get_y(zoom, max_lat) dx = self.delta_x dy = self.delta_y oxmin, oymin = self._scatter.to_local(self.x, self.y) oxmax, oymax = self._scatter.to_local(self.right, self.top) s = self._scale cxmin = (oxmin - dx) if cxmin < xmin: self._scatter.x += (cxmin - xmin) * s cymin = (oymin - dy) if cymin < ymin: self._scatter.y += (cymin - ymin) * s cxmax = (oxmax - dx) if cxmax > xmax: self._scatter.x -= (xmax - cxmax) * s cymax = (oymax - dy) if cymax > ymax: self._scatter.y -= (ymax - cymax) * s def on__pause(self, instance, value): if not value: self.trigger_update(True) def trigger_update(self, full): self._need_redraw_full = full or self._need_redraw_full Clock.unschedule(self.do_update) Clock.schedule_once(self.do_update, -1) def do_update(self, dt): zoom = self._zoom scale = self._scale self.lon = self.map_source.get_lon(zoom, (self.center_x - self._scatter.x) / scale - self.delta_x) self.lat = self.map_source.get_lat(zoom, (self.center_y - self._scatter.y) / scale - self.delta_y) self.dispatch("on_map_relocated", zoom, Coordinate(self.lon, self.lat)) for layer in self._layers: layer.reposition() if self._need_redraw_full: self._need_redraw_full = False self.move_tiles_to_background() self.load_visible_tiles() else: self.load_visible_tiles() def bbox_for_zoom(self, vx, vy, w, h, zoom): # return a tile-bbox for the zoom map_source = self.map_source size = map_source.dp_tile_size scale = self._scale max_x_end = map_source.get_col_count(zoom) max_y_end = map_source.get_row_count(zoom) x_count = int(ceil(w / scale / float(size))) + 1 y_count = int(ceil(h / scale / float(size))) + 1 tile_x_first = int(clamp(vx / float(size), 0, max_x_end)) tile_y_first = int(clamp(vy / float(size), 0, max_y_end)) tile_x_last = tile_x_first + x_count tile_y_last = tile_y_first + y_count tile_x_last = int(clamp(tile_x_last, tile_x_first, max_x_end)) tile_y_last = int(clamp(tile_y_last, tile_y_first, max_y_end)) x_count = tile_x_last - tile_x_first y_count = tile_y_last - tile_y_first return (tile_x_first, tile_y_first, tile_x_last, tile_y_last, x_count, y_count) def load_visible_tiles(self): map_source = self.map_source vx, vy = self.viewport_pos zoom = self._zoom dirs = [0, 1, 0, -1, 0] bbox_for_zoom = self.bbox_for_zoom size = map_source.dp_tile_size tile_x_first, tile_y_first, tile_x_last, tile_y_last, \ x_count, y_count = bbox_for_zoom(vx, vy, self.width, self.height, zoom) #print "Range {},{} to {},{}".format( # tile_x_first, tile_y_first, # tile_x_last, tile_y_last) # Adjust tiles behind us for tile in self._tiles_bg[:]: tile_x = tile.tile_x tile_y = tile.tile_y f = 2 ** (zoom - tile.zoom) w = self.width / f h = self.height / f btile_x_first, btile_y_first, btile_x_last, btile_y_last, \ _, _ = bbox_for_zoom(vx / f, vy / f, w, h, tile.zoom) if tile_x < btile_x_first or tile_x >= btile_x_last or \ tile_y < btile_y_first or tile_y >= btile_y_last: tile.state = "done" self._tiles_bg.remove(tile) self.canvas_map.before.remove(tile.g_color) self.canvas_map.before.remove(tile) continue tsize = size * f tile.size = tsize, tsize tile.pos = ( tile_x * tsize + self.delta_x, tile_y * tsize + self.delta_y) # Get rid of old tiles first for tile in self._tiles[:]: tile_x = tile.tile_x tile_y = tile.tile_y if tile_x < tile_x_first or tile_x >= tile_x_last or \ tile_y < tile_y_first or tile_y >= tile_y_last: tile.state = "done" self.tile_map_set(tile_x, tile_y, False) self._tiles.remove(tile) self.canvas_map.remove(tile) self.canvas_map.remove(tile.g_color) else: tile.size = (size, size) tile.pos = (tile_x * size + self.delta_x, tile_y * size + self.delta_y) # Load new tiles if needed x = tile_x_first + x_count // 2 - 1 y = tile_y_first + y_count // 2 - 1 arm_max = max(x_count, y_count) + 2 arm_size = 1 turn = 0 while arm_size < arm_max: for i in range(arm_size): if not self.tile_in_tile_map(x, y) and \ y >= tile_y_first and y < tile_y_last and \ x >= tile_x_first and x < tile_x_last: self.load_tile(x, y, size, zoom) x += dirs[turn % 4 + 1] y += dirs[turn % 4] if turn % 2 == 1: arm_size += 1 turn += 1 def load_tile(self, x, y, size, zoom): if self.tile_in_tile_map(x, y) or zoom != self._zoom: return self.load_tile_for_source(self.map_source, 1., size, x, y, zoom) # XXX do overlay support self.tile_map_set(x, y, True) def load_tile_for_source(self, map_source, opacity, size, x, y, zoom): tile = Tile(size=(size, size)) tile.g_color = Color(1, 1, 1, 0) tile.tile_x = x tile.tile_y = y tile.zoom = zoom tile.pos = (x * size + self.delta_x, y * size + self.delta_y) tile.map_source = map_source tile.state = "loading" if not self._pause: map_source.fill_tile(tile) self.canvas_map.add(tile.g_color) self.canvas_map.add(tile) self._tiles.append(tile) def move_tiles_to_background(self): # remove all the tiles of the main map to the background map # retain only the one who are on the current zoom level # for all the tile in the background, stop the download if not yet started. zoom = self._zoom tiles = self._tiles btiles = self._tiles_bg canvas_map = self.canvas_map tile_size = self.map_source.tile_size # move all tiles to background while tiles: tile = tiles.pop() if tile.state == "loading": tile.state == "done" continue btiles.append(tile) # clear the canvas canvas_map.clear() canvas_map.before.clear() self._tilemap = {} # unsure if it's really needed, i personnally didn't get issues right now #btiles.sort(key=lambda z: -z.zoom) # add all the btiles into the back canvas. # except for the tiles that are owned by the current zoom level for tile in btiles[:]: if tile.zoom == zoom: btiles.remove(tile) tiles.append(tile) tile.size = tile_size, tile_size canvas_map.add(tile.g_color) canvas_map.add(tile) self.tile_map_set(tile.tile_x, tile.tile_y, True) continue canvas_map.before.add(tile.g_color) canvas_map.before.add(tile) def remove_all_tiles(self): # clear the map of all tiles. self.canvas_map.clear() self.canvas_map.before.clear() for tile in self._tiles: tile.state = "done" del self._tiles[:] del self._tiles_bg[:] self._tilemap = {} def tile_map_set(self, tile_x, tile_y, value): key = tile_y * self.map_source.get_col_count(self._zoom) + tile_x if value: self._tilemap[key] = value else: self._tilemap.pop(key, None) def tile_in_tile_map(self, tile_x, tile_y): key = tile_y * self.map_source.get_col_count(self._zoom) + tile_x return key in self._tilemap def on_size(self, instance, size): for layer in self._layers: layer.size = size self.center_on(self.lat, self.lon) self.trigger_update(True) def on_pos(self, instance, pos): self.center_on(self.lat, self.lon) self.trigger_update(True) def on_map_source(self, instance, source): if isinstance(source, string_types): self.map_source = MapSource.from_provider(source) elif isinstance(source, (tuple, list)): cache_key, min_zoom, max_zoom, url, attribution, options = source self.map_source = MapSource(url=url, cache_key=cache_key, min_zoom=min_zoom, max_zoom=max_zoom, attribution=attribution, **options) elif isinstance(source, MapSource): self.map_source = source else: raise Exception("Invalid map source provider") self.zoom = clamp(self.zoom, self.map_source.min_zoom, self.map_source.max_zoom) self.remove_all_tiles() self.trigger_update(True)
class SMAA(Widget): debug = OptionProperty("", options=("", "edges", "blend", "source")) """Texture to show instead of the result: - `edges` will show you the result of the edges detection shader - `blend` will show you the result of the blending shader - `source` will show you the initial drawing of children, before any pass. """ quality = OptionProperty("ultra", options=("low", "medium", "high", "ultra")) """Quality of the shader. The more you ask, the slower it will be. """ def __init__(self, **kwargs): self._g_debug = [] self._g_debug_added = False if "size" not in kwargs: from kivy.core.window import Window kwargs["size"] = Window.size self.size = kwargs["size"] self.init_smaa() super(SMAA, self).__init__() self.canvas.add(self.smaa_canvas) self.canvas.ask_update() def add_widget(self, *args): canvas = self.smaa_canvas self.canvas = self.albedo_fbo super(SMAA, self).add_widget(*args) self.canvas = canvas def remove_widget(self, *args): canvas = self.smaa_canvas self.canvas = self.albedo_fbo super(SMAA, self).remove_widget(*args) self.canvas = canvas def init_smaa(self): curdir = dirname(__file__) # load shaders sources with open(join(curdir, "SMAA.h"), "r") as fd: smaa_h = fd.read() config = """ #version 410 compatibility #define SMAA_PIXEL_SIZE vec2(1.0 / {width}, 1.0 / {height}) #define SMAA_PRESET_{quality} 1 #define SMAA_GLSL_4 1 """.format( width=self.width, height=self.height, quality=self.quality.upper() ) header_vs = ( config + """ #define SMAA_ONLY_COMPILE_VS 1 in vec2 vPosition; in vec2 vTexCoords0; uniform mat4 modelview_mat; uniform mat4 projection_mat; """ + smaa_h ) header_fs = ( config + """ #define SMAA_ONLY_COMPILE_PS 1 """ + smaa_h ) edge_vs = ( header_vs + """ out vec2 texcoord; out vec4 offset[3]; out vec4 dummy2; void main() { texcoord = vTexCoords0; vec4 dummy1 = vec4(0); SMAAEdgeDetectionVS(dummy1, dummy2, texcoord, offset); gl_Position = projection_mat * modelview_mat * vec4(vPosition.xy, 0.0, 1.0); } """ ) edge_fs = ( header_fs + """ uniform sampler2D albedo_tex; in vec2 texcoord; in vec4 offset[3]; in vec4 dummy2; void main() { #if SMAA_PREDICATION == 1 gl_FragColor = SMAAColorEdgeDetectionPS(texcoord, offset, albedo_tex, depthTex); #else gl_FragColor = SMAAColorEdgeDetectionPS(texcoord, offset, albedo_tex); #endif } """ ) blend_vs = ( header_vs + """ out vec2 texcoord; out vec2 pixcoord; out vec4 offset[3]; out vec4 dummy2; void main() { texcoord = vTexCoords0; vec4 dummy1 = vec4(0); SMAABlendingWeightCalculationVS(dummy1, dummy2, texcoord, pixcoord, offset); gl_Position = projection_mat * modelview_mat * vec4(vPosition.xy, 0.0, 1.0); } """ ) blend_fs = ( header_fs + """ uniform sampler2D edge_tex; uniform sampler2D area_tex; uniform sampler2D search_tex; in vec2 texcoord; in vec2 pixcoord; in vec4 offset[3]; in vec4 dummy2; void main() { gl_FragColor = SMAABlendingWeightCalculationPS(texcoord, pixcoord, offset, edge_tex, area_tex, search_tex, ivec4(0)); } """ ) neighborhood_vs = ( header_vs + """ out vec2 texcoord; out vec4 offset[2]; out vec4 dummy2; void main() { texcoord = vTexCoords0; vec4 dummy1 = vec4(0); SMAANeighborhoodBlendingVS(dummy1, dummy2, texcoord, offset); gl_Position = projection_mat * modelview_mat * vec4(vPosition.xy, 0.0, 1.0); } """ ) neighborhood_fs = ( header_fs + """ uniform sampler2D albedo_tex; uniform sampler2D blend_tex; in vec2 texcoord; in vec4 offset[2]; in vec4 dummy2; void main() { gl_FragColor = SMAANeighborhoodBlendingPS(texcoord, offset, albedo_tex, blend_tex); } """ ) size = self.size self.albedo_tex = Texture.create(size=size, bufferfmt="float") self.albedo_fbo = Fbo(size=size, texture=self.albedo_tex) self.edge_tex = Texture.create(size=size, bufferfmt="float") self.edge_fbo = Fbo(size=size, vs=edge_vs, fs=edge_fs, texture=self.edge_tex) self.edge_fbo.bind() self.edge_fbo["albedo_tex"] = 0 self.edge_fbo.release() self.blend_tex = Texture.create(size=size, bufferfmt="float") self.blend_fbo = Fbo(size=size, vs=blend_vs, fs=blend_fs, texture=self.blend_tex) self.blend_fbo.bind() self.blend_fbo["edge_tex"] = 0 self.blend_fbo["area_tex"] = 1 self.blend_fbo["search_tex"] = 2 self.blend_fbo.release() self.neighborhood = RenderContext( use_parent_modelview=True, use_parent_projection=True, vs=neighborhood_vs, fs=neighborhood_fs ) with self.neighborhood: self.neighborhood["albedo_tex"] = 0 self.neighborhood["blend_tex"] = 1 self.area_tex = Texture.create(size=(AREATEX_WIDTH, AREATEX_HEIGHT), colorfmt="rg", icolorfmt="rg8") with open(join(curdir, "smaa_area.raw"), "rb") as fd: self.area_tex.blit_buffer(fd.read(), colorfmt="rg") self.search_tex = Texture.create(size=(SEARCHTEX_WIDTH, SEARCHTEX_HEIGHT), colorfmt="red", icolorfmt="r8") self.search_tex.min_filter = "nearest" self.search_tex.mag_filter = "nearest" with open(join(curdir, "smaa_search.raw"), "rb") as fd: self.search_tex.blit_buffer(fd.read(), colorfmt="red") with self.albedo_fbo: ClearColor(0, 0, 0, 0) ClearBuffers() with self.edge_fbo: Rectangle(size=self.size, texture=self.albedo_tex) with self.blend_fbo: BindTexture(index=1, texture=self.area_tex) BindTexture(index=2, texture=self.search_tex) Rectangle(size=self.size, texture=self.edge_tex) self.neighborhood.add(self.albedo_fbo) self.neighborhood.add(Callback(lambda *x: glDisable(GL_BLEND))) self.neighborhood.add(self.edge_fbo) self.neighborhood.add(self.blend_fbo) self.neighborhood.add(Callback(lambda *x: glEnable(GL_BLEND))) with self.neighborhood: BindTexture(index=1, texture=self.blend_tex) Rectangle(size=self.size, texture=self.albedo_tex) self.smaa_canvas = Canvas() with self.smaa_canvas.before: def do_stuff(*args): self.albedo_fbo.bind() self.albedo_fbo.clear_buffer() self.albedo_fbo.release() self.edge_fbo.bind() self.edge_fbo.clear_buffer() self.edge_fbo.release() self.blend_fbo.bind() self.blend_fbo.clear_buffer() self.blend_fbo.release() self.albedo_fbo.ask_update() self.edge_fbo.ask_update() self.blend_fbo.ask_update() self.neighborhood.ask_update() Callback(do_stuff) self.smaa_canvas.add(self.neighborhood) self._g_debug_added = False self._g_debug = [ Callback(lambda *x: glDisable(GL_BLEND)), Color(0, 0, 0, 1), Rectangle(size=self.size), Color(1, 1, 1, 1), Rectangle(size=self.size), Callback(lambda *x: glEnable(GL_BLEND)), ] def on_debug(self, instance, value): g_debug = self._g_debug if self._g_debug_added: for instr in g_debug: self.canvas.after.remove(instr) if value == "": return elif value == "edges": g_debug[-2].texture = self.edge_tex elif value == "blend": g_debug[-2].texture = self.blend_tex elif value == "source": g_debug[-2].texture = self.albedo_tex self._g_debug_added = True for instr in g_debug: self.canvas.after.add(instr) def on_quality(self, instance, value): self.reload_smaa() def reload_smaa(self): debug = self.debug self.debug = "" children = self.children[:] for child in children: self.remove_widget(child) self.canvas.remove(self.smaa_canvas) self.init_smaa() self.canvas.add(self.smaa_canvas) for child in children: self.add_widget(child) self.debug = debug