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

trivial fixes #29

Merged
merged 7 commits into from
Sep 19, 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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

Inspired by:

* [drag_n_drop][drag_n_drop] (`Draggable` is based on this, so please read its documentation first to get the basic idea of this one)
* [drag_n_drop][drag_n_drop] (`Draggable` is based on this, so reading its documentation may help you understand `Draggable`)
* [Flutter][flutter]

This flower adds a drag and drop functionality to layouts and widgets. There are 3
Expand Down Expand Up @@ -76,7 +76,7 @@ stateDiagram-v2

## Cancellation

When your app switches a scene, you may want to cancel all the ongoing drags.
When your app switches a scene, you may want to cancel all ongoing drags.
`ongoing_drags()` and `draggable.drag_cancel()` are what you want.

```python
Expand Down
27 changes: 22 additions & 5 deletions README_jp.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
- `KXDraggableBehavior` ... dragできるようにしたいwidgetが継承すべきclass
- `KXDroppableBehavior`と`KXReorderableBehavior` ... dragされているwidgetを受け入れられるようにしたいwidgetが継承すべきclass

`KXDroppableBehavior`と`KXReorderableBehavior`の違いはFlutterにおける[DragTarget][flutter_draggable_video]と[reorderables][flutter_reorderables]の違いに相当し、drag操作によってwidgetを並び替えたいなら`KXReorderableBehavior`を、そうじゃなければ`KXDroppableBehavior`を使うと良い。これらの名前は長ったらしいので以後は、dragを受け入れられるwidgetをまとめて「droppable」と呼び、dragできるwidgetを「draggable」と呼ぶ。
`KXDroppableBehavior`と`KXReorderableBehavior`の違いはFlutterにおける[DragTarget][flutter_draggable_video]と[reorderables][flutter_reorderables]の違いに相当し、
drag操作によってwidgetを並び替えたいなら`KXReorderableBehavior`を、そうじゃなければ`KXDroppableBehavior`を使うと良いです。
これらの名前は長ったらしいので以後は、dragを受け入れられるwidgetをまとめて「droppable」と呼び、dragできるwidgetを「draggable」と呼ぶ事にします。

## Install方法

Expand Down Expand Up @@ -94,8 +96,8 @@ def cancel_all_ongoing_drags():

上で述べたようにdragはdraggableを長押しすることで引き起こされるので、
dragを引き起こすwidgetとdragされるwidgetは基本同じです。
でも例えばcard gameを作っているとして画面上に山札があったとして
drag操作によって山札から札を引けるようにしたいとします
でも例えばカードゲームを作っていて画面上に山札があったとして
drag操作によって山札から札を引けるようにしたかったとします
具体的には利用者が山札に指を触れた時に札を作り出し、
そのまま指の動きに沿って札を追わせたいとします。
このような
Expand Down Expand Up @@ -168,13 +170,28 @@ class MyDraggable(KXDraggableBehavior, Widget):

async def _fade_out(self, touch):
await ak.animate(self, opacity=0)
self.parent.remove_widget(self)
self.parent.remove_widget(self) # A
```

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

後これはこのモジュール特有ではなくKivyのイベントシステム共通の事なのですが、
`.bind()`で結びつけた関数が真を返すとその関数よりも前に結びつけた関数とdefault handerが呼ばれなくなります。
これを利用すれば一時的に振る舞いを変えられます。

```python
def 即座に元の位置へ戻す(draggable, touch, ctx):
restore_widget_state(draggable, ctx.original_state)
return True

draggable = MyDraggable()
draggable.bind(on_drag_fail=即座に元の位置へ戻す) # このインスタンスの振る舞いを変える
...
draggable.unbind(on_drag_fail=即座に元の位置へ戻す) # 元に戻す
```

## その他

- [drag_n_drop][drag_n_drop] ... この拡張機能の元になった物
Expand Down
78 changes: 34 additions & 44 deletions examples/flutter_style_draggable.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from kivy.app import App
from kivy.lang import Builder
from kivy.uix.floatlayout import FloatLayout
from kivy.uix.screenmanager import ScreenManager, NoTransition
from kivy.uix.relativelayout import RelativeLayout

