예제 #1
0
def test_wait_for_an_already_cancelled_task(wait_for, expected):
    task1 = ak.Task(ak.sleep_forever())
    ak.start(task1)
    task1.cancel()
    assert task1.cancelled
    task2 = ak.Task(task1.wait(wait_for))
    ak.start(task2)
    assert task2.state is expected
예제 #2
0
def test_wait_for_an_already_finished_task(wait_for, expected):
    task1 = ak.Task(ak.sleep_forever())
    ak.start(task1)
    with pytest.raises(StopIteration):
        task1.root_coro.send(None)
    assert task1.done
    task2 = ak.Task(task1.wait(wait_for))
    ak.start(task2)
    assert task2.state is expected
예제 #3
0
def test_cancel_the_waiter_before_the_awaited():
    task1 = ak.Task(ak.sleep_forever())
    task2 = ak.Task(task1.wait())
    ak.start(task1)
    ak.start(task2)
    task2.cancel()
    assert task1.state is TS.STARTED
    assert task2.state is TS.CANCELLED
    with pytest.raises(StopIteration):
        task1.root_coro.send(None)
    assert task1.state is TS.DONE
    assert task2.state is TS.CANCELLED
예제 #4
0
def test_multiple_tasks_wait_for_the_same_task_to_be_cancelled(
    wait_for_a,
    expected_a,
    wait_for_b,
    expected_b,
):
    task1 = ak.Task(ak.sleep_forever())
    task2a = ak.Task(task1.wait(wait_for_a))
    task2b = ak.Task(task1.wait(wait_for_b))
    ak.start(task1)
    ak.start(task2a)
    ak.start(task2b)
    task1.cancel()
    assert task2a.state is expected_a
    assert task2b.state is expected_b
예제 #5
0
def test_the_state_and_the_result():
    job_state = 'A'

    async def job():
        nonlocal job_state
        job_state = 'B'
        await ak.sleep_forever()
        job_state = 'C'
        return 'result'

    task = ak.Task(job())
    root_coro = task.root_coro
    assert task.state is TS.CREATED
    assert job_state == 'A'
    with pytest.raises(ak.InvalidStateError):
        task.result

    ak.start(task)
    assert task.state is TS.STARTED
    assert job_state == 'B'
    with pytest.raises(ak.InvalidStateError):
        task.result

    with pytest.raises(StopIteration):
        root_coro.send(None)
    assert task.state is TS.DONE
    assert task.done
    assert not task.cancelled
    assert task.result == 'result'
예제 #6
0
def test_the_state_and_the_result__ver_uncaught_exception():
    job_state = 'A'

    async def job():
        nonlocal job_state
        job_state = 'B'
        await ak.sleep_forever()
        job_state = 'C'
        raise ZeroDivisionError
        return 'result'

    task = ak.Task(job(), name='pytest')
    root_coro = task.root_coro
    assert task.state is TS.CREATED
    assert job_state == 'A'
    with pytest.raises(ak.InvalidStateError):
        task.result

    ak.start(task)
    assert task.state is TS.STARTED
    assert job_state == 'B'
    with pytest.raises(ak.InvalidStateError):
        task.result

    with pytest.raises(ZeroDivisionError):
        root_coro.send(None)
    assert task.state is TS.CANCELLED
    job_state = 'C'
    assert not task.done
    assert task.cancelled
    with pytest.raises(ak.CancelledError):
        task.result
예제 #7
0
def test_the_state_and_the_result__ver_cancel():
    job_state = 'A'

    async def job():
        nonlocal job_state
        job_state = 'B'
        await ak.sleep_forever()
        job_state = 'C'
        return 'result'

    task = ak.Task(job(), name='pytest')
    root_coro = task.root_coro
    assert task.state is TS.CREATED
    assert job_state == 'A'
    with pytest.raises(ak.InvalidStateError):
        task.result

    ak.start(task)
    assert task.state is TS.STARTED
    assert job_state == 'B'
    with pytest.raises(ak.InvalidStateError):
        task.result

    root_coro.close()
    assert task.state is TS.CANCELLED
    assert not task.done
    assert task.cancelled
    with pytest.raises(ak.CancelledError):
        task.result
