drawer.py (8493B)
1 '''Drawer Widget to hold the main window and the menu/hidden section that 2 can be swiped in from the left. This Menu would be only hidden in phone mode 3 and visible in Tablet Mode. 4 5 This class is specifically in lined to save on start up speed(minimize i/o). 6 ''' 7 8 from kivy.app import App 9 from kivy.factory import Factory 10 from kivy.properties import OptionProperty, NumericProperty, ObjectProperty 11 from kivy.clock import Clock 12 from kivy.lang import Builder 13 from kivy.logger import Logger 14 15 import gc 16 17 # delayed imports 18 app = None 19 20 21 class Drawer(Factory.RelativeLayout): 22 '''Drawer Widget to hold the main window and the menu/hidden section that 23 can be swiped in from the left. This Menu would be only hidden in phone mode 24 and visible in Tablet Mode. 25 26 ''' 27 28 state = OptionProperty('closed', 29 options=('closed', 'open', 'opening', 'closing')) 30 '''This indicates the current state the drawer is in. 31 32 :attr:`state` is a `OptionProperty` defaults to `closed`. Can be one of 33 `closed`, `open`, `opening`, `closing`. 34 ''' 35 36 scroll_timeout = NumericProperty(200) 37 '''Timeout allowed to trigger the :data:`scroll_distance`, 38 in milliseconds. If the user has not moved :data:`scroll_distance` 39 within the timeout, the scrolling will be disabled and the touch event 40 will go to the children. 41 42 :data:`scroll_timeout` is a :class:`~kivy.properties.NumericProperty` 43 and defaults to 200 (milliseconds) 44 ''' 45 46 scroll_distance = NumericProperty('9dp') 47 '''Distance to move before scrolling the :class:`Drawer` in pixels. 48 As soon as the distance has been traveled, the :class:`Drawer` will 49 start to scroll, and no touch event will go to children. 50 It is advisable that you base this value on the dpi of your target 51 device's screen. 52 53 :data:`scroll_distance` is a :class:`~kivy.properties.NumericProperty` 54 and defaults to 20dp. 55 ''' 56 57 drag_area = NumericProperty('9dp') 58 '''The percentage of area on the left edge that triggers the opening of 59 the drawer. from 0-1 60 61 :attr:`drag_area` is a `NumericProperty` defaults to 2 62 ''' 63 64 hidden_widget = ObjectProperty(None) 65 ''' This is the widget that is hidden in phone mode on the left side of 66 drawer or displayed on the left of the overlay widget in tablet mode. 67 68 :attr:`hidden_widget` is a `ObjectProperty` defaults to None. 69 ''' 70 71 overlay_widget = ObjectProperty(None) 72 '''This a pointer to the default widget that is overlayed either on top or 73 to the right of the hidden widget. 74 ''' 75 76 def __init__(self, **kwargs): 77 super(Drawer, self).__init__(**kwargs) 78 79 self._triigger_gc = Clock.create_trigger(self._re_enable_gc, .2) 80 81 def toggle_drawer(self): 82 if app.ui_mode[0] == 't': 83 return 84 Factory.Animation.cancel_all(self.overlay_widget) 85 anim = Factory.Animation(x=self.hidden_widget.width 86 if self.state in ('opening', 'closed') else 0, 87 d=.1, t='linear') 88 anim.bind(on_complete = self._complete_drawer_animation) 89 anim.start(self.overlay_widget) 90 91 def _re_enable_gc(self, dt): 92 global gc 93 gc.enable() 94 95 def on_touch_down(self, touch): 96 if self.disabled: 97 return 98 99 if not self.collide_point(*touch.pos): 100 return 101 102 touch.grab(self) 103 104 # disable gc for smooth interaction 105 # This is still not enough while wallet is synchronising 106 # look into pausing all background tasks while ui interaction like this 107 gc.disable() 108 109 global app 110 if not app: 111 app = App.get_running_app() 112 113 # skip on tablet mode 114 if app.ui_mode[0] == 't': 115 return super(Drawer, self).on_touch_down(touch) 116 117 state = self.state 118 touch.ud['send_touch_down'] = False 119 start = 0 #if state[0] == 'c' else self.hidden_widget.right 120 drag_area = self.drag_area\ 121 if self.state[0] == 'c' else\ 122 (self.overlay_widget.x) 123 124 if touch.x < start or touch.x > drag_area: 125 if self.state == 'open': 126 self.toggle_drawer() 127 return 128 return super(Drawer, self).on_touch_down(touch) 129 130 self._touch = touch 131 Clock.schedule_once(self._change_touch_mode, 132 self.scroll_timeout/1000.) 133 touch.ud['in_drag_area'] = True 134 touch.ud['send_touch_down'] = True 135 return 136 137 def on_touch_move(self, touch): 138 if not touch.grab_current is self: 139 return 140 self._touch = False 141 # skip on tablet mode 142 if app.ui_mode[0] == 't': 143 return super(Drawer, self).on_touch_move(touch) 144 145 if not touch.ud.get('in_drag_area', None): 146 return super(Drawer, self).on_touch_move(touch) 147 148 ov = self.overlay_widget 149 ov.x=min(self.hidden_widget.width, 150 max(ov.x + touch.dx*2, 0)) 151 152 #_anim = Animation(x=x, duration=1/2, t='in_out_quart') 153 #_anim.cancel_all(ov) 154 #_anim.start(ov) 155 156 if abs(touch.x - touch.ox) < self.scroll_distance: 157 return 158 159 touch.ud['send_touch_down'] = False 160 Clock.unschedule(self._change_touch_mode) 161 self._touch = None 162 self.state = 'opening' if touch.dx > 0 else 'closing' 163 touch.ox = touch.x 164 return 165 166 def _change_touch_mode(self, *args): 167 if not self._touch: 168 return 169 touch = self._touch 170 touch.ungrab(self) 171 touch.ud['in_drag_area'] = False 172 touch.ud['send_touch_down'] = False 173 self._touch = None 174 super(Drawer, self).on_touch_down(touch) 175 return 176 177 def on_touch_up(self, touch): 178 if not touch.grab_current is self: 179 return 180 181 self._triigger_gc() 182 183 touch.ungrab(self) 184 touch.grab_current = None 185 186 # skip on tablet mode 187 get = touch.ud.get 188 if app.ui_mode[0] == 't': 189 return super(Drawer, self).on_touch_up(touch) 190 191 self.old_x = [1, ] * 10 192 self.speed = sum(( 193 (self.old_x[x + 1] - self.old_x[x]) for x in range(9))) / 9. 194 195 if get('send_touch_down', None): 196 # touch up called before moving 197 Clock.unschedule(self._change_touch_mode) 198 self._touch = None 199 Clock.schedule_once( 200 lambda dt: super(Drawer, self).on_touch_down(touch)) 201 if get('in_drag_area', None): 202 if abs(touch.x - touch.ox) < self.scroll_distance: 203 anim_to = (0 if self.state[0] == 'c' 204 else self.hidden_widget.width) 205 Factory.Animation(x=anim_to, d=.1).start(self.overlay_widget) 206 return 207 touch.ud['in_drag_area'] = False 208 if not get('send_touch_down', None): 209 self.toggle_drawer() 210 Clock.schedule_once(lambda dt: super(Drawer, self).on_touch_up(touch)) 211 212 def _complete_drawer_animation(self, *args): 213 self.state = 'open' if self.state in ('opening', 'closed') else 'closed' 214 215 def add_widget(self, widget, index=1): 216 if not widget: 217 return 218 219 iget = self.ids.get 220 if not iget('hidden_widget') or not iget('overlay_widget'): 221 super(Drawer, self).add_widget(widget) 222 return 223 224 if not self.hidden_widget: 225 self.hidden_widget = self.ids.hidden_widget 226 if not self.overlay_widget: 227 self.overlay_widget = self.ids.overlay_widget 228 229 if self.overlay_widget.children and self.hidden_widget.children: 230 Logger.debug('Drawer: Accepts only two widgets. discarding rest') 231 return 232 233 if not self.hidden_widget.children: 234 self.hidden_widget.add_widget(widget) 235 else: 236 self.overlay_widget.add_widget(widget) 237 widget.x = 0 238 239 def remove_widget(self, widget): 240 if self.overlay_widget.children[0] == widget: 241 self.overlay_widget.clear_widgets() 242 return 243 if widget == self.hidden_widget.children: 244 self.hidden_widget.clear_widgets() 245 return 246 247 def clear_widgets(self): 248 self.overlay_widget.clear_widgets() 249 self.hidden_widget.clear_widgets() 250 251 if __name__ == '__main__': 252 from kivy.app import runTouchApp 253 from kivy.lang import Builder 254 runTouchApp(Builder.load_string(''' 255 Drawer: 256 Button: 257 Button 258 '''))