Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Publish version 0.2.0 #28

Merged
merged 3 commits into from
Aug 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ From now on, I use the term `droppable` to refer both `KXReorderableBehavior` an
It's recommended to pin the minor version, because if it changed, it means some important breaking changes occurred.

```
poetry add kivy_garden.draggable@~0.1
pip install "kivy_garden.draggable>=0.1,<0.2"
poetry add kivy_garden.draggable@~0.2
pip install "kivy_garden.draggable>=0.2,<0.3"
```

## Main differences from drag_n_drop
Expand Down Expand Up @@ -156,7 +156,7 @@ But note that **only the default handler of `on_drag_succeed` and `on_drag_fail`
can be an async function. Those two only.**

You might say "What's the point of implementing a default handler as an async function,
when you can just launch any number of tasks from regular function by using ``asynckivy.start()``?".
when you can just launch any number of tasks from a regular function by using ``asynckivy.start()``?".
Well, if you use ``asynckivy.start()``, that task will run independently from the dragging process,
which means the draggable might fire ``on_drag_end`` and might start another drag while the task is still running.
If a default handler is an async function,
Expand Down
54 changes: 28 additions & 26 deletions README_jp.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
![](http://img.youtube.com/vi/CjiRZjiSqgA/0.jpg)
[Youtube][youtube]

`kivy_garden.draggable`はdrag&dropの機能を実現するための拡張機能で以下の三つの部品で構成される
`kivy_garden.draggable`はdrag&dropの機能を実現するための拡張機能で以下の三つの部品で構成されます

- `KXDraggableBehavior` ... dragできるようにしたいwidgetが継承すべきclass
- `KXDroppableBehavior`と`KXReorderableBehavior` ... dragされているwidgetを受け入れられるようにしたいwidgetが継承すべきclass
Expand All @@ -15,17 +15,18 @@
このmoduleのminor versionが変わった時は何らかの重要な互換性の無い変更が加えられた可能性が高いので、使う際はminor versionまでを固定してください。

```
poetry add kivy_garden.draggable@~0.1
pip install "kivy_garden.draggable>=0.1,<0.2"
poetry add kivy_garden.draggable@~0.2
pip install "kivy_garden.draggable>=0.2,<0.3"
```

## dragが始まる条件

dragは長押しによって引き起こされる。より具体的には利用者の指がdraggable内に降りてから`draggable.drag_distance`pixel以上動かずに`draggable.drag_timeout`ミリ秒以上指が離れなかった場合のみ引き起こされる。このためscroll操作(指がすぐさま動き出す)やtap動作(指がすぐに離れる)として誤認されにくい。
dragは長押しによって引き起こされます。より具体的には利用者の指がdraggable内に降りてから`draggable.drag_distance`pixel以上動かずに`draggable.drag_timeout`ミリ秒以上指が離れなかった場合のみ引き起こされます。
このためscroll操作(指がすぐさま動き出す)やtap動作(指がすぐに離れる)として誤認されにくいです。

## dragが始まった後の処理の流れ

ユーザーがdraggableの上に指を降ろしてdragが始まった後の流れは以下のようになる
ユーザーがdraggableの上に指を降ろしてdragが始まった後の流れは以下のようになります

```mermaid
stateDiagram-v2
Expand Down Expand Up @@ -60,26 +61,26 @@ stateDiagram-v2
## 受け入れるdragの選別

図に書かれているように利用者の指が離れた時にdragが受け入れられるか否かの判断がなされ、
指がdroppableの上じゃない所で離れた場合や`draggable.drag_cls`が`droppable.drag_classes`に含まれていない場合はまず即drag失敗となる
指がdroppableの上じゃない所で離れた場合や`draggable.drag_cls`が`droppable.drag_classes`に含まれていない場合はまず即drag失敗となります

その選別をくぐり抜けたdraggableは`droppable.accepts_drag()`へ渡され、そこでdragが受け入れられるか否かの最終判断が下される。例えばmethodが
その選別をくぐり抜けたdraggableは`droppable.accepts_drag()`へ渡され、そこでdragが受け入れられるか否かの最終判断が下されます。例えばmethodが

```python
class MyDroppable(KXDroppableBehavior, Widget):
def accepts_drag(self, touch, draggable) -> bool:
return not self.children
```

という風に実装されていたら、このdroppableは自分が子を持っている間は例え適切な`drag_cls`を持つdraggableであっても受け付けない
という風に実装されていたら、このdroppableは自分が子を持っている間は例え適切な`drag_cls`を持つdraggableであっても受け付けません

## dragの中止

アプリが次のシーンに移りたい時にまだdrag中のwidgetがあると不都合かもれしない。そのような事態に備えて
アプリが次のシーンに移りたい時にまだdrag中のwidgetがあると不都合かもしれません。そのような事態に備えて

- 現在進行中のdragを列挙する`ongoing_drags()`と
- dragを中止する`draggable.drag_cancel()`がある
- dragを中止する`draggable.drag_cancel()`があります

これらを用いる事で以下のように進行中のdragを全て中止できる
これらを用いる事で以下のように進行中のdragを全て中止できます

```python
from kivy_garden.draggable import ongoing_drags
Expand All @@ -92,17 +93,17 @@ def cancel_all_ongoing_drags():
## dragを引き起こすwidgetとdragされるwidgetを別にする

上で述べたようにdragはdraggableを長押しすることで引き起こされるので、
dragを引き起こすwidgetとdragされるwidgetは基本同じである
でも例えばcard gameを作っているとして画面上に山札があったとする。
そしてdrag操作によって山札から札を引けるようにしたいとする
dragを引き起こすwidgetとdragされるwidgetは基本同じです
でも例えばcard gameを作っているとして画面上に山札があったとして
drag操作によって山札から札を引けるようにしたいとします
具体的には利用者が山札に指を触れた時に札を作り出し、
そのまま指の動きに沿って札を追わせたいとする
そのまま指の動きに沿って札を追わせたいとします
このような

- dragを引き起こすwidget(山札)と
- dragされるwidget(山札から引かれた札)

が別である状況では`draggable.start_dragging_from_others_touch()`が使える
が別である状況では`draggable.start_dragging_from_others_touch()`が使えます

```python
class Card(KXDraggableBehavior, Widget):
Expand All @@ -117,23 +118,24 @@ class Deck(Widget):

## 自由に振る舞いを変える

dragが失敗/成功/中止した時に何をするかは完全にあなたに委ねられている。例えばdrag失敗時は既定ではアニメーションしながら元の場所に戻るが、これをアニメーション無しで瞬時に戻したいなら以下のようにdefault handlerを上書きすれば良い。
dragが失敗/成功/中止した時に何をするかは完全にあなたに委ねられています。
例えばdrag失敗時は既定ではアニメーションしながら元の場所に戻りますが、これをアニメーション無しで瞬時に戻したいなら以下のようにdefault handlerを上書きすれば良いです。

```python
class MyDraggable(KXDraggableBehavior, Widget):
def on_drag_fail(self, touch, ctx):
restore_widget_state(self, ctx.original_state)
```

また何もせずにその場に残って欲しいなら以下のようにすれば良い
また何もせずにその場に残って欲しいなら以下のようにすれば良いです

```python
class MyDraggable(KXDraggableBehavior, Widget):
def on_drag_fail(self, touch, ctx):
pass
```

成功時も同様で、既定では受け入れてくれたdroppableの子widgetになるように実装されているが以下のようにすれば子widgetにはならずに現在の位置で徐々に透明になって消える事になる
成功時も同様で、既定では受け入れてくれたdroppableの子widgetになるように実装されていますが以下のようにすると子widgetにはならずに現在の位置で徐々に透明になって消える事になります

```python
import asynckivy as ak
Expand All @@ -144,17 +146,17 @@ class MyDraggable(KXDraggableBehavior, Widget):
self.parent.remove_widget(self)
```

このようにdefault handlerを上書きすることで自由に振るまいを変えられる
ただし**async関数になれるのは`on_drag_succeed`と`on_drag_fail`のdefault handlerだけ**なので注意されたし
このようにdefault handlerを上書きすることで自由に振るまいを変えられます
ただし**async関数になれるのは`on_drag_succeed`と`on_drag_fail`のdefault handlerだけ**なので注意してください

ここで

- default handlerをasync関数にするのと
- default handlerは普通の関数のままにしておいて内部で`asynckivy.start()`を用いてasync関数を立ち上げるのと

の違いについて説明する
前者ではasync関数のcodeがdrag処理の間に挟み込まれ、codeが`on_drag_end`が起こるより前に完遂される事が保証されるのに対し、
後者ではcodeがdrag処理とは独立して進むので`on_drag_end`が起こるより前に完了する保証はない
の違いについて説明します
前者ではasync関数のcodeがdrag処理の間に挟み込まれcodeが`on_drag_end`が起こるより前に完遂される事が保証されるのに対し、
後者ではcodeがdrag処理とは独立して進むので`on_drag_end`が起こるより前に完了する保証はありません
なのでもし上の`on_drag_succeed`の例を後者のやり方で実装すると

```python
Expand All @@ -170,8 +172,8 @@ class MyDraggable(KXDraggableBehavior, Widget):
```

`ak.animate()`の進行中にdragが完了し、そこで利用者が再び指を触れたことで次のdragが始まり、
その最中に`self.parent.remove_widget(self)`が実行されてdraggableが親widgetから切り離されてしまうなんて事が起こりうる
なので **drag完了前に完遂させたい非同期処理があるのなら必ず前者の方法を使うべし**。
その最中に`self.parent.remove_widget(self)`が実行されてdraggableが親widgetから切り離されてしまうなんて事が起こりえます
なので **drag完了前に完遂させたい非同期処理があるのなら必ず前者の方法を使ってください**。

## その他

Expand Down
12 changes: 5 additions & 7 deletions examples/reacting_to_entering_and_leaving.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from kivy.utils import reify
from functools import cached_property
from kivy.properties import NumericProperty
from kivy.app import App
from kivy.lang import Builder
Expand Down Expand Up @@ -95,7 +95,7 @@ class ReactiveDroppableBehavior(KXDroppableBehavior):
'''
__events__ = ('on_drag_enter', 'on_drag_leave', )

@reify
@cached_property
def __ud_key(self):
return 'ReactiveDroppableBehavior.' + str(self.uid)

Expand All @@ -107,9 +107,7 @@ def on_touch_move(self, touch):
if drag_cls is not None:
touch_ud[ud_key] = None
if drag_cls in self.drag_classes:
# Start watching the touch. Use ``ak.or_()`` so that ``_watch_touch()`` will be automatically
# cancelled when the drag is cancelled.
ak.start(ak.or_(
ak.start(ak.wait_any(
self._watch_touch(touch),
ak.event(touch.ud['kivyx_draggable'], 'on_drag_end'),
))
Expand All @@ -122,8 +120,8 @@ async def _watch_touch(self, touch):

self.dispatch('on_drag_enter', touch, ctx, draggable)
try:
async with ak.watch_touch(self, touch) as is_touch_move:
while await is_touch_move():
async with ak.watch_touch(self, touch) as in_progress:
while await in_progress():
if not collide_point(*touch.pos):
return
finally:
Expand Down
7 changes: 3 additions & 4 deletions examples/reorderable_stacklayout.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
'''
When you want to make ``StackLayout`` re-orderable, you may want to disable
the ``size_hint`` of each child, or may want to limit the maximum size of
each child, otherwise the layout may be messed up. You can confirm it by
commenting/uncommenting the ``SampleApp.on_start()``'s body.
When you want to make a :class:`kivy.uix.stacklayout.StackLayout` re-orderable, you may want to disable the
``size_hint`` of each child, or may want to limit the maximum size of each child, otherwise the layout will be messed
up. You can confirm that by commenting/uncommenting the part of ``SampleApp.on_start()``.
'''

from kivy.lang import Builder
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "kivy-garden-draggable"
version = "0.2.0.dev1"
version = "0.2.0"
description = "Drag & Drop Extension for Kivy"
authors = ["Nattōsai Mitō <[email protected]>"]
license = "MIT"
Expand Down
17 changes: 9 additions & 8 deletions src/kivy_garden/draggable/_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
import asynckivy as ak

from ._utils import (
temp_transform, temp_grab_current, _create_spacer,
temp_transform, _create_spacer,
save_widget_state, restore_widget_state,
)

Expand Down Expand Up @@ -140,7 +140,7 @@ def start_dragging_from_others_touch(self, receiver: Widget, touch):
---------

* ``receiver`` ... The widget that received the ``touch``.
* ``touch`` ... A touch that is going to drag me.
* ``touch`` ... The touch that is going to drag me.
'''
if touch.time_end != -1:
return
Expand Down Expand Up @@ -228,10 +228,13 @@ async def _treat_a_touch_as_a_drag(self, touch, *, do_transform=False, touch_rec

async def _simulate_a_normal_touch(self, touch, *, do_transform=False, do_touch_up=False):
# simulate 'on_touch_down'
with temp_grab_current(touch):
original = touch.grab_current
try:
touch.grab_current = None
with temp_transform(touch, self.parent.to_widget) if do_transform else nullcontext():
super().on_touch_down(touch)
finally:
touch.grab_current = original

if not do_touch_up:
return
Expand Down Expand Up @@ -367,9 +370,7 @@ def on_touch_move(self, touch):
if drag_cls is not None:
touch_ud[ud_key] = None
if drag_cls in self.drag_classes:
# Start watching the touch. Use ``ak.or_()`` so that ``_watch_touch()`` will be automatically
# cancelled when the drag is cancelled.
ak.start(ak.or_(
ak.start(ak.wait_any(
self._watch_touch(touch),
ak.event(touch.ud['kivyx_draggable'], 'on_drag_end'),
))
Expand All @@ -392,8 +393,8 @@ async def _watch_touch(self, touch):
touch_ud['kivyx_drag_ctx'].original_state,
ignore_parent=True)
add_widget(spacer)
async with ak.watch_touch(self, touch) as is_touch_move:
while await is_touch_move():
async with ak.watch_touch(self, touch) as in_progress:
while await in_progress():
x, y = touch.pos
if collide_point(x, y):
new_idx = get_drop_insertion_index_move(x, y, spacer)
Expand Down
16 changes: 1 addition & 15 deletions src/kivy_garden/draggable/_utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
__all__ = (
'temp_transform', 'temp_grab_current',
'temp_transform',
'save_widget_state', 'restore_widget_state',
'save_widget_location', 'restore_widget_location',
)
Expand All @@ -24,20 +24,6 @@ def __exit__(self, *args):
self._touch.pop()


class temp_grab_current:
__slots__ = ('_touch', '_original', )

def __init__(self, touch):
self._touch = touch
self._original = touch.grab_current

def __enter__(self):
pass

def __exit__(self, *args):
self._touch.grab_current = self._original


_shallow_copyable_property_names = (
'x', 'y', 'width', 'height',
'size_hint_x', 'size_hint_y',
Expand Down