예제 #8
0
def test_multiple_tasks_wait_for_the_same_task_to_complete(
    wait_for_a,
    expected_a,
    wait_for_b,
    expected_b,
):
    task1 = ak.Task(ak.sleep_forever())
    task2a = ak.Task(task1.wait(wait_for_a))
    task2b = ak.Task(task1.wait(wait_for_b))
    ak.start(task1)
    ak.start(task2a)
    ak.start(task2b)
    with pytest.raises(StopIteration):
        task1.root_coro.send(None)
    assert task2a.state is expected_a
    assert task2b.state is expected_b
예제 #9
0
def test_various_wait_flag(wait_for, should_raise):
    task = ak.Task(ak.sleep_forever())
    ak.start(task)  # just for suppressing a warning
    coro = task.wait(wait_for)
    if should_raise:
        with pytest.raises(ValueError):
            coro.send(None)
    else:
        coro.send(None)
예제 #10
0
def test__get_current_task():
    import asynckivy as ak
    done = False

    async def job():
        assert await ak.get_current_task() is task
        nonlocal done
        done = True

    task = ak.Task(job())
    ak.start(task)
    assert done
예제 #11
0
 def on_touch_down(self, touch):
     if self._is_a_touch_potentially_a_drag(touch):
         touch.ud[self.__ud_key] = None
         if self.drag_timeout:
             ak.start(self._see_if_a_touch_can_be_treated_as_a_drag(touch))
         else:
             self._drag_task.cancel()
             self._drag_task = ak.Task(self._treat_a_touch_as_a_drag(touch))
             ak.start(self._drag_task)
         return True
     else:
         touch.ud[self.__ud_key] = None
         return super().on_touch_down(touch)
예제 #12
0
    def drag_start_from_other_widget(self, drag_from: Widget, touch):
        '''
        Starts dragging as if the draggable existed where ``drag_from`` is.

        * Sizing/Positioning properties will be overwritten by ``drag_from``'s.
        * The draggable shouldn't have a parent.
        * This method should be called from ``drag_from``'s touch events.
        '''
        if self.parent is not None:
            raise ak.InvalidStateError("Draggable shouldn't have a parent")
        if touch.time_end != -1:
            return
        touch.ud[self.__ud_key] = None
        self._drag_task = ak.Task(
            self._treat_a_touch_as_a_drag(touch, drag_from=drag_from))
        ak.start(self._drag_task)
예제 #13
0
 async def _see_if_a_touch_actually_is_a_dragging_gesture(self, touch):
     tasks = await ak.or_(
         ak.sleep(self.drag_timeout / 1000.),
         self._true_when_a_touch_ended_false_when_it_moved_too_much(touch),
     )
     if tasks[0].done:
         # The given touch is a dragging gesture.
         if self._can_be_dragged:
             self._drag_task.cancel()
             self._drag_task = ak.Task(
                 self._treat_a_touch_as_a_drag(touch, do_transform=True))
             ak.start(self._drag_task)
         else:
             ak.start(
                 self._simulate_a_normal_touch(touch, do_transform=True))
     else:
         # The given touch is not a dragging gesture.
         ak.start(
             self._simulate_a_normal_touch(touch,
                                           do_touch_up=tasks[1].result))