from kivy_garden.draggable import KXDroppableBehavior, KXDraggableBehavior, restore_widget_state

Expand All @@ -24,24 +24,20 @@
<FlutterStyleDraggable>:
drag_cls: 'test'
drag_timeout: 0
# current: 'feedback' if self.is_being_dragged else 'child'
Screen:
name: 'child'
Label:
text: 'child'
bold: True
Screen:
name: 'childWhenDragging'
Label:
text: 'childWhenDragging'
bold: True
color: 1, 0, 1, 1
Screen:
name: 'feedback'
Label:
text: 'feedback'
bold: True
color: 1, 1, 0, 1
Label:
id: child
text: 'child'
bold: True
Label:
id: childWhenDragging
text: 'childWhenDragging'
bold: True
color: 1, 0, 1, 1
Label:
id: feedback
text: 'feedback'
bold: True
color: 1, 1, 0, 1

GridLayout:
id: board
Expand All @@ -52,37 +48,31 @@
'''


class FlutterStyleDraggable(KXDraggableBehavior, ScreenManager):
class FlutterStyleDraggable(KXDraggableBehavior, RelativeLayout):

def on_kv_post(self, *args, **kwargs):
super().on_kv_post(*args, **kwargs)
self.transition = NoTransition()
self.current = 'child'
self.fbind('is_being_dragged', FlutterStyleDraggable.__on_is_being_dragged)

def __on_is_being_dragged(self, value):
self.current = 'feedback' if value else 'child'
self._widgets = ws = {
name: self.ids[name].__self__
for name in ('child', 'childWhenDragging', 'feedback', )
}
self.clear_widgets()
self.add_widget(ws['child'])

def on_drag_start(self, touch, ctx):
if self.has_screen('childWhenDragging'):
restore_widget_state(
self.get_screen('childWhenDragging'),
ctx.original_state,
)
ws = self._widgets
self.remove_widget(ws['child'])
self.add_widget(ws['feedback'])
restore_widget_state(ws['childWhenDragging'], ctx.original_state)
return super().on_drag_start(touch, ctx)

def on_drag_fail(self, touch, ctx):
if self.has_screen('childWhenDragging'):
w = self.get_screen('childWhenDragging')
if w.parent is not None:
w.parent.remove_widget(w)
return super().on_drag_fail(touch, ctx)

def on_drag_succeed(self, touch, ctx):
if self.has_screen('childWhenDragging'):
w = self.get_screen('childWhenDragging')
if w.parent is not None:
w.parent.remove_widget(w)
return super().on_drag_succeed(touch, ctx)
def on_drag_end(self, touch, ctx):
ws = self._widgets
w = ws['childWhenDragging']
w.parent.remove_widget(w)
self.remove_widget(ws['feedback'])
self.add_widget(ws['child'])
return super().on_drag_end(touch, ctx)


class Cell(KXDroppableBehavior, FloatLayout):
Expand Down
4 changes: 2 additions & 2 deletions examples/reorderable_stacklayout.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'''
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()``.
``size_hint`` of its children, or may want to limit the maximum size of them, otherwise the layout will be messed
up. You can confirm that by commenting/uncommenting the specified part of ``SampleApp.on_start()``.
'''

from kivy.lang import Builder
Expand Down
7 changes: 5 additions & 2 deletions examples/using_other_widget_as_an_emitter.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
size_hint_x: .1

# Put the board inside a RelativeLayout just to confirm the coordinates are properly transformed.
# It's not necessary for this example to work though.
# This is not necessary for this example to work.
RelativeLayout:
GridLayout:
id: board
Expand Down Expand Up @@ -88,10 +88,13 @@ class Deck(Label):
def on_touch_down(self, touch):
if self.collide_point(*touch.opos):
if (text := next(self.text_iter, None)) is not None:
card = Card(text=text, size=self.board.children[0].size)
card = Card(text=text, size=self._get_cell_size())
card.start_dragging_from_others_touch(self, touch)
return True

def _get_cell_size(self):
return self.board.children[0].size


class SampleApp(App):
def build(self):
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"
version = "0.2.1.dev0"
description = "Drag & Drop Extension for Kivy"
authors = ["Nattōsai Mitō <[email protected]>"]
license = "MIT"
Expand Down
3 changes: 2 additions & 1 deletion src/kivy_garden/draggable/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
__all__ = (
'DragContext',
'KXDraggableBehavior', 'KXDroppableBehavior', 'KXReorderableBehavior',
'save_widget_state', 'restore_widget_state',
'save_widget_location', 'restore_widget_location', 'ongoing_drags',
)

from ._impl import KXDraggableBehavior, KXDroppableBehavior, KXReorderableBehavior, ongoing_drags
from ._impl import KXDraggableBehavior, KXDroppableBehavior, KXReorderableBehavior, ongoing_drags, DragContext
from ._utils import save_widget_state, restore_widget_state
from ._utils import save_widget_location, restore_widget_location
31 changes: 12 additions & 19 deletions src/kivy_garden/draggable/_impl.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
__all__ = (
'KXDraggableBehavior', 'KXDroppableBehavior', 'KXReorderableBehavior',
'ongoing_drags',
)
from typing import List, Tuple, Union
from inspect import isawaitable
from dataclasses import dataclass
from contextlib import nullcontext
from functools import cached_property

from kivy.properties import (
BooleanProperty, ListProperty, StringProperty, NumericProperty, OptionProperty, AliasProperty,
Expand Down Expand Up @@ -41,7 +36,7 @@ class DragContext:
'''(read-only) The widget where the draggable dropped to. This is always None on_drag_start/on_drag_cancel, and is
always a widget on_drag_succeed, and can be either on_drag_fail/on_drag_end.'''

@cached_property
@property
def original_location(self) -> dict:
'''
This exists solely for backward compatibility.
Expand Down Expand Up @@ -354,14 +349,6 @@ def get_widget_under_drag(self, x, y) -> Tuple[Widget, int]:
return (widget, index)
return (None, None)

