├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── pythonapp.yml ├── .gitignore ├── Makefile ├── README.md ├── README_jp.md ├── examples ├── cancelling_ongoing_drags.py ├── customizing_animation.py ├── customizing_animation_ver2.py ├── draggable_can_work_with_scrollview.py ├── flutter_style_draggable.py ├── reacting_to_entering_and_leaving.py ├── reorderable_stacklayout.py ├── shopping.py └── using_other_widget_as_an_emitter.py ├── poetry.lock ├── pyproject.toml ├── setup.cfg ├── src └── kivy_garden │ └── draggable │ ├── __init__.py │ ├── _impl.py │ └── _utils.py ├── tests ├── test_import.py ├── test_utils_restore_widget_state.py └── test_utils_save_widget_state.py └── tools └── hooks └── pre-commit /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Code example showing the issue: 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Logs/output** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | **Platform (please complete the following information):** 23 | - OS: [e.g. windows 10 /OSX 10.12 /linux/android 8/IOS 12…] 24 | - Python version. 25 | - release or git branch/commit 26 | 27 | **Additional context** 28 | Add any other context about the problem here. 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/pythonapp.yml: -------------------------------------------------------------------------------- 1 | name: Garden flower 2 | on: 3 | push: 4 | branches: [ main ] 5 | pull_request: 6 | branches: [ main ] 7 | 8 | jobs: 9 | linux_test: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] 14 | env: 15 | DISPLAY: ':99.0' 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Setup env 23 | run: | 24 | sudo apt-get update 25 | sudo apt-get -y install xvfb 26 | /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1280x720x24 -ac +extension GLX 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | python -m pip install pytest flake8 kivy[base]==2.3.1 "asyncgui>=0.7,<0.8" "asynckivy>=0.7.1,<0.9" 31 | - name: Install flower 32 | run: python -m pip install -e . 33 | - name: Lint with flake8 34 | run: make style 35 | - name: Test with pytest 36 | run: make test 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | 62 | # Flask stuff: 63 | instance/ 64 | .webassets-cache 65 | 66 | # Scrapy stuff: 67 | .scrapy 68 | 69 | # Sphinx documentation 70 | docs/_build/ 71 | 72 | # PyBuilder 73 | target/ 74 | 75 | # Jupyter Notebook 76 | .ipynb_checkpoints 77 | 78 | # IPython 79 | profile_default/ 80 | ipython_config.py 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # pipenv 86 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 87 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 88 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 89 | # install all needed dependencies. 90 | #Pipfile.lock 91 | 92 | # celery beat schedule file 93 | celerybeat-schedule 94 | 95 | # SageMath parsed files 96 | *.sage.py 97 | 98 | # Environments 99 | .env 100 | .venv 101 | env/ 102 | venv/ 103 | ENV/ 104 | env.bak/ 105 | venv.bak/ 106 | .vscode 107 | 108 | # Spyder project settings 109 | .spyderproject 110 | .spyproject 111 | 112 | # Rope project settings 113 | .ropeproject 114 | 115 | # mkdocs documentation 116 | /site 117 | 118 | # mypy 119 | .mypy_cache/ 120 | .dmypy.json 121 | dmypy.json 122 | 123 | # Pyre type checker 124 | .pyre/ 125 | 126 | # 127 | *.sqlite3 128 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PYTHON = python 2 | PYTEST = $(PYTHON) -m pytest 3 | FLAKE8 = $(PYTHON) -m flake8 4 | 5 | test: 6 | $(PYTEST) ./tests 7 | 8 | style: 9 | $(FLAKE8) ./src/kivy_garden/draggable 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Draggable 2 | 3 | ![](http://img.youtube.com/vi/CjiRZjiSqgA/0.jpg) 4 | [Youtube][youtube] 5 | [README(Japanese)](README_jp.md) 6 | 7 | Inspired by: 8 | 9 | * [drag_n_drop][drag_n_drop] (`Draggable` is based on this, so reading its documentation may help you understand `Draggable`) 10 | * [Flutter][flutter] 11 | 12 | This flower adds a drag and drop functionality to layouts and widgets. There are 3 13 | main components used to have drag and drop: 14 | 15 | - The `KXDraggableBehavior`. An equivalent of drag_n_drop's 16 | `DraggableObjectBehavior`. 17 | - The `KXReorderableBehavior`. An equivalent of drag_n_drop's 18 | `DraggableLayoutBehavior`. 19 | - The `KXDroppableBehavior`. An equivalent of Flutter's `DragTarget`. 20 | 21 | From now on, I use the term `droppable` to refer both `KXReorderableBehavior` and `KXDroppableBehavior`, and use the term `draggable` to refer `KXDraggableBehavior`. 22 | 23 | ## Installation 24 | 25 | It's recommended to pin the minor version, because if it changed, it means some important breaking changes occurred. 26 | 27 | ``` 28 | poetry add kivy_garden.draggable@~0.2 29 | pip install "kivy_garden.draggable>=0.2,<0.3" 30 | ``` 31 | 32 | ## Main differences from drag_n_drop 33 | 34 | - Drag is triggered by a long-press. More precisely, when a finger of the user 35 | dropped inside a draggable, if the finger stays for `draggable.drag_timeout` 36 | milli seconds without traveling more than `draggable.drag_distance` pixels, it will 37 | be recognized as a dragging gesture. 38 | - Droppables can handle multiple drags simultaneously. 39 | - Drag can be canceled by calling `draggable.drag_cancel()`. 40 | - Nested `KXReorderableBehavior` is not officially supported. It may or may 41 | not work depending on how `drag_classes` and `drag_cls` are set. 42 | 43 | ## Flow 44 | 45 | Once a drag has started, it will go through the following path. 46 | 47 | ```mermaid 48 | stateDiagram-v2 49 | state cancelled? <> 50 | state on_a_droppable? <> 51 | state listed? <> 52 | state accepted? <> 53 | 54 | [*] --> on_drag_start 55 | on_drag_start --> cancelled? 56 | cancelled? --> on_a_droppable?: User lifted their finger up 57 | cancelled? --> on_drag_cancel: 'draggable.cancel()' was called before the user lifts their finger up 58 | 59 | on_a_droppable? --> listed?: Finger was on a droppable 60 | on_a_droppable? --> on_drag_fail: not on a droppable 61 | 62 | droppable_is_set: 'ctx.droppable' is set to the droppable 63 | listed? --> droppable_is_set: 'draggable.drag_cls' was listed in the 'droppable.drag_classes' 64 | listed? --> on_drag_fail: not listed 65 | 66 | droppable_is_set --> accepted? 67 | accepted? --> on_drag_succeed: Droppable accepted the drag ('droppable.accepts_drag()' returned True.) 68 | accepted? --> on_drag_fail 69 | 70 | on_drag_cancel --> on_drag_end 71 | on_drag_fail --> on_drag_end 72 | on_drag_succeed --> on_drag_end 73 | 74 | on_drag_end --> [*] 75 | ``` 76 | 77 | ## Cancellation 78 | 79 | When your app switches a scene, you may want to cancel all ongoing drags. 80 | `ongoing_drags()` and `draggable.drag_cancel()` are what you want. 81 | 82 | ```python 83 | from kivy_garden.draggable import ongoing_drags 84 | 85 | def cancel_all_ongoing_drags(): 86 | for draggable in ongoing_drags(): 87 | draggable.drag_cancel() 88 | ``` 89 | 90 | ## Using other widgets as an emitter 91 | 92 | Let's say you are creating a card game, and there is a deck on the screen. 93 | Say, you want the deck to emit a card when the user drops a finger on it, 94 | and want the card to follow the finger until the user lifts it up. 95 | In this situation, a widget that triggers a drag and a widget that is dragged are different. 96 | You can implement it as follows: 97 | 98 | ```python 99 | class Card(KXDraggableBehavior, Widget): 100 | pass 101 | 102 | 103 | class Deck(Widget): 104 | def on_touch_down(self, touch): 105 | if self.collide_point(*touch.opos): 106 | Card(...).start_dragging_from_others_touch(self, touch) 107 | ``` 108 | 109 | ## Customization 110 | 111 | What draggables do `on_drag_succeed` / `on_drag_fail` / `on_drag_cancel` are completely customizable. 112 | For example, by default, when a drag fails, the draggable will go back to where it came from with little animation. 113 | This is because the default handler of `on_drag_fail` is implemented as follows: 114 | 115 | ```python 116 | class KXDraggableBehavior: 117 | async def on_drag_fail(self, touch, ctx): 118 | await ak.anim_attrs( 119 | self, duration=.1, 120 | x=ctx.original_pos_win[0], 121 | y=ctx.original_pos_win[1], 122 | ) 123 | restore_widget_state(self, ctx.original_state) 124 | ``` 125 | 126 | If you don't need the animation, and want the draggable to go back instantly, overwrite the handler as follows: 127 | 128 | ```python 129 | class MyDraggable(KXDraggableBehavior, Widget): 130 | def on_drag_fail(self, touch, ctx): 131 | restore_widget_state(self, ctx.original_state) 132 | ``` 133 | 134 | Or if you want the draggable to not go back, and want it to stay the current position, overwrite the handler as follows: 135 | 136 | ```python 137 | class MyDraggable(KXDraggableBehavior, Widget): 138 | def on_drag_fail(self, touch, ctx): 139 | pass 140 | ``` 141 | 142 | Another example: when a drag succeed, the draggable will become a child of droppable, by default. 143 | If you don't like it, and want the draggable to fade-out, 144 | overwrite the handler as follows: 145 | 146 | ```python 147 | class MyDraggable(KXDraggableBehavior, Widget): 148 | async def on_drag_succeed(self, touch, ctx): 149 | import asynckivy 150 | await asynckivy.anim_attrs(self, opacity=0) 151 | self.parent.remove_widget(self) 152 | ``` 153 | 154 | Just like that, you have free rein to change those behaviors. 155 | But note that **only the default handler of `on_drag_succeed` and `on_drag_fail` 156 | can be an async function. Those two only.** 157 | 158 | You might say "What's the point of implementing a default handler as an async function, 159 | when you can just launch any number of async functions from a regular function by using ``asynckivy.managed_start()``?". 160 | Well, if you use ``asynckivy.managed_start()``, that task will run independently from the dragging process, 161 | which means the draggable might fire ``on_drag_end`` and might start another drag while the task is still running. 162 | If a default handler is an async function, 163 | its code will be a part of dragging process and is guaranteed to be finished before ``on_drag_end`` gets fired. 164 | 165 | ## License 166 | 167 | This software is released under the terms of the MIT License. 168 | 169 | [drag_n_drop]:https://github.com/kivy-garden/drag_n_drop 170 | [flutter]:https://api.flutter.dev/flutter/widgets/Draggable-class.html 171 | [youtube]:https://www.youtube.com/playlist?list=PLNdhqAjzeEGiepWKfP43Dh7IWqn3cQtpQ 172 | -------------------------------------------------------------------------------- /README_jp.md: -------------------------------------------------------------------------------- 1 | # Draggable 2 | 3 | ![](http://img.youtube.com/vi/CjiRZjiSqgA/0.jpg) 4 | [Youtube][youtube] 5 | 6 | `kivy_garden.draggable`はdrag&dropの機能を実現するための拡張機能で以下の三つの部品で構成されます。 7 | 8 | - `KXDraggableBehavior` ... dragできるようにしたいwidgetが継承すべきclass 9 | - `KXDroppableBehavior`と`KXReorderableBehavior` ... dragされているwidgetを受け入れられるようにしたいwidgetが継承すべきclass 10 | 11 | `KXDroppableBehavior`と`KXReorderableBehavior`の違いはFlutterにおける[DragTarget][flutter_draggable_video]と[reorderables][flutter_reorderables]の違いに相当し、 12 | drag操作によってwidgetを並び替えたいなら`KXReorderableBehavior`を、そうじゃなければ`KXDroppableBehavior`を使うと良いです。 13 | これらの名前は長ったらしいので以後は、dragを受け入れられるwidgetをまとめて「droppable」と呼び、dragできるwidgetを「draggable」と呼ぶ事にします。 14 | 15 | ## Install方法 16 | 17 | このmoduleのminor versionが変わった時は何らかの重要な互換性の無い変更が加えられた可能性が高いので、使う際はminor versionまでを固定してください。 18 | 19 | ``` 20 | poetry add kivy_garden.draggable@~0.2 21 | pip install "kivy_garden.draggable>=0.2,<0.3" 22 | ``` 23 | 24 | ## dragが始まる条件 25 | 26 | dragは長押しによって引き起こされます。より具体的には利用者の指がdraggable内に降りてから`draggable.drag_distance`pixel以上動かずに`draggable.drag_timeout`ミリ秒以上指が離れなかった場合のみ引き起こされます。 27 | このためscroll操作(指がすぐさま動き出す)やtap動作(指がすぐに離れる)として誤認されにくいです。 28 | 29 | ## dragが始まった後の処理の流れ 30 | 31 | ユーザーがdraggableの上に指を降ろしてdragが始まった後の流れは以下のようになります。 32 | 33 | ```mermaid 34 | stateDiagram-v2 35 | state cancelled? <> 36 | state on_a_droppable? <> 37 | state listed? <> 38 | state accepted? <> 39 | 40 | [*] --> on_drag_start 41 | on_drag_start --> cancelled? 42 | cancelled? --> on_a_droppable?: 指が離れる 43 | cancelled? --> on_drag_cancel: 指が離れる前に 'draggable.cancel()' が呼ばれる 44 | 45 | on_a_droppable? --> listed?: 指が離れたのはdroppableの上 46 | on_a_droppable? --> on_drag_fail: 上ではない 47 | 48 | droppable_is_set: 'ctx.droppable'の値がそのdroppableになる 49 | listed? --> droppable_is_set: 'draggable.drag_cls' が 'droppable.drag_classes' に含まれている 50 | listed? --> on_drag_fail: 含まれていない 51 | 52 | droppable_is_set --> accepted? 53 | accepted? --> on_drag_succeed: droppableがdragを受け入れる('droppable.accepts_drag()'が真を返す) 54 | accepted? --> on_drag_fail 55 | 56 | on_drag_cancel --> on_drag_end 57 | on_drag_fail --> on_drag_end 58 | on_drag_succeed --> on_drag_end 59 | 60 | on_drag_end --> [*] 61 | ``` 62 | 63 | ## 受け入れるdragの選別 64 | 65 | 図に書かれているように利用者の指が離れた時にdragが受け入れられるか否かの判断がなされ、 66 | 指がdroppableの上じゃない所で離れた場合や`draggable.drag_cls`が`droppable.drag_classes`に含まれていない場合はまず即drag失敗となります。 67 | 68 | その選別をくぐり抜けたdraggableは`droppable.accepts_drag()`へ渡され、そこでdragが受け入れられるか否かの最終判断が下されます。例えばmethodが 69 | 70 | ```python 71 | class MyDroppable(KXDroppableBehavior, Widget): 72 | def accepts_drag(self, touch, draggable) -> bool: 73 | return not self.children 74 | ``` 75 | 76 | という風に実装されていたら、このdroppableは自分が子を持っている間は例え適切な`drag_cls`を持つdraggableであっても受け付けません。 77 | 78 | ## dragの中止 79 | 80 | アプリが次のシーンに移りたい時にまだdrag中のwidgetがあると不都合かもしれません。そのような事態に備えて 81 | 82 | - 現在進行中のdragを列挙する`ongoing_drags()`と 83 | - dragを中止する`draggable.drag_cancel()`があります。 84 | 85 | これらを用いる事で以下のように進行中のdragを全て中止できます。 86 | 87 | ```python 88 | from kivy_garden.draggable import ongoing_drags 89 | 90 | def cancel_all_ongoing_drags(): 91 | for draggable in ongoing_drags(): 92 | draggable.drag_cancel() 93 | ``` 94 | 95 | ## dragを引き起こすwidgetとdragされるwidgetを別にする 96 | 97 | 上で述べたようにdragはdraggableを長押しすることで引き起こされるので、 98 | dragを引き起こすwidgetとdragされるwidgetは基本同じです。 99 | でも例えばカードゲームを作っていて画面上に山札があったとして 100 | drag操作によって山札から札を引けるようにしたかったとします。 101 | 具体的には利用者が山札に指を触れた時に札を作り出し、 102 | そのまま指の動きに沿って札を追わせたいとします。 103 | このような 104 | 105 | - dragを引き起こすwidget(山札)と 106 | - dragされるwidget(山札から引かれた札) 107 | 108 | が別である状況では`draggable.start_dragging_from_others_touch()`が使えます。 109 | 110 | ```python 111 | class Card(KXDraggableBehavior, Widget): 112 | '''札''' 113 | 114 | class Deck(Widget): 115 | '''山札''' 116 | def on_touch_down(self, touch): 117 | if self.collide_point(*touch.opos): 118 | Card(...).start_dragging_from_others_touch(self, touch) 119 | ``` 120 | 121 | ## 自由に振る舞いを変える 122 | 123 | dragが失敗/成功/中止した時に何をするかは完全にあなたに委ねられています。 124 | 例えばdrag失敗時は既定ではアニメーションしながら元の場所に戻りますが、これをアニメーション無しで瞬時に戻したいなら以下のようにdefault handlerを上書きすれば良いです。 125 | 126 | ```python 127 | class MyDraggable(KXDraggableBehavior, Widget): 128 | def on_drag_fail(self, touch, ctx): 129 | restore_widget_state(self, ctx.original_state) 130 | ``` 131 | 132 | また何もせずにその場に残って欲しいなら以下のようにすれば良いです。 133 | 134 | ```python 135 | class MyDraggable(KXDraggableBehavior, Widget): 136 | def on_drag_fail(self, touch, ctx): 137 | pass 138 | ``` 139 | 140 | 成功時も同様で、既定では受け入れてくれたdroppableの子widgetになるように実装されていますが以下のようにすると子widgetにはならずに現在の位置で徐々に透明になって消える事になります。 141 | 142 | ```python 143 | import asynckivy as ak 144 | 145 | class MyDraggable(KXDraggableBehavior, Widget): 146 | async def on_drag_succeed(self, touch, ctx): 147 | await ak.anim_attrs(self, opacity=0) 148 | self.parent.remove_widget(self) 149 | ``` 150 | 151 | このようにdefault handlerを上書きすることで自由に振るまいを変えられます。 152 | ただし**async関数になれるのは`on_drag_succeed`と`on_drag_fail`のdefault handlerだけ**なので注意してください。 153 | 154 | ここで 155 | 156 | - default handlerをasync関数にするのと 157 | - default handlerは普通の関数のままにしておいて内部で`asynckivy.managed_start()`を用いてasync関数を立ち上げるのと 158 | 159 | の違いについて説明します。 160 | 前者ではasync関数のcodeがdrag処理の間に挟み込まれcodeが`on_drag_end`が起こるより前に完遂される事が保証されるのに対し、 161 | 後者ではcodeがdrag処理とは独立して進むので`on_drag_end`が起こるより前に完了する保証はありません。 162 | なのでもし上の`on_drag_succeed`の例を後者のやり方で実装すると 163 | 164 | ```python 165 | import asynckivy as ak 166 | 167 | class MyDraggable(KXDraggableBehavior, Widget): 168 | def on_drag_succeed(self, touch, ctx): 169 | ak.managed_start(self._fade_out(touch)) 170 | 171 | async def _fade_out(self, touch): 172 | await ak.anim_attrs(self, opacity=0) 173 | self.parent.remove_widget(self) # A 174 | ``` 175 | 176 | `ak.anim_attrs()`の進行中にdragが完了し、そこで利用者が再び指を触れたことで次のdragが始まり、 177 | その最中にA行が実行されてdraggableが親widgetから切り離されてしまうなんて事が起こりえます。 178 | なので **drag完了前に完遂させたい非同期処理があるのなら必ず前者の方法を使ってください**。 179 | 180 | 後これはこのモジュール特有ではなくKivyのイベントシステム共通の事なのですが、 181 | `.bind()`で結びつけた関数が真を返すとその関数よりも前に結びつけた関数とdefault handerが呼ばれなくなります。 182 | これを利用すれば一時的に振る舞いを変えられます。 183 | 184 | ```python 185 | def 即座に元の位置へ戻す(draggable, touch, ctx): 186 | restore_widget_state(draggable, ctx.original_state) 187 | return True 188 | 189 | draggable = MyDraggable() 190 | draggable.bind(on_drag_fail=即座に元の位置へ戻す) # このインスタンスの振る舞いを変える 191 | ... 192 | draggable.unbind(on_drag_fail=即座に元の位置へ戻す) # 元に戻す 193 | ``` 194 | 195 | ## その他 196 | 197 | - [drag_n_drop][drag_n_drop] ... この拡張機能の元になった物 198 | 199 | [flutter_draggable_video]:https://youtu.be/QzA4c4QHZCY 200 | [flutter_reorderables]:https://pub.dev/packages/reorderables 201 | [drag_n_drop]:https://github.com/kivy-garden/drag_n_drop 202 | [youtube]:https://www.youtube.com/playlist?list=PLNdhqAjzeEGiepWKfP43Dh7IWqn3cQtpQ 203 | -------------------------------------------------------------------------------- /examples/cancelling_ongoing_drags.py: -------------------------------------------------------------------------------- 1 | ''' 2 | An examples of cancelling ongoing drags. 3 | ''' 4 | 5 | from kivy.app import App 6 | from kivy.lang import Builder 7 | from kivy.factory import Factory 8 | 9 | from kivy_garden.draggable import ongoing_drags 10 | 11 | 12 | KV_CODE = ''' 13 | #:import create_spacer kivy_garden.draggable._utils._create_spacer 14 | 15 | : 16 | 17 | : 18 | drag_cls: 'test' 19 | drag_timeout: 50 20 | font_size: 30 21 | opacity: .5 if self.is_being_dragged else 1. 22 | size_hint_min: 50, 50 23 | pos_hint: {'center_x': .5, 'center_y': .5, } 24 | canvas.after: 25 | Color: 26 | rgba: .5, 1, 0, 1 if root.is_being_dragged else .5 27 | Line: 28 | width: 2 if root.is_being_dragged else 1 29 | rectangle: [*self.pos, *self.size, ] 30 | 31 | : 32 | font_size: sp(20) 33 | size_hint_min_x: self.texture_size[0] + dp(10) 34 | size_hint_min_y: self.texture_size[1] + dp(10) 35 | 36 | BoxLayout: 37 | spacing: 10 38 | padding: 10 39 | orientation: 'vertical' 40 | ReorderableGridLayout: 41 | id: gl 42 | spacing: 10 43 | drag_classes: ['test', ] 44 | cols: 6 45 | spacer_widgets: [create_spacer(color=color) for color in "#000044 #002200 #440000".split()] 46 | BoxLayout: 47 | spacing: 10 48 | orientation: 'vertical' 49 | size_hint_y: None 50 | height: self.minimum_height 51 | MyButton: 52 | text: 'cancel all ongoing drags' 53 | on_press: app.cancel_ongoing_drags() 54 | ''' 55 | 56 | 57 | class SampleApp(App): 58 | def build(self): 59 | return Builder.load_string(KV_CODE) 60 | 61 | def on_start(self): 62 | gl = self.root.ids.gl 63 | DraggableItem = Factory.DraggableItem 64 | for i in range(23): 65 | gl.add_widget(DraggableItem(text=str(i))) 66 | 67 | def cancel_ongoing_drags(self): 68 | for draggable in ongoing_drags(): 69 | draggable.drag_cancel() 70 | 71 | 72 | if __name__ == '__main__': 73 | SampleApp().run() 74 | -------------------------------------------------------------------------------- /examples/customizing_animation.py: -------------------------------------------------------------------------------- 1 | '''This example shows how to customize animations by overwriting 2 | ``on_drag_fail()`` and ``on_drag_succeed()``. 3 | ''' 4 | 5 | 6 | from kivy.app import App 7 | from kivy.lang import Builder 8 | from kivy.properties import NumericProperty 9 | from kivy.uix.floatlayout import FloatLayout 10 | from kivy.uix.label import Label 11 | import asynckivy as ak 12 | 13 | from kivy_garden.draggable import KXDroppableBehavior, KXDraggableBehavior 14 | 15 | KV_CODE = ''' 16 | : 17 | drag_classes: ['test', ] 18 | canvas.before: 19 | Color: 20 | rgba: .1, .1, .1, 1 21 | Rectangle: 22 | pos: self.pos 23 | size: self.size 24 | 25 | : 26 | drag_cls: 'test' 27 | drag_timeout: 0 28 | font_size: 50 29 | canvas.before: 30 | Color: 31 | rgba: .4, 1, .2, 1 if self.is_being_dragged else 0 32 | Line: 33 | width: 2 34 | rectangle: [*self.pos, *self.size, ] 35 | PushMatrix: 36 | Rotate: 37 | origin: self.center 38 | angle: self._angle 39 | Scale: 40 | origin: self.center 41 | xyz: self._scale, self._scale, 1. 42 | canvas.after: 43 | PopMatrix: 44 | 45 | GridLayout: 46 | id: board 47 | cols: 3 48 | rows: 3 49 | spacing: 20 50 | padding: 20 51 | ''' 52 | 53 | 54 | class MyDraggable(KXDraggableBehavior, Label): 55 | _angle = NumericProperty() 56 | _scale = NumericProperty(1.) 57 | 58 | async def on_drag_fail(self, touch, ctx): 59 | await ak.anim_attrs(self, _angle=720, opacity=0, duration=.4) 60 | self.parent.remove_widget(self) 61 | 62 | async def on_drag_succeed(self, touch, ctx): 63 | self.parent.remove_widget(self) 64 | ctx.droppable.add_widget(self) 65 | abs_ = abs 66 | async for p in ak.anim_with_ratio(base=.2): 67 | if p > 1.: 68 | break 69 | self._scale = abs_(p * .8 - .4) + .6 70 | 71 | 72 | class Cell(KXDroppableBehavior, FloatLayout): 73 | def accepts_drag(self, touch, ctx, draggable) -> bool: 74 | return not self.children 75 | 76 | def add_widget(self, widget, *args, **kwargs): 77 | widget.size_hint = (1, 1, ) 78 | widget.pos_hint = {'x': 0, 'y': 0, } 79 | return super().add_widget(widget, *args, **kwargs) 80 | 81 | 82 | class SampleApp(App): 83 | def build(self): 84 | return Builder.load_string(KV_CODE) 85 | 86 | def on_start(self): 87 | board = self.root 88 | for __ in range(board.cols * board.rows): 89 | board.add_widget(Cell()) 90 | cells = board.children 91 | for cell, i in zip(cells, range(4)): 92 | cell.add_widget(MyDraggable(text=str(i))) 93 | 94 | 95 | if __name__ == '__main__': 96 | SampleApp().run() 97 | -------------------------------------------------------------------------------- /examples/customizing_animation_ver2.py: -------------------------------------------------------------------------------- 1 | '''Alternative version of 'customizing_animation.py'. This one adds graphics instructions on demand, 2 | requires less bindings, thus probably more efficient than the original. 3 | ''' 4 | 5 | from kivy.app import App 6 | from kivy.lang import Builder 7 | from kivy.uix.floatlayout import FloatLayout 8 | from kivy.uix.label import Label 9 | from kivy.graphics import Rotate, Scale 10 | 11 | import asynckivy as ak 12 | 13 | from kivy_garden.draggable import KXDroppableBehavior, KXDraggableBehavior 14 | 15 | KV_CODE = ''' 16 | : 17 | drag_classes: ['test', ] 18 | canvas.before: 19 | Color: 20 | rgba: .1, .1, .1, 1 21 | Rectangle: 22 | pos: self.pos 23 | size: self.size 24 | 25 | : 26 | drag_cls: 'test' 27 | drag_timeout: 0 28 | font_size: 50 29 | canvas.before: 30 | Color: 31 | rgba: .4, 1, .2, 1 if self.is_being_dragged else 0 32 | Line: 33 | width: 2 34 | rectangle: [*self.pos, *self.size, ] 35 | 36 | GridLayout: 37 | id: board 38 | cols: 3 39 | rows: 3 40 | spacing: 20 41 | padding: 20 42 | ''' 43 | 44 | 45 | class MyDraggable(KXDraggableBehavior, Label): 46 | async def on_drag_fail(self, touch, ctx): 47 | with ak.transform(self) as ig: 48 | ig.add(rotate := Rotate(origin=self.center)) 49 | async for p in ak.anim_with_ratio(base=.4): 50 | if p > 1.: 51 | break 52 | rotate.angle = p * 720. 53 | self.opacity = 1. - p 54 | self.parent.remove_widget(self) 55 | 56 | async def on_drag_succeed(self, touch, ctx): 57 | self.parent.remove_widget(self) 58 | ctx.droppable.add_widget(self) 59 | await ak.sleep(0) # wait for the layout to complete 60 | abs_ = abs 61 | with ak.transform(self) as ig: 62 | ig.add(scale := Scale(origin=self.center)) 63 | async for p in ak.anim_with_ratio(base=.2): 64 | if p > 1.: 65 | break 66 | scale.x = scale.y = abs_(p * .8 - .4) + .6 67 | 68 | 69 | class Cell(KXDroppableBehavior, FloatLayout): 70 | def accepts_drag(self, touch, ctx, draggable) -> bool: 71 | return not self.children 72 | 73 | def add_widget(self, widget, *args, **kwargs): 74 | widget.size_hint = (1, 1, ) 75 | widget.pos_hint = {'x': 0, 'y': 0, } 76 | return super().add_widget(widget, *args, **kwargs) 77 | 78 | 79 | class SampleApp(App): 80 | def build(self): 81 | return Builder.load_string(KV_CODE) 82 | 83 | def on_start(self): 84 | board = self.root 85 | for __ in range(board.cols * board.rows): 86 | board.add_widget(Cell()) 87 | cells = board.children 88 | for cell, i in zip(cells, range(4)): 89 | cell.add_widget(MyDraggable(text=str(i))) 90 | 91 | 92 | if __name__ == '__main__': 93 | SampleApp().run() 94 | -------------------------------------------------------------------------------- /examples/draggable_can_work_with_scrollview.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Nothing special. Just an example of re-orderable BoxLayout. 3 | ''' 4 | 5 | from kivy.app import App 6 | from kivy.lang import Builder 7 | from kivy.factory import Factory 8 | import kivy_garden.draggable 9 | 10 | KV_CODE = ''' 11 | #:import Label kivy.uix.label.Label 12 | 13 | : 14 | drag_cls: 'test' 15 | opacity: .5 if self.is_being_dragged else 1. 16 | size_hint_min_y: sp(50) 17 | canvas.after: 18 | Color: 19 | rgba: 0, 1, 0, 1 if root.is_being_dragged else 0 20 | Line: 21 | width: 2 22 | rectangle: [*self.pos, *self.size, ] 23 | 24 | : 25 | 26 | ScrollView: 27 | do_scroll_x: False 28 | ReorderableBoxLayout: 29 | id: boxlayout 30 | drag_classes: ['test', ] 31 | spacer_widgets: 32 | [ 33 | Label(text='3rd spacer', font_size=40, size_hint_min_y='50sp'), 34 | Label(text='2nd spacer', font_size=40, size_hint_min_y='50sp'), 35 | Label(text='1st spacer', font_size=40, size_hint_min_y='50sp'), 36 | ] 37 | orientation: 'vertical' 38 | padding: 10 39 | size_hint_min_y: self.minimum_height 40 | ''' 41 | 42 | 43 | class SampleApp(App): 44 | def build(self): 45 | return Builder.load_string(KV_CODE) 46 | 47 | def on_start(self): 48 | DraggableItem = Factory.DraggableItem 49 | add_widget = self.root.ids.boxlayout.add_widget 50 | for i in range(100): 51 | add_widget(DraggableItem(text=str(i))) 52 | 53 | 54 | if __name__ == '__main__': 55 | SampleApp().run() 56 | -------------------------------------------------------------------------------- /examples/flutter_style_draggable.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This example shows how to achieve the same functionality as Flutter one's 3 | 'child', 'feedback' and 'childWhenDragging'. 4 | https://api.flutter.dev/flutter/widgets/Draggable-class.html 5 | ''' 6 | 7 | from kivy.app import App 8 | from kivy.lang import Builder 9 | from kivy.uix.floatlayout import FloatLayout 10 | from kivy.uix.relativelayout import RelativeLayout 11 | 12 | from kivy_garden.draggable import KXDroppableBehavior, KXDraggableBehavior, restore_widget_state 13 | 14 | KV_CODE = ''' 15 | : 16 | drag_classes: ['test', ] 17 | canvas.before: 18 | Color: 19 | rgba: .1, .1, .1, 1 20 | Rectangle: 21 | pos: self.pos 22 | size: self.size 23 | 24 | : 25 | drag_cls: 'test' 26 | drag_timeout: 0 27 | Label: 28 | id: child 29 | text: 'child' 30 | bold: True 31 | Label: 32 | id: childWhenDragging 33 | text: 'childWhenDragging' 34 | bold: True 35 | color: 1, 0, 1, 1 36 | Label: 37 | id: feedback 38 | text: 'feedback' 39 | bold: True 40 | color: 1, 1, 0, 1 41 | 42 | GridLayout: 43 | id: board 44 | cols: 4 45 | rows: 4 46 | spacing: 10 47 | padding: 10 48 | ''' 49 | 50 | 51 | class FlutterStyleDraggable(KXDraggableBehavior, RelativeLayout): 52 | 53 | def on_kv_post(self, *args, **kwargs): 54 | super().on_kv_post(*args, **kwargs) 55 | self._widgets = ws = { 56 | name: self.ids[name].__self__ 57 | for name in ('child', 'childWhenDragging', 'feedback', ) 58 | } 59 | self.clear_widgets() 60 | self.add_widget(ws['child']) 61 | 62 | def on_drag_start(self, touch, ctx): 63 | ws = self._widgets 64 | self.remove_widget(ws['child']) 65 | self.add_widget(ws['feedback']) 66 | restore_widget_state(ws['childWhenDragging'], ctx.original_state) 67 | return super().on_drag_start(touch, ctx) 68 | 69 | def on_drag_end(self, touch, ctx): 70 | ws = self._widgets 71 | w = ws['childWhenDragging'] 72 | w.parent.remove_widget(w) 73 | self.remove_widget(ws['feedback']) 74 | self.add_widget(ws['child']) 75 | return super().on_drag_end(touch, ctx) 76 | 77 | 78 | class Cell(KXDroppableBehavior, FloatLayout): 79 | def accepts_drag(self, touch, ctx, draggable) -> bool: 80 | return not self.children 81 | 82 | def add_widget(self, widget, *args, **kwargs): 83 | widget.size_hint = (1, 1, ) 84 | widget.pos_hint = {'x': 0, 'y': 0, } 85 | return super().add_widget(widget, *args, **kwargs) 86 | 87 | 88 | class SampleApp(App): 89 | def build(self): 90 | return Builder.load_string(KV_CODE) 91 | 92 | def on_start(self): 93 | board = self.root 94 | for __ in range(board.cols * board.rows): 95 | board.add_widget(Cell()) 96 | cells = board.children 97 | for cell, __ in zip(cells, range(4)): 98 | cell.add_widget(FlutterStyleDraggable()) 99 | 100 | 101 | if __name__ == '__main__': 102 | SampleApp().run() 103 | -------------------------------------------------------------------------------- /examples/reacting_to_entering_and_leaving.py: -------------------------------------------------------------------------------- 1 | from functools import cached_property 2 | from kivy.properties import NumericProperty 3 | from kivy.app import App 4 | from kivy.lang import Builder 5 | from kivy.uix.label import Label 6 | import asynckivy as ak 7 | 8 | from kivy_garden.draggable import KXDraggableBehavior, KXDroppableBehavior 9 | 10 | KV_CODE = ''' 11 | : 12 | text: ''.join(root.drag_classes) 13 | font_size: 100 14 | color: 1, .2, 1, .8 15 | canvas.before: 16 | Color: 17 | rgba: 1, 1, 1, self.n_ongoing_drags_inside * 0.12 18 | Rectangle: 19 | pos: self.pos 20 | size: self.size 21 | 22 | : 23 | drag_timeout: 0 24 | font_size: 40 25 | opacity: .3 if root.is_being_dragged else 1. 26 | 27 | : 28 | canvas: 29 | Color: 30 | rgb: 1, 1, 1 31 | Rectangle: 32 | pos: self.pos 33 | size: self.size 34 | : 35 | size_hint_y: None 36 | height: 1 37 | : 38 | size_hint_x: None 39 | width: 1 40 | 41 | BoxLayout: 42 | orientation: 'vertical' 43 | BoxLayout: 44 | MyDroppable: 45 | drag_classes: ['A', ] 46 | VDivider: 47 | MyDroppable: 48 | drag_classes: ['A', 'B', ] 49 | VDivider: 50 | MyDroppable: 51 | drag_classes: ['B', ] 52 | HDivider: 53 | BoxLayout: 54 | MyDraggable: 55 | drag_cls: 'A' 56 | text: 'A1' 57 | MyDraggable: 58 | drag_cls: 'A' 59 | text: 'A2' 60 | MyDraggable: 61 | drag_cls: 'A' 62 | text: 'A3' 63 | MyDraggable: 64 | drag_cls: 'A' 65 | text: 'A4' 66 | MyDraggable: 67 | drag_cls: 'B' 68 | text: 'B1' 69 | MyDraggable: 70 | drag_cls: 'B' 71 | text: 'B2' 72 | MyDraggable: 73 | drag_cls: 'B' 74 | text: 'B3' 75 | MyDraggable: 76 | drag_cls: 'B' 77 | text: 'B4' 78 | ''' 79 | 80 | 81 | class MyDraggable(KXDraggableBehavior, Label): 82 | def on_drag_succeed(self, touch, ctx): 83 | self.parent.remove_widget(self) 84 | 85 | 86 | class ReactiveDroppableBehavior(KXDroppableBehavior): 87 | ''' 88 | ``KXDroppableBehavior`` + leaving/entering events. 89 | 90 | .. note:: 91 | 92 | This class probably deserves to be an official component. But not now 93 | because I'm not sure the word 'reactive' is appropriate to describe 94 | its behavior. 95 | ''' 96 | __events__ = ('on_drag_enter', 'on_drag_leave', ) 97 | 98 | @cached_property 99 | def __ud_key(self): 100 | return 'ReactiveDroppableBehavior.' + str(self.uid) 101 | 102 | def on_touch_move(self, touch): 103 | ud_key = self.__ud_key 104 | touch_ud = touch.ud 105 | if ud_key not in touch_ud and self.collide_point(*touch.pos): 106 | drag_cls = touch_ud.get('kivyx_drag_cls', None) 107 | if drag_cls is not None: 108 | touch_ud[ud_key] = None 109 | if drag_cls in self.drag_classes: 110 | ak.managed_start(ak.wait_any( 111 | self._watch_touch(touch), 112 | ak.event(touch.ud['kivyx_draggable'], 'on_drag_end'), 113 | )) 114 | return super().on_touch_move(touch) 115 | 116 | async def _watch_touch(self, touch): 117 | draggable = touch.ud['kivyx_draggable'] 118 | ctx = touch.ud['kivyx_drag_ctx'] 119 | collide_point = self.collide_point 120 | 121 | self.dispatch('on_drag_enter', touch, ctx, draggable) 122 | try: 123 | async for __ in ak.rest_of_touch_events(self, touch): 124 | if not collide_point(*touch.pos): 125 | return 126 | finally: 127 | self.dispatch('on_drag_leave', touch, ctx, draggable) 128 | del touch.ud[self.__ud_key] 129 | 130 | def on_drag_enter(self, touch, ctx, draggable): 131 | pass 132 | 133 | def on_drag_leave(self, touch, ctx, draggable): 134 | pass 135 | 136 | 137 | class MyDroppable(ReactiveDroppableBehavior, Label): 138 | n_ongoing_drags_inside = NumericProperty(0) 139 | 140 | def on_drag_enter(self, touch, ctx, draggable): 141 | self.n_ongoing_drags_inside += 1 142 | print(f"{draggable.text} entered {self.text}.") 143 | 144 | def on_drag_leave(self, touch, ctx, draggable): 145 | self.n_ongoing_drags_inside -= 1 146 | print(f"{draggable.text} left {self.text}.") 147 | 148 | 149 | class SampleApp(App): 150 | def build(self): 151 | return Builder.load_string(KV_CODE) 152 | 153 | 154 | if __name__ == '__main__': 155 | SampleApp().run() 156 | -------------------------------------------------------------------------------- /examples/reorderable_stacklayout.py: -------------------------------------------------------------------------------- 1 | ''' 2 | When you want to make a :class:`kivy.uix.stacklayout.StackLayout` re-orderable, you may want to disable the 3 | ``size_hint`` of its children, or may want to limit the maximum size of them, otherwise the layout will be messed 4 | up. You can confirm that by commenting/uncommenting the specified part of ``SampleApp.on_start()``. 5 | ''' 6 | 7 | from kivy.lang import Builder 8 | from kivy.factory import Factory 9 | from kivy.app import App 10 | import kivy_garden.draggable 11 | 12 | 13 | KV_CODE = ''' 14 | : 15 | spacing: 10 16 | padding: 10 17 | cols: 4 18 | drag_classes: ['test', ] 19 | 20 | : 21 | font_size: 30 22 | text: root.text 23 | drag_timeout: 0 24 | drag_cls: 'test' 25 | canvas.after: 26 | Color: 27 | rgba: .5, 1, 0, 1 if root.is_being_dragged else .5 28 | Line: 29 | width: 2 if root.is_being_dragged else 1 30 | rectangle: [*self.pos, *self.size, ] 31 | 32 | ScrollView: 33 | MyReorderableLayout: 34 | id: layout 35 | size_hint_min: self.minimum_size 36 | ''' 37 | 38 | 39 | def random_size(): 40 | from random import uniform 41 | return (uniform(50.0, 150.0), uniform(50.0, 150.0), ) 42 | 43 | 44 | class SampleApp(App): 45 | def build(self): 46 | return Builder.load_string(KV_CODE) 47 | 48 | def on_start(self): 49 | Item = Factory.MyDraggableItem 50 | Item() 51 | add_widget = self.root.ids.layout.add_widget 52 | for i in range(30): 53 | # A (works) 54 | add_widget(Item(text=str(i), size=random_size(), size_hint=(None, None))) 55 | 56 | # B (works) 57 | # add_widget(Item(text=str(i), size_hint_max=random_size())) 58 | 59 | # C (does not work) 60 | # add_widget(Item(text=str(i), size_hint_min=random_size())) 61 | 62 | # D (does not work) 63 | # add_widget(Item(text=str(i))) 64 | 65 | 66 | if __name__ == '__main__': 67 | SampleApp().run() 68 | -------------------------------------------------------------------------------- /examples/shopping.py: -------------------------------------------------------------------------------- 1 | ''' 2 | https://www.youtube.com/watch?v=PNj8uEdd5c0 3 | 4 | To animate widgets like in the video: pip install kivy-garden-posani 5 | ''' 6 | 7 | from collections.abc import Iterable 8 | import itertools 9 | from contextlib import closing 10 | from os import PathLike 11 | import sqlite3 12 | from dataclasses import dataclass 13 | 14 | from kivy.app import App 15 | from kivy.properties import ObjectProperty 16 | from kivy.clock import Clock 17 | from kivy.lang import Builder 18 | from kivy.factory import Factory as F 19 | import asynckivy as ak 20 | from kivy_garden.draggable import KXDraggableBehavior 21 | 22 | try: 23 | from kivy_garden import posani 24 | posani.install(target="SHFood") 25 | except ImportError: 26 | import types 27 | 28 | def do_nothing(*args, **kwargs): 29 | pass 30 | posani = types.SimpleNamespace( 31 | activate=do_nothing, 32 | deactivate=do_nothing, 33 | ) 34 | 35 | 36 | def detect_image_format(image_data: bytes) -> str: 37 | if image_data.startswith(b"\x89\x50\x4E\x47\x0D\x0A\x1A\x0A"): 38 | return "png" 39 | elif image_data.startswith(b"\xFF\xD8"): 40 | return "jpg" 41 | raise ValueError("Unknown image format") 42 | 43 | 44 | KV_CODE = r''' 45 | #:import ak asynckivy 46 | #:import posani __main__.posani 47 | 48 | : 49 | size_hint_min: [v + dp(8) for v in self.texture_size] 50 | halign: 'center' 51 | 52 | : 53 | orientation: 'vertical' 54 | spacing: '4dp' 55 | drag_timeout: 0 56 | drag_cls: 'food' 57 | size: '200dp', '200dp' 58 | size_hint: None, None 59 | opacity: .5 if self.is_being_dragged else 1. 60 | on_drag_start: posani.deactivate(self) 61 | on_drag_end: posani.activate(self) 62 | canvas.before: 63 | Color: 64 | rgba: .4, .4, .4, 1 65 | Line: 66 | rectangle: (*self.pos, *self.size, ) 67 | Image: 68 | fit_mode: "contain" 69 | texture: root.datum.texture 70 | size_hint_y: 3. 71 | SHLabel: 72 | text: '{} ({} yen)'.format(root.datum.name, root.datum.price) 73 | 74 | : 75 | padding: '10dp' 76 | spacing: '10dp' 77 | size_hint_min_y: self.minimum_height 78 | drag_classes: ['food', ] 79 | viewclass: 'SHFood' 80 | 81 | : 82 | orientation: 'vertical' 83 | padding: '10dp' 84 | spacing: '10dp' 85 | EquitableBoxLayout: 86 | BoxLayout: 87 | orientation: 'vertical' 88 | SHLabel: 89 | text: 'Shelf' 90 | font_size: max(20, sp(16)) 91 | bold: True 92 | color: rgba("#44AA44") 93 | ScrollView: 94 | size_hint_y: 1000. 95 | always_overscroll: False 96 | do_scroll_x: False 97 | SHShelf: 98 | id: shelf 99 | Splitter: 100 | sizable_from: 'left' 101 | min_size: 100 102 | max_size: root.width 103 | BoxLayout: 104 | orientation: 'vertical' 105 | SHLabel: 106 | text: 'Your Shopping Cart' 107 | font_size: max(20, sp(16)) 108 | bold: True 109 | color: rgba("#4466FF") 110 | ScrollView: 111 | size_hint_y: 1000. 112 | always_overscroll: False 113 | do_scroll_x: False 114 | SHShelf: 115 | id: cart 116 | BoxLayout: 117 | size_hint_y: None 118 | height: self.minimum_height 119 | SHButton: 120 | text: 'sort by price\n(ascend)' 121 | on_press: shelf.data = sorted(shelf.data, key=lambda d: d.price) 122 | SHButton: 123 | text: 'sort by price\n(descend)' 124 | on_press: shelf.data = sorted(shelf.data, key=lambda d: d.price, reverse=True) 125 | SHButton: 126 | text: 'sort by name\n(ascend)' 127 | on_press: shelf.data = sorted(shelf.data, key=lambda d: d.name) 128 | SHButton: 129 | text: 'sort by name\n(descend)' 130 | on_press: shelf.data = sorted(shelf.data, key=lambda d: d.name, reverse=True) 131 | Widget: 132 | SHButton: 133 | text: 'total price' 134 | on_press: ak.managed_start(root.show_total_price()) 135 | SHButton: 136 | text: 'sort by price\n(ascend)' 137 | on_press: cart.data = sorted(cart.data, key=lambda d: d.price) 138 | SHButton: 139 | text: 'sort by price\n(descend)' 140 | on_press: cart.data = sorted(cart.data, key=lambda d: d.price, reverse=True) 141 | ''' 142 | 143 | 144 | @dataclass 145 | class Food: 146 | name: str = '' 147 | price: int = 0 148 | texture: F.Texture = None 149 | 150 | 151 | class ShoppingApp(App): 152 | def build(self): 153 | Builder.load_string(KV_CODE) 154 | return SHMain() 155 | 156 | def on_start(self): 157 | self.root.main(db_path=__file__ + r".sqlite3") 158 | 159 | 160 | class SHMain(F.BoxLayout): 161 | async def show_total_price(self, *, _cache=[]): 162 | popup = _cache.pop() if _cache else F.Popup( 163 | size_hint=(.5, .2, ), 164 | title="Total", 165 | title_size="20sp", 166 | content=F.Label(font_size="20sp"), 167 | ) 168 | total_price = sum(d.price for d in self.ids.cart.data) 169 | popup.content.text = f"{total_price} yen" 170 | popup.open() 171 | try: 172 | await ak.event(popup, "on_dismiss") 173 | finally: 174 | await ak.sleep(popup._anim_duration + 0.1) 175 | _cache.append(popup) 176 | 177 | def main(self, db_path: PathLike): 178 | import os.path 179 | from random import randint 180 | 181 | if not os.path.exists(db_path): 182 | try: 183 | self._init_database(db_path) 184 | except Exception: 185 | os.remove(db_path) 186 | raise 187 | self.ids.shelf.data = [ 188 | food 189 | for food in self._load_database(db_path) 190 | for __ in range(randint(2, 4)) 191 | ] 192 | 193 | @staticmethod 194 | def _load_database(db_path: PathLike) -> list[Food]: 195 | from io import BytesIO 196 | from kivy.core.image import Image as CoreImage 197 | 198 | with sqlite3.connect(str(db_path)) as conn: 199 | # FIXME: It's probably better to ``Texture.add_reload_observer()``. 200 | return [ 201 | Food(name=name, price=price, texture=CoreImage(BytesIO(image_data), ext=image_type).texture) 202 | for name, price, image_data, image_type in conn.execute("SELECT name, price, image, image_type FROM Foods") 203 | ] 204 | 205 | @staticmethod 206 | def _init_database(db_path: PathLike): 207 | import requests 208 | 209 | FOOD_DATA = ( 210 | # (name, price, image_url) 211 | ("blueberry", 500, r"https://3.bp.blogspot.com/-RVk4JCU_K2M/UvTd-IhzTvI/AAAAAAAAdhY/VMzFjXNoRi8/s180-c/fruit_blueberry.png"), 212 | ("cacao", 800, r"https://3.bp.blogspot.com/-WT_RsvpvAhc/VPQT6ngLlmI/AAAAAAAAsEA/aDIU_F9TYc8/s180-c/fruit_cacao_kakao.png"), 213 | ("dragon fruit", 1200, r"https://1.bp.blogspot.com/-hATAhM4UmCY/VGLLK4mVWYI/AAAAAAAAou4/-sW2fvsEnN0/s180-c/fruit_dragonfruit.png"), 214 | ("kiwi", 130, r"https://2.bp.blogspot.com/-Y8xgv2nvwEs/WCdtGij7aTI/AAAAAAAA_fo/PBXfb8zCiQAZ8rRMx-DNclQvOHBbQkQEwCLcB/s180-c/fruit_kiwi_green.png"), 215 | ("lemon", 200, r"https://2.bp.blogspot.com/-UqVL2dBOyMc/WxvKDt8MQbI/AAAAAAABMmk/qHrz-vwCKo8okZsZpZVDsHLsKFXdI1BjgCLcBGAs/s180-c/fruit_lemon_tategiri.png"), 216 | ("mangosteen", 300, r"https://4.bp.blogspot.com/-tc72dGzUpww/WGYjEAwIauI/AAAAAAABAv8/xKvtWmqeKFcro6otVdLi5FFF7EoVxXiEwCLcB/s180-c/fruit_mangosteen.png"), 217 | ("apple", 150, r"https://4.bp.blogspot.com/-uY6ko43-ABE/VD3RiIglszI/AAAAAAAAoEA/kI39usefO44/s180-c/fruit_ringo.png"), 218 | ("orange", 100, r"https://1.bp.blogspot.com/-fCrHtwXvM6w/Vq89A_TvuzI/AAAAAAAA3kE/fLOFjPDSRn8/s180-c/fruit_slice10_orange.png"), 219 | ("soldum", 400, r"https://2.bp.blogspot.com/-FtWOiJkueNA/WK7e09oIUyI/AAAAAAABB_A/ry22yAU3W9sbofMUmA5-nn3D45ix_Y5RwCLcB/s180-c/fruit_soldum.png"), 220 | ("corn", 50, r"https://1.bp.blogspot.com/-RAJBy7nx2Ro/XkZdTINEtOI/AAAAAAABXWE/x8Sbcghba9UzR8Ppafozi4_cdmD1pawowCNcBGAsYHQ/s180-c/vegetable_toumorokoshi_corn_wagiri.png"), 221 | ("aloe", 400, r"https://4.bp.blogspot.com/-v7OAB-ULlrs/VVGVQ1FCjxI/AAAAAAAAtjg/H09xS1Nf9_A/s180-c/plant_aloe_kaniku.png"), 222 | ) 223 | with requests.Session() as session: 224 | FOOD_DATA = tuple( 225 | (name, price, c := session.get(image_url).content, detect_image_format(c)) 226 | for name, price, image_url in FOOD_DATA 227 | ) 228 | with sqlite3.connect(str(db_path)) as conn, closing(conn.cursor()) as cur: 229 | cur.execute(""" 230 | CREATE TABLE Foods ( 231 | name TEXT NOT NULL UNIQUE, 232 | price INT NOT NULL, 233 | image BLOB NOT NULL, 234 | image_type TEXT NOT NULL, 235 | PRIMARY KEY (name) 236 | ); 237 | """) 238 | cur.executemany("INSERT INTO Foods(name, price, image, image_type) VALUES (?, ?, ?, ?)", FOOD_DATA) 239 | 240 | 241 | class SHFood(KXDraggableBehavior, F.BoxLayout): 242 | datum: Food = ObjectProperty(Food(), rebind=True) 243 | 244 | 245 | class EquitableBoxLayout(F.BoxLayout): 246 | '''Always dispatches touch events to all its children''' 247 | def on_touch_down(self, touch): 248 | return any([c.dispatch('on_touch_down', touch) for c in self.children]) 249 | def on_touch_move(self, touch): 250 | return any([c.dispatch('on_touch_move', touch) for c in self.children]) 251 | def on_touch_up(self, touch): 252 | return any([c.dispatch('on_touch_up', touch) for c in self.children]) 253 | 254 | 255 | class SemiRecycleBehavior: 256 | ''' 257 | Mix-in class that adds RecyclewView-like interface to layouts. 258 | But unlike RecycleView, this one creates view widgets as much as the number of the data. 259 | ''' 260 | 261 | viewclass = ObjectProperty() 262 | '''widget-class or its name''' 263 | 264 | def __init__(self, **kwargs): 265 | self._rv_refresh_params = {} 266 | self._rv_trigger_refresh = Clock.create_trigger(self._rv_refresh, -1) 267 | super().__init__(**kwargs) 268 | 269 | def on_viewclass(self, *args): 270 | self._rv_refresh_params['viewclass'] = None 271 | self._rv_trigger_refresh() 272 | 273 | def _get_data(self) -> Iterable: 274 | data = self._rv_refresh_params.get('data') 275 | return [c.datum for c in reversed(self.children)] if data is None else data 276 | 277 | def _set_data(self, new_data: Iterable): 278 | self._rv_refresh_params['data'] = new_data 279 | self._rv_trigger_refresh() 280 | 281 | data = property(_get_data, _set_data) 282 | 283 | def _rv_refresh(self, *args): 284 | viewclass = self.viewclass 285 | if not viewclass: 286 | self.clear_widgets() 287 | return 288 | data = self.data 289 | params = self._rv_refresh_params 290 | reusable_widgets = '' if 'viewclass' in params else self.children[::-1] 291 | self.clear_widgets() 292 | if isinstance(viewclass, str): 293 | viewclass = F.get(viewclass) 294 | for datum, w in zip(data, itertools.chain(reusable_widgets, iter(viewclass, None))): 295 | w.datum = datum 296 | self.add_widget(w) 297 | params.clear() 298 | F.register("SemiRecycleBehavior", cls=SemiRecycleBehavior) 299 | 300 | 301 | if __name__ == '__main__': 302 | ShoppingApp().run() 303 | -------------------------------------------------------------------------------- /examples/using_other_widget_as_an_emitter.py: -------------------------------------------------------------------------------- 1 | from kivy.properties import ObjectProperty 2 | from kivy.app import App 3 | from kivy.lang import Builder 4 | from kivy.uix.label import Label 5 | from kivy.uix.floatlayout import FloatLayout 6 | 7 | from kivy_garden.draggable import KXDroppableBehavior, KXDraggableBehavior 8 | 9 | KV_CODE = ''' 10 | #:import ascii_uppercase string.ascii_uppercase 11 | 12 | : 13 | drag_classes: ['card', ] 14 | canvas.before: 15 | Color: 16 | rgba: .1, .1, .1, 1 17 | Rectangle: 18 | pos: self.pos 19 | size: self.size 20 | : 21 | drag_cls: 'card' 22 | drag_timeout: 0 23 | font_size: 100 24 | opacity: .3 if self.is_being_dragged else 1. 25 | canvas.after: 26 | Color: 27 | rgba: 1, 1, 1, 1 28 | Line: 29 | rectangle: [*self.pos, *self.size, ] 30 | : 31 | canvas.after: 32 | Color: 33 | rgba: 1, 1, 1, 1 34 | Line: 35 | rectangle: [*self.pos, *self.size, ] 36 | 37 | BoxLayout: 38 | Widget: 39 | size_hint_x: .1 40 | 41 | # Put the board inside a RelativeLayout just to confirm the coordinates are properly transformed. 42 | # This is not necessary for this example to work. 43 | RelativeLayout: 44 | GridLayout: 45 | id: board 46 | cols: 4 47 | rows: 4 48 | spacing: 10 49 | padding: 10 50 | 51 | BoxLayout: 52 | orientation: 'vertical' 53 | size_hint_x: .2 54 | padding: '20dp', '40dp' 55 | spacing: '80dp' 56 | RelativeLayout: # Put a deck inside a RelativeLayout just ... 57 | Deck: 58 | board: board 59 | text: 'numbers' 60 | font_size: '20sp' 61 | text_iter: (str(i) for i in range(10)) 62 | Deck: 63 | board: board 64 | text: 'letters' 65 | font_size: '20sp' 66 | text_iter: iter(ascii_uppercase) 67 | ''' 68 | 69 | 70 | class Cell(KXDroppableBehavior, FloatLayout): 71 | def accepts_drag(self, touch, ctx, draggable) -> bool: 72 | return not self.children 73 | 74 | def add_widget(self, widget, *args, **kwargs): 75 | widget.size_hint = (1, 1, ) 76 | widget.pos_hint = {'x': 0, 'y': 0, } 77 | return super().add_widget(widget, *args, **kwargs) 78 | 79 | 80 | class Card(KXDraggableBehavior, Label): 81 | pass 82 | 83 | 84 | class Deck(Label): 85 | text_iter = ObjectProperty() 86 | board = ObjectProperty() 87 | 88 | def on_touch_down(self, touch): 89 | if self.collide_point(*touch.opos): 90 | if (text := next(self.text_iter, None)) is not None: 91 | card = Card(text=text, size=self._get_cell_size()) 92 | card.start_dragging_from_others_touch(self, touch) 93 | return True 94 | 95 | def _get_cell_size(self): 96 | return self.board.children[0].size 97 | 98 | 99 | class SampleApp(App): 100 | def build(self): 101 | return Builder.load_string(KV_CODE) 102 | 103 | def on_start(self): 104 | board = self.root.ids.board 105 | for __ in range(board.cols * board.rows): 106 | board.add_widget(Cell()) 107 | 108 | 109 | if __name__ == '__main__': 110 | SampleApp().run() 111 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "asyncgui" 5 | version = "0.8.0" 6 | description = "A minimalistic async library that focuses on fast responsiveness" 7 | optional = false 8 | python-versions = "<4.0,>=3.9" 9 | groups = ["main"] 10 | files = [ 11 | {file = "asyncgui-0.8.0-py3-none-any.whl", hash = "sha256:69b5d9aae17283c2bbb948c8f93cd03c2bbca5c060bbbce11ceeecb0292e5c03"}, 12 | {file = "asyncgui-0.8.0.tar.gz", hash = "sha256:daca14460f42c04586a4f824ccb7b43c24ea6cbda5ee3852aef43db5b69fe83c"}, 13 | ] 14 | 15 | [package.dependencies] 16 | exceptiongroup = {version = ">=1.0.4,<2.0.0", markers = "python_version < \"3.11\""} 17 | 18 | [[package]] 19 | name = "asynckivy" 20 | version = "0.8.1" 21 | description = "Async library for Kivy" 22 | optional = false 23 | python-versions = "<4.0,>=3.9" 24 | groups = ["main"] 25 | files = [ 26 | {file = "asynckivy-0.8.1-py3-none-any.whl", hash = "sha256:cbc69c52c1b0285093984d5c03c0981892fc5c505ba88d94690fb907e3896fb6"}, 27 | {file = "asynckivy-0.8.1.tar.gz", hash = "sha256:ecf9c99452ab561bbf57c65b55eb77dec45f60f6abc2d7e99612dd9e08750cbe"}, 28 | ] 29 | 30 | [package.dependencies] 31 | asyncgui = ">=0.7.2,<0.9" 32 | 33 | [[package]] 34 | name = "certifi" 35 | version = "2024.7.4" 36 | description = "Python package for providing Mozilla's CA Bundle." 37 | optional = false 38 | python-versions = ">=3.6" 39 | groups = ["dev"] 40 | files = [ 41 | {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, 42 | {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, 43 | ] 44 | 45 | [[package]] 46 | name = "charset-normalizer" 47 | version = "3.3.2" 48 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 49 | optional = false 50 | python-versions = ">=3.7.0" 51 | groups = ["dev"] 52 | files = [ 53 | {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, 54 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, 55 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, 56 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, 57 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, 58 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, 59 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, 60 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, 61 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, 62 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, 63 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, 64 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, 65 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, 66 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, 67 | {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, 68 | {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, 69 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, 70 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, 71 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, 72 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, 73 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, 74 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, 75 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, 76 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, 77 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, 78 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, 79 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, 80 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, 81 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, 82 | {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, 83 | {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, 84 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, 85 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, 86 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, 87 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, 88 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, 89 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, 90 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, 91 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, 92 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, 93 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, 94 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, 95 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, 96 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, 97 | {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, 98 | {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, 99 | {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, 100 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, 101 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, 102 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, 103 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, 104 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, 105 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, 106 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, 107 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, 108 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, 109 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, 110 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, 111 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, 112 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, 113 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, 114 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, 115 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, 116 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, 117 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, 118 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, 119 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, 120 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, 121 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, 122 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, 123 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, 124 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, 125 | {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, 126 | {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, 127 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, 128 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, 129 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, 130 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, 131 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, 132 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, 133 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, 134 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, 135 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, 136 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, 137 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, 138 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, 139 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, 140 | {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, 141 | {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, 142 | {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, 143 | ] 144 | 145 | [[package]] 146 | name = "colorama" 147 | version = "0.4.6" 148 | description = "Cross-platform colored terminal text." 149 | optional = false 150 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 151 | groups = ["dev"] 152 | markers = "sys_platform == \"win32\"" 153 | files = [ 154 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 155 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 156 | ] 157 | 158 | [[package]] 159 | name = "docutils" 160 | version = "0.21.2" 161 | description = "Docutils -- Python Documentation Utilities" 162 | optional = false 163 | python-versions = ">=3.9" 164 | groups = ["dev"] 165 | files = [ 166 | {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, 167 | {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, 168 | ] 169 | 170 | [[package]] 171 | name = "exceptiongroup" 172 | version = "1.2.2" 173 | description = "Backport of PEP 654 (exception groups)" 174 | optional = false 175 | python-versions = ">=3.7" 176 | groups = ["main", "dev"] 177 | markers = "python_version < \"3.11\"" 178 | files = [ 179 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, 180 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, 181 | ] 182 | 183 | [package.extras] 184 | test = ["pytest (>=6)"] 185 | 186 | [[package]] 187 | name = "flake8" 188 | version = "5.0.4" 189 | description = "the modular source code checker: pep8 pyflakes and co" 190 | optional = false 191 | python-versions = ">=3.6.1" 192 | groups = ["dev"] 193 | files = [ 194 | {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, 195 | {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, 196 | ] 197 | 198 | [package.dependencies] 199 | mccabe = ">=0.7.0,<0.8.0" 200 | pycodestyle = ">=2.9.0,<2.10.0" 201 | pyflakes = ">=2.5.0,<2.6.0" 202 | 203 | [[package]] 204 | name = "idna" 205 | version = "3.7" 206 | description = "Internationalized Domain Names in Applications (IDNA)" 207 | optional = false 208 | python-versions = ">=3.5" 209 | groups = ["dev"] 210 | files = [ 211 | {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, 212 | {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, 213 | ] 214 | 215 | [[package]] 216 | name = "iniconfig" 217 | version = "2.0.0" 218 | description = "brain-dead simple config-ini parsing" 219 | optional = false 220 | python-versions = ">=3.7" 221 | groups = ["dev"] 222 | files = [ 223 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 224 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 225 | ] 226 | 227 | [[package]] 228 | name = "kivy" 229 | version = "2.3.0" 230 | description = "An open-source Python framework for developing GUI apps that work cross-platform, including desktop, mobile and embedded platforms." 231 | optional = false 232 | python-versions = ">=3.7" 233 | groups = ["dev"] 234 | files = [ 235 | {file = "Kivy-2.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fcd8fdc742ae10d27e578df2052b4c3e99a754e91baad77d1f2e4f4d1238917f"}, 236 | {file = "Kivy-2.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7492593a5d5d916c48b14a06fbe177341b1efb5753c9984be2fb84e3b3313c89"}, 237 | {file = "Kivy-2.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:199c30e8daeace61392329766eeb68daa49631cd9793bec9440dda5cf30d68d5"}, 238 | {file = "Kivy-2.3.0-cp310-cp310-win32.whl", hash = "sha256:03fc4b26c7d6a5ecee2c97ffa8d622e97ac8a8c4e0a00d333c156d64e09e4e19"}, 239 | {file = "Kivy-2.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:aa5d57494cab405d395d65570d8481ab87869ba6daf4efb6c985bd16b32e7abf"}, 240 | {file = "Kivy-2.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ec36ab3b74a525fa463b61895d3a2d76e9e4d206641233defae0d604e75df7ad"}, 241 | {file = "Kivy-2.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd3e923397779776ac97ad87a1b9dd603b7f1c911a6ae04f1d1658712eaaf7cb"}, 242 | {file = "Kivy-2.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7766baac2509d699df84b284579fa25ee31383d48893660cd8dba62081453a29"}, 243 | {file = "Kivy-2.3.0-cp311-cp311-win32.whl", hash = "sha256:d654aaec6ddf9ca0edf73abd79e6aea423299c825a7ac432df17b031adaa7900"}, 244 | {file = "Kivy-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:33dca85a520fe958e7134b96025b0625eb769adfb8829359959c8b314b7bc8d4"}, 245 | {file = "Kivy-2.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:7b1307521843d316265481d963344e85870ae5fa0c7d0881129749acfe61da7b"}, 246 | {file = "Kivy-2.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:521105a4ca1db3e1203c3cdba4abe737533874d9c29bbfb1e1ae941238507440"}, 247 | {file = "Kivy-2.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6672959894f652856d1dfcbcdcc09263de5f1cbed768b997dc8dcecab4385a4f"}, 248 | {file = "Kivy-2.3.0-cp312-cp312-win32.whl", hash = "sha256:cf0bccc95b1344b79fbfdf54155d40438490f9801fd77279f068a4f66db72e4e"}, 249 | {file = "Kivy-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:710648c987a63e37c723e6622853efe0278767596631a38728a54474b2cb77f2"}, 250 | {file = "Kivy-2.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d2c6a411e2d837684d91b46231dd12db74fb1db6a2628e9f27581ce1583e5c8a"}, 251 | {file = "Kivy-2.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8757f189d8a41a4b164150144037359405906a46b07572e8e1c602a782cacebf"}, 252 | {file = "Kivy-2.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06c9b0a4bff825793e150e2cdbc823b59f635ce51e575d470d0fc3a06159596c"}, 253 | {file = "Kivy-2.3.0-cp37-cp37m-win32.whl", hash = "sha256:d72599b80c8a7c2698769b4129ff52f2c4e28b6a75f9401180052c7d80763f19"}, 254 | {file = "Kivy-2.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:0c42cf3c33e1aa3dee9c8acb6f91f8a4ad6c9de76064dcb8fdb1c60809643788"}, 255 | {file = "Kivy-2.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:da8dd7ade7b7859642f53c3f32e10513877ce650367b68591b3aaacb46dcf012"}, 256 | {file = "Kivy-2.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb6191bb51983f9e8257356aa53a71ccff5b6cf92f0bdcd5756973a6ac4b4446"}, 257 | {file = "Kivy-2.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa3e7ce4fbd22284b303939676c5ae5448bb1e4d405f066dfc76c7cf56595cd"}, 258 | {file = "Kivy-2.3.0-cp38-cp38-win32.whl", hash = "sha256:221f809220e518ae8b88a9b31310f9fef73727569e5cb13436572674fce4507b"}, 259 | {file = "Kivy-2.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:8d2c3e5927fcf021d32124f915d56ae29e3a126c4f53db098436ea3959758a4c"}, 260 | {file = "Kivy-2.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e8b91dfa2ad83739cc12d0f7bbe6410a3af2c2b3afd7b1d08919d9ec92826d61"}, 261 | {file = "Kivy-2.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d79cb4a8649c476db18a079c447e57f8dbd4ad41459dc2162133a45cbb8cae96"}, 262 | {file = "Kivy-2.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3be8db1eecc2d18859a7324b5cea79afb44095ccd73671987840afa26c68b0c9"}, 263 | {file = "Kivy-2.3.0-cp39-cp39-win32.whl", hash = "sha256:5e6c431088584132d685696592e281fac217a5fd662f92cc6c6b48316e30b9c2"}, 264 | {file = "Kivy-2.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:c332ff319db7648004d486c40fc4e700972f8e79a882d698e18eb238b2009e98"}, 265 | {file = "Kivy-2.3.0.tar.gz", hash = "sha256:e8b8610c7f8ef6db908a139d369b247378f18105c96981e492eab2b4706c79d5"}, 266 | ] 267 | 268 | [package.dependencies] 269 | docutils = "*" 270 | "kivy-deps.angle" = {version = ">=0.4.0,<0.5.0", markers = "sys_platform == \"win32\""} 271 | "kivy-deps.glew" = {version = ">=0.3.1,<0.4.0", markers = "sys_platform == \"win32\""} 272 | "kivy-deps.sdl2" = {version = ">=0.7.0,<0.8.0", markers = "sys_platform == \"win32\""} 273 | Kivy-Garden = ">=0.1.4" 274 | pygments = "*" 275 | pypiwin32 = {version = "*", markers = "sys_platform == \"win32\""} 276 | 277 | [package.extras] 278 | angle = ["kivy-deps.angle (>=0.4.0,<0.5.0) ; sys_platform == \"win32\""] 279 | base = ["docutils", "kivy-deps.angle (>=0.4.0,<0.5.0) ; sys_platform == \"win32\"", "kivy-deps.glew (>=0.3.1,<0.4.0) ; sys_platform == \"win32\"", "kivy-deps.sdl2 (>=0.7.0,<0.8.0) ; sys_platform == \"win32\"", "pillow (>=9.5.0,<11)", "pygments", "pypiwin32 ; sys_platform == \"win32\"", "requests"] 280 | dev = ["flake8", "funcparserlib (==1.0.0a0)", "kivy-deps.glew-dev (>=0.3.1,<0.4.0) ; sys_platform == \"win32\"", "kivy-deps.gstreamer-dev (>=0.3.3,<0.4.0) ; sys_platform == \"win32\"", "kivy-deps.sdl2-dev (>=0.7.0,<0.8.0) ; sys_platform == \"win32\"", "pre-commit", "pyinstaller", "pytest (>=3.6)", "pytest-asyncio (!=0.11.0)", "pytest-benchmark", "pytest-cov", "pytest-timeout", "responses", "sphinx (<=6.2.1)", "sphinxcontrib-actdiag", "sphinxcontrib-blockdiag", "sphinxcontrib-jquery", "sphinxcontrib-nwdiag", "sphinxcontrib-seqdiag"] 281 | full = ["docutils", "ffpyplayer ; sys_platform == \"linux\" or sys_platform == \"darwin\"", "kivy-deps.angle (>=0.4.0,<0.5.0) ; sys_platform == \"win32\"", "kivy-deps.glew (>=0.3.1,<0.4.0) ; sys_platform == \"win32\"", "kivy-deps.gstreamer (>=0.3.3,<0.4.0) ; sys_platform == \"win32\"", "kivy-deps.sdl2 (>=0.7.0,<0.8.0) ; sys_platform == \"win32\"", "pillow (>=9.5.0,<11)", "pygments", "pypiwin32 ; sys_platform == \"win32\""] 282 | glew = ["kivy-deps.glew (>=0.3.1,<0.4.0) ; sys_platform == \"win32\""] 283 | gstreamer = ["kivy-deps.gstreamer (>=0.3.3,<0.4.0) ; sys_platform == \"win32\""] 284 | media = ["ffpyplayer ; sys_platform == \"linux\" or sys_platform == \"darwin\"", "kivy-deps.gstreamer (>=0.3.3,<0.4.0) ; sys_platform == \"win32\""] 285 | sdl2 = ["kivy-deps.sdl2 (>=0.7.0,<0.8.0) ; sys_platform == \"win32\""] 286 | tuio = ["oscpy"] 287 | 288 | [[package]] 289 | name = "kivy-deps-angle" 290 | version = "0.4.0" 291 | description = "Repackaged binary dependency of Kivy." 292 | optional = false 293 | python-versions = "*" 294 | groups = ["dev"] 295 | markers = "sys_platform == \"win32\"" 296 | files = [ 297 | {file = "kivy_deps.angle-0.4.0-cp310-cp310-win32.whl", hash = "sha256:7873a551e488afa5044c4949a4aa42c4a4c4290469f0a6dd861e6b95283c9638"}, 298 | {file = "kivy_deps.angle-0.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:71f2f01a3a7bbe1d4790e2a64e64a0ea8ae154418462ea407799ed66898b2c1f"}, 299 | {file = "kivy_deps.angle-0.4.0-cp311-cp311-win32.whl", hash = "sha256:c3899ff1f3886b80b155955bad07bfa33bbebd97718cdf46dfd788dc467124bc"}, 300 | {file = "kivy_deps.angle-0.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:574381d4e66f3198bc48aa10f238e7a3816ad56b80ec939f5d56fb33a378d0b1"}, 301 | {file = "kivy_deps.angle-0.4.0-cp312-cp312-win32.whl", hash = "sha256:4fa7a6366899fba13f7624baf4645787165f45731db08d14557da29c12ee48f0"}, 302 | {file = "kivy_deps.angle-0.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:668e670d4afd2551af0af2c627ceb0feac884bd799fb6a3dff78fdbfa2ea0451"}, 303 | {file = "kivy_deps.angle-0.4.0-cp37-cp37m-win32.whl", hash = "sha256:24cfc0076d558080a00c443c7117311b4a977c1916fe297232eff1fd6f62651e"}, 304 | {file = "kivy_deps.angle-0.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:48592ac6f7c183c5cd10d9ebe43d4148d0b2b9e400a2b0bcb5d21014cc929ce2"}, 305 | {file = "kivy_deps.angle-0.4.0-cp38-cp38-win32.whl", hash = "sha256:1bbacf20bf6bd6ee965388f95d937c8fba2c54916fb44faa166c2ba58276753c"}, 306 | {file = "kivy_deps.angle-0.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:e2ba4e390b02ad5bcb57b43a9227fa27ff55e69cd715a87217b324195eb267c3"}, 307 | {file = "kivy_deps.angle-0.4.0-cp39-cp39-win32.whl", hash = "sha256:6546a62aba2b7e18a800b3df79daa757af3a980c297646c986896522395794e2"}, 308 | {file = "kivy_deps.angle-0.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:bfaf9b37f2ecc3e4e7736657eed507716477af35cdd3118903e999d9d567ae8c"}, 309 | ] 310 | 311 | [[package]] 312 | name = "kivy-deps-glew" 313 | version = "0.3.1" 314 | description = "Repackaged binary dependency of Kivy." 315 | optional = false 316 | python-versions = "*" 317 | groups = ["dev"] 318 | markers = "sys_platform == \"win32\"" 319 | files = [ 320 | {file = "kivy_deps.glew-0.3.1-cp310-cp310-win32.whl", hash = "sha256:8f4b3ed15acb62474909b6d41661ffb4da9eb502bb5684301fb2da668f288a58"}, 321 | {file = "kivy_deps.glew-0.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:aef2d2a93f129d8425c75234e7f6cc0a34b59a4aee67f6d2cd7a5fdfa9915b53"}, 322 | {file = "kivy_deps.glew-0.3.1-cp311-cp311-win32.whl", hash = "sha256:ee2f80ef7ac70f4b61c50da8101b024308a8c59a57f7f25a6e09762b6c48f942"}, 323 | {file = "kivy_deps.glew-0.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:22e155ec59ce717387f5d8804811206d200a023ba3d0bc9bbf1393ee28d0053e"}, 324 | {file = "kivy_deps.glew-0.3.1-cp37-cp37m-win32.whl", hash = "sha256:5bf6a63fe9cc4fe7bbf280ec267ec8c47914020a1175fb22152525ff1837b436"}, 325 | {file = "kivy_deps.glew-0.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:d64a8625799fab7a7efeb3661ef8779a7f9c6d80da53eed87a956320f55530fa"}, 326 | {file = "kivy_deps.glew-0.3.1-cp38-cp38-win32.whl", hash = "sha256:00f4ae0a4682d951266458ddb639451edb24baa54a35215dce889209daf19a06"}, 327 | {file = "kivy_deps.glew-0.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f8b89dcf1846032d7a9c5ef88b0ee9cbd13366e9b4c85ada61e01549a910677"}, 328 | {file = "kivy_deps.glew-0.3.1-cp39-cp39-win32.whl", hash = "sha256:4e377ed97670dfda619a1b63a82345a8589be90e7c616a458fba2810708810b1"}, 329 | {file = "kivy_deps.glew-0.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:081a09b92f7e7817f489f8b6b31c9c9623661378de1dce1d6b097af5e7d42b45"}, 330 | ] 331 | 332 | [[package]] 333 | name = "kivy-deps-sdl2" 334 | version = "0.7.0" 335 | description = "Repackaged binary dependency of Kivy." 336 | optional = false 337 | python-versions = "*" 338 | groups = ["dev"] 339 | markers = "sys_platform == \"win32\"" 340 | files = [ 341 | {file = "kivy_deps.sdl2-0.7.0-cp310-cp310-win32.whl", hash = "sha256:3c4b2bf1e473e6124563e1ff58cf3475c4f19fe9248940872c9e3c248bac3cb4"}, 342 | {file = "kivy_deps.sdl2-0.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:ac0f4a6fe989899a60bbdb39516f45e4d90e2499864ab5d63e3706001cde48e8"}, 343 | {file = "kivy_deps.sdl2-0.7.0-cp311-cp311-win32.whl", hash = "sha256:b727123d059c0c00c7d13cc1db8c8cfd0e48388cf24c11ec71cc6783811063c8"}, 344 | {file = "kivy_deps.sdl2-0.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd946ca4e36a403bcafbe202033948c17f54bd5d28a343d98efd61f976822855"}, 345 | {file = "kivy_deps.sdl2-0.7.0-cp312-cp312-win32.whl", hash = "sha256:2a8f23fe201dea368b47adfecf8fb9133315788d314ad32f33000254aa2388e4"}, 346 | {file = "kivy_deps.sdl2-0.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:e56d5d651f81545c24f920f6f6e5d67b4100802152521022ccde53e822c507a2"}, 347 | {file = "kivy_deps.sdl2-0.7.0-cp37-cp37m-win32.whl", hash = "sha256:c75626f6a3f8979b1c6a59e5070c7a547bb7c379a8e03f249af6b4c399305fc1"}, 348 | {file = "kivy_deps.sdl2-0.7.0-cp37-cp37m-win_amd64.whl", hash = "sha256:95005fb3ae5b9e1d5edd32a6c0cfae9019efa2aeb3d909738dd73c5b9eea9dc1"}, 349 | {file = "kivy_deps.sdl2-0.7.0-cp38-cp38-win32.whl", hash = "sha256:9728eaf70af514e0df163b062944fec008a5ceb73e53897ac89e62fcd2b0bac2"}, 350 | {file = "kivy_deps.sdl2-0.7.0-cp38-cp38-win_amd64.whl", hash = "sha256:a23811df7359e62acf4002fe5240d968a25e7aeaf7989b78b59cd6437f34f7b9"}, 351 | {file = "kivy_deps.sdl2-0.7.0-cp39-cp39-win32.whl", hash = "sha256:ecbbcbd562a14a4a3870c8b6a0b1612eda24e9435df74fbb8e5f670560f0a9d6"}, 352 | {file = "kivy_deps.sdl2-0.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:a5ef494d2f57224b93649df5f7a20c4f4cbc22416167732bf9f62d1cb263fef4"}, 353 | ] 354 | 355 | [[package]] 356 | name = "kivy-garden" 357 | version = "0.1.5" 358 | description = "" 359 | optional = false 360 | python-versions = "*" 361 | groups = ["dev"] 362 | files = [ 363 | {file = "Kivy Garden-0.1.5.tar.gz", hash = "sha256:2b8377378e87501d5d271f33d94f0e44c089884572c64f89c9d609b1f86a2748"}, 364 | {file = "Kivy_Garden-0.1.5-py3-none-any.whl", hash = "sha256:ef50f44b96358cf10ac5665f27a4751bb34ef54051c54b93af891f80afe42929"}, 365 | ] 366 | 367 | [package.dependencies] 368 | requests = "*" 369 | 370 | [[package]] 371 | name = "mccabe" 372 | version = "0.7.0" 373 | description = "McCabe checker, plugin for flake8" 374 | optional = false 375 | python-versions = ">=3.6" 376 | groups = ["dev"] 377 | files = [ 378 | {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, 379 | {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, 380 | ] 381 | 382 | [[package]] 383 | name = "packaging" 384 | version = "24.1" 385 | description = "Core utilities for Python packages" 386 | optional = false 387 | python-versions = ">=3.8" 388 | groups = ["dev"] 389 | files = [ 390 | {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, 391 | {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, 392 | ] 393 | 394 | [[package]] 395 | name = "pluggy" 396 | version = "1.5.0" 397 | description = "plugin and hook calling mechanisms for python" 398 | optional = false 399 | python-versions = ">=3.8" 400 | groups = ["dev"] 401 | files = [ 402 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 403 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 404 | ] 405 | 406 | [package.extras] 407 | dev = ["pre-commit", "tox"] 408 | testing = ["pytest", "pytest-benchmark"] 409 | 410 | [[package]] 411 | name = "pycodestyle" 412 | version = "2.9.1" 413 | description = "Python style guide checker" 414 | optional = false 415 | python-versions = ">=3.6" 416 | groups = ["dev"] 417 | files = [ 418 | {file = "pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"}, 419 | {file = "pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"}, 420 | ] 421 | 422 | [[package]] 423 | name = "pyflakes" 424 | version = "2.5.0" 425 | description = "passive checker of Python programs" 426 | optional = false 427 | python-versions = ">=3.6" 428 | groups = ["dev"] 429 | files = [ 430 | {file = "pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"}, 431 | {file = "pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"}, 432 | ] 433 | 434 | [[package]] 435 | name = "pygments" 436 | version = "2.18.0" 437 | description = "Pygments is a syntax highlighting package written in Python." 438 | optional = false 439 | python-versions = ">=3.8" 440 | groups = ["dev"] 441 | files = [ 442 | {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, 443 | {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, 444 | ] 445 | 446 | [package.extras] 447 | windows-terminal = ["colorama (>=0.4.6)"] 448 | 449 | [[package]] 450 | name = "pypiwin32" 451 | version = "223" 452 | description = "" 453 | optional = false 454 | python-versions = "*" 455 | groups = ["dev"] 456 | markers = "sys_platform == \"win32\"" 457 | files = [ 458 | {file = "pypiwin32-223-py3-none-any.whl", hash = "sha256:67adf399debc1d5d14dffc1ab5acacb800da569754fafdc576b2a039485aa775"}, 459 | {file = "pypiwin32-223.tar.gz", hash = "sha256:71be40c1fbd28594214ecaecb58e7aa8b708eabfa0125c8a109ebd51edbd776a"}, 460 | ] 461 | 462 | [package.dependencies] 463 | pywin32 = ">=223" 464 | 465 | [[package]] 466 | name = "pytest" 467 | version = "7.4.4" 468 | description = "pytest: simple powerful testing with Python" 469 | optional = false 470 | python-versions = ">=3.7" 471 | groups = ["dev"] 472 | files = [ 473 | {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, 474 | {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, 475 | ] 476 | 477 | [package.dependencies] 478 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 479 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 480 | iniconfig = "*" 481 | packaging = "*" 482 | pluggy = ">=0.12,<2.0" 483 | tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} 484 | 485 | [package.extras] 486 | testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 487 | 488 | [[package]] 489 | name = "pywin32" 490 | version = "306" 491 | description = "Python for Window Extensions" 492 | optional = false 493 | python-versions = "*" 494 | groups = ["dev"] 495 | markers = "sys_platform == \"win32\"" 496 | files = [ 497 | {file = "pywin32-306-cp310-cp310-win32.whl", hash = "sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d"}, 498 | {file = "pywin32-306-cp310-cp310-win_amd64.whl", hash = "sha256:84f4471dbca1887ea3803d8848a1616429ac94a4a8d05f4bc9c5dcfd42ca99c8"}, 499 | {file = "pywin32-306-cp311-cp311-win32.whl", hash = "sha256:e65028133d15b64d2ed8f06dd9fbc268352478d4f9289e69c190ecd6818b6407"}, 500 | {file = "pywin32-306-cp311-cp311-win_amd64.whl", hash = "sha256:a7639f51c184c0272e93f244eb24dafca9b1855707d94c192d4a0b4c01e1100e"}, 501 | {file = "pywin32-306-cp311-cp311-win_arm64.whl", hash = "sha256:70dba0c913d19f942a2db25217d9a1b726c278f483a919f1abfed79c9cf64d3a"}, 502 | {file = "pywin32-306-cp312-cp312-win32.whl", hash = "sha256:383229d515657f4e3ed1343da8be101000562bf514591ff383ae940cad65458b"}, 503 | {file = "pywin32-306-cp312-cp312-win_amd64.whl", hash = "sha256:37257794c1ad39ee9be652da0462dc2e394c8159dfd913a8a4e8eb6fd346da0e"}, 504 | {file = "pywin32-306-cp312-cp312-win_arm64.whl", hash = "sha256:5821ec52f6d321aa59e2db7e0a35b997de60c201943557d108af9d4ae1ec7040"}, 505 | {file = "pywin32-306-cp37-cp37m-win32.whl", hash = "sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65"}, 506 | {file = "pywin32-306-cp37-cp37m-win_amd64.whl", hash = "sha256:72c5f621542d7bdd4fdb716227be0dd3f8565c11b280be6315b06ace35487d36"}, 507 | {file = "pywin32-306-cp38-cp38-win32.whl", hash = "sha256:e4c092e2589b5cf0d365849e73e02c391c1349958c5ac3e9d5ccb9a28e017b3a"}, 508 | {file = "pywin32-306-cp38-cp38-win_amd64.whl", hash = "sha256:e8ac1ae3601bee6ca9f7cb4b5363bf1c0badb935ef243c4733ff9a393b1690c0"}, 509 | {file = "pywin32-306-cp39-cp39-win32.whl", hash = "sha256:e25fd5b485b55ac9c057f67d94bc203f3f6595078d1fb3b458c9c28b7153a802"}, 510 | {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, 511 | ] 512 | 513 | [[package]] 514 | name = "requests" 515 | version = "2.32.3" 516 | description = "Python HTTP for Humans." 517 | optional = false 518 | python-versions = ">=3.8" 519 | groups = ["dev"] 520 | files = [ 521 | {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, 522 | {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, 523 | ] 524 | 525 | [package.dependencies] 526 | certifi = ">=2017.4.17" 527 | charset-normalizer = ">=2,<4" 528 | idna = ">=2.5,<4" 529 | urllib3 = ">=1.21.1,<3" 530 | 531 | [package.extras] 532 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 533 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 534 | 535 | [[package]] 536 | name = "tomli" 537 | version = "2.0.1" 538 | description = "A lil' TOML parser" 539 | optional = false 540 | python-versions = ">=3.7" 541 | groups = ["dev"] 542 | markers = "python_version < \"3.11\"" 543 | files = [ 544 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 545 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 546 | ] 547 | 548 | [[package]] 549 | name = "urllib3" 550 | version = "2.2.2" 551 | description = "HTTP library with thread-safe connection pooling, file post, and more." 552 | optional = false 553 | python-versions = ">=3.8" 554 | groups = ["dev"] 555 | files = [ 556 | {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"}, 557 | {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"}, 558 | ] 559 | 560 | [package.extras] 561 | brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] 562 | h2 = ["h2 (>=4,<5)"] 563 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 564 | zstd = ["zstandard (>=0.18.0)"] 565 | 566 | [metadata] 567 | lock-version = "2.1" 568 | python-versions = "^3.9" 569 | content-hash = "a9a95f2296423ee29192f7bac35481dc7b2a570760e1b21cde48d9898fb38678" 570 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "kivy-garden-draggable" 3 | version = "0.2.2" 4 | description = "Drag & Drop Extension for Kivy" 5 | authors = ["Nattōsai Mitō "] 6 | license = "MIT" 7 | readme = 'README.md' 8 | repository = 'https://github.com/kivy-garden/draggable' 9 | homepage = 'https://github.com/kivy-garden/draggable' 10 | keywords = ['kivy'] 11 | classifiers=[ 12 | 'Development Status :: 5 - Production/Stable', 13 | 'License :: OSI Approved :: MIT License', 14 | 'Intended Audience :: Developers', 15 | 'Programming Language :: Python', 16 | 'Programming Language :: Python :: 3.9', 17 | 'Programming Language :: Python :: 3.10', 18 | 'Programming Language :: Python :: 3.11', 19 | 'Programming Language :: Python :: 3.12', 20 | 'Programming Language :: Python :: 3.13', 21 | 'Topic :: Software Development :: Libraries', 22 | 'Operating System :: OS Independent', 23 | ] 24 | packages = [ 25 | { include = "kivy_garden", from = "src" }, 26 | ] 27 | 28 | [tool.poetry.dependencies] 29 | python = "^3.9" 30 | asynckivy = ">=0.7.1,<0.9" 31 | asyncgui = ">=0.7,<0.9" 32 | 33 | [tool.poetry.group.dev.dependencies] 34 | pytest = "^7.1.2" 35 | flake8 = "^5.0.4" 36 | kivy = "==2.3.0" 37 | 38 | [build-system] 39 | requires = ["poetry-core"] 40 | build-backend = "poetry.core.masonry.api" 41 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 120 3 | exclude = __pycache__,.tox,.git/,doc/ 4 | extend-ignore = 5 | E125, 6 | E126, 7 | E127, 8 | E128, 9 | E402, 10 | E731, 11 | E741, 12 | F401, 13 | F841, 14 | W503, 15 | W504 16 | -------------------------------------------------------------------------------- /src/kivy_garden/draggable/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ( 2 | 'DragContext', 3 | 'KXDraggableBehavior', 'KXDroppableBehavior', 'KXReorderableBehavior', 4 | 'save_widget_state', 'restore_widget_state', 5 | 'save_widget_location', 'restore_widget_location', 'ongoing_drags', 6 | ) 7 | 8 | from ._impl import KXDraggableBehavior, KXDroppableBehavior, KXReorderableBehavior, ongoing_drags, DragContext 9 | from ._utils import save_widget_state, restore_widget_state 10 | from ._utils import save_widget_location, restore_widget_location 11 | -------------------------------------------------------------------------------- /src/kivy_garden/draggable/_impl.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple, Union 2 | from inspect import isawaitable 3 | from dataclasses import dataclass 4 | from contextlib import nullcontext 5 | 6 | from kivy.properties import ( 7 | BooleanProperty, ListProperty, StringProperty, NumericProperty, OptionProperty, AliasProperty, 8 | ) 9 | from kivy.clock import Clock 10 | from kivy.factory import Factory 11 | import kivy.core.window 12 | from kivy.uix.widget import Widget 13 | from kivy.uix.scrollview import ScrollView 14 | import asynckivy as ak 15 | 16 | from ._utils import ( 17 | temp_transform, _create_spacer, 18 | save_widget_state, restore_widget_state, 19 | ) 20 | 21 | 22 | @dataclass 23 | class DragContext: 24 | original_pos_win: tuple = None 25 | '''(read-only) The position of the draggable at the time the drag has 26 | started. (window coordinates). 27 | ''' 28 | 29 | original_state: dict = None 30 | ''' 31 | (read-only) The state of the draggable at the time the drag has started. 32 | This can be passed to ``restore_widget_state()``. 33 | ''' 34 | 35 | droppable: Union[None, 'KXDroppableBehavior', 'KXReorderableBehavior'] = None 36 | '''(read-only) The widget where the draggable dropped to. This is always None on_drag_start/on_drag_cancel, and is 37 | always a widget on_drag_succeed, and can be either on_drag_fail/on_drag_end.''' 38 | 39 | @property 40 | def original_location(self) -> dict: 41 | ''' 42 | This exists solely for backward compatibility. 43 | Use :attr:`original_state` instead. 44 | ''' 45 | return self.original_state 46 | 47 | 48 | class KXDraggableBehavior: 49 | __events__ = ( 50 | 'on_drag_start', 'on_drag_end', 'on_drag_succeed', 'on_drag_fail', 51 | 'on_drag_cancel', 52 | ) 53 | 54 | drag_cls = StringProperty() 55 | '''Same as drag_n_drop's ''' 56 | 57 | drag_distance = NumericProperty(ScrollView.scroll_distance.defaultvalue) 58 | 59 | drag_timeout = NumericProperty(ScrollView.scroll_timeout.defaultvalue) 60 | 61 | drag_enabled = BooleanProperty(True) 62 | '''Indicates whether this draggable can be dragged or not. Changing this 63 | doesn't affect ongoing drag. Call `drag_cancel()` if you want to do that. 64 | ''' 65 | 66 | drag_state = OptionProperty(None, options=('started', 'succeeded', 'failed', 'cancelled'), allownone=True) 67 | '''(read-only)''' 68 | 69 | is_being_dragged = AliasProperty(lambda self: self.drag_state is not None, bind=('drag_state', ), cache=True) 70 | '''(read-only)''' 71 | 72 | def drag_cancel(self): 73 | ''' 74 | If the draggable is currently being dragged, cancel it. 75 | ''' 76 | self._drag_task.cancel() 77 | 78 | def __init__(self, **kwargs): 79 | super().__init__(**kwargs) 80 | self._drag_task = ak.dummy_task 81 | self.__ud_key = 'KXDraggableBehavior.' + str(self.uid) 82 | 83 | def _is_a_touch_potentially_a_dragging_gesture(self, touch) -> bool: 84 | return self.collide_point(*touch.opos) \ 85 | and (not touch.is_mouse_scrolling) \ 86 | and (self.__ud_key not in touch.ud) \ 87 | and (touch.time_end == -1) 88 | 89 | @property 90 | def _can_be_dragged(self) -> bool: 91 | return self.drag_enabled and (not self.is_being_dragged) 92 | 93 | def on_touch_down(self, touch): 94 | if self._is_a_touch_potentially_a_dragging_gesture(touch) and self._can_be_dragged: 95 | touch.ud[self.__ud_key] = None 96 | if self.drag_timeout: 97 | ak.managed_start(self._see_if_a_touch_actually_is_a_dragging_gesture(touch)) 98 | else: 99 | ak.managed_start(self._treat_a_touch_as_a_drag(touch)) 100 | return True 101 | else: 102 | touch.ud[self.__ud_key] = None 103 | return super().on_touch_down(touch) 104 | 105 | async def _see_if_a_touch_actually_is_a_dragging_gesture(self, touch): 106 | async with ak.move_on_after(self.drag_timeout / 1000.) as bg_task: 107 | # LOAD_FAST 108 | abs_ = abs 109 | drag_distance = self.drag_distance 110 | ox, oy = touch.opos 111 | 112 | do_touch_up = True 113 | async for __ in ak.rest_of_touch_events(self, touch): 114 | dx = abs_(touch.x - ox) 115 | dy = abs_(touch.y - oy) 116 | if dy > drag_distance or dx > drag_distance: 117 | do_touch_up = False 118 | break 119 | 120 | is_a_dragging_gesture = bg_task.finished 121 | if is_a_dragging_gesture: 122 | ak.managed_start( 123 | self._treat_a_touch_as_a_drag(touch, do_transform=True) 124 | if self._can_be_dragged else 125 | self._simulate_a_normal_touch(touch, do_transform=True) 126 | ) 127 | else: 128 | ak.managed_start(self._simulate_a_normal_touch(touch, do_touch_up=do_touch_up)) 129 | 130 | def start_dragging_from_others_touch(self, receiver: Widget, touch): 131 | ''' 132 | Arguments 133 | --------- 134 | 135 | * ``receiver`` ... The widget that received the ``touch``. 136 | * ``touch`` ... The touch that is going to drag me. 137 | ''' 138 | if touch.time_end != -1: 139 | return 140 | touch.ud[self.__ud_key] = None 141 | ak.managed_start(self._treat_a_touch_as_a_drag(touch, touch_receiver=receiver)) 142 | 143 | async def _treat_a_touch_as_a_drag(self, touch, *, do_transform=False, touch_receiver=None): 144 | try: 145 | if touch_receiver is None: 146 | original_pos_win = self.to_window(*self.pos) 147 | with temp_transform(touch, self.parent.to_widget) if do_transform else nullcontext(): 148 | offset_x = touch.ox - self.x 149 | offset_y = touch.oy - self.y 150 | else: 151 | offset_x = self.width * 0.5 152 | offset_y = self.height * 0.5 153 | x, y = touch_receiver.to_window(*touch.opos) 154 | original_pos_win = (x - offset_x, y - offset_y, ) 155 | 156 | # NOTE: I don't know the difference from 'get_root_window()' 157 | window = self.get_parent_window() 158 | if window is None: 159 | from kivy.core.window import Window 160 | window = Window 161 | touch_ud = touch.ud 162 | original_state = save_widget_state(self) 163 | ctx = DragContext( 164 | original_pos_win=original_pos_win, 165 | original_state=original_state, 166 | ) 167 | 168 | # move self under the Window 169 | if self.parent is not None: 170 | self.parent.remove_widget(self) 171 | self.size_hint = (None, None, ) 172 | self.pos_hint = {} 173 | self.pos = ( 174 | original_pos_win[0] + touch.x - touch.ox, 175 | original_pos_win[1] + touch.y - touch.oy, 176 | ) 177 | window.add_widget(self) 178 | 179 | # mark the touch so that other widgets can react to this drag 180 | touch_ud['kivyx_drag_cls'] = self.drag_cls 181 | touch_ud['kivyx_draggable'] = self 182 | touch_ud['kivyx_drag_ctx'] = ctx 183 | 184 | # store the task instance so that the user can cancel it later 185 | self._drag_task.cancel() 186 | self._drag_task = await ak.current_task() 187 | 188 | # actual dragging process 189 | self.dispatch('on_drag_start', touch, ctx) 190 | self.drag_state = 'started' 191 | async for __ in ak.rest_of_touch_events(self, touch): 192 | self.x = touch.x - offset_x 193 | self.y = touch.y - offset_y 194 | 195 | # wait for other widgets to react to 'on_touch_up' 196 | await ak.sleep(-1) 197 | 198 | ctx.droppable = droppable = touch_ud.get('kivyx_droppable', None) 199 | if droppable is None or (not droppable.accepts_drag(touch, ctx, self)): 200 | r = self.dispatch('on_drag_fail', touch, ctx) 201 | self.drag_state = 'failed' 202 | else: 203 | r = self.dispatch('on_drag_succeed', touch, ctx) 204 | self.drag_state = 'succeeded' 205 | async with ak.disable_cancellation(): 206 | if isawaitable(r): 207 | await r 208 | await ak.sleep(-1) # This is necessary in order to work with Magnet iirc. 209 | except ak.Cancelled: 210 | self.dispatch('on_drag_cancel', touch, ctx) 211 | self.drag_state = 'cancelled' 212 | raise 213 | finally: 214 | self.dispatch('on_drag_end', touch, ctx) 215 | self.drag_state = None 216 | touch_ud['kivyx_droppable'] = None 217 | del touch_ud['kivyx_drag_cls'] 218 | del touch_ud['kivyx_draggable'] 219 | del touch_ud['kivyx_drag_ctx'] 220 | 221 | async def _simulate_a_normal_touch(self, touch, *, do_transform=False, do_touch_up=False): 222 | # simulate 'on_touch_down' 223 | original = touch.grab_current 224 | try: 225 | touch.grab_current = None 226 | with temp_transform(touch, self.parent.to_widget) if do_transform else nullcontext(): 227 | super().on_touch_down(touch) 228 | finally: 229 | touch.grab_current = original 230 | 231 | if not do_touch_up: 232 | return 233 | await ak.sleep(.1) 234 | 235 | # simulate 'on_touch_up' 236 | to_widget = self.to_widget if self.parent is None else self.parent.to_widget 237 | touch.grab_current = None 238 | with temp_transform(touch, to_widget): 239 | super().on_touch_up(touch) 240 | 241 | # simulate the grabbed one as well 242 | for x in tuple(touch.grab_list): 243 | touch.grab_list.remove(x) 244 | x = x() 245 | if x is None: 246 | continue 247 | touch.grab_current = x 248 | with temp_transform(touch, x.parent.to_widget): 249 | x.dispatch('on_touch_up', touch) 250 | 251 | touch.grab_current = None 252 | return 253 | 254 | def on_drag_start(self, touch, ctx: DragContext): 255 | pass 256 | 257 | def on_drag_end(self, touch, ctx: DragContext): 258 | pass 259 | 260 | def on_drag_succeed(self, touch, ctx: DragContext): 261 | original_state = ctx.original_state 262 | self.parent.remove_widget(self) 263 | self.size_hint_x = original_state['size_hint_x'] 264 | self.size_hint_y = original_state['size_hint_y'] 265 | self.pos_hint = original_state['pos_hint'] 266 | ctx.droppable.add_widget(self, index=touch.ud.get('kivyx_droppable_index', 0)) 267 | 268 | async def on_drag_fail(self, touch, ctx: DragContext): 269 | await ak.anim_attrs( 270 | self, duration=.1, 271 | x=ctx.original_pos_win[0], 272 | y=ctx.original_pos_win[1], 273 | ) 274 | restore_widget_state(self, ctx.original_state) 275 | 276 | def on_drag_cancel(self, touch, ctx: DragContext): 277 | restore_widget_state(self, ctx.original_state) 278 | 279 | 280 | def ongoing_drags(*, window=kivy.core.window.Window) -> List[KXDraggableBehavior]: 281 | '''Returns a list of draggables currently being dragged''' 282 | return [ 283 | c for c in window.children 284 | # maybe it's better not to check the type, like: 285 | # getattr(c, 'is_being_dragged', False) 286 | if isinstance(c, KXDraggableBehavior) and c.is_being_dragged 287 | ] 288 | 289 | 290 | class KXDroppableBehavior: 291 | drag_classes = ListProperty([]) 292 | '''Same as drag_n_drop's ''' 293 | 294 | def on_touch_up(self, touch): 295 | r = super().on_touch_up(touch) 296 | touch_ud = touch.ud 297 | if touch_ud.get('kivyx_drag_cls', None) in self.drag_classes: 298 | if self.collide_point(*touch.pos): 299 | touch_ud.setdefault('kivyx_droppable', self) 300 | return r 301 | 302 | def accepts_drag(self, touch, ctx: DragContext, draggable: KXDraggableBehavior) -> bool: 303 | '''Determines whether the droppable is willing to accept the drag''' 304 | return True 305 | 306 | 307 | class KXReorderableBehavior: 308 | drag_classes = ListProperty([]) 309 | '''Same as drag_n_drop's ''' 310 | 311 | spacer_widgets = ListProperty([]) 312 | '''A list of spacer widgets. The number of them will be the 313 | maximum number of simultaneous drags ``KXReorderableBehavior`` can handle. 314 | 315 | This property can be changed only when there is no ongoing drag. 316 | ''' 317 | 318 | def __init__(self, **kwargs): 319 | self._active_spacers = [] 320 | self._inactive_spacers = None 321 | Clock.schedule_once(self._init_spacers) 322 | super().__init__(**kwargs) 323 | self.__ud_key = 'KXReorderableBehavior.' + str(self.uid) 324 | 325 | def accepts_drag(self, touch, ctx: DragContext, draggable: KXDraggableBehavior) -> bool: 326 | '''Determines whether the reorderable is willing to accept the drag''' 327 | return True 328 | 329 | def _init_spacers(self, dt): 330 | if self._inactive_spacers is None: 331 | self.spacer_widgets.append(_create_spacer()) 332 | 333 | def on_spacer_widgets(self, __, spacer_widgets): 334 | if self._active_spacers: 335 | raise Exception("Do not change the 'spacer_widgets' when there is an ongoing drag.") 336 | self._inactive_spacers = [w.__self__ for w in spacer_widgets] 337 | 338 | def get_widget_under_drag(self, x, y) -> Tuple[Widget, int]: 339 | """Returns a tuple of the widget in children that is under the 340 | given position and its index. Returns (None, None) if there is no 341 | widget under that position. 342 | """ 343 | x, y = self.to_local(x, y) 344 | for index, widget in enumerate(self.children): 345 | if widget.collide_point(x, y): 346 | return (widget, index) 347 | return (None, None) 348 | 349 | def on_touch_move(self, touch): 350 | ud_key = self.__ud_key 351 | touch_ud = touch.ud 352 | if ud_key not in touch_ud and self._inactive_spacers and self.collide_point(*touch.pos): 353 | drag_cls = touch_ud.get('kivyx_drag_cls', None) 354 | if drag_cls is not None: 355 | touch_ud[ud_key] = None 356 | if drag_cls in self.drag_classes: 357 | ak.managed_start(ak.wait_any( 358 | self._place_a_spacer_widget_under_the_drag(touch), 359 | ak.event(touch.ud['kivyx_draggable'], 'on_drag_end'), 360 | )) 361 | return super().on_touch_move(touch) 362 | 363 | async def _place_a_spacer_widget_under_the_drag(self, touch): 364 | spacer = self._inactive_spacers.pop() 365 | self._active_spacers.append(spacer) 366 | 367 | # LOAD_FAST 368 | collide_point = self.collide_point 369 | get_widget_under_drag = self.get_widget_under_drag 370 | remove_widget = self.remove_widget 371 | add_widget = self.add_widget 372 | touch_ud = touch.ud 373 | 374 | try: 375 | restore_widget_state( 376 | spacer, 377 | touch_ud['kivyx_drag_ctx'].original_state, 378 | ignore_parent=True) 379 | add_widget(spacer) 380 | async for __ in ak.rest_of_touch_events(self, touch): 381 | x, y = touch.pos 382 | if collide_point(x, y): 383 | widget, idx = get_widget_under_drag(x, y) 384 | if widget is spacer: 385 | continue 386 | if widget is None: 387 | if self.children: 388 | continue 389 | else: 390 | idx = 0 391 | remove_widget(spacer) 392 | add_widget(spacer, index=idx) 393 | else: 394 | del touch_ud[self.__ud_key] 395 | return 396 | if 'kivyx_droppable' not in touch_ud: 397 | touch_ud['kivyx_droppable'] = self 398 | touch_ud['kivyx_droppable_index'] = self.children.index(spacer) 399 | finally: 400 | self.remove_widget(spacer) 401 | self._inactive_spacers.append(spacer) 402 | self._active_spacers.remove(spacer) 403 | 404 | 405 | r = Factory.register 406 | r('KXDraggableBehavior', cls=KXDraggableBehavior) 407 | r('KXDroppableBehavior', cls=KXDroppableBehavior) 408 | r('KXReorderableBehavior', cls=KXReorderableBehavior) 409 | -------------------------------------------------------------------------------- /src/kivy_garden/draggable/_utils.py: -------------------------------------------------------------------------------- 1 | from copy import deepcopy 2 | 3 | 4 | class temp_transform: 5 | __slots__ = ('_touch', '_func') 6 | 7 | def __init__(self, touch, func): 8 | self._touch = touch 9 | self._func = func 10 | 11 | def __enter__(self): 12 | t = self._touch 13 | t.push() 14 | t.apply_transform_2d(self._func) 15 | 16 | def __exit__(self, *args): 17 | self._touch.pop() 18 | 19 | 20 | _shallow_copyable_property_names = ( 21 | 'x', 'y', 'width', 'height', 22 | 'size_hint_x', 'size_hint_y', 23 | 'size_hint_min_x', 'size_hint_min_y', 24 | 'size_hint_max_x', 'size_hint_max_y', 25 | ) 26 | 27 | 28 | def save_widget_state(widget, *, ignore_parent=False) -> dict: 29 | w = widget.__self__ 30 | getattr_ = getattr 31 | state = {name: getattr_(w, name) for name in _shallow_copyable_property_names} 32 | state['pos_hint'] = deepcopy(w.pos_hint) 33 | if ignore_parent: 34 | return state 35 | parent = w.parent 36 | state['parent'] = parent 37 | if parent is not None: 38 | state['index'] = parent.children.index(w) 39 | return state 40 | 41 | 42 | def restore_widget_state(widget, state: dict, *, ignore_parent=False): 43 | w = widget.__self__ 44 | setattr_ = setattr 45 | for name in _shallow_copyable_property_names: 46 | setattr_(w, name, state[name]) 47 | w.pos_hint = deepcopy(state['pos_hint']) 48 | if ignore_parent or 'parent' not in state: 49 | return 50 | if w.parent is not None: 51 | w.parent.remove_widget(w) 52 | parent = state['parent'] 53 | if parent is None: 54 | return 55 | 56 | # The 'Window.add_widget()' doesn't have the 'index' parameter so we need to check whether the 'parent' is Window 57 | # or not. The way to do it here is unreliable, and might not work in the future. 58 | if parent.parent is parent: 59 | parent.add_widget(w) 60 | else: 61 | parent.add_widget(w, index=state['index']) 62 | 63 | 64 | def _create_spacer(**kwargs): 65 | '''(internal)''' 66 | from kivy.uix.widget import Widget 67 | from kivy.utils import rgba 68 | from kivy.graphics import Color, Rectangle 69 | spacer = Widget(size_hint_min=('50dp', '50dp')) 70 | with spacer.canvas: 71 | color = kwargs.get('color', None) 72 | if color is None: 73 | Color(.2, .2, .2, .7) 74 | else: 75 | Color(*rgba(color)) 76 | rect_inst = Rectangle(size=spacer.size) 77 | spacer.bind( 78 | pos=lambda __, value: setattr(rect_inst, 'pos', value), 79 | size=lambda __, value: setattr(rect_inst, 'size', value), 80 | ) 81 | return spacer 82 | 83 | 84 | # leave the old names for backward compatibility 85 | save_widget_location = save_widget_state 86 | restore_widget_location = restore_widget_state 87 | -------------------------------------------------------------------------------- /tests/test_import.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_flower(): 5 | from kivy_garden.draggable import ( 6 | DragContext, 7 | KXDraggableBehavior, KXDroppableBehavior, KXReorderableBehavior, 8 | restore_widget_state, save_widget_state, 9 | restore_widget_location, save_widget_location, ongoing_drags, 10 | ) 11 | -------------------------------------------------------------------------------- /tests/test_utils_restore_widget_state.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture(scope='module') 5 | def state_factory(): 6 | from copy import deepcopy 7 | state = { 8 | 'x': 2, 'y': 2, 'width': 2, 'height': 2, 9 | 'size_hint_x': 2, 'size_hint_y': 2, 10 | 'pos_hint': {'center': [2, 2, ], }, 11 | 'size_hint_min_x': 2, 'size_hint_min_y': 2, 12 | 'size_hint_max_x': 2, 'size_hint_max_y': 2, 13 | } 14 | return lambda: deepcopy(state) 15 | 16 | 17 | @pytest.mark.parametrize('ignore_parent', (True, False, )) 18 | def test_sizing_info(state_factory, ignore_parent): 19 | from kivy.uix.widget import Widget 20 | from kivy_garden.draggable import restore_widget_state 21 | w = Widget() 22 | state = state_factory() 23 | restore_widget_state(w, state, ignore_parent=ignore_parent) 24 | state['width'] = 0 25 | state['x'] = 0 26 | state['size_hint_x'] = 0 27 | state['pos_hint']['center'][0] = 0 28 | state['size_hint_min_x'] = 0 29 | state['size_hint_min_y'] = 0 30 | assert w.size == [2, 2, ] 31 | assert w.pos == [2, 2, ] 32 | assert w.size_hint == [2, 2, ] 33 | assert w.pos_hint == {'center': [2, 2, ], } 34 | assert w.size_hint_min == [2, 2, ] 35 | assert w.size_hint_max == [2, 2, ] 36 | 37 | 38 | @pytest.mark.parametrize('ignore_parent', (True, False, )) 39 | @pytest.mark.parametrize('has_parent', (True, False, )) 40 | def test_parent_is_none(state_factory, ignore_parent, has_parent): 41 | from kivy.uix.widget import Widget 42 | from kivy_garden.draggable import restore_widget_state 43 | state = state_factory() 44 | state['parent'] = None 45 | w = Widget() 46 | if has_parent: 47 | parent = Widget() 48 | parent.add_widget(w) 49 | restore_widget_state(w, state, ignore_parent=ignore_parent) 50 | if ignore_parent and has_parent: 51 | assert w.parent is parent 52 | else: 53 | assert w.parent is None 54 | 55 | 56 | @pytest.mark.parametrize('ignore_parent', (True, False, )) 57 | @pytest.mark.parametrize('has_parent', (True, False, )) 58 | def test_parent_not_in_the_dict(state_factory, ignore_parent, has_parent): 59 | from kivy.uix.widget import Widget 60 | from kivy_garden.draggable import restore_widget_state 61 | state = state_factory() 62 | assert 'parent' not in state 63 | w = Widget() 64 | if has_parent: 65 | parent = Widget() 66 | parent.add_widget(w) 67 | restore_widget_state(w, state, ignore_parent=ignore_parent) 68 | if has_parent: 69 | assert w.parent is parent 70 | else: 71 | assert w.parent is None 72 | 73 | 74 | @pytest.mark.parametrize('ignore_parent', (True, False, )) 75 | @pytest.mark.parametrize('has_parent', (True, False, )) 76 | def test_parent_is_not_none(state_factory, ignore_parent, has_parent): 77 | from kivy.uix.widget import Widget 78 | from kivy_garden.draggable import restore_widget_state 79 | prev_parent = Widget() 80 | state = state_factory() 81 | state['parent'] = prev_parent 82 | state['index'] = 0 83 | w = Widget() 84 | if has_parent: 85 | parent = Widget() 86 | parent.add_widget(w) 87 | parent.add_widget(Widget()) 88 | assert parent.children.index(w) == 1 89 | restore_widget_state(w, state, ignore_parent=ignore_parent) 90 | if ignore_parent: 91 | if has_parent: 92 | assert w.parent is parent 93 | assert parent.children.index(w) == 1 94 | else: 95 | assert w.parent is None 96 | else: 97 | assert w.parent is prev_parent 98 | assert prev_parent.children.index(w) == 0 99 | -------------------------------------------------------------------------------- /tests/test_utils_save_widget_state.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.parametrize('ignore_parent', (True, False, )) 5 | def test_no_parent(ignore_parent): 6 | from kivy.uix.widget import Widget 7 | from kivy_garden.draggable import save_widget_state 8 | w = Widget() 9 | state = save_widget_state(w, ignore_parent=ignore_parent) 10 | expectation = { 11 | 'x': 0, 'y': 0, 'width': 100, 'height': 100, 12 | 'size_hint_x': 1, 'size_hint_y': 1, 'pos_hint': {}, 13 | 'size_hint_min_x': None, 'size_hint_min_y': None, 14 | 'size_hint_max_x': None, 'size_hint_max_y': None, 15 | 'parent': None, 16 | } 17 | if ignore_parent: 18 | del expectation['parent'] 19 | assert state == expectation 20 | 21 | 22 | @pytest.mark.parametrize('ignore_parent', (True, False, )) 23 | def test_has_parent(ignore_parent): 24 | from kivy.uix.widget import Widget 25 | from kivy_garden.draggable import save_widget_state 26 | parent = Widget() 27 | w = Widget() 28 | parent.add_widget(w) 29 | parent.add_widget(Widget()) 30 | state = save_widget_state(w, ignore_parent=ignore_parent) 31 | expectation = { 32 | 'x': 0, 'y': 0, 'width': 100, 'height': 100, 33 | 'size_hint_x': 1, 'size_hint_y': 1, 'pos_hint': {}, 34 | 'size_hint_min_x': None, 'size_hint_min_y': None, 35 | 'size_hint_max_x': None, 'size_hint_max_y': None, 36 | 'parent': parent, 'index': 1, 37 | } 38 | if ignore_parent: 39 | del expectation['parent'] 40 | del expectation['index'] 41 | assert state == expectation 42 | 43 | 44 | @pytest.mark.parametrize('ignore_parent', (True, False, )) 45 | def test_pos_hint_is_deepcopied(ignore_parent): 46 | from kivy.uix.widget import Widget 47 | from kivy_garden.draggable import save_widget_state 48 | w = Widget(pos_hint={'center': [.5, .5, ], }) 49 | state = save_widget_state(w, ignore_parent=ignore_parent) 50 | state['pos_hint']['center'][0] = 0 51 | state['pos_hint']['x'] = 0 52 | assert w.pos_hint == {'center': [.5, .5, ], } 53 | -------------------------------------------------------------------------------- /tools/hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | # An example hook script to verify what is about to be committed. 4 | # Called by "git commit" with no arguments. The hook should 5 | # exit with non-zero status after issuing an appropriate message if 6 | # it wants to stop the commit. 7 | # 8 | # To enable this hook, rename this file to "pre-commit". 9 | 10 | if git rev-parse --verify HEAD >/dev/null 2>&1 11 | then 12 | against=HEAD 13 | else 14 | # Initial commit: diff against an empty tree object 15 | against=4b825dc642cb6eb9a060e54bf8d69288fbee4904 16 | fi 17 | 18 | # If you want to allow non-ASCII filenames set this variable to true. 19 | allownonascii=$(git config --bool hooks.allownonascii) 20 | 21 | # Redirect output to stderr. 22 | exec 1>&2 23 | 24 | # Cross platform projects tend to avoid non-ASCII filenames; prevent 25 | # them from being added to the repository. We exploit the fact that the 26 | # printable range starts at the space character and ends with tilde. 27 | if [ "$allownonascii" != "true" ] && 28 | # Note that the use of brackets around a tr range is ok here, (it's 29 | # even required, for portability to Solaris 10's /usr/bin/tr), since 30 | # the square bracket bytes happen to fall in the designated range. 31 | test $(git diff --cached --name-only --diff-filter=A -z $against | 32 | LC_ALL=C tr -d '[ -~]\0' | wc -c) != 0 33 | then 34 | cat <<\EOF 35 | Error: Attempt to add a non-ASCII file name. 36 | 37 | This can cause problems if you want to work with people on other platforms. 38 | 39 | To be portable it is advisable to rename the file. 40 | 41 | If you know what you are doing you can disable this check using: 42 | 43 | git config hooks.allownonascii true 44 | EOF 45 | exit 1 46 | fi 47 | 48 | # If there are whitespace errors, print the offending file names and fail. 49 | # exec git diff-index --check --cached $against -- 50 | 51 | git stash save --keep-index --quiet 52 | pytest && pycodestyle && pydocstyle 53 | err=$? 54 | git stash pop --quiet 55 | exit $err 56 | --------------------------------------------------------------------------------