예제 #14
0
class KXDraggableBehavior:
    __events__ = (
        'on_drag_start',
        'on_drag_end',
        'on_drag_success',
        'on_drag_fail',
        'on_drag_cancel',
    )

    drag_cls = StringProperty()
    '''Same as drag_n_drop's '''

    drag_distance = NumericProperty(_scroll_distance)

    drag_timeout = NumericProperty(_scroll_timeout)

    drag_enabled = BooleanProperty(True)
    '''Indicates whether this draggable can be dragged or not. Changing this
    doesn't affect ongoing drag.
    '''

    is_being_dragged = BooleanProperty(False)
    '''(read-only)'''

    # default value of the instance attributes
    _drag_task = ak.Task(
        ak.sleep_forever(),
        name=r"KXDraggableBehavior's dummy task",
    )

    @staticmethod
    @deprecated(msg=r"'KXDraggableBehavior.ongoing_drags()' is deprecated. "
                r"Use the stand-alone 'ongoing_drags()' function instead")
    def ongoing_drags(*, window=None) -> Iterator['KXDraggableBehavior']:
        if window is None:
            from kivy.core.window import Window
            window = Window
        return (c for c in window.children
                if isinstance(c, KXDraggableBehavior) and c.is_being_dragged)

    @property
    def drag_context(self) -> Union[None, DragContext]:
        return self._drag_ctx

    def drag_cancel(self):
        '''Cancels drag as soon as possible. Does nothing if the draggable is
        not being dragged.
        '''
        self._drag_task.cancel()

    def __init__(self, **kwargs):
        self._drag_ctx = None
        super().__init__(**kwargs)
        self.__ud_key = 'KXDraggableBehavior.' + str(self.uid)

    def _is_a_touch_potentially_a_dragging_gesture(self, touch) -> bool:
        return self.collide_point(*touch.opos) \
            and (not touch.is_mouse_scrolling) \
            and (self.__ud_key not in touch.ud) \
            and (touch.time_end == -1)

    @property
    def _can_be_dragged(self) -> bool:
        return self.drag_enabled and (not self.is_being_dragged)

    def on_touch_down(self, touch):
        if self._is_a_touch_potentially_a_dragging_gesture(touch) \
                and self._can_be_dragged:
            touch.ud[self.__ud_key] = None
            if self.drag_timeout:
                ak.start(
                    self._see_if_a_touch_actually_is_a_dragging_gesture(touch))
            else:
                self._drag_task.cancel()
                self._drag_task = ak.Task(self._treat_a_touch_as_a_drag(touch))
                ak.start(self._drag_task)
            return True
        else:
            touch.ud[self.__ud_key] = None
            return super().on_touch_down(touch)

    async def _see_if_a_touch_actually_is_a_dragging_gesture(self, touch):
        tasks = await ak.or_(
            ak.sleep(self.drag_timeout / 1000.),
            self._true_when_a_touch_ended_false_when_it_moved_too_much(touch),
        )
        if tasks[0].done:
            # The given touch is a dragging gesture.
            if self._can_be_dragged:
                self._drag_task.cancel()
                self._drag_task = ak.Task(
                    self._treat_a_touch_as_a_drag(touch, do_transform=True))
                ak.start(self._drag_task)
            else:
                ak.start(
                    self._simulate_a_normal_touch(touch, do_transform=True))
        else:
            # The given touch is not a dragging gesture.
            ak.start(
                self._simulate_a_normal_touch(touch,
                                              do_touch_up=tasks[1].result))

    async def _true_when_a_touch_ended_false_when_it_moved_too_much(
            self, touch):
        # assigning to a local variable might improve performance
        abs_ = abs
        drag_distance = self.drag_distance

        ox, oy = touch.opos
        async for __ in ak.rest_of_touch_moves(self, touch):
            dx = abs_(touch.x - ox)
            dy = abs_(touch.y - oy)
            if dy > drag_distance or dx > drag_distance:
                return False
        return True

    def drag_start_from_other_widget(self, drag_from: Widget, touch):
        '''
        Starts dragging as if the draggable existed where ``drag_from`` is.

        * Sizing/Positioning properties will be overwritten by ``drag_from``'s.
        * The draggable shouldn't have a parent.
        * This method should be called from ``drag_from``'s touch events.
        '''
        if self.parent is not None:
            raise ak.InvalidStateError("Draggable shouldn't have a parent")
        if touch.time_end != -1:
            return
        touch.ud[self.__ud_key] = None
        self._drag_task = ak.Task(
            self._treat_a_touch_as_a_drag(touch, drag_from=drag_from))
        ak.start(self._drag_task)

    async def _treat_a_touch_as_a_drag(self,
                                       touch,
                                       *,
                                       do_transform=False,
                                       drag_from=None):
        if drag_from is None:
            drag_from = self
        self.is_being_dragged = True
        try:
            # NOTE: I don't know the difference from 'get_root_window()'
            window = drag_from.get_parent_window()
            touch_ud = touch.ud
            original_pos_win = drag_from.to_window(*drag_from.pos)
            original_location = save_widget_location(
                drag_from, ignore_parent=(drag_from is not self))
            original_location.setdefault('weak_parent', None)
            self._drag_ctx = ctx = DragContext(
                original_pos_win=original_pos_win,
                original_location=original_location,
            )

            if do_transform:
                touch.push()
                touch.apply_transform_2d(drag_from.parent.to_widget)
            offset_x = touch.ox - drag_from.x
            offset_y = touch.oy - drag_from.y
            if do_transform:
                touch.pop()

            # move self under the Window
            if self.parent is None:  # more like (drag_from is not self)
                restore_widget_location(self,
                                        original_location,
                                        ignore_parent=True)
            else:
                self.parent.remove_widget(self)
            self.size_hint = (
                None,
                None,
            )
            self.pos_hint = {}
            self.pos = (
                original_pos_win[0] + touch.x - touch.ox,
                original_pos_win[1] + touch.y - touch.oy,
            )
            window.add_widget(self)

            # mark the touch so that other widgets can react to the drag
            touch_ud['kivyx_drag_cls'] = self.drag_cls
            touch_ud['kivyx_draggable'] = self

            self.dispatch('on_drag_start', touch)
            async for __ in ak.rest_of_touch_moves(self, touch):
                self.x = touch.x - offset_x
                self.y = touch.y - offset_y

            # wait for other widgets to react to 'on_touch_up'
            await ak.sleep(-1)

            ctx.droppable = droppable = touch_ud.get('kivyx_droppable', None)
            if droppable is None or (not droppable.accepts_drag(touch, self)):
                ctx.state = 'failed'
                r = self.dispatch('on_drag_fail', touch)
            else:
                ctx.state = 'succeeded'
                r = self.dispatch('on_drag_success', touch)
            async with ak.cancel_protection():
                if isawaitable(r):
                    await r
                # I cannot remember why this 'ak.sleep()' exists.
                # It might be unnecessary.
                await ak.sleep(-1)
        except GeneratorExit:
            ctx.state = 'cancelled'
            self.dispatch('on_drag_cancel', touch)
            raise
        finally:
            self.dispatch('on_drag_end', touch)
            self.is_being_dragged = False
            self._drag_ctx = None
            touch_ud['kivyx_droppable'] = None
            del touch_ud['kivyx_drag_cls']
            del touch_ud['kivyx_draggable']

    async def _simulate_a_normal_touch(self,
                                       touch,
                                       *,
                                       do_transform=False,
                                       do_touch_up=False):
        # simulate 'on_touch_down'
        with temp_grab_current(touch):
            touch.grab_current = None
            if do_transform:
                touch.push()
                touch.apply_transform_2d(self.parent.to_widget)
            super().on_touch_down(touch)
            if do_transform:
                touch.pop()

        if not do_touch_up:
            return
        await ak.sleep(.1)

        # simulate 'on_touch_up'
        to_widget = self.to_widget if self.parent is None \
            else self.parent.to_widget
        touch.grab_current = None
        with temp_transform(touch):
            touch.apply_transform_2d(to_widget)
            super().on_touch_up(touch)

        # simulate the grabbed one as well
        for x in tuple(touch.grab_list):
            touch.grab_list.remove(x)
            x = x()
            if x is None:
                continue
            touch.grab_current = x
            with temp_transform(touch):
                touch.apply_transform_2d(x.parent.to_widget)
                x.dispatch('on_touch_up', touch)

        touch.grab_current = None
        return

    def on_drag_start(self, touch):
        pass

    def on_drag_end(self, touch):
        pass

    def on_drag_success(self, touch):
        ctx = self._drag_ctx
        original_location = ctx.original_location
        self.parent.remove_widget(self)
        self.size_hint_x = original_location['size_hint_x']
        self.size_hint_y = original_location['size_hint_y']
        self.pos_hint = original_location['pos_hint']
        ctx.droppable.add_widget(self,
                                 index=touch.ud.get('kivyx_droppable_index',
                                                    0))

    async def on_drag_fail(self, touch):
        ctx = self._drag_ctx
        await ak.animate(
            self,
            d=.1,
            x=ctx.original_pos_win[0],
            y=ctx.original_pos_win[1],
        )
        restore_widget_location(self, ctx.original_location)

    def on_drag_cancel(self, touch):
        restore_widget_location(self, self._drag_ctx.original_location)
