~ubuntu-branches/debian/stretch/electrum/stretch

« back to all changes in this revision

Viewing changes to gui/kivy/tools/.buildozer/android/platform/python-for-android/dist/kivy/python-install/lib/python2.7/site-packages/kivy/uix/scrollview.py

  • Committer: Package Import Robot
  • Author(s): Tristan Seligmann
  • Date: 2016-04-04 03:02:39 UTC
  • mfrom: (1.1.10)
  • Revision ID: package-import@ubuntu.com-20160404030239-0szgkio8yryjv7c9
Tags: 2.6.3-1
* New upstream release.
  - Drop backported install-wizard-connect.patch.
* Add Suggests: python-zbar and update the installation hint to suggest
  apt-get instead of pip (closes: #819517).
* Bump Standards-Version to 3.9.7 (no changes).
* Update Vcs-* links.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
'''Scroll View
 
2
===========
 
3
 
 
4
.. versionadded:: 1.0.4
 
5
 
 
6
The :class:`ScrollView` widget provides a scrollable/pannable viewport that is
 
7
clipped at the scrollview's bounding box.
 
8
 
 
9
 
 
10
Scrolling Behavior
 
11
------------------
 
12
 
 
13
The ScrollView accepts only one child and applies a viewport/window to
 
14
it according to the :attr:`ScrollView.scroll_x` and
 
15
:attr:`ScrollView.scroll_y` properties. Touches are analyzed to
 
16
determine if the user wants to scroll or control the child in some
 
17
other manner - you cannot do both at the same time. To determine if
 
18
interaction is a scrolling gesture, these properties are used:
 
19
 
 
20
    - :attr:`ScrollView.scroll_distance`: the minimum distance to travel,
 
21
         defaults to 20 pixels.
 
22
    - :attr:`ScrollView.scroll_timeout`: the maximum time period, defaults
 
23
         to 250 milliseconds.
 
24
 
 
25
If a touch travels :attr:`~ScrollView.scroll_distance` pixels within the
 
26
:attr:`~ScrollView.scroll_timeout` period, it is recognized as a scrolling
 
27
gesture and translation (scroll/pan) will begin. If the timeout occurs, the
 
28
touch down event is dispatched to the child instead (no translation).
 
29
 
 
30
The default value for those settings can be changed in the configuration file::
 
31
 
 
32
    [widgets]
 
33
    scroll_timeout = 250
 
34
    scroll_distance = 20
 
35
 
 
36
.. versionadded:: 1.1.1
 
37
 
 
38
    ScrollView now animates scrolling in Y when a mousewheel is used.
 
39
 
 
40
 
 
41
Limiting to the X or Y Axis
 
42
---------------------------
 
43
 
 
44
By default, the ScrollView allows scrolling in both the X and Y axes. You can
 
45
explicitly disable scrolling on an axis by setting
 
46
:attr:`ScrollView.do_scroll_x` or :attr:`ScrollView.do_scroll_y` to False.
 
47
 
 
48
 
 
49
Managing the Content Size and Position
 
50
--------------------------------------
 
51
 
 
52
ScrollView manages the position of its children similarly to a
 
53
RelativeLayout (see :mod:`~kivy.uix.relativelayout`) but not the size. You must
 
54
carefully specify the `size_hint` of your content to get the desired
 
55
scroll/pan effect.
 
56
 
 
57
By default, size_hint is (1, 1), so the content size will fit your ScrollView
 
58
exactly (you will have nothing to scroll). You must deactivate at least one of
 
59
the size_hint instructions (x or y) of the child to enable scrolling.
 
60
 
 
61
To scroll a :class:`GridLayout` on Y-axis/vertically, set the child's width
 
62
identical to that of the ScrollView (size_hint_x=1, default), and set the
 
63
size_hint_y property to None::
 
64
 
 
65
    layout = GridLayout(cols=1, spacing=10, size_hint_y=None)
 
66
    # Make sure the height is such that there is something to scroll.
 
67
    layout.bind(minimum_height=layout.setter('height'))
 
68
    for i in range(30):
 
69
        btn = Button(text=str(i), size_hint_y=None, height=40)
 
70
        layout.add_widget(btn)
 
71
    root = ScrollView(size_hint=(None, None), size=(400, 400))
 
72
    root.add_widget(layout)
 
73
 
 
74
 
 
75
Overscroll Effects
 
76
------------------
 
77
 
 
78
.. versionadded:: 1.7.0
 
79
 
 
80
When scrolling would exceed the bounds of the :class:`ScrollView`, it
 
81
uses a :class:`~kivy.effects.scroll.ScrollEffect` to handle the
 
82
overscroll. These effects can perform actions like bouncing back,
 
83
changing opacity, or simply preventing scrolling beyond the normal
 
84
boundaries. Note that complex effects may perform many computations,
 
85
which can be slow on weaker hardware.
 
86
 
 
87
You can change what effect is being used by setting
 
88
:attr:`ScrollView.effect_cls` to any effect class. Current options
 
89
include:
 
90
 
 
91
    - :class:`~kivy.effects.scroll.ScrollEffect`: Does not allow
 
92
      scrolling beyond the :class:`ScrollView` boundaries.
 
93
    - :class:`~kivy.effects.dampedscroll.DampedScrollEffect`: The
 
94
      current default. Allows the user to scroll beyond the normal
 
95
      boundaries, but has the content spring back once the
 
96
      touch/click is released.
 
97
    - :class:`~kivy.effects.opacityscroll.OpacityScrollEffect`: Similar
 
98
      to the :class:`~kivy.effect.dampedscroll.DampedScrollEffect`, but
 
99
      also reduces opacity during overscroll.
 
100
 
 
101
You can also create your own scroll effect by subclassing one of these,
 
102
then pass it as the :attr:`~ScrollView.effect_cls` in the same way.
 
103
 
 
104
Alternatively, you can set :attr:`ScrollView.effect_x` and/or
 
105
:attr:`ScrollView.effect_y` to an *instance* of the effect you want to
 
106
use. This will override the default effect set in
 
107
:attr:`ScrollView.effect_cls`.
 
108
 
 
109
All the effects are located in the :mod:`kivy.effects`.
 
110
 
 
111
'''
 
112
 
 
113
__all__ = ('ScrollView', )
 
114
 
 
115
from functools import partial
 
116
from kivy.animation import Animation
 
117
from kivy.compat import string_types
 
118
from kivy.config import Config
 
119
from kivy.clock import Clock
 
120
from kivy.factory import Factory
 
121
from kivy.uix.stencilview import StencilView
 
122
from kivy.metrics import sp, dp
 
123
from kivy.effects.dampedscroll import DampedScrollEffect
 
124
from kivy.properties import NumericProperty, BooleanProperty, AliasProperty, \
 
125
    ObjectProperty, ListProperty, ReferenceListProperty, OptionProperty
 
126
from kivy.uix.behaviors import FocusBehavior
 
127
 
 
128
 
 
129
# When we are generating documentation, Config doesn't exist
 
130
_scroll_timeout = _scroll_distance = 0
 
131
if Config:
 
132
    _scroll_timeout = Config.getint('widgets', 'scroll_timeout')
 
133
    _scroll_distance = sp(Config.getint('widgets', 'scroll_distance'))
 
134
 
 
135
 
 
136
class ScrollView(StencilView):
 
137
    '''ScrollView class. See module documentation for more information.
 
138
 
 
139
    :Events:
 
140
        `on_scroll_start`
 
141
            Generic event fired when scrolling starts from touch.
 
142
        `on_scroll_move`
 
143
            Generic event fired when scrolling move from touch.
 
144
        `on_scroll_stop`
 
145
            Generic event fired when scrolling stops from touch.
 
146
 
 
147
    .. versionchanged:: 1.9.0
 
148
        `on_scroll_start`, `on_scroll_move` and `on_scroll_stop` events are
 
149
        now dispatched when scrolling to handle nested ScrollViews.
 
150
 
 
151
    .. versionchanged:: 1.7.0
 
152
        `auto_scroll`, `scroll_friction`, `scroll_moves`, `scroll_stoptime' has
 
153
        been deprecated, use :attr:`effect_cls` instead.
 
154
    '''
 
155
 
 
156
    scroll_distance = NumericProperty(_scroll_distance)
 
157
    '''Distance to move before scrolling the :class:`ScrollView`, in pixels. As
 
158
    soon as the distance has been traveled, the :class:`ScrollView` will start
 
159
    to scroll, and no touch event will go to children.
 
160
    It is advisable that you base this value on the dpi of your target device's
 
161
    screen.
 
162
 
 
163
    :attr:`scroll_distance` is a :class:`~kivy.properties.NumericProperty` and
 
164
    defaults to 20 (pixels), according to the default value in user
 
165
    configuration.
 
166
    '''
 
167
 
 
168
    scroll_wheel_distance = NumericProperty(20)
 
169
    '''Distance to move when scrolling with a mouse wheel.
 
170
    It is advisable that you base this value on the dpi of your target device's
 
171
    screen.
 
172
 
 
173
    .. versionadded:: 1.8.0
 
174
 
 
175
    :attr:`scroll_wheel_distance` is a
 
176
    :class:`~kivy.properties.NumericProperty` , defaults to 20 pixels.
 
177
    '''
 
178
 
 
179
    scroll_timeout = NumericProperty(_scroll_timeout)
 
180
    '''Timeout allowed to trigger the :attr:`scroll_distance`, in milliseconds.
 
181
    If the user has not moved :attr:`scroll_distance` within the timeout,
 
182
    the scrolling will be disabled, and the touch event will go to the
 
183
    children.
 
184
 
 
185
    :attr:`scroll_timeout` is a :class:`~kivy.properties.NumericProperty` and
 
186
    defaults to 55 (milliseconds) according to the default value in user
 
187
    configuration.
 
188
 
 
189
    .. versionchanged:: 1.5.0
 
190
        Default value changed from 250 to 55.
 
191
    '''
 
192
 
 
193
    scroll_x = NumericProperty(0.)
 
194
    '''X scrolling value, between 0 and 1. If 0, the content's left side will
 
195
    touch the left side of the ScrollView. If 1, the content's right side will
 
196
    touch the right side.
 
197
 
 
198
    This property is controled by :class:`ScrollView` only if
 
199
    :attr:`do_scroll_x` is True.
 
200
 
 
201
    :attr:`scroll_x` is a :class:`~kivy.properties.NumericProperty` and
 
202
    defaults to 0.
 
203
    '''
 
204
 
 
205
    scroll_y = NumericProperty(1.)
 
206
    '''Y scrolling value, between 0 and 1. If 0, the content's bottom side will
 
207
    touch the bottom side of the ScrollView. If 1, the content's top side will
 
208
    touch the top side.
 
209
 
 
210
    This property is controled by :class:`ScrollView` only if
 
211
    :attr:`do_scroll_y` is True.
 
212
 
 
213
    :attr:`scroll_y` is a :class:`~kivy.properties.NumericProperty` and
 
214
    defaults to 1.
 
215
    '''
 
216
 
 
217
    do_scroll_x = BooleanProperty(True)
 
218
    '''Allow scroll on X axis.
 
219
 
 
220
    :attr:`do_scroll_x` is a :class:`~kivy.properties.BooleanProperty` and
 
221
    defaults to True.
 
222
    '''
 
223
 
 
224
    do_scroll_y = BooleanProperty(True)
 
225
    '''Allow scroll on Y axis.
 
226
 
 
227
    :attr:`do_scroll_y` is a :class:`~kivy.properties.BooleanProperty` and
 
228
    defaults to True.
 
229
    '''
 
230
 
 
231
    def _get_do_scroll(self):
 
232
        return (self.do_scroll_x, self.do_scroll_y)
 
233
 
 
234
    def _set_do_scroll(self, value):
 
235
        if type(value) in (list, tuple):
 
236
            self.do_scroll_x, self.do_scroll_y = value
 
237
        else:
 
238
            self.do_scroll_x = self.do_scroll_y = bool(value)
 
239
    do_scroll = AliasProperty(_get_do_scroll, _set_do_scroll,
 
240
                              bind=('do_scroll_x', 'do_scroll_y'))
 
241
    '''Allow scroll on X or Y axis.
 
242
 
 
243
    :attr:`do_scroll` is a :class:`~kivy.properties.AliasProperty` of
 
244
    (:attr:`do_scroll_x` + :attr:`do_scroll_y`)
 
245
    '''
 
246
 
 
247
    def _get_vbar(self):
 
248
        # must return (y, height) in %
 
249
        # calculate the viewport size / scrollview size %
 
250
        if self._viewport is None:
 
251
            return 0, 1.
 
252
        vh = self._viewport.height
 
253
        h = self.height
 
254
        if vh < h or vh == 0:
 
255
            return 0, 1.
 
256
        ph = max(0.01, h / float(vh))
 
257
        sy = min(1.0, max(0.0, self.scroll_y))
 
258
        py = (1. - ph) * sy
 
259
        return (py, ph)
 
260
 
 
261
    vbar = AliasProperty(_get_vbar, None, bind=(
 
262
        'scroll_y', '_viewport', 'viewport_size'))
 
263
    '''Return a tuple of (position, size) of the vertical scrolling bar.
 
264
 
 
265
    .. versionadded:: 1.2.0
 
266
 
 
267
    The position and size are normalized between 0-1, and represent a
 
268
    percentage of the current scrollview height. This property is used
 
269
    internally for drawing the little vertical bar when you're scrolling.
 
270
 
 
271
    :attr:`vbar` is a :class:`~kivy.properties.AliasProperty`, readonly.
 
272
    '''
 
273
 
 
274
    def _get_hbar(self):
 
275
        # must return (x, width) in %
 
276
        # calculate the viewport size / scrollview size %
 
277
        if self._viewport is None:
 
278
            return 0, 1.
 
279
        vw = self._viewport.width
 
280
        w = self.width
 
281
        if vw < w or vw == 0:
 
282
            return 0, 1.
 
283
        pw = max(0.01, w / float(vw))
 
284
        sx = min(1.0, max(0.0, self.scroll_x))
 
285
        px = (1. - pw) * sx
 
286
        return (px, pw)
 
287
 
 
288
    hbar = AliasProperty(_get_hbar, None, bind=(
 
289
        'scroll_x', '_viewport', 'viewport_size'))
 
290
    '''Return a tuple of (position, size) of the horizontal scrolling bar.
 
291
 
 
292
    .. versionadded:: 1.2.0
 
293
 
 
294
    The position and size are normalized between 0-1, and represent a
 
295
    percentage of the current scrollview height. This property is used
 
296
    internally for drawing the little horizontal bar when you're scrolling.
 
297
 
 
298
    :attr:`vbar` is a :class:`~kivy.properties.AliasProperty`, readonly.
 
299
    '''
 
300
 
 
301
    bar_color = ListProperty([.7, .7, .7, .9])
 
302
    '''Color of horizontal / vertical scroll bar, in RGBA format.
 
303
 
 
304
    .. versionadded:: 1.2.0
 
305
 
 
306
    :attr:`bar_color` is a :class:`~kivy.properties.ListProperty` and defaults
 
307
    to [.7, .7, .7, .9].
 
308
    '''
 
309
 
 
310
    bar_inactive_color = ListProperty([.7, .7, .7, .2])
 
311
    '''Color of horizontal / vertical scroll bar (in RGBA format), when no
 
312
    scroll is happening.
 
313
 
 
314
    .. versionadded:: 1.9.0
 
315
 
 
316
    :attr:`bar_inactive_color` is a
 
317
    :class:`~kivy.properties.ListProperty` and defaults to [.7, .7, .7, .2].
 
318
    '''
 
319
 
 
320
    bar_width = NumericProperty('2dp')
 
321
    '''Width of the horizontal / vertical scroll bar. The width is interpreted
 
322
    as a height for the horizontal bar.
 
323
 
 
324
    .. versionadded:: 1.2.0
 
325
 
 
326
    :attr:`bar_width` is a :class:`~kivy.properties.NumericProperty` and
 
327
    defaults to 2.
 
328
    '''
 
329
 
 
330
    bar_pos_x = OptionProperty('bottom', options=('top', 'bottom'))
 
331
    '''Which side of the ScrollView the horizontal scroll bar should go
 
332
    on. Possible values are 'top' and 'bottom'.
 
333
 
 
334
    .. versionadded:: 1.8.0
 
335
 
 
336
    :attr:`bar_pos_x` is an :class:`~kivy.properties.OptionProperty`,
 
337
    defaults to 'bottom'.
 
338
 
 
339
    '''
 
340
 
 
341
    bar_pos_y = OptionProperty('right', options=('left', 'right'))
 
342
    '''Which side of the ScrollView the vertical scroll bar should go
 
343
    on. Possible values are 'left' and 'right'.
 
344
 
 
345
    .. versionadded:: 1.8.0
 
346
 
 
347
    :attr:`bar_pos_y` is an :class:`~kivy.properties.OptionProperty` and
 
348
    defaults to 'right'.
 
349
 
 
350
    '''
 
351
 
 
352
    bar_pos = ReferenceListProperty(bar_pos_x, bar_pos_y)
 
353
    '''Which side of the scroll view to place each of the bars on.
 
354
 
 
355
    :attr:`bar_pos` is a :class:`~kivy.properties.ReferenceListProperty` of
 
356
    (:attr:`bar_pos_x`, :attr:`bar_pos_y`)
 
357
    '''
 
358
 
 
359
    bar_margin = NumericProperty(0)
 
360
    '''Margin between the bottom / right side of the scrollview when drawing
 
361
    the horizontal / vertical scroll bar.
 
362
 
 
363
    .. versionadded:: 1.2.0
 
364
 
 
365
    :attr:`bar_margin` is a :class:`~kivy.properties.NumericProperty`, default
 
366
    to 0
 
367
    '''
 
368
 
 
369
    effect_cls = ObjectProperty(DampedScrollEffect, allownone=True)
 
370
    '''Class effect to instanciate for X and Y axis.
 
371
 
 
372
    .. versionadded:: 1.7.0
 
373
 
 
374
    :attr:`effect_cls` is an :class:`~kivy.properties.ObjectProperty` and
 
375
    defaults to :class:`DampedScrollEffect`.
 
376
 
 
377
    .. versionchanged:: 1.8.0
 
378
        If you set a string, the :class:`~kivy.factory.Factory` will be used to
 
379
        resolve the class.
 
380
 
 
381
    '''
 
382
 
 
383
    effect_x = ObjectProperty(None, allownone=True)
 
384
    '''Effect to apply for the X axis. If None is set, an instance of
 
385
    :attr:`effect_cls` will be created.
 
386
 
 
387
    .. versionadded:: 1.7.0
 
388
 
 
389
    :attr:`effect_x` is an :class:`~kivy.properties.ObjectProperty` and
 
390
    defaults to None.
 
391
    '''
 
392
 
 
393
    effect_y = ObjectProperty(None, allownone=True)
 
394
    '''Effect to apply for the Y axis. If None is set, an instance of
 
395
    :attr:`effect_cls` will be created.
 
396
 
 
397
    .. versionadded:: 1.7.0
 
398
 
 
399
    :attr:`effect_y` is an :class:`~kivy.properties.ObjectProperty` and
 
400
    defaults to None, read-only.
 
401
    '''
 
402
 
 
403
    viewport_size = ListProperty([0, 0])
 
404
    '''(internal) Size of the internal viewport. This is the size of your only
 
405
    child in the scrollview.
 
406
    '''
 
407
 
 
408
    scroll_type = OptionProperty(['content'], options=(['content'], ['bars'],
 
409
                                 ['bars', 'content'], ['content', 'bars']))
 
410
    '''Sets the type of scrolling to use for the content of the scrollview.
 
411
    Available options are: ['content'], ['bars'], ['bars', 'content'].
 
412
 
 
413
    .. versionadded:: 1.8.0
 
414
 
 
415
    :attr:`scroll_type` is a :class:`~kivy.properties.OptionProperty`, defaults
 
416
    to ['content'].
 
417
    '''
 
418
 
 
419
    # private, for internal use only
 
420
 
 
421
    _viewport = ObjectProperty(None, allownone=True)
 
422
    _bar_color = ListProperty([0, 0, 0, 0])
 
423
 
 
424
    def _set_viewport_size(self, instance, value):
 
425
        self.viewport_size = value
 
426
 
 
427
    def on__viewport(self, instance, value):
 
428
        if value:
 
429
            value.bind(size=self._set_viewport_size)
 
430
            self.viewport_size = value.size
 
431
 
 
432
    __events__ = ('on_scroll_start', 'on_scroll_move', 'on_scroll_stop')
 
433
 
 
434
    def __init__(self, **kwargs):
 
435
        self._touch = None
 
436
        self._trigger_update_from_scroll = Clock.create_trigger(
 
437
            self.update_from_scroll, -1)
 
438
        # create a specific canvas for the viewport
 
439
        from kivy.graphics import PushMatrix, Translate, PopMatrix, Canvas
 
440
        self.canvas_viewport = Canvas()
 
441
        self.canvas = Canvas()
 
442
        with self.canvas_viewport.before:
 
443
            PushMatrix()
 
444
            self.g_translate = Translate(0, 0)
 
445
        with self.canvas_viewport.after:
 
446
            PopMatrix()
 
447
 
 
448
        super(ScrollView, self).__init__(**kwargs)
 
449
 
 
450
        self.register_event_type('on_scroll_start')
 
451
        self.register_event_type('on_scroll_move')
 
452
        self.register_event_type('on_scroll_stop')
 
453
 
 
454
        # now add the viewport canvas to our canvas
 
455
        self.canvas.add(self.canvas_viewport)
 
456
 
 
457
        effect_cls = self.effect_cls
 
458
        if isinstance(effect_cls, string_types):
 
459
            effect_cls = Factory.get(effect_cls)
 
460
        if self.effect_x is None and effect_cls is not None:
 
461
            self.effect_x = effect_cls(target_widget=self._viewport)
 
462
        if self.effect_y is None and effect_cls is not None:
 
463
            self.effect_y = effect_cls(target_widget=self._viewport)
 
464
 
 
465
        trigger_update_from_scroll = self._trigger_update_from_scroll
 
466
        update_effect_widget = self._update_effect_widget
 
467
        update_effect_x_bounds = self._update_effect_x_bounds
 
468
        update_effect_y_bounds = self._update_effect_y_bounds
 
469
        fbind = self.fbind
 
470
        fbind('width', update_effect_x_bounds)
 
471
        fbind('height', update_effect_y_bounds)
 
472
        fbind('viewport_size', self._update_effect_bounds)
 
473
        fbind('_viewport', update_effect_widget)
 
474
        fbind('scroll_x', trigger_update_from_scroll)
 
475
        fbind('scroll_y', trigger_update_from_scroll)
 
476
        fbind('pos', trigger_update_from_scroll)
 
477
        fbind('size', trigger_update_from_scroll)
 
478
 
 
479
        update_effect_widget()
 
480
        update_effect_x_bounds()
 
481
        update_effect_y_bounds()
 
482
 
 
483
    def on_effect_x(self, instance, value):
 
484
        if value:
 
485
            value.bind(scroll=self._update_effect_x)
 
486
            value.target_widget = self._viewport
 
487
 
 
488
    def on_effect_y(self, instance, value):
 
489
        if value:
 
490
            value.bind(scroll=self._update_effect_y)
 
491
            value.target_widget = self._viewport
 
492
 
 
493
    def on_effect_cls(self, instance, cls):
 
494
        if isinstance(cls, string_types):
 
495
            cls = Factory.get(cls)
 
496
        self.effect_x = cls(target_widget=self._viewport)
 
497
        self.effect_x.bind(scroll=self._update_effect_x)
 
498
        self.effect_y = cls(target_widget=self._viewport)
 
499
        self.effect_y.bind(scroll=self._update_effect_y)
 
500
 
 
501
    def _update_effect_widget(self, *args):
 
502
        if self.effect_x:
 
503
            self.effect_x.target_widget = self._viewport
 
504
        if self.effect_y:
 
505
            self.effect_y.target_widget = self._viewport
 
506
 
 
507
    def _update_effect_x_bounds(self, *args):
 
508
        if not self._viewport or not self.effect_x:
 
509
            return
 
510
        self.effect_x.min = -(self.viewport_size[0] - self.width)
 
511
        self.effect_x.max = 0
 
512
        self.effect_x.value = self.effect_x.min * self.scroll_x
 
513
 
 
514
    def _update_effect_y_bounds(self, *args):
 
515
        if not self._viewport or not self.effect_y:
 
516
            return
 
517
        self.effect_y.min = -(self.viewport_size[1] - self.height)
 
518
        self.effect_y.max = 0
 
519
        self.effect_y.value = self.effect_y.min * self.scroll_y
 
520
 
 
521
    def _update_effect_bounds(self, *args):
 
522
        if not self._viewport:
 
523
            return
 
524
        if self.effect_x:
 
525
            self._update_effect_x_bounds()
 
526
        if self.effect_y:
 
527
            self._update_effect_y_bounds()
 
528
 
 
529
    def _update_effect_x(self, *args):
 
530
        vp = self._viewport
 
531
        if not vp or not self.effect_x:
 
532
            return
 
533
        sw = vp.width - self.width
 
534
        if sw < 1:
 
535
            return
 
536
        sx = self.effect_x.scroll / float(sw)
 
537
        self.scroll_x = -sx
 
538
        self._trigger_update_from_scroll()
 
539
 
 
540
    def _update_effect_y(self, *args):
 
541
        vp = self._viewport
 
542
        if not vp or not self.effect_y:
 
543
            return
 
544
        sh = vp.height - self.height
 
545
        if sh < 1:
 
546
            return
 
547
        sy = self.effect_y.scroll / float(sh)
 
548
        self.scroll_y = -sy
 
549
        self._trigger_update_from_scroll()
 
550
 
 
551
    def to_local(self, x, y, **k):
 
552
        tx, ty = self.g_translate.xy
 
553
        return x - tx, y - ty
 
554
 
 
555
    def to_parent(self, x, y, **k):
 
556
        tx, ty = self.g_translate.xy
 
557
        return x + tx, y + ty
 
558
 
 
559
    def _apply_transform(self, m, pos=None):
 
560
        tx, ty = self.g_translate.xy
 
561
        m.translate(tx, ty, 0)
 
562
        return super(ScrollView, self)._apply_transform(m, (0, 0))
 
563
 
 
564
    def simulate_touch_down(self, touch):
 
565
        # at this point the touch is in parent coords
 
566
        touch.push()
 
567
        touch.apply_transform_2d(self.to_local)
 
568
        ret = super(ScrollView, self).on_touch_down(touch)
 
569
        touch.pop()
 
570
        return ret
 
571
 
 
572
    def on_touch_down(self, touch):
 
573
        if self.dispatch('on_scroll_start', touch):
 
574
            self._touch = touch
 
575
            touch.grab(self)
 
576
            return True
 
577
 
 
578
    def on_scroll_start(self, touch, check_children=True):
 
579
        if check_children:
 
580
            touch.push()
 
581
            touch.apply_transform_2d(self.to_local)
 
582
            if self.dispatch_children('on_scroll_start', touch):
 
583
                return True
 
584
            touch.pop()
 
585
 
 
586
        if not self.collide_point(*touch.pos):
 
587
            touch.ud[self._get_uid('svavoid')] = True
 
588
            return
 
589
        if self.disabled:
 
590
            return True
 
591
        if self._touch or (not (self.do_scroll_x or self.do_scroll_y)):
 
592
            return self.simulate_touch_down(touch)
 
593
 
 
594
        # handle mouse scrolling, only if the viewport size is bigger than the
 
595
        # scrollview size, and if the user allowed to do it
 
596
        vp = self._viewport
 
597
        if not vp:
 
598
            return True
 
599
        scroll_type = self.scroll_type
 
600
        ud = touch.ud
 
601
        scroll_bar = 'bars' in scroll_type
 
602
 
 
603
        # check if touch is in bar_x(horizontal) or bay_y(bertical)
 
604
        ud['in_bar_x'] = ud['in_bar_y'] = False
 
605
        width_scrollable = vp.width > self.width
 
606
        height_scrollable = vp.height > self.height
 
607
        bar_pos_x = self.bar_pos_x[0]
 
608
        bar_pos_y = self.bar_pos_y[0]
 
609
 
 
610
        d = {'b': True if touch.y < self.y + self.bar_width else False,
 
611
             't': True if touch.y > self.top - self.bar_width else False,
 
612
             'l': True if touch.x < self.x + self.bar_width else False,
 
613
             'r': True if touch.x > self.right - self.bar_width else False}
 
614
        if scroll_bar:
 
615
            if (width_scrollable and d[bar_pos_x]):
 
616
                ud['in_bar_x'] = True
 
617
            if (height_scrollable and d[bar_pos_y]):
 
618
                ud['in_bar_y'] = True
 
619
 
 
620
        if vp and 'button' in touch.profile and \
 
621
                touch.button.startswith('scroll'):
 
622
            btn = touch.button
 
623
            m = sp(self.scroll_wheel_distance)
 
624
            e = None
 
625
 
 
626
            if ((btn == 'scrolldown' and self.scroll_y >= 1) or
 
627
                (btn == 'scrollup' and self.scroll_y <= 0) or
 
628
                (btn == 'scrollleft' and self.scroll_x >= 1) or
 
629
                (btn == 'scrollright' and self.scroll_x <= 0)):
 
630
                return False
 
631
 
 
632
            if (self.effect_x and self.do_scroll_y and height_scrollable
 
633
                    and btn in ('scrolldown', 'scrollup')):
 
634
                e = self.effect_x if ud['in_bar_x'] else self.effect_y
 
635
 
 
636
            elif (self.effect_y and self.do_scroll_x and width_scrollable
 
637
                    and btn in ('scrollleft', 'scrollright')):
 
638
                e = self.effect_y if ud['in_bar_y'] else self.effect_x
 
639
 
 
640
            if e:
 
641
                if btn in ('scrolldown', 'scrollleft'):
 
642
                    e.value = max(e.value - m, e.min)
 
643
                    e.velocity = 0
 
644
                elif btn in ('scrollup', 'scrollright'):
 
645
                    e.value = min(e.value + m, e.max)
 
646
                    e.velocity = 0
 
647
                touch.ud[self._get_uid('svavoid')] = True
 
648
                e.trigger_velocity_update()
 
649
            return True
 
650
 
 
651
        # no mouse scrolling, so the user is going to drag the scrollview with
 
652
        # this touch.
 
653
        self._touch = touch
 
654
        uid = self._get_uid()
 
655
        FocusBehavior.ignored_touch.append(touch)
 
656
 
 
657
        ud[uid] = {
 
658
            'mode': 'unknown',
 
659
            'dx': 0,
 
660
            'dy': 0,
 
661
            'user_stopped': False,
 
662
            'frames': Clock.frames,
 
663
            'time': touch.time_start}
 
664
 
 
665
        if self.do_scroll_x and self.effect_x and not ud['in_bar_x']:
 
666
            self.effect_x.start(touch.x)
 
667
            self._scroll_x_mouse = self.scroll_x
 
668
        if self.do_scroll_y and self.effect_y and not ud['in_bar_y']:
 
669
            self.effect_y.start(touch.y)
 
670
            self._scroll_y_mouse = self.scroll_y
 
671
 
 
672
        if (ud.get('in_bar_x', False) or ud.get('in_bar_y', False)):
 
673
            return True
 
674
 
 
675
        Clock.schedule_once(self._change_touch_mode,
 
676
                                self.scroll_timeout / 1000.)
 
677
        if scroll_type == ['bars']:
 
678
            return False
 
679
        else:
 
680
            return True
 
681
 
 
682
    def on_touch_move(self, touch):
 
683
        if self._touch is not touch:
 
684
            # touch is in parent
 
685
            touch.push()
 
686
            touch.apply_transform_2d(self.to_local)
 
687
            super(ScrollView, self).on_touch_move(touch)
 
688
            touch.pop()
 
689
            return self._get_uid() in touch.ud
 
690
        if touch.grab_current is not self:
 
691
            return True
 
692
 
 
693
        if not (self.do_scroll_y or self.do_scroll_x):
 
694
            return super(ScrollView, self).on_touch_move(touch)
 
695
 
 
696
        touch.ud['sv.handled'] = {'x': False, 'y': False}
 
697
        if self.dispatch('on_scroll_move', touch):
 
698
            return True
 
699
 
 
700
    def on_scroll_move(self, touch):
 
701
        if self._get_uid('svavoid') in touch.ud:
 
702
            return False
 
703
 
 
704
        touch.push()
 
705
        touch.apply_transform_2d(self.to_local)
 
706
        if self.dispatch_children('on_scroll_move', touch):
 
707
            return True
 
708
        touch.pop()
 
709
 
 
710
        rv = True
 
711
 
 
712
        uid = self._get_uid()
 
713
        if not uid in touch.ud:
 
714
            self._touch = False
 
715
            return self.on_scroll_start(touch, False)
 
716
        ud = touch.ud[uid]
 
717
        mode = ud['mode']
 
718
 
 
719
        # check if the minimum distance has been travelled
 
720
        if mode == 'unknown' or mode == 'scroll':
 
721
            if not touch.ud['sv.handled']['x'] and self.do_scroll_x \
 
722
                    and self.effect_x:
 
723
                width = self.width
 
724
                if touch.ud.get('in_bar_x', False):
 
725
                    dx = touch.dx / float(width - width * self.hbar[1])
 
726
                    self.scroll_x = min(max(self.scroll_x + dx, 0.), 1.)
 
727
                    self._trigger_update_from_scroll()
 
728
                else:
 
729
                    if self.scroll_type != ['bars']:
 
730
                        self.effect_x.update(touch.x)
 
731
                if self.scroll_x < 0 or self.scroll_x > 1:
 
732
                    rv = False
 
733
                else:
 
734
                    touch.ud['sv.handled']['x'] = True
 
735
            if not touch.ud['sv.handled']['y'] and self.do_scroll_y \
 
736
                    and self.effect_y:
 
737
                height = self.height
 
738
                if touch.ud.get('in_bar_y', False):
 
739
                    dy = touch.dy / float(height - height * self.vbar[1])
 
740
                    self.scroll_y = min(max(self.scroll_y + dy, 0.), 1.)
 
741
                    self._trigger_update_from_scroll()
 
742
                else:
 
743
                    if self.scroll_type != ['bars']:
 
744
                        self.effect_y.update(touch.y)
 
745
                if self.scroll_y < 0 or self.scroll_y > 1:
 
746
                    rv = False
 
747
                else:
 
748
                    touch.ud['sv.handled']['y'] = True
 
749
 
 
750
        if mode == 'unknown':
 
751
            ud['dx'] += abs(touch.dx)
 
752
            ud['dy'] += abs(touch.dy)
 
753
            if ud['dx'] or ud['dy'] > self.scroll_distance:
 
754
                if not self.do_scroll_x and not self.do_scroll_y:
 
755
                    # touch is in parent, but _change expects window coords
 
756
                    touch.push()
 
757
                    touch.apply_transform_2d(self.to_local)
 
758
                    touch.apply_transform_2d(self.to_window)
 
759
                    self._change_touch_mode()
 
760
                    touch.pop()
 
761
                    return
 
762
                mode = 'scroll'
 
763
            ud['mode'] = mode
 
764
 
 
765
        if mode == 'scroll':
 
766
            ud['dt'] = touch.time_update - ud['time']
 
767
            ud['time'] = touch.time_update
 
768
            ud['user_stopped'] = True
 
769
 
 
770
        return rv
 
771
 
 
772
    def on_touch_up(self, touch):
 
773
        if self._touch is not touch and self.uid not in touch.ud:
 
774
            # touch is in parents
 
775
            touch.push()
 
776
            touch.apply_transform_2d(self.to_local)
 
777
            if super(ScrollView, self).on_touch_up(touch):
 
778
                return True
 
779
            touch.pop()
 
780
            return False
 
781
 
 
782
        if self.dispatch('on_scroll_stop', touch):
 
783
            touch.ungrab(self)
 
784
            return True
 
785
 
 
786
    def on_scroll_stop(self, touch, check_children=True):
 
787
        self._touch = None
 
788
 
 
789
        if check_children:
 
790
            touch.push()
 
791
            touch.apply_transform_2d(self.to_local)
 
792
            if self.dispatch_children('on_scroll_stop', touch):
 
793
                return True
 
794
            touch.pop()
 
795
 
 
796
        if self._get_uid('svavoid') in touch.ud:
 
797
            return
 
798
        if self._get_uid() not in touch.ud:
 
799
            return False
 
800
 
 
801
        self._touch = None
 
802
        uid = self._get_uid()
 
803
        ud = touch.ud[uid]
 
804
        if self.do_scroll_x and self.effect_x:
 
805
            if not touch.ud.get('in_bar_x', False) and\
 
806
                    self.scroll_type != ['bars']:
 
807
                self.effect_x.stop(touch.x)
 
808
        if self.do_scroll_y and self.effect_y and\
 
809
                self.scroll_type != ['bars']:
 
810
            if not touch.ud.get('in_bar_y', False):
 
811
                self.effect_y.stop(touch.y)
 
812
        if ud['mode'] == 'unknown':
 
813
            # we must do the click at least..
 
814
            # only send the click if it was not a click to stop
 
815
            # autoscrolling
 
816
            if not ud['user_stopped']:
 
817
                self.simulate_touch_down(touch)
 
818
            Clock.schedule_once(partial(self._do_touch_up, touch), .2)
 
819
        Clock.unschedule(self._update_effect_bounds)
 
820
        Clock.schedule_once(self._update_effect_bounds)
 
821
 
 
822
        # if we do mouse scrolling, always accept it
 
823
        if 'button' in touch.profile and touch.button.startswith('scroll'):
 
824
            return True
 
825
 
 
826
        return self._get_uid() in touch.ud
 
827
 
 
828
    def scroll_to(self, widget, padding=10, animate=True):
 
829
        '''Scrolls the viewport to ensure that the given widget is visible,
 
830
        optionally with padding and animation. If animate is True (the
 
831
        default), then the default animation parameters will be used.
 
832
        Otherwise, it should be a dict containing arguments to pass to
 
833
        :class:`~kivy.animation.Animation` constructor.
 
834
 
 
835
        .. versionadded:: 1.9.1
 
836
        '''
 
837
        if not self.parent:
 
838
            return
 
839
 
 
840
        if isinstance(padding, (int, float)):
 
841
            padding = (padding, padding)
 
842
 
 
843
        pos = self.parent.to_widget(*widget.to_window(*widget.pos))
 
844
        cor = self.parent.to_widget(*widget.to_window(widget.right,
 
845
                                                      widget.top))
 
846
 
 
847
        dx = dy = 0
 
848
 
 
849
        if pos[1] < self.y:
 
850
            dy = self.y - pos[1] + dp(padding[1])
 
851
        elif cor[1] > self.top:
 
852
            dy = self.top - cor[1] - dp(padding[1])
 
853
 
 
854
        if pos[0] < self.x:
 
855
            dx = self.x - pos[0] + dp(padding[0])
 
856
        elif cor[0] > self.right:
 
857
            dx = self.right - cor[0] - dp(padding[0])
 
858
 
 
859
        dsx, dsy = self.convert_distance_to_scroll(dx, dy)
 
860
        sxp = min(1, max(0, self.scroll_x - dsx))
 
861
        syp = min(1, max(0, self.scroll_y - dsy))
 
862
 
 
863
        if animate:
 
864
            if animate is True:
 
865
                animate = {'d': 0.2, 't': 'out_quad'}
 
866
            Animation.stop_all(self, 'scroll_x', 'scroll_y')
 
867
            Animation(scroll_x=sxp, scroll_y=syp, **animate).start(self)
 
868
        else:
 
869
            self.scroll_x = sxp
 
870
            self.scroll_y = syp
 
871
 
 
872
    def convert_distance_to_scroll(self, dx, dy):
 
873
        '''Convert a distance in pixels to a scroll distance, depending on the
 
874
        content size and the scrollview size.
 
875
 
 
876
        The result will be a tuple of scroll distance that can be added to
 
877
        :data:`scroll_x` and :data:`scroll_y`
 
878
        '''
 
879
        if not self._viewport:
 
880
            return 0, 0
 
881
        vp = self._viewport
 
882
        if vp.width > self.width:
 
883
            sw = vp.width - self.width
 
884
            sx = dx / float(sw)
 
885
        else:
 
886
            sx = 0
 
887
        if vp.height > self.height:
 
888
            sh = vp.height - self.height
 
889
            sy = dy / float(sh)
 
890
        else:
 
891
            sy = 1
 
892
        return sx, sy
 
893
 
 
894
    def update_from_scroll(self, *largs):
 
895
        '''Force the reposition of the content, according to current value of
 
896
        :attr:`scroll_x` and :attr:`scroll_y`.
 
897
 
 
898
        This method is automatically called when one of the :attr:`scroll_x`,
 
899
        :attr:`scroll_y`, :attr:`pos` or :attr:`size` properties change, or
 
900
        if the size of the content changes.
 
901
        '''
 
902
        if not self._viewport:
 
903
            return
 
904
        vp = self._viewport
 
905
 
 
906
        # update from size_hint
 
907
        if vp.size_hint_x is not None:
 
908
            vp.width = vp.size_hint_x * self.width
 
909
        if vp.size_hint_y is not None:
 
910
            vp.height = vp.size_hint_y * self.height
 
911
 
 
912
        if vp.width > self.width:
 
913
            sw = vp.width - self.width
 
914
            x = self.x - self.scroll_x * sw
 
915
        else:
 
916
            x = self.x
 
917
        if vp.height > self.height:
 
918
            sh = vp.height - self.height
 
919
            y = self.y - self.scroll_y * sh
 
920
        else:
 
921
            y = self.top - vp.height
 
922
 
 
923
        # from 1.8.0, we now use a matrix by default, instead of moving the
 
924
        # widget position behind. We set it here, but it will be a no-op most of
 
925
        # the time.
 
926
        vp.pos = 0, 0
 
927
        self.g_translate.xy = x, y
 
928
 
 
929
        # New in 1.2.0, show bar when scrolling happens and (changed in 1.9.0)
 
930
        # fade to bar_inactive_color when no scroll is happening.
 
931
        Clock.unschedule(self._bind_inactive_bar_color)
 
932
        self.funbind('bar_inactive_color', self._change_bar_color)
 
933
        Animation.stop_all(self, '_bar_color')
 
934
        self.fbind('bar_color', self._change_bar_color)
 
935
        self._bar_color = self.bar_color
 
936
        Clock.schedule_once(self._bind_inactive_bar_color, .5)
 
937
 
 
938
    def _bind_inactive_bar_color(self, *l):
 
939
        self.funbind('bar_color', self._change_bar_color)
 
940
        self.fbind('bar_inactive_color', self._change_bar_color)
 
941
        Animation(
 
942
            _bar_color=self.bar_inactive_color, d=.5, t='out_quart').start(self)
 
943
 
 
944
    def _change_bar_color(self, inst, value):
 
945
        self._bar_color = value
 
946
 
 
947
    #
 
948
    # Private
 
949
    #
 
950
    def add_widget(self, widget, index=0):
 
951
        if self._viewport:
 
952
            raise Exception('ScrollView accept only one widget')
 
953
        canvas = self.canvas
 
954
        self.canvas = self.canvas_viewport
 
955
        super(ScrollView, self).add_widget(widget, index)
 
956
        self.canvas = canvas
 
957
        self._viewport = widget
 
958
        widget.bind(size=self._trigger_update_from_scroll)
 
959
        self._trigger_update_from_scroll()
 
960
 
 
961
    def remove_widget(self, widget):
 
962
        canvas = self.canvas
 
963
        self.canvas = self.canvas_viewport
 
964
        super(ScrollView, self).remove_widget(widget)
 
965
        self.canvas = canvas
 
966
        if widget is self._viewport:
 
967
            self._viewport = None
 
968
 
 
969
    def _get_uid(self, prefix='sv'):
 
970
        return '{0}.{1}'.format(prefix, self.uid)
 
971
 
 
972
    def _change_touch_mode(self, *largs):
 
973
        if not self._touch:
 
974
            return
 
975
        uid = self._get_uid()
 
976
        touch = self._touch
 
977
        if uid not in touch.ud:
 
978
            self._touch = False
 
979
            return
 
980
        ud = touch.ud[uid]
 
981
        if ud['mode'] != 'unknown' or ud['user_stopped']:
 
982
            return
 
983
        diff_frames = Clock.frames - ud['frames']
 
984
 
 
985
        # in order to be able to scroll on very slow devices, let at least 3
 
986
        # frames displayed to accumulate some velocity. And then, change the
 
987
        # touch mode. Otherwise, we might never be able to compute velocity, and
 
988
        # no way to scroll it. See #1464 and #1499
 
989
        if diff_frames < 3:
 
990
            Clock.schedule_once(self._change_touch_mode, 0)
 
991
            return
 
992
 
 
993
        if self.do_scroll_x and self.effect_x:
 
994
            self.effect_x.cancel()
 
995
        if self.do_scroll_y and self.effect_y:
 
996
            self.effect_y.cancel()
 
997
        # XXX the next line was in the condition. But this stop
 
998
        # the possibily to "drag" an object out of the scrollview in the
 
999
        # non-used direction: if you have an horizontal scrollview, a
 
1000
        # vertical gesture will not "stop" the scroll view to look for an
 
1001
        # horizontal gesture, until the timeout is done.
 
1002
        # and touch.dx + touch.dy == 0:
 
1003
        touch.ungrab(self)
 
1004
        self._touch = None
 
1005
        # touch is in window coords
 
1006
        touch.push()
 
1007
        touch.apply_transform_2d(self.to_widget)
 
1008
        touch.apply_transform_2d(self.to_parent)
 
1009
        self.simulate_touch_down(touch)
 
1010
        touch.pop()
 
1011
        return
 
1012
 
 
1013
    def _do_touch_up(self, touch, *largs):
 
1014
        # touch is in window coords
 
1015
        touch.push()
 
1016
        touch.apply_transform_2d(self.to_widget)
 
1017
        super(ScrollView, self).on_touch_up(touch)
 
1018
        touch.pop()
 
1019
        # don't forget about grab event!
 
1020
        for x in touch.grab_list[:]:
 
1021
            touch.grab_list.remove(x)
 
1022
            x = x()
 
1023
            if not x:
 
1024
                continue
 
1025
            touch.grab_current = x
 
1026
            # touch is in window coords
 
1027
            touch.push()
 
1028
            touch.apply_transform_2d(self.to_widget)
 
1029
            super(ScrollView, self).on_touch_up(touch)
 
1030
            touch.pop()
 
1031
        touch.grab_current = None
 
1032
 
 
1033
 
 
1034
if __name__ == '__main__':
 
1035
    from kivy.app import App
 
1036
 
 
1037
    from kivy.uix.gridlayout import GridLayout
 
1038
    from kivy.uix.button import Button
 
1039
 
 
1040
    class ScrollViewApp(App):
 
1041
 
 
1042
        def build(self):
 
1043
            layout1 = GridLayout(cols=4, spacing=10, size_hint=(None, None))
 
1044
            layout1.bind(minimum_height=layout1.setter('height'),
 
1045
                         minimum_width=layout1.setter('width'))
 
1046
            for i in range(40):
 
1047
                btn = Button(text=str(i), size_hint=(None, None),
 
1048
                             size=(200, 100))
 
1049
                layout1.add_widget(btn)
 
1050
            scrollview1 = ScrollView(bar_width='2dp')
 
1051
            scrollview1.add_widget(layout1)
 
1052
 
 
1053
            layout2 = GridLayout(cols=4, spacing=10, size_hint=(None, None))
 
1054
            layout2.bind(minimum_height=layout2.setter('height'),
 
1055
                         minimum_width=layout2.setter('width'))
 
1056
            for i in range(40):
 
1057
                btn = Button(text=str(i), size_hint=(None, None),
 
1058
                             size=(200, 100))
 
1059
                layout2.add_widget(btn)
 
1060
            scrollview2 = ScrollView(scroll_type=['bars'],
 
1061
                                     bar_width='9dp',
 
1062
                                     scroll_wheel_distance=100)
 
1063
            scrollview2.add_widget(layout2)
 
1064
 
 
1065
            root = GridLayout(cols=2)
 
1066
            root.add_widget(scrollview1)
 
1067
            root.add_widget(scrollview2)
 
1068
            return root
 
1069
 
 
1070
    ScrollViewApp().run()