def get_drop_insertion_index_move(self, x, y, spacer):
widget, idx = self.get_widget_under_drag(x, y)
if widget is spacer:
return None
if widget is None:
return None if self.children else 0
return idx

def on_touch_move(self, touch):
ud_key = self.__ud_key
touch_ud = touch.ud
Expand All @@ -382,7 +369,7 @@ async def _watch_touch(self, touch):

# LOAD_FAST
collide_point = self.collide_point
get_drop_insertion_index_move = self.get_drop_insertion_index_move
get_widget_under_drag = self.get_widget_under_drag
remove_widget = self.remove_widget
add_widget = self.add_widget
touch_ud = touch.ud
Expand All @@ -397,10 +384,16 @@ async def _watch_touch(self, touch):
while await in_progress():
x, y = touch.pos
if collide_point(x, y):
new_idx = get_drop_insertion_index_move(x, y, spacer)
if new_idx is not None:
remove_widget(spacer)
add_widget(spacer, index=new_idx)
widget, idx = get_widget_under_drag(x, y)
if widget is spacer:
continue
if widget is None:
if self.children:
continue
else:
idx = 0
remove_widget(spacer)
add_widget(spacer, index=idx)
else:
del touch_ud[self.__ud_key]
return
Expand Down
6 changes: 0 additions & 6 deletions src/kivy_garden/draggable/_utils.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
__all__ = (
'temp_transform',
'save_widget_state', 'restore_widget_state',
'save_widget_location', 'restore_widget_location',
)

from weakref import ref
from copy import deepcopy

Expand Down
1 change: 1 addition & 0 deletions tests/test_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

def test_flower():
from kivy_garden.draggable import (
DragContext,
KXDraggableBehavior, KXDroppableBehavior, KXReorderableBehavior,
restore_widget_state, save_widget_state,
restore_widget_location, save_widget_location, ongoing_drags,
Expand Down