예제 #15
0
class KXDraggableBehavior:
    __events__ = (
        'on_drag_start',
        'on_drag_end',
        'on_drag_success',
        'on_drag_fail',
    )

    drag_cls = StringProperty()
    '''Same as drag_n_drop's '''

    drag_distance = NumericProperty(_scroll_distance)

    drag_timeout = NumericProperty(_scroll_timeout)

    drag_enabled = BooleanProperty(True)

    is_being_dragged = BooleanProperty(False)
    '''(read-only)'''

    # default value of the instance attributes
    _drag_task = ak.Task(
        ak.sleep_forever(),
        name=r"KXDraggableBehavior's dummy task",
    )

    @staticmethod
    def ongoing_drags(*, window=None) -> Iterator['KXDraggableBehavior']:
        if window is None:
            from kivy.core.window import Window
            window = Window
        return (c for c in window.children
                if isinstance(c, KXDraggableBehavior) and c.is_being_dragged)

    @property
    def drag_context(self) -> Union[None, DragContext]:
        return self._drag_ctx

    def drag_cancel(self):
        '''Cancels drag. Might be delayed depending on the internal state.'''
        task = self._drag_task
        if task.is_cancellable:
            task.cancel()
        else:
            ak.close_soon(task)

    def __init__(self, **kwargs):
        self._drag_ctx = None
        super().__init__(**kwargs)
        self.__ud_key = 'KXDraggableBehavior.' + str(self.uid)

    def _is_a_touch_potentially_a_drag(self, touch) -> bool:
        return self.collide_point(*touch.opos) \
            and self.drag_enabled \
            and (not self.is_being_dragged) \
            and (not touch.is_mouse_scrolling) \
            and (self.__ud_key not in touch.ud) \
            and (touch.time_end == -1)

    def on_touch_down(self, touch):
        if self._is_a_touch_potentially_a_drag(touch):
            touch.ud[self.__ud_key] = None
            if self.drag_timeout:
                ak.start(self._see_if_a_touch_can_be_treated_as_a_drag(touch))
            else:
                self._drag_task.cancel()
                self._drag_task = ak.Task(self._treat_a_touch_as_a_drag(touch))
                ak.start(self._drag_task)
            return True
        else:
            touch.ud[self.__ud_key] = None
            return super().on_touch_down(touch)

    async def _see_if_a_touch_can_be_treated_as_a_drag(self, touch):
        tasks = await ak.or_(
            ak.sleep(self.drag_timeout / 1000.),
            self._true_when_a_touch_ended_false_when_it_moved_too_much(touch),
        )
        if tasks[0].done:
            # The given touch is a dragging gesture.
            if self.is_being_dragged or (not self.drag_enabled):
                ak.start(
                    self._simulate_a_normal_touch(touch, do_transform=True))
            else:
                self._drag_task.cancel()
                self._drag_task = ak.Task(
                    self._treat_a_touch_as_a_drag(touch, do_transform=True))
                ak.start(self._drag_task)
        else:
            # The given touch is not a dragging gesture.
            ak.start(
                self._simulate_a_normal_touch(touch,
                                              do_touch_up=tasks[1].result))

    async def _true_when_a_touch_ended_false_when_it_moved_too_much(
            self, touch):
        drag_distance = self.drag_distance
        ox, oy = touch.opos
        async for __ in ak.rest_of_touch_moves(self, touch):
            dx = abs(touch.x - ox)
            dy = abs(touch.y - oy)
            if dy > drag_distance or dx > drag_distance:
                return False
        return True

    async def _treat_a_touch_as_a_drag(self, touch, *, do_transform=False):
        self.is_being_dragged = True
        try:
            # NOTE: I don't know the difference from 'get_root_window()'
            window = self.get_parent_window()
            touch_ud = touch.ud
            original_pos_win = self.to_window(*self.pos)
            original_location = save_widget_location(self)
            self._drag_ctx = ctx = DragContext(
                original_pos_win=original_pos_win,
                original_location=original_location,
            )

            if do_transform:
                touch.push()
                touch.apply_transform_2d(self.parent.to_widget)
            offset_x = touch.ox - self.x
            offset_y = touch.oy - self.y
            if do_transform:
                touch.pop()

            # move self under the Window
            self.parent.remove_widget(self)
            self.size_hint = (
                None,
                None,
            )
            self.pos_hint = {}
            self.pos = (
                original_pos_win[0] + touch.x - touch.ox,
                original_pos_win[1] + touch.y - touch.oy,
            )
            window.add_widget(self)

            # mark the touch so that the other widgets can react to the drag
            touch_ud['kivyx_drag_cls'] = self.drag_cls
            touch_ud['kivyx_draggable'] = self

            self.dispatch('on_drag_start', touch)
            async for __ in ak.rest_of_touch_moves(self, touch):
                self.x = touch.x - offset_x
                self.y = touch.y - offset_y

            # we need to give the other widgets the time to react to
            # 'on_touch_up'
            await ak.sleep(-1)

            ctx.droppable = droppable = touch_ud.get('kivyx_droppable', None)
            failed = droppable is None or \
                not droppable.accepts_drag(touch, self)
            r = self.dispatch('on_drag_fail' if failed else 'on_drag_success',
                              touch)
            if isawaitable(r):
                await r
            # I cannot remember why this 'ak.sleep()' exists.
            # It might be unnecessary.
            await ak.sleep(-1)
        except GeneratorExit:
            ctx.cancelled = True
            raise
        finally:
            self.dispatch('on_drag_end', touch)
            self.is_being_dragged = False
            self._drag_ctx = None
            touch_ud['kivyx_droppable'] = None
            del touch_ud['kivyx_drag_cls']
            del touch_ud['kivyx_draggable']

    async def _simulate_a_normal_touch(self,
                                       touch,
                                       *,
                                       do_transform=False,
                                       do_touch_up=False):
        # simulate 'on_touch_down'
        with temp_grab_current(touch):
            touch.grab_current = None
            if do_transform:
                touch.push()
                touch.apply_transform_2d(self.parent.to_widget)
            super().on_touch_down(touch)
            if do_transform:
                touch.pop()

        if not do_touch_up:
            return
        await ak.sleep(.1)

        # simulate 'on_touch_up'
        to_widget = self.to_widget if self.parent is None \
            else self.parent.to_widget
        touch.grab_current = None
        with temp_transform(touch):
            touch.apply_transform_2d(to_widget)
            super().on_touch_up(touch)

        # simulate the grabbed one as well
        for x in tuple(touch.grab_list):
            touch.grab_list.remove(x)
            x = x()
            if x is None:
                continue
            touch.grab_current = x
            with temp_transform(touch):
                touch.apply_transform_2d(x.parent.to_widget)
                x.dispatch('on_touch_up', touch)

        touch.grab_current = None
        return

    def on_drag_start(self, touch):
        pass

    def on_drag_end(self, touch):
        pass

    def on_drag_success(self, touch):
        ctx = self._drag_ctx
        original_location = ctx.original_location
        self.parent.remove_widget(self)
        self.size_hint_x = original_location['size_hint_x']
        self.size_hint_y = original_location['size_hint_y']
        self.pos_hint = original_location['pos_hint']
        ctx.droppable.add_widget(self,
                                 index=touch.ud.get('kivyx_droppable_index',
                                                    0))

    async def on_drag_fail(self, touch):
        ctx = self._drag_ctx
        await ak.animate(
            self,
            d=.1,
            x=ctx.original_pos_win[0],
            y=ctx.original_pos_win[1],
        )
        restore_widget_location(self, ctx.original_location)