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
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
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
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
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'
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
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
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
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)
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
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)
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 _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))
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)
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)