Kivy-examples 2.3.1.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/3Drendering/main.py +77 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/3Drendering/monkey.obj +7966 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/3Drendering/objloader.py +147 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/3Drendering/simple.glsl +47 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/RST_Editor/editor.kv +72 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/RST_Editor/main.py +64 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/android/compass/android.txt +3 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/android/compass/compass.kv +25 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/android/compass/main.py +77 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/android/compass/needle.png +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/android/compass/rose.png +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/android/takepicture/android.txt +3 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/android/takepicture/main.py +80 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/android/takepicture/shadow32.png +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/android/takepicture/takepicture.kv +40 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/animation/animate.py +42 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/application/app_suite.py +152 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/application/app_suite_data/testkvdir.kv +4 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/application/app_with_build.py +24 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/application/app_with_config.py +49 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/application/app_with_kv.py +25 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/application/app_with_kv_in_template1.py +27 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/application/template1/test.kv +4 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/application/test.kv +4 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/application/testkvfile.kv +4 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/async/asyncio_advanced.py +90 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/async/asyncio_basic.py +59 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/async/trio_advanced.py +93 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/async/trio_basic.py +58 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/audio/12908_sweet_trip_mm_clap_hi.wav +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/audio/12909_sweet_trip_mm_clap_lo.wav +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/audio/12910_sweet_trip_mm_clap_mid.wav +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/audio/12911_sweet_trip_mm_hat_cl.wav +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/audio/12913_sweet_trip_mm_kick_hi.wav +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/audio/12914_sweet_trip_mm_kick_lo.wav +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/audio/12915_sweet_trip_mm_kick_mid.wav +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/audio/12916_sweet_trip_mm_kwik_mod_01.wav +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/audio/12917_sweet_trip_mm_kwik_mod_02.wav +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/audio/12918_sweet_trip_mm_kwik_mod_03.wav +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/audio/12919_sweet_trip_mm_kwik_mod_04.wav +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/audio/12920_sweet_trip_mm_kwik_mod_05.wav +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/audio/12921_sweet_trip_mm_kwik_mod_06.wav +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/audio/12922_sweet_trip_mm_kwik_mod_07.wav +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/audio/12923_sweet_trip_mm_metal_clave.wav +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/audio/12925_sweet_trip_mm_sweep_x.wav +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/audio/12926_sweet_trip_mm_sweep_y.wav +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/audio/12927_sweet_trip_mm_sweep_z.wav +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/audio/audio.kv +59 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/audio/main.py +81 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/audio/pitch.py +42 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/camera/main.py +59 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/canvas/bezier.py +105 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/canvas/canvas_stress.py +72 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/canvas/circle.py +84 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/canvas/fbo_canvas.py +91 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/canvas/kiwi.jpg +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/canvas/lines.py +296 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/canvas/lines_extended.py +183 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/canvas/mesh.py +57 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/canvas/mesh_manipulation.py +101 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/canvas/mtexture1.png +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/canvas/mtexture2.png +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/canvas/multitexture.py +110 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/canvas/repeat_texture.py +65 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/canvas/rotation.py +35 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/canvas/rounded_rectangle.py +153 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/canvas/scale.py +37 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/canvas/stencil_canvas.py +85 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/canvas/tesselate.py +146 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/canvas/texture.py +143 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/canvas/texture_example_image.png +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/container/kv/1.kv +28 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/container/kv/2.kv +28 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/container/kv/3.kv +28 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/container/kv/root.kv +32 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/container/main.py +63 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/cover/cover_image.py +25 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/cover/cover_video.py +31 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/camera_puzzle.py +98 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/kivycatalog/container_kvs/AnchorLayoutContainer.kv +15 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/kivycatalog/container_kvs/BoxLayoutContainer.kv +14 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/kivycatalog/container_kvs/ButtonContainer.kv +27 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/kivycatalog/container_kvs/CheckBoxContainer.kv +28 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/kivycatalog/container_kvs/FileChooserContainer.kv +19 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/kivycatalog/container_kvs/FloatLayoutContainer.kv +16 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/kivycatalog/container_kvs/GridLayoutContainer.kv +20 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/kivycatalog/container_kvs/LabelContainer.kv +62 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/kivycatalog/container_kvs/MediaContainer.kv +9 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/kivycatalog/container_kvs/PlaygroundContainer.kv +4 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/kivycatalog/container_kvs/PopupContainer.kv +39 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/kivycatalog/container_kvs/ProgressBarContainer.kv +17 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/kivycatalog/container_kvs/RestContainer.kv +5 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/kivycatalog/container_kvs/ScatterContainer.kv +16 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/kivycatalog/container_kvs/SelectorsContainer.kv +10 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/kivycatalog/container_kvs/StackLayoutContainer.kv +25 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/kivycatalog/container_kvs/TextContainer.kv +20 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/kivycatalog/kivycatalog.kv +144 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/kivycatalog/main.py +192 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/multistroke/gesturedatabase.kv +117 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/multistroke/gesturedatabase.py +180 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/multistroke/helpers.py +30 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/multistroke/historymanager.kv +164 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/multistroke/historymanager.py +276 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/multistroke/main.py +144 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/multistroke/multistroke.kv +101 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/multistroke/settings.kv +110 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/multistroke/settings.py +97 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/pictures/android.txt +3 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/pictures/images/Bubbles.jpg +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/pictures/images/Ill1.jpg +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/pictures/images/Wall.jpg +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/pictures/images/faust_github.jpg +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/pictures/main.py +73 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/pictures/pictures.kv +57 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/pictures/shadow32.png +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/shadereditor/main.py +147 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/shadereditor/shadereditor.kv +39 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/showcase/README.txt +11 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/showcase/android.txt +3 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/showcase/data/background.png +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/showcase/data/faust_github.jpg +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/showcase/data/icons/README +4 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/showcase/data/icons/bug.png +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/showcase/data/icons/chevron-left.png +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/showcase/data/icons/chevron-right.png +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/showcase/data/screens/accordions.kv +39 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/showcase/data/screens/bubbles.kv +33 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/showcase/data/screens/buttons.kv +26 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/showcase/data/screens/carousel.kv +51 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/showcase/data/screens/checkboxes.kv +33 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/showcase/data/screens/codeinput.kv +9 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/showcase/data/screens/dropdown.kv +40 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/showcase/data/screens/filechoosers.kv +24 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/showcase/data/screens/popups.kv +17 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/showcase/data/screens/progressbar.kv +15 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/showcase/data/screens/rstdocument.kv +24 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/showcase/data/screens/scatter.kv +14 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/showcase/data/screens/screenmanager.kv +46 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/showcase/data/screens/sliders.kv +55 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/showcase/data/screens/spinner.kv +11 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/showcase/data/screens/splitter.kv +13 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/showcase/data/screens/switches.kv +39 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/showcase/data/screens/tabbedpanel + layouts.kv +69 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/showcase/data/screens/textinputs.kv +48 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/showcase/data/screens/togglebutton.kv +39 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/showcase/main.py +243 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/showcase/showcase.kv +100 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/touchtracer/README.txt +12 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/touchtracer/android.txt +3 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/touchtracer/icon.png +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/touchtracer/main.py +170 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/touchtracer/particle.png +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/demo/touchtracer/touchtracer.kv +29 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/frameworks/twisted/echo_client_app.py +82 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/frameworks/twisted/echo_server_app.py +49 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/frameworks/twisted/twistd_app.py +66 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/gestures/gesture_board.py +95 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/gestures/my_gestures.py +90 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/guide/designwithkv/controller.kv +16 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/guide/designwithkv/main.py +29 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/guide/firstwidget/1_skeleton.py +15 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/guide/firstwidget/2_print_touch.py +16 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/guide/firstwidget/3_draw_ellipse.py +22 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/guide/firstwidget/4_draw_line.py +26 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/guide/firstwidget/5_random_colors.py +28 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/guide/firstwidget/6_button.py +38 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/guide/quickstart/main.py +15 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/includes/button.kv +9 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/includes/layout.kv +6 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/includes/main.py +19 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/includes/test.kv +5 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/keyboard/android.txt +3 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/keyboard/main.py +226 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/keyboard/numeric.json +38 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/kinect/README.txt +13 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/kinect/kinectviewer.py +283 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/kv/app_button.kv +9 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/kv/app_camera.kv +25 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/kv/app_fbo.kv +11 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/kv/app_layout.kv +23 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/kv/app_logo.kv +36 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/kv/app_scatter.kv +6 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/kv/app_stencil.kv +27 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/kv/app_video.kv +42 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/kv/builder_template.py +28 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/kv/ids/id_in_kv/id_in_kv.py +20 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/kv/ids/id_in_kv/test.kv +11 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/kv/ids/kv_and_py/kv_and_py.py +34 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/kv/ids/kv_and_py/test.kv +11 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/kv/kivy.jpg +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/kv/kvrun.py +38 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/miscellaneous/clipboard.py +63 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/miscellaneous/custom_titlebar.py +56 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/miscellaneous/imagesave.py +14 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/miscellaneous/joystick.py +83 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/miscellaneous/multiple_dropfile.py +51 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/miscellaneous/on_textedit_event.py +104 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/miscellaneous/opacitywindow.py +28 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/miscellaneous/shapecollisions.py +397 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/miscellaneous/shapedwindow.py +98 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/miscellaneous/two_panes.py +47 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/miscellaneous/urlrequest.py +128 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/settings/android.txt +3 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/settings/main.py +122 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/shader/plasma.kv +9 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/shader/plasma.py +86 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/shader/rotated.kv +91 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/shader/rotated.py +102 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/shader/shadertree.kv +29 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/shader/shadertree.py +193 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/shader/tex3.jpg +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/svg/benchmark.py +19 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/svg/cloud.svg +25 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/svg/main-smaa.py +155 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/svg/main.py +50 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/svg/music.svg +68 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/svg/rotated.svg +23 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/svg/ship.svg +181 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/svg/sun.svg +35 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/svg/tiger.svg +813 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/text/pango_demo.py +296 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/tutorials/notes/final/data/icon.png +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/tutorials/notes/final/main.py +163 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/tutorials/notes/final/note.kv +154 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/tutorials/pong/main.py +82 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/tutorials/pong/pong.kv +53 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/tutorials/pong/steps/step1/main.py +15 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/tutorials/pong/steps/step1/pong.kv +2 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/tutorials/pong/steps/step2/main.py +15 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/tutorials/pong/steps/step2/pong.kv +21 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/tutorials/pong/steps/step3/main.py +26 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/tutorials/pong/steps/step3/pong.kv +31 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/tutorials/pong/steps/step4/main.py +48 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/tutorials/pong/steps/step4/pong.kv +34 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/tutorials/pong/steps/step5/main.py +75 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/tutorials/pong/steps/step5/pong.kv +53 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/accordion_1.py +17 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/actionbar.py +32 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/asyncimage.py +35 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/boxlayout_poshint.py +54 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/bubble_test.py +63 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/camera.py +33 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/carousel_buttons.py +50 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/cityCC0.mpg +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/cityCC0.png +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/codeinput.py +225 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/codeinputtest.kv +53 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/colorpicker.py +234 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/colorusage.py +62 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/compound_selection.py +83 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/customcollide.py +82 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/effectwidget.py +193 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/effectwidget2.py +47 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/effectwidget3_advanced.py +62 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/fbowidget.py +111 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/focus_behavior.py +98 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/image_mipmap.py +31 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/keyboardlistener.py +42 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/label_mipmap.py +28 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/label_sizing.py +193 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/label_text_size.py +49 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/label_with_markup.py +21 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/lang_dynamic_classes.py +18 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/pagelayout.py +64 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/popup_with_kv.py +35 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/recycleview/basic_data.py +124 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/recycleview/infinite_scrolling.py +143 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/recycleview/key_viewclass.py +149 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/recycleview/messenger.py +196 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/recycleview/pull_to_refresh.py +95 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/recycleview/rv_animate_items.py +109 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/rstexample.py +128 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/scatter.kv +53 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/scatter.py +16 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/screenmanager.py +106 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/scrollview.kv +16 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/scrollview.py +40 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/sequenced_images/android.txt +3 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/sequenced_images/data/images/bird.zip +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/sequenced_images/data/images/button_white.png +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/sequenced_images/data/images/button_white_animated.zip +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/sequenced_images/data/images/cube.zip +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/sequenced_images/data/images/info.png +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/sequenced_images/data/images/info.zip +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/sequenced_images/data/images/simple_cv_joint_animated.gif +0 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/sequenced_images/main.kv +63 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/sequenced_images/main.py +150 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/sequenced_images/uix/__init__.py +7 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/sequenced_images/uix/custom_button.py +98 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/settings.py +93 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/shorten_text.py +80 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/spinner.py +17 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/splitter.py +74 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/tabbed_panel_showcase.py +316 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/tabbedpanel.py +50 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/textalign.kv +49 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/textalign.py +48 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/textinput.py +59 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/unicode_textinput.py +244 -0
- Kivy_examples-2.3.1.dev0.data/data/share/kivy-examples/widgets/videoplayer.py +26 -0
- Kivy_examples-2.3.1.dev0.dist-info/AUTHORS +66 -0
- Kivy_examples-2.3.1.dev0.dist-info/LICENSE +21 -0
- Kivy_examples-2.3.1.dev0.dist-info/METADATA +382 -0
- Kivy_examples-2.3.1.dev0.dist-info/RECORD +624 -0
- Kivy_examples-2.3.1.dev0.dist-info/WHEEL +5 -0
- Kivy_examples-2.3.1.dev0.dist-info/dependency_links.txt +1 -0
- Kivy_examples-2.3.1.dev0.dist-info/top_level.txt +1 -0
- kivy/__init__.py +519 -0
- kivy/_version.py +18 -0
- kivy/animation.py +831 -0
- kivy/app.py +1213 -0
- kivy/atlas.py +456 -0
- kivy/base.py +617 -0
- kivy/cache.py +262 -0
- kivy/clock.py +1172 -0
- kivy/compat.py +82 -0
- kivy/config.py +1004 -0
- kivy/context.py +102 -0
- kivy/core/__init__.py +247 -0
- kivy/core/audio/__init__.py +223 -0
- kivy/core/audio/audio_android.py +104 -0
- kivy/core/audio/audio_avplayer.py +82 -0
- kivy/core/audio/audio_ffpyplayer.py +185 -0
- kivy/core/audio/audio_gstplayer.py +101 -0
- kivy/core/audio/audio_pygame.py +127 -0
- kivy/core/camera/__init__.py +151 -0
- kivy/core/camera/camera_android.py +206 -0
- kivy/core/camera/camera_gi.py +170 -0
- kivy/core/camera/camera_opencv.py +163 -0
- kivy/core/camera/camera_picamera.py +96 -0
- kivy/core/clipboard/__init__.py +157 -0
- kivy/core/clipboard/_clipboard_ext.py +36 -0
- kivy/core/clipboard/clipboard_android.py +91 -0
- kivy/core/clipboard/clipboard_dbusklipper.py +41 -0
- kivy/core/clipboard/clipboard_dummy.py +26 -0
- kivy/core/clipboard/clipboard_gtk3.py +47 -0
- kivy/core/clipboard/clipboard_nspaste.py +44 -0
- kivy/core/clipboard/clipboard_pygame.py +67 -0
- kivy/core/clipboard/clipboard_sdl2.py +36 -0
- kivy/core/clipboard/clipboard_winctypes.py +107 -0
- kivy/core/clipboard/clipboard_xclip.py +29 -0
- kivy/core/clipboard/clipboard_xsel.py +26 -0
- kivy/core/gl/__init__.py +90 -0
- kivy/core/image/__init__.py +996 -0
- kivy/core/image/img_dds.py +40 -0
- kivy/core/image/img_ffpyplayer.py +87 -0
- kivy/core/image/img_pil.py +123 -0
- kivy/core/image/img_pygame.py +119 -0
- kivy/core/image/img_sdl2.py +66 -0
- kivy/core/image/img_tex.py +58 -0
- kivy/core/spelling/__init__.py +135 -0
- kivy/core/spelling/spelling_enchant.py +50 -0
- kivy/core/spelling/spelling_osxappkit.py +64 -0
- kivy/core/text/__init__.py +1090 -0
- kivy/core/text/markup.py +893 -0
- kivy/core/text/text_pango.py +145 -0
- kivy/core/text/text_pil.py +84 -0
- kivy/core/text/text_pygame.py +117 -0
- kivy/core/text/text_sdl2.py +50 -0
- kivy/core/video/__init__.py +219 -0
- kivy/core/video/video_ffmpeg.py +106 -0
- kivy/core/video/video_ffpyplayer.py +449 -0
- kivy/core/video/video_gstplayer.py +140 -0
- kivy/core/video/video_null.py +12 -0
- kivy/core/window/__init__.py +2515 -0
- kivy/core/window/window_egl_rpi.py +86 -0
- kivy/core/window/window_pygame.py +441 -0
- kivy/core/window/window_sdl2.py +999 -0
- kivy/deps/__init__.py +6 -0
- kivy/effects/__init__.py +23 -0
- kivy/effects/dampedscroll.py +114 -0
- kivy/effects/kinetic.py +193 -0
- kivy/effects/opacityscroll.py +27 -0
- kivy/effects/scroll.py +126 -0
- kivy/event.py +12 -0
- kivy/eventmanager/__init__.py +185 -0
- kivy/extras/__init__.py +0 -0
- kivy/extras/highlight.py +52 -0
- kivy/factory.py +190 -0
- kivy/factory_registers.py +201 -0
- kivy/garden/__init__.py +186 -0
- kivy/geometry.py +121 -0
- kivy/gesture.py +424 -0
- kivy/graphics/__init__.py +126 -0
- kivy/graphics/cgl_backend/__init__.py +0 -0
- kivy/input/__init__.py +39 -0
- kivy/input/factory.py +35 -0
- kivy/input/motionevent.py +587 -0
- kivy/input/postproc/__init__.py +27 -0
- kivy/input/postproc/calibration.py +200 -0
- kivy/input/postproc/dejitter.py +74 -0
- kivy/input/postproc/doubletap.py +101 -0
- kivy/input/postproc/ignorelist.py +47 -0
- kivy/input/postproc/retaintouch.py +93 -0
- kivy/input/postproc/tripletap.py +106 -0
- kivy/input/provider.py +40 -0
- kivy/input/providers/__init__.py +68 -0
- kivy/input/providers/androidjoystick.py +117 -0
- kivy/input/providers/hidinput.py +778 -0
- kivy/input/providers/leapfinger.py +113 -0
- kivy/input/providers/linuxwacom.py +396 -0
- kivy/input/providers/mactouch.py +220 -0
- kivy/input/providers/mouse.py +424 -0
- kivy/input/providers/mtdev.py +380 -0
- kivy/input/providers/probesysfs.py +254 -0
- kivy/input/providers/tuio.py +326 -0
- kivy/input/providers/wm_common.py +162 -0
- kivy/input/providers/wm_pen.py +121 -0
- kivy/input/providers/wm_touch.py +157 -0
- kivy/input/recorder.py +335 -0
- kivy/input/shape.py +27 -0
- kivy/interactive.py +335 -0
- kivy/lang/__init__.py +883 -0
- kivy/lang/builder.py +1004 -0
- kivy/lang/parser.py +822 -0
- kivy/lib/__init__.py +21 -0
- kivy/lib/ddsfile.py +419 -0
- kivy/lib/gstplayer/__init__.py +27 -0
- kivy/lib/mtdev.py +254 -0
- kivy/lib/vidcore_lite/__init__.py +0 -0
- kivy/loader.py +579 -0
- kivy/logger.py +646 -0
- kivy/metrics.py +421 -0
- kivy/modules/__init__.py +300 -0
- kivy/modules/_webdebugger.py +200 -0
- kivy/modules/console.py +1059 -0
- kivy/modules/cursor.py +75 -0
- kivy/modules/inspector.py +755 -0
- kivy/modules/joycursor.py +299 -0
- kivy/modules/keybinding.py +64 -0
- kivy/modules/monitor.py +90 -0
- kivy/modules/recorder.py +94 -0
- kivy/modules/screen.py +183 -0
- kivy/modules/showborder.py +33 -0
- kivy/modules/touchring.py +97 -0
- kivy/modules/webdebugger.py +30 -0
- kivy/multistroke.py +1477 -0
- kivy/network/__init__.py +7 -0
- kivy/network/urlrequest.py +794 -0
- kivy/parser.py +154 -0
- kivy/resources.py +102 -0
- kivy/storage/__init__.py +405 -0
- kivy/storage/dictstore.py +98 -0
- kivy/storage/jsonstore.py +92 -0
- kivy/storage/redisstore.py +103 -0
- kivy/support.py +267 -0
- kivy/tests/__init__.py +11 -0
- kivy/tests/async_common.py +568 -0
- kivy/tests/common.py +524 -0
- kivy/tests/conftest.py +33 -0
- kivy/tests/fixtures.py +169 -0
- kivy/tests/perf_test_textinput.py +195 -0
- kivy/tests/pyinstaller/simple_widget/main.py +12 -0
- kivy/tests/pyinstaller/simple_widget/project/__init__.py +0 -0
- kivy/tests/pyinstaller/simple_widget/project/widget.py +12 -0
- kivy/tests/pyinstaller/test_pyinstaller.py +115 -0
- kivy/tests/pyinstaller/video_widget/main.py +6 -0
- kivy/tests/pyinstaller/video_widget/project/__init__.py +38 -0
- kivy/tests/test_animations.py +415 -0
- kivy/tests/test_app.py +216 -0
- kivy/tests/test_audio.py +69 -0
- kivy/tests/test_benchmark.py +292 -0
- kivy/tests/test_clipboard.py +48 -0
- kivy/tests/test_clock.py +292 -0
- kivy/tests/test_compat.py +6 -0
- kivy/tests/test_config.py +35 -0
- kivy/tests/test_coverage.py +83 -0
- kivy/tests/test_doc_gallery.py +38 -0
- kivy/tests/test_environ_cli.py +50 -0
- kivy/tests/test_fbo_py2py3.py +74 -0
- kivy/tests/test_filechooser.py +11 -0
- kivy/tests/test_filechooser_unicode.py +82 -0
- kivy/tests/test_fonts.py +42 -0
- kivy/tests/test_garden.py +23 -0
- kivy/tests/test_graphics.py +1149 -0
- kivy/tests/test_graphics_svg.py +84 -0
- kivy/tests/test_image.py +74 -0
- kivy/tests/test_imageloader.py +485 -0
- kivy/tests/test_invalid_lang.py +42 -0
- kivy/tests/test_issues/test_6315.py +55 -0
- kivy/tests/test_issues/test_issue_1084.py +47 -0
- kivy/tests/test_issues/test_issue_1091.py +19 -0
- kivy/tests/test_issues/test_issue_599.py +21 -0
- kivy/tests/test_issues/test_issue_609.py +19 -0
- kivy/tests/test_issues/test_issue_6909.py +28 -0
- kivy/tests/test_issues/test_issue_883.py +12 -0
- kivy/tests/test_kivy_init.py +30 -0
- kivy/tests/test_knspace.py +247 -0
- kivy/tests/test_lang.py +431 -0
- kivy/tests/test_lang_complex.py +87 -0
- kivy/tests/test_lang_pre_process_and_post_process.py +394 -0
- kivy/tests/test_logger.py +516 -0
- kivy/tests/test_metrics.py +30 -0
- kivy/tests/test_module_inspector.py +382 -0
- kivy/tests/test_motion_event.py +44 -0
- kivy/tests/test_mouse_hover_event.py +249 -0
- kivy/tests/test_mouse_multitouchsim.py +554 -0
- kivy/tests/test_multistroke.py +389 -0
- kivy/tests/test_properties.py +1332 -0
- kivy/tests/test_resources.py +79 -0
- kivy/tests/test_rst_replace.py +63 -0
- kivy/tests/test_screen.py +38 -0
- kivy/tests/test_storage.py +135 -0
- kivy/tests/test_uix_actionbar.py +363 -0
- kivy/tests/test_uix_anchorlayout.py +79 -0
- kivy/tests/test_uix_asyncimage.py +133 -0
- kivy/tests/test_uix_boxlayout.py +94 -0
- kivy/tests/test_uix_bubble.py +425 -0
- kivy/tests/test_uix_carousel.py +93 -0
- kivy/tests/test_uix_colorpicker.py +77 -0
- kivy/tests/test_uix_dropdown.py +91 -0
- kivy/tests/test_uix_gridlayout.py +244 -0
- kivy/tests/test_uix_layout.py +16 -0
- kivy/tests/test_uix_modal.py +101 -0
- kivy/tests/test_uix_recyclegridlayout.py +383 -0
- kivy/tests/test_uix_relativelayout.py +30 -0
- kivy/tests/test_uix_scrollview.py +358 -0
- kivy/tests/test_uix_settings.py +33 -0
- kivy/tests/test_uix_slider.py +156 -0
- kivy/tests/test_uix_stacklayout.py +120 -0
- kivy/tests/test_uix_textinput.py +577 -0
- kivy/tests/test_uix_translate_coordinates.py +72 -0
- kivy/tests/test_uix_videoplayer.py +112 -0
- kivy/tests/test_uix_widget.py +137 -0
- kivy/tests/test_urlrequest/test_urlrequest_requests.py +378 -0
- kivy/tests/test_urlrequest/test_urlrequest_urllib.py +155 -0
- kivy/tests/test_utils.py +237 -0
- kivy/tests/test_vector.py +256 -0
- kivy/tests/test_video.py +25 -0
- kivy/tests/test_weakmethod.py +44 -0
- kivy/tests/test_widget.py +127 -0
- kivy/tests/test_widget_walk.py +70 -0
- kivy/tests/test_window_base.py +60 -0
- kivy/tests/test_window_info.py +29 -0
- kivy/tests/visual_test_label.py +291 -0
- kivy/tools/__init__.py +44 -0
- kivy/tools/benchmark.py +286 -0
- kivy/tools/changelog_parser.py +120 -0
- kivy/tools/coverage.py +135 -0
- kivy/tools/gallery.py +323 -0
- kivy/tools/generate-icons.py +117 -0
- kivy/tools/gles_compat/subset_gles.py +125 -0
- kivy/tools/highlight/__init__.py +9 -0
- kivy/tools/kviewer.py +80 -0
- kivy/tools/packaging/__init__.py +12 -0
- kivy/tools/packaging/cython_cfg.py +82 -0
- kivy/tools/packaging/factory.py +105 -0
- kivy/tools/packaging/pyinstaller_hooks/__init__.py +381 -0
- kivy/tools/packaging/pyinstaller_hooks/__main__.py +29 -0
- kivy/tools/packaging/pyinstaller_hooks/hook-kivy.py +9 -0
- kivy/tools/packaging/pyinstaller_hooks/pyi_rth_kivy.py +17 -0
- kivy/tools/pep8checker/pep8.py +2316 -0
- kivy/tools/pep8checker/pep8kivy.py +109 -0
- kivy/tools/report.py +223 -0
- kivy/tools/stub-gl-debug.py +201 -0
- kivy/tools/texturecompress.py +233 -0
- kivy/uix/__init__.py +56 -0
- kivy/uix/accordion.py +484 -0
- kivy/uix/actionbar.py +934 -0
- kivy/uix/anchorlayout.py +122 -0
- kivy/uix/behaviors/__init__.py +95 -0
- kivy/uix/behaviors/button.py +212 -0
- kivy/uix/behaviors/codenavigation.py +167 -0
- kivy/uix/behaviors/compoundselection.py +688 -0
- kivy/uix/behaviors/cover.py +160 -0
- kivy/uix/behaviors/drag.py +234 -0
- kivy/uix/behaviors/emacs.py +140 -0
- kivy/uix/behaviors/focus.py +595 -0
- kivy/uix/behaviors/knspace.py +590 -0
- kivy/uix/behaviors/togglebutton.py +156 -0
- kivy/uix/behaviors/touchripple.py +318 -0
- kivy/uix/boxlayout.py +331 -0
- kivy/uix/bubble.py +576 -0
- kivy/uix/button.py +137 -0
- kivy/uix/camera.py +118 -0
- kivy/uix/carousel.py +695 -0
- kivy/uix/checkbox.py +197 -0
- kivy/uix/codeinput.py +238 -0
- kivy/uix/colorpicker.py +486 -0
- kivy/uix/dropdown.py +391 -0
- kivy/uix/effectwidget.py +772 -0
- kivy/uix/filechooser.py +1134 -0
- kivy/uix/floatlayout.py +148 -0
- kivy/uix/gesturesurface.py +625 -0
- kivy/uix/gridlayout.py +629 -0
- kivy/uix/image.py +528 -0
- kivy/uix/label.py +1200 -0
- kivy/uix/layout.py +322 -0
- kivy/uix/modalview.py +339 -0
- kivy/uix/pagelayout.py +233 -0
- kivy/uix/popup.py +266 -0
- kivy/uix/progressbar.py +95 -0
- kivy/uix/recycleboxlayout.py +183 -0
- kivy/uix/recyclegridlayout.py +255 -0
- kivy/uix/recyclelayout.py +446 -0
- kivy/uix/recycleview/__init__.py +624 -0
- kivy/uix/recycleview/datamodel.py +194 -0
- kivy/uix/recycleview/layout.py +253 -0
- kivy/uix/recycleview/views.py +423 -0
- kivy/uix/relativelayout.py +324 -0
- kivy/uix/rst.py +1492 -0
- kivy/uix/sandbox.py +198 -0
- kivy/uix/scatter.py +645 -0
- kivy/uix/scatterlayout.py +93 -0
- kivy/uix/screenmanager.py +1275 -0
- kivy/uix/scrollview.py +1288 -0
- kivy/uix/settings.py +1343 -0
- kivy/uix/slider.py +421 -0
- kivy/uix/spinner.py +221 -0
- kivy/uix/splitter.py +430 -0
- kivy/uix/stacklayout.py +331 -0
- kivy/uix/stencilview.py +40 -0
- kivy/uix/switch.py +122 -0
- kivy/uix/tabbedpanel.py +880 -0
- kivy/uix/textinput.py +4049 -0
- kivy/uix/togglebutton.py +37 -0
- kivy/uix/treeview.py +665 -0
- kivy/uix/video.py +302 -0
- kivy/uix/videoplayer.py +697 -0
- kivy/uix/vkeyboard.py +879 -0
- kivy/uix/widget.py +1629 -0
- kivy/utils.py +550 -0
- kivy/vector.py +412 -0
- kivy/weakmethod.py +72 -0
kivy/uix/textinput.py
ADDED
|
@@ -0,0 +1,4049 @@
|
|
|
1
|
+
'''
|
|
2
|
+
Text Input
|
|
3
|
+
==========
|
|
4
|
+
|
|
5
|
+
.. versionadded:: 1.0.4
|
|
6
|
+
|
|
7
|
+
.. image:: images/textinput-mono.jpg
|
|
8
|
+
.. image:: images/textinput-multi.jpg
|
|
9
|
+
|
|
10
|
+
The :class:`TextInput` widget provides a box for editable plain text.
|
|
11
|
+
|
|
12
|
+
Unicode, multiline, cursor navigation, selection and clipboard features
|
|
13
|
+
are supported.
|
|
14
|
+
|
|
15
|
+
The :class:`TextInput` uses two different coordinate systems:
|
|
16
|
+
|
|
17
|
+
* (x, y) - coordinates in pixels, mostly used for rendering on screen.
|
|
18
|
+
* (col, row) - cursor index in characters / lines, used for selection
|
|
19
|
+
and cursor movement.
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
Usage example
|
|
23
|
+
-------------
|
|
24
|
+
|
|
25
|
+
To create a multiline :class:`TextInput` (the 'enter' key adds a new line)::
|
|
26
|
+
|
|
27
|
+
from kivy.uix.textinput import TextInput
|
|
28
|
+
textinput = TextInput(text='Hello world')
|
|
29
|
+
|
|
30
|
+
To create a singleline :class:`TextInput`, set the :class:`TextInput.multiline`
|
|
31
|
+
property to False (the 'enter' key will defocus the TextInput and emit an
|
|
32
|
+
:meth:`TextInput.on_text_validate` event)::
|
|
33
|
+
|
|
34
|
+
def on_enter(instance, value):
|
|
35
|
+
print('User pressed enter in', instance)
|
|
36
|
+
|
|
37
|
+
textinput = TextInput(text='Hello world', multiline=False)
|
|
38
|
+
textinput.bind(on_text_validate=on_enter)
|
|
39
|
+
|
|
40
|
+
The textinput's text is stored in its :attr:`TextInput.text` property. To run a
|
|
41
|
+
callback when the text changes::
|
|
42
|
+
|
|
43
|
+
def on_text(instance, value):
|
|
44
|
+
print('The widget', instance, 'have:', value)
|
|
45
|
+
|
|
46
|
+
textinput = TextInput()
|
|
47
|
+
textinput.bind(text=on_text)
|
|
48
|
+
|
|
49
|
+
You can set the :class:`focus <kivy.uix.behaviors.FocusBehavior>` to a
|
|
50
|
+
Textinput, meaning that the input box will be highlighted and keyboard focus
|
|
51
|
+
will be requested::
|
|
52
|
+
|
|
53
|
+
textinput = TextInput(focus=True)
|
|
54
|
+
|
|
55
|
+
The textinput is defocused if the 'escape' key is pressed, or if another
|
|
56
|
+
widget requests the keyboard. You can bind a callback to the focus property to
|
|
57
|
+
get notified of focus changes::
|
|
58
|
+
|
|
59
|
+
def on_focus(instance, value):
|
|
60
|
+
if value:
|
|
61
|
+
print('User focused', instance)
|
|
62
|
+
else:
|
|
63
|
+
print('User defocused', instance)
|
|
64
|
+
|
|
65
|
+
textinput = TextInput()
|
|
66
|
+
textinput.bind(focus=on_focus)
|
|
67
|
+
|
|
68
|
+
See :class:`~kivy.uix.behaviors.FocusBehavior`, from which the
|
|
69
|
+
:class:`TextInput` inherits, for more details.
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
Selection
|
|
73
|
+
---------
|
|
74
|
+
|
|
75
|
+
The selection is automatically updated when the cursor position changes.
|
|
76
|
+
You can get the currently selected text from the
|
|
77
|
+
:attr:`TextInput.selection_text` property.
|
|
78
|
+
|
|
79
|
+
Handles
|
|
80
|
+
-------
|
|
81
|
+
|
|
82
|
+
One can enable :attr:`TextInput.use_handles` property to enable or disable
|
|
83
|
+
the usage of selection handles. This property is True by default on mobiles.
|
|
84
|
+
|
|
85
|
+
Selection Handles uses the class :class:`Selector` as the base class for
|
|
86
|
+
the selection handles. You can customize the color for selection handles
|
|
87
|
+
like so ::
|
|
88
|
+
|
|
89
|
+
<Selector>
|
|
90
|
+
color: 0, 1, 0, 1
|
|
91
|
+
# or <Textinput_instance>.selection_color or app.selection_color
|
|
92
|
+
|
|
93
|
+
TextInput instantiates the selection handles and stores it in the following
|
|
94
|
+
properties. :attr:`TextInput._handle_middle`,
|
|
95
|
+
:attr:`TextInput._handle_left`, :attr:`TextInput._handle_right`.
|
|
96
|
+
|
|
97
|
+
You should set the selection template before the Instantiating TextInput,
|
|
98
|
+
so as to get the selection handles to take the changes you set to apply.
|
|
99
|
+
|
|
100
|
+
Filtering
|
|
101
|
+
---------
|
|
102
|
+
|
|
103
|
+
You can control which text can be added to the :class:`TextInput` by
|
|
104
|
+
overwriting :meth:`TextInput.insert_text`. Every string that is typed, pasted
|
|
105
|
+
or inserted by any other means into the :class:`TextInput` is passed through
|
|
106
|
+
this function. By overwriting it you can reject or change unwanted characters.
|
|
107
|
+
|
|
108
|
+
For example, to write only in capitalized characters::
|
|
109
|
+
|
|
110
|
+
class CapitalInput(TextInput):
|
|
111
|
+
|
|
112
|
+
def insert_text(self, substring, from_undo=False):
|
|
113
|
+
s = substring.upper()
|
|
114
|
+
return super().insert_text(s, from_undo=from_undo)
|
|
115
|
+
|
|
116
|
+
Or to only allow floats (0 - 9 and a single period)::
|
|
117
|
+
|
|
118
|
+
class FloatInput(TextInput):
|
|
119
|
+
|
|
120
|
+
pat = re.compile('[^0-9]')
|
|
121
|
+
def insert_text(self, substring, from_undo=False):
|
|
122
|
+
pat = self.pat
|
|
123
|
+
if '.' in self.text:
|
|
124
|
+
s = re.sub(pat, '', substring)
|
|
125
|
+
else:
|
|
126
|
+
s = '.'.join(
|
|
127
|
+
re.sub(pat, '', s)
|
|
128
|
+
for s in substring.split('.', 1)
|
|
129
|
+
)
|
|
130
|
+
return super().insert_text(s, from_undo=from_undo)
|
|
131
|
+
|
|
132
|
+
Default shortcuts
|
|
133
|
+
-----------------
|
|
134
|
+
|
|
135
|
+
=============== ========================================================
|
|
136
|
+
Shortcuts Description
|
|
137
|
+
--------------- --------------------------------------------------------
|
|
138
|
+
Left Move cursor to left
|
|
139
|
+
Right Move cursor to right
|
|
140
|
+
Up Move cursor to up
|
|
141
|
+
Down Move cursor to down
|
|
142
|
+
Home Move cursor at the beginning of the line
|
|
143
|
+
End Move cursor at the end of the line
|
|
144
|
+
PageUp Move cursor to 3 lines before
|
|
145
|
+
PageDown Move cursor to 3 lines after
|
|
146
|
+
Backspace Delete the selection or character before the cursor
|
|
147
|
+
Del Delete the selection of character after the cursor
|
|
148
|
+
Shift + <dir> Start a text selection. Dir can be Up, Down, Left or
|
|
149
|
+
Right
|
|
150
|
+
Control + c Copy selection
|
|
151
|
+
Control + x Cut selection
|
|
152
|
+
Control + v Paste clipboard content
|
|
153
|
+
Control + a Select all the content
|
|
154
|
+
Control + z undo
|
|
155
|
+
Control + r redo
|
|
156
|
+
=============== ========================================================
|
|
157
|
+
|
|
158
|
+
.. note::
|
|
159
|
+
To enable Emacs-style keyboard shortcuts, you can use
|
|
160
|
+
:class:`~kivy.uix.behaviors.emacs.EmacsBehavior`.
|
|
161
|
+
|
|
162
|
+
'''
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
import re
|
|
166
|
+
import sys
|
|
167
|
+
import math
|
|
168
|
+
from os import environ
|
|
169
|
+
from weakref import ref
|
|
170
|
+
from itertools import chain, islice
|
|
171
|
+
|
|
172
|
+
from kivy.animation import Animation
|
|
173
|
+
from kivy.base import EventLoop
|
|
174
|
+
from kivy.cache import Cache
|
|
175
|
+
from kivy.clock import Clock
|
|
176
|
+
from kivy.config import Config
|
|
177
|
+
from kivy.core.window import Window
|
|
178
|
+
from kivy.metrics import inch
|
|
179
|
+
from kivy.utils import boundary, platform
|
|
180
|
+
from kivy.uix.behaviors import FocusBehavior
|
|
181
|
+
|
|
182
|
+
from kivy.core.text import Label, DEFAULT_FONT
|
|
183
|
+
from kivy.graphics import Color, Rectangle, PushMatrix, PopMatrix, Callback
|
|
184
|
+
from kivy.graphics.context_instructions import Transform
|
|
185
|
+
from kivy.graphics.texture import Texture
|
|
186
|
+
|
|
187
|
+
from kivy.uix.widget import Widget
|
|
188
|
+
from kivy.uix.bubble import Bubble
|
|
189
|
+
from kivy.uix.behaviors import ButtonBehavior
|
|
190
|
+
from kivy.uix.image import Image
|
|
191
|
+
|
|
192
|
+
from kivy.properties import StringProperty, NumericProperty, \
|
|
193
|
+
BooleanProperty, AliasProperty, OptionProperty, \
|
|
194
|
+
ListProperty, ObjectProperty, VariableListProperty, ColorProperty, \
|
|
195
|
+
BoundedNumericProperty
|
|
196
|
+
|
|
197
|
+
__all__ = ('TextInput', )
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
if 'KIVY_DOC' in environ:
|
|
201
|
+
def triggered(*_, **__):
|
|
202
|
+
def decorator_func(func):
|
|
203
|
+
def decorated_func(*args, **kwargs):
|
|
204
|
+
return func(*args, **kwargs)
|
|
205
|
+
return decorated_func
|
|
206
|
+
return decorator_func
|
|
207
|
+
else:
|
|
208
|
+
from kivy.clock import triggered
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
Cache_register = Cache.register
|
|
212
|
+
Cache_append = Cache.append
|
|
213
|
+
Cache_get = Cache.get
|
|
214
|
+
Cache_remove = Cache.remove
|
|
215
|
+
Cache_register('textinput.label', timeout=60.)
|
|
216
|
+
Cache_register('textinput.width', timeout=60.)
|
|
217
|
+
|
|
218
|
+
FL_IS_LINEBREAK = 0x01
|
|
219
|
+
FL_IS_WORDBREAK = 0x02
|
|
220
|
+
FL_IS_NEWLINE = FL_IS_LINEBREAK | FL_IS_WORDBREAK
|
|
221
|
+
|
|
222
|
+
# late binding
|
|
223
|
+
Clipboard = None
|
|
224
|
+
CutBuffer = None
|
|
225
|
+
MarkupLabel = None
|
|
226
|
+
_platform = platform
|
|
227
|
+
|
|
228
|
+
# for reloading, we need to keep a list of textinput to retrigger the rendering
|
|
229
|
+
_textinput_list = []
|
|
230
|
+
|
|
231
|
+
# cache the result
|
|
232
|
+
_is_osx = sys.platform == 'darwin'
|
|
233
|
+
|
|
234
|
+
# When we are generating documentation, Config doesn't exist
|
|
235
|
+
_is_desktop = False
|
|
236
|
+
_scroll_timeout = _scroll_distance = 0
|
|
237
|
+
if Config:
|
|
238
|
+
_is_desktop = Config.getboolean('kivy', 'desktop')
|
|
239
|
+
_scroll_timeout = Config.getint('widgets', 'scroll_timeout')
|
|
240
|
+
_scroll_distance = '{}sp'.format(Config.getint('widgets',
|
|
241
|
+
'scroll_distance'))
|
|
242
|
+
|
|
243
|
+
# register an observer to clear the textinput cache when OpenGL will reload
|
|
244
|
+
if 'KIVY_DOC' not in environ:
|
|
245
|
+
|
|
246
|
+
def _textinput_clear_cache(*l):
|
|
247
|
+
Cache_remove('textinput.label')
|
|
248
|
+
Cache_remove('textinput.width')
|
|
249
|
+
for wr in _textinput_list[:]:
|
|
250
|
+
textinput = wr()
|
|
251
|
+
if textinput is None:
|
|
252
|
+
_textinput_list.remove(wr)
|
|
253
|
+
else:
|
|
254
|
+
textinput._trigger_refresh_text()
|
|
255
|
+
textinput._refresh_hint_text()
|
|
256
|
+
|
|
257
|
+
from kivy.graphics.context import get_context
|
|
258
|
+
get_context().add_reload_observer(_textinput_clear_cache, True)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class Selector(ButtonBehavior, Image):
|
|
262
|
+
'''Default template for the selection Handles
|
|
263
|
+
|
|
264
|
+
In order to customize the look of the Selection Handles,
|
|
265
|
+
you should adjust its template like so ::
|
|
266
|
+
|
|
267
|
+
<Selector>
|
|
268
|
+
color: 1, 0, 1, 1
|
|
269
|
+
'''
|
|
270
|
+
|
|
271
|
+
window = ObjectProperty()
|
|
272
|
+
target = ObjectProperty()
|
|
273
|
+
matrix = ObjectProperty()
|
|
274
|
+
|
|
275
|
+
def __init__(self, **kwargs):
|
|
276
|
+
super().__init__(**kwargs)
|
|
277
|
+
self.always_release = True
|
|
278
|
+
self.matrix = self.target.get_window_matrix()
|
|
279
|
+
|
|
280
|
+
with self.canvas.before:
|
|
281
|
+
Callback(self.update_transform)
|
|
282
|
+
PushMatrix()
|
|
283
|
+
self.transform = Transform()
|
|
284
|
+
|
|
285
|
+
with self.canvas.after:
|
|
286
|
+
PopMatrix()
|
|
287
|
+
|
|
288
|
+
def update_transform(self, cb):
|
|
289
|
+
matrix = self.target.get_window_matrix()
|
|
290
|
+
if self.matrix != matrix:
|
|
291
|
+
self.matrix = matrix
|
|
292
|
+
self.transform.identity()
|
|
293
|
+
self.transform.transform(self.matrix)
|
|
294
|
+
|
|
295
|
+
def transform_touch(self, touch):
|
|
296
|
+
matrix = self.matrix.inverse()
|
|
297
|
+
touch.apply_transform_2d(
|
|
298
|
+
lambda x, y: matrix.transform_point(x, y, 0)[:2]
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
def on_touch_down(self, touch):
|
|
302
|
+
if self.parent is not EventLoop.window:
|
|
303
|
+
return
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
touch.push()
|
|
307
|
+
self.transform_touch(touch)
|
|
308
|
+
self._touch_diff = self.top - touch.y
|
|
309
|
+
if self.collide_point(*touch.pos):
|
|
310
|
+
FocusBehavior.ignored_touch.append(touch)
|
|
311
|
+
return super().on_touch_down(touch)
|
|
312
|
+
finally:
|
|
313
|
+
touch.pop()
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class TextInputCutCopyPaste(Bubble):
|
|
317
|
+
# Internal class used for showing the little bubble popup when
|
|
318
|
+
# copy/cut/paste happen.
|
|
319
|
+
|
|
320
|
+
textinput = ObjectProperty(None)
|
|
321
|
+
''' Holds a reference to the TextInput this Bubble belongs to.
|
|
322
|
+
'''
|
|
323
|
+
|
|
324
|
+
but_cut = ObjectProperty(None)
|
|
325
|
+
but_copy = ObjectProperty(None)
|
|
326
|
+
but_paste = ObjectProperty(None)
|
|
327
|
+
but_selectall = ObjectProperty(None)
|
|
328
|
+
|
|
329
|
+
matrix = ObjectProperty(None)
|
|
330
|
+
|
|
331
|
+
_check_parent_ev = None
|
|
332
|
+
|
|
333
|
+
def __init__(self, **kwargs):
|
|
334
|
+
self.mode = 'normal'
|
|
335
|
+
super().__init__(**kwargs)
|
|
336
|
+
self._check_parent_ev = Clock.schedule_interval(self._check_parent, .5)
|
|
337
|
+
self.matrix = self.textinput.get_window_matrix()
|
|
338
|
+
|
|
339
|
+
with self.canvas.before:
|
|
340
|
+
Callback(self.update_transform)
|
|
341
|
+
PushMatrix()
|
|
342
|
+
self.transform = Transform()
|
|
343
|
+
|
|
344
|
+
with self.canvas.after:
|
|
345
|
+
PopMatrix()
|
|
346
|
+
|
|
347
|
+
def update_transform(self, cb):
|
|
348
|
+
m = self.textinput.get_window_matrix()
|
|
349
|
+
if self.matrix != m:
|
|
350
|
+
self.matrix = m
|
|
351
|
+
self.transform.identity()
|
|
352
|
+
self.transform.transform(self.matrix)
|
|
353
|
+
|
|
354
|
+
def transform_touch(self, touch):
|
|
355
|
+
matrix = self.matrix.inverse()
|
|
356
|
+
touch.apply_transform_2d(
|
|
357
|
+
lambda x, y: matrix.transform_point(x, y, 0)[:2])
|
|
358
|
+
|
|
359
|
+
def on_touch_down(self, touch):
|
|
360
|
+
try:
|
|
361
|
+
touch.push()
|
|
362
|
+
self.transform_touch(touch)
|
|
363
|
+
if self.collide_point(*touch.pos):
|
|
364
|
+
FocusBehavior.ignored_touch.append(touch)
|
|
365
|
+
return super().on_touch_down(touch)
|
|
366
|
+
finally:
|
|
367
|
+
touch.pop()
|
|
368
|
+
|
|
369
|
+
def on_touch_up(self, touch):
|
|
370
|
+
try:
|
|
371
|
+
touch.push()
|
|
372
|
+
self.transform_touch(touch)
|
|
373
|
+
for child in self.content.children:
|
|
374
|
+
if ref(child) in touch.grab_list:
|
|
375
|
+
touch.grab_current = child
|
|
376
|
+
break
|
|
377
|
+
return super().on_touch_up(touch)
|
|
378
|
+
finally:
|
|
379
|
+
touch.pop()
|
|
380
|
+
|
|
381
|
+
def on_textinput(self, instance, value):
|
|
382
|
+
global Clipboard
|
|
383
|
+
if value and not Clipboard and not _is_desktop:
|
|
384
|
+
value._ensure_clipboard()
|
|
385
|
+
|
|
386
|
+
def _check_parent(self, dt):
|
|
387
|
+
# this is a prevention to get the Bubble staying on the screen, if the
|
|
388
|
+
# attached textinput is not on the screen anymore.
|
|
389
|
+
parent = self.textinput
|
|
390
|
+
while parent is not None:
|
|
391
|
+
if parent == parent.parent:
|
|
392
|
+
break
|
|
393
|
+
parent = parent.parent
|
|
394
|
+
if parent is None:
|
|
395
|
+
self._check_parent_ev.cancel()
|
|
396
|
+
if self.textinput:
|
|
397
|
+
self.textinput._hide_cut_copy_paste()
|
|
398
|
+
|
|
399
|
+
def on_parent(self, instance, value):
|
|
400
|
+
parent = self.textinput
|
|
401
|
+
mode = self.mode
|
|
402
|
+
|
|
403
|
+
if parent:
|
|
404
|
+
self.content.clear_widgets()
|
|
405
|
+
if mode == 'paste':
|
|
406
|
+
# show only paste on long touch
|
|
407
|
+
self.but_selectall.opacity = 1
|
|
408
|
+
widget_list = [self.but_selectall, ]
|
|
409
|
+
if not parent.readonly:
|
|
410
|
+
widget_list.append(self.but_paste)
|
|
411
|
+
elif parent.readonly:
|
|
412
|
+
# show only copy for read only text input
|
|
413
|
+
widget_list = (self.but_copy, )
|
|
414
|
+
else:
|
|
415
|
+
# normal mode
|
|
416
|
+
widget_list = (self.but_cut, self.but_copy, self.but_paste)
|
|
417
|
+
|
|
418
|
+
for widget in widget_list:
|
|
419
|
+
self.content.add_widget(widget)
|
|
420
|
+
|
|
421
|
+
def do(self, action):
|
|
422
|
+
textinput = self.textinput
|
|
423
|
+
|
|
424
|
+
if action == 'cut':
|
|
425
|
+
textinput._cut(textinput.selection_text)
|
|
426
|
+
elif action == 'copy':
|
|
427
|
+
textinput.copy()
|
|
428
|
+
elif action == 'paste':
|
|
429
|
+
textinput.paste()
|
|
430
|
+
elif action == 'selectall':
|
|
431
|
+
textinput.select_all()
|
|
432
|
+
self.mode = ''
|
|
433
|
+
anim = Animation(opacity=0, d=.333)
|
|
434
|
+
anim.bind(on_complete=lambda *args:
|
|
435
|
+
self.on_parent(self, self.parent))
|
|
436
|
+
anim.start(self.but_selectall)
|
|
437
|
+
return
|
|
438
|
+
|
|
439
|
+
self.hide()
|
|
440
|
+
|
|
441
|
+
def hide(self):
|
|
442
|
+
parent = self.parent
|
|
443
|
+
if not parent:
|
|
444
|
+
return
|
|
445
|
+
|
|
446
|
+
anim = Animation(opacity=0, d=.225)
|
|
447
|
+
anim.bind(on_complete=lambda *args: parent.remove_widget(self))
|
|
448
|
+
anim.start(self)
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
class TextInput(FocusBehavior, Widget):
|
|
452
|
+
'''TextInput class. See module documentation for more information.
|
|
453
|
+
|
|
454
|
+
:Events:
|
|
455
|
+
`on_text_validate`
|
|
456
|
+
Fired only in multiline=False mode when the user hits 'enter'.
|
|
457
|
+
This will also unfocus the textinput.
|
|
458
|
+
`on_double_tap`
|
|
459
|
+
Fired when a double tap happens in the text input. The default
|
|
460
|
+
behavior selects the text around the cursor position. More info at
|
|
461
|
+
:meth:`on_double_tap`.
|
|
462
|
+
`on_triple_tap`
|
|
463
|
+
Fired when a triple tap happens in the text input. The default
|
|
464
|
+
behavior selects the line around the cursor position. More info at
|
|
465
|
+
:meth:`on_triple_tap`.
|
|
466
|
+
`on_quad_touch`
|
|
467
|
+
Fired when four fingers are touching the text input. The default
|
|
468
|
+
behavior selects the whole text. More info at
|
|
469
|
+
:meth:`on_quad_touch`.
|
|
470
|
+
|
|
471
|
+
.. warning::
|
|
472
|
+
When changing a :class:`TextInput` property that requires re-drawing,
|
|
473
|
+
e.g. modifying the :attr:`text`, the updates occur on the next
|
|
474
|
+
clock cycle and not instantly. This might cause any changes to the
|
|
475
|
+
:class:`TextInput` that occur between the modification and the next
|
|
476
|
+
cycle to be ignored, or to use previous values. For example, after
|
|
477
|
+
a update to the :attr:`text`, changing the cursor in the same clock
|
|
478
|
+
frame will move it using the previous text and will likely end up in an
|
|
479
|
+
incorrect position. The solution is to schedule any updates to occur
|
|
480
|
+
on the next clock cycle using
|
|
481
|
+
:meth:`~kivy.clock.ClockBase.schedule_once`.
|
|
482
|
+
|
|
483
|
+
.. Note::
|
|
484
|
+
Selection is cancelled when TextInput is focused. If you need to
|
|
485
|
+
show selection when TextInput is focused, you should delay
|
|
486
|
+
(use Clock.schedule) the call to the functions for selecting
|
|
487
|
+
text (select_all, select_text).
|
|
488
|
+
|
|
489
|
+
.. versionchanged:: 1.10.0
|
|
490
|
+
`background_disabled_active` has been removed.
|
|
491
|
+
|
|
492
|
+
.. versionchanged:: 1.9.0
|
|
493
|
+
|
|
494
|
+
:class:`TextInput` now inherits from
|
|
495
|
+
:class:`~kivy.uix.behaviors.FocusBehavior`.
|
|
496
|
+
:attr:`~kivy.uix.behaviors.FocusBehavior.keyboard_mode`,
|
|
497
|
+
:meth:`~kivy.uix.behaviors.FocusBehavior.show_keyboard`,
|
|
498
|
+
:meth:`~kivy.uix.behaviors.FocusBehavior.hide_keyboard`,
|
|
499
|
+
:meth:`~kivy.uix.behaviors.FocusBehavior.focus`,
|
|
500
|
+
and :attr:`~kivy.uix.behaviors.FocusBehavior.input_type`
|
|
501
|
+
have been removed since they are now inherited
|
|
502
|
+
from :class:`~kivy.uix.behaviors.FocusBehavior`.
|
|
503
|
+
|
|
504
|
+
.. versionchanged:: 1.7.0
|
|
505
|
+
`on_double_tap`, `on_triple_tap` and `on_quad_touch` events added.
|
|
506
|
+
|
|
507
|
+
.. versionchanged:: 2.1.0
|
|
508
|
+
:attr:`~kivy.uix.behaviors.FocusBehavior.keyboard_suggestions`
|
|
509
|
+
is now inherited from :class:`~kivy.uix.behaviors.FocusBehavior`.
|
|
510
|
+
'''
|
|
511
|
+
|
|
512
|
+
__events__ = ('on_text_validate', 'on_double_tap', 'on_triple_tap',
|
|
513
|
+
'on_quad_touch')
|
|
514
|
+
|
|
515
|
+
_resolved_base_dir = None
|
|
516
|
+
|
|
517
|
+
def __init__(self, **kwargs):
|
|
518
|
+
self._update_graphics_ev = Clock.create_trigger(
|
|
519
|
+
self._update_graphics, -1)
|
|
520
|
+
self.is_focusable = kwargs.get('is_focusable', True)
|
|
521
|
+
self._cursor = [0, 0]
|
|
522
|
+
self._selection = False
|
|
523
|
+
self._selection_finished = True
|
|
524
|
+
self._selection_touch = None
|
|
525
|
+
self.selection_text = u''
|
|
526
|
+
self._selection_from = None
|
|
527
|
+
self._selection_to = None
|
|
528
|
+
self._selection_callback = None
|
|
529
|
+
self._handle_left = None
|
|
530
|
+
self._handle_right = None
|
|
531
|
+
self._handle_middle = None
|
|
532
|
+
self._bubble = None
|
|
533
|
+
self._lines_flags = []
|
|
534
|
+
self._lines_labels = []
|
|
535
|
+
self._lines_rects = []
|
|
536
|
+
self._hint_text_flags = []
|
|
537
|
+
self._hint_text_labels = []
|
|
538
|
+
self._hint_text_rects = []
|
|
539
|
+
self._label_cached = None
|
|
540
|
+
self._line_options = None
|
|
541
|
+
self._keyboard_mode = Config.get('kivy', 'keyboard_mode')
|
|
542
|
+
self._command_mode = False
|
|
543
|
+
self._command = ''
|
|
544
|
+
self.reset_undo()
|
|
545
|
+
self._touch_count = 0
|
|
546
|
+
self._ctrl_l = False
|
|
547
|
+
self._ctrl_r = False
|
|
548
|
+
self._alt_l = False
|
|
549
|
+
self._alt_r = False
|
|
550
|
+
self._refresh_text_from_property_ev = None
|
|
551
|
+
self._long_touch_ev = None
|
|
552
|
+
self._do_blink_cursor_ev = Clock.create_trigger(
|
|
553
|
+
self._do_blink_cursor, .5, interval=True)
|
|
554
|
+
self._refresh_line_options_ev = None
|
|
555
|
+
self._scroll_distance_x = 0
|
|
556
|
+
self._scroll_distance_y = 0
|
|
557
|
+
self._enable_scroll = True
|
|
558
|
+
self._have_scrolled = False
|
|
559
|
+
|
|
560
|
+
# [from; to) range of lines being partially or fully rendered
|
|
561
|
+
# in TextInput's viewport
|
|
562
|
+
self._visible_lines_range = 0, 0
|
|
563
|
+
|
|
564
|
+
self.interesting_keys = {
|
|
565
|
+
8: 'backspace',
|
|
566
|
+
13: 'enter',
|
|
567
|
+
127: 'del',
|
|
568
|
+
271: 'enter',
|
|
569
|
+
273: 'cursor_up',
|
|
570
|
+
274: 'cursor_down',
|
|
571
|
+
275: 'cursor_right',
|
|
572
|
+
276: 'cursor_left',
|
|
573
|
+
278: 'cursor_home',
|
|
574
|
+
279: 'cursor_end',
|
|
575
|
+
280: 'cursor_pgup',
|
|
576
|
+
281: 'cursor_pgdown',
|
|
577
|
+
303: 'shift_L',
|
|
578
|
+
304: 'shift_R',
|
|
579
|
+
305: 'ctrl_L',
|
|
580
|
+
306: 'ctrl_R',
|
|
581
|
+
308: 'alt_L',
|
|
582
|
+
307: 'alt_R'
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
super().__init__(**kwargs)
|
|
586
|
+
|
|
587
|
+
fbind = self.fbind
|
|
588
|
+
refresh_line_options = self._trigger_refresh_line_options
|
|
589
|
+
update_text_options = self._update_text_options
|
|
590
|
+
trigger_update_graphics = self._trigger_update_graphics
|
|
591
|
+
|
|
592
|
+
fbind('font_size', refresh_line_options)
|
|
593
|
+
fbind('font_name', refresh_line_options)
|
|
594
|
+
fbind('font_context', refresh_line_options)
|
|
595
|
+
fbind('font_family', refresh_line_options)
|
|
596
|
+
fbind('base_direction', refresh_line_options)
|
|
597
|
+
fbind('text_language', refresh_line_options)
|
|
598
|
+
|
|
599
|
+
def handle_readonly(instance, value):
|
|
600
|
+
if value and (not _is_desktop or not self.allow_copy):
|
|
601
|
+
self.is_focusable = False
|
|
602
|
+
if (not (value or self.disabled) or _is_desktop and
|
|
603
|
+
self._keyboard_mode == 'system'):
|
|
604
|
+
self._editable = True
|
|
605
|
+
else:
|
|
606
|
+
self._editable = False
|
|
607
|
+
|
|
608
|
+
fbind('padding', update_text_options)
|
|
609
|
+
fbind('tab_width', update_text_options)
|
|
610
|
+
fbind('font_size', update_text_options)
|
|
611
|
+
fbind('font_name', update_text_options)
|
|
612
|
+
fbind('size', update_text_options)
|
|
613
|
+
fbind('password', update_text_options)
|
|
614
|
+
fbind('password_mask', update_text_options)
|
|
615
|
+
|
|
616
|
+
fbind('pos', trigger_update_graphics)
|
|
617
|
+
fbind('halign', trigger_update_graphics)
|
|
618
|
+
fbind('readonly', handle_readonly)
|
|
619
|
+
fbind('focus', self._on_textinput_focused)
|
|
620
|
+
handle_readonly(self, self.readonly)
|
|
621
|
+
|
|
622
|
+
handles = self._trigger_position_handles = Clock.create_trigger(
|
|
623
|
+
self._position_handles)
|
|
624
|
+
self._trigger_show_handles = Clock.create_trigger(
|
|
625
|
+
self._show_handles, .05)
|
|
626
|
+
self._trigger_cursor_reset = Clock.create_trigger(
|
|
627
|
+
self._reset_cursor_blink)
|
|
628
|
+
self._trigger_update_cutbuffer = Clock.create_trigger(
|
|
629
|
+
self._update_cutbuffer)
|
|
630
|
+
refresh_line_options()
|
|
631
|
+
self._trigger_refresh_text()
|
|
632
|
+
|
|
633
|
+
fbind('pos', handles)
|
|
634
|
+
fbind('size', handles)
|
|
635
|
+
|
|
636
|
+
# when the gl context is reloaded, trigger the text rendering again.
|
|
637
|
+
_textinput_list.append(ref(self, TextInput._reload_remove_observer))
|
|
638
|
+
|
|
639
|
+
if platform == 'linux':
|
|
640
|
+
self._ensure_clipboard()
|
|
641
|
+
|
|
642
|
+
def on_text_validate(self):
|
|
643
|
+
pass
|
|
644
|
+
|
|
645
|
+
def cursor_index(self, cursor=None):
|
|
646
|
+
'''Return the cursor index in the text/value.
|
|
647
|
+
'''
|
|
648
|
+
if not cursor:
|
|
649
|
+
cursor = self.cursor
|
|
650
|
+
try:
|
|
651
|
+
lines = self._lines
|
|
652
|
+
if not lines:
|
|
653
|
+
return 0
|
|
654
|
+
|
|
655
|
+
flags = self._lines_flags
|
|
656
|
+
index, cursor_row = cursor
|
|
657
|
+
|
|
658
|
+
for _, line, flag in zip(
|
|
659
|
+
range(min(cursor_row, len(lines))),
|
|
660
|
+
lines,
|
|
661
|
+
flags
|
|
662
|
+
):
|
|
663
|
+
index += len(line)
|
|
664
|
+
if flag & FL_IS_LINEBREAK:
|
|
665
|
+
index += 1
|
|
666
|
+
|
|
667
|
+
if flags[cursor_row] & FL_IS_LINEBREAK:
|
|
668
|
+
index += 1
|
|
669
|
+
return index
|
|
670
|
+
|
|
671
|
+
except IndexError:
|
|
672
|
+
return 0
|
|
673
|
+
|
|
674
|
+
def cursor_offset(self):
|
|
675
|
+
'''Get the cursor x offset on the current line.
|
|
676
|
+
'''
|
|
677
|
+
offset = 0
|
|
678
|
+
row = int(self.cursor_row)
|
|
679
|
+
col = int(self.cursor_col)
|
|
680
|
+
lines = self._lines
|
|
681
|
+
if col and row < len(lines):
|
|
682
|
+
offset = self._get_text_width(
|
|
683
|
+
lines[row][:col],
|
|
684
|
+
self.tab_width,
|
|
685
|
+
self._label_cached
|
|
686
|
+
)
|
|
687
|
+
return offset
|
|
688
|
+
|
|
689
|
+
def get_cursor_from_index(self, index):
|
|
690
|
+
'''Return the (col, row) of the cursor from text index.
|
|
691
|
+
'''
|
|
692
|
+
index = boundary(index, 0, len(self.text))
|
|
693
|
+
if index <= 0:
|
|
694
|
+
return 0, 0
|
|
695
|
+
flags = self._lines_flags
|
|
696
|
+
lines = self._lines
|
|
697
|
+
if not lines:
|
|
698
|
+
return 0, 0
|
|
699
|
+
|
|
700
|
+
i = 0
|
|
701
|
+
for row, line in enumerate(lines):
|
|
702
|
+
count = i + len(line)
|
|
703
|
+
if flags[row] & FL_IS_LINEBREAK:
|
|
704
|
+
count += 1
|
|
705
|
+
i += 1
|
|
706
|
+
if count >= index:
|
|
707
|
+
return index - i, row
|
|
708
|
+
i = count
|
|
709
|
+
return int(index), int(row)
|
|
710
|
+
|
|
711
|
+
def select_text(self, start, end):
|
|
712
|
+
''' Select a portion of text displayed in this TextInput.
|
|
713
|
+
|
|
714
|
+
.. versionadded:: 1.4.0
|
|
715
|
+
|
|
716
|
+
:Parameters:
|
|
717
|
+
`start`
|
|
718
|
+
Index of textinput.text from where to start selection
|
|
719
|
+
`end`
|
|
720
|
+
Index of textinput.text till which the selection should be
|
|
721
|
+
displayed
|
|
722
|
+
'''
|
|
723
|
+
if end < start:
|
|
724
|
+
raise Exception('end must be superior to start')
|
|
725
|
+
text_length = len(self.text)
|
|
726
|
+
self._selection_from = boundary(start, 0, text_length)
|
|
727
|
+
self._selection_to = boundary(end, 0, text_length)
|
|
728
|
+
self._selection_finished = True
|
|
729
|
+
self._update_selection(True)
|
|
730
|
+
self._update_graphics_selection()
|
|
731
|
+
|
|
732
|
+
def select_all(self):
|
|
733
|
+
''' Select all of the text displayed in this TextInput.
|
|
734
|
+
|
|
735
|
+
.. versionadded:: 1.4.0
|
|
736
|
+
'''
|
|
737
|
+
self.select_text(0, len(self.text))
|
|
738
|
+
|
|
739
|
+
re_indent = re.compile(r'^(\s*|)')
|
|
740
|
+
|
|
741
|
+
def _auto_indent(self, substring):
|
|
742
|
+
index = self.cursor_index()
|
|
743
|
+
if index > 0:
|
|
744
|
+
_text = self.text
|
|
745
|
+
line_start = _text.rfind('\n', 0, index)
|
|
746
|
+
if line_start > -1:
|
|
747
|
+
line = _text[line_start + 1:index]
|
|
748
|
+
indent = self.re_indent.match(line).group()
|
|
749
|
+
substring += indent
|
|
750
|
+
return substring
|
|
751
|
+
|
|
752
|
+
def insert_text(self, substring, from_undo=False):
|
|
753
|
+
'''Insert new text at the current cursor position. Override this
|
|
754
|
+
function in order to pre-process text for input validation.
|
|
755
|
+
'''
|
|
756
|
+
_lines = self._lines
|
|
757
|
+
_lines_flags = self._lines_flags
|
|
758
|
+
|
|
759
|
+
if self.readonly or not substring or not self._lines:
|
|
760
|
+
return
|
|
761
|
+
|
|
762
|
+
if isinstance(substring, bytes):
|
|
763
|
+
substring = substring.decode('utf8')
|
|
764
|
+
|
|
765
|
+
if self.replace_crlf:
|
|
766
|
+
substring = substring.replace(u'\r\n', u'\n')
|
|
767
|
+
|
|
768
|
+
self._hide_handles(EventLoop.window)
|
|
769
|
+
|
|
770
|
+
if not from_undo and self.multiline and self.auto_indent \
|
|
771
|
+
and substring == u'\n':
|
|
772
|
+
substring = self._auto_indent(substring)
|
|
773
|
+
|
|
774
|
+
mode = self.input_filter
|
|
775
|
+
if mode not in (None, 'int', 'float'):
|
|
776
|
+
substring = mode(substring, from_undo)
|
|
777
|
+
if not substring:
|
|
778
|
+
return
|
|
779
|
+
|
|
780
|
+
col, row = self.cursor
|
|
781
|
+
cindex = self.cursor_index()
|
|
782
|
+
text = _lines[row]
|
|
783
|
+
len_str = len(substring)
|
|
784
|
+
new_text = text[:col] + substring + text[col:]
|
|
785
|
+
if mode is not None:
|
|
786
|
+
if mode == 'int':
|
|
787
|
+
if not re.match(self._insert_int_pat, new_text):
|
|
788
|
+
return
|
|
789
|
+
elif mode == 'float':
|
|
790
|
+
if not re.match(self._insert_float_pat, new_text):
|
|
791
|
+
return
|
|
792
|
+
self._set_line_text(row, new_text)
|
|
793
|
+
|
|
794
|
+
if len_str > 1 or substring == u'\n' or\
|
|
795
|
+
(substring == u' ' and _lines_flags[row] != FL_IS_LINEBREAK) or\
|
|
796
|
+
(row + 1 < len(_lines) and
|
|
797
|
+
_lines_flags[row + 1] != FL_IS_LINEBREAK) or\
|
|
798
|
+
(self._get_text_width(
|
|
799
|
+
new_text,
|
|
800
|
+
self.tab_width,
|
|
801
|
+
self._label_cached) > (self.width - self.padding[0] -
|
|
802
|
+
self.padding[2])):
|
|
803
|
+
# Avoid refreshing text on every keystroke.
|
|
804
|
+
# Allows for faster typing of text when the amount of text in
|
|
805
|
+
# TextInput gets large.
|
|
806
|
+
|
|
807
|
+
(
|
|
808
|
+
start, finish, lines, lines_flags, len_lines
|
|
809
|
+
) = self._get_line_from_cursor(row, new_text)
|
|
810
|
+
|
|
811
|
+
# calling trigger here could lead to wrong cursor positioning
|
|
812
|
+
# and repeating of text when keys are added rapidly in a automated
|
|
813
|
+
# fashion. From Android Keyboard for example.
|
|
814
|
+
self._refresh_text_from_property(
|
|
815
|
+
'insert', start, finish, lines, lines_flags, len_lines
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
self.cursor = self.get_cursor_from_index(cindex + len_str)
|
|
819
|
+
# handle undo and redo
|
|
820
|
+
self._set_unredo_insert(cindex, cindex + len_str, substring, from_undo)
|
|
821
|
+
|
|
822
|
+
def _get_line_from_cursor(self, start, new_text, lines=None,
|
|
823
|
+
lines_flags=None):
|
|
824
|
+
# get current paragraph from cursor position
|
|
825
|
+
if lines is None:
|
|
826
|
+
lines = self._lines
|
|
827
|
+
if lines_flags is None:
|
|
828
|
+
lines_flags = self._lines_flags
|
|
829
|
+
finish = start
|
|
830
|
+
_next = start + 1
|
|
831
|
+
if start > 0 and lines_flags[start] != FL_IS_LINEBREAK:
|
|
832
|
+
start -= 1
|
|
833
|
+
new_text = lines[start] + new_text
|
|
834
|
+
i = _next
|
|
835
|
+
for i in range(_next, len(lines_flags)):
|
|
836
|
+
if lines_flags[i] == FL_IS_LINEBREAK:
|
|
837
|
+
finish = i - 1
|
|
838
|
+
break
|
|
839
|
+
else:
|
|
840
|
+
finish = i
|
|
841
|
+
|
|
842
|
+
new_text = new_text + u''.join(lines[_next:finish + 1])
|
|
843
|
+
lines, lines_flags = self._split_smart(new_text)
|
|
844
|
+
|
|
845
|
+
len_lines = max(1, len(lines))
|
|
846
|
+
return start, finish, lines, lines_flags, len_lines
|
|
847
|
+
|
|
848
|
+
def _set_unredo_insert(self, ci, sci, substring, from_undo):
|
|
849
|
+
# handle undo and redo
|
|
850
|
+
if from_undo:
|
|
851
|
+
return
|
|
852
|
+
self._undo.append({
|
|
853
|
+
'undo_command': ('insert', ci, sci),
|
|
854
|
+
'redo_command': (ci, substring)
|
|
855
|
+
})
|
|
856
|
+
# reset redo when undo is appended to
|
|
857
|
+
self._redo = []
|
|
858
|
+
|
|
859
|
+
def reset_undo(self):
|
|
860
|
+
'''Reset undo and redo lists from memory.
|
|
861
|
+
|
|
862
|
+
.. versionadded:: 1.3.0
|
|
863
|
+
|
|
864
|
+
'''
|
|
865
|
+
self._redo = self._undo = []
|
|
866
|
+
|
|
867
|
+
def do_redo(self):
|
|
868
|
+
'''Do redo operation.
|
|
869
|
+
|
|
870
|
+
.. versionadded:: 1.3.0
|
|
871
|
+
|
|
872
|
+
This action re-does any command that has been un-done by
|
|
873
|
+
do_undo/ctrl+z. This function is automatically called when
|
|
874
|
+
`ctrl+r` keys are pressed.
|
|
875
|
+
'''
|
|
876
|
+
try:
|
|
877
|
+
x_item = self._redo.pop()
|
|
878
|
+
undo_type = x_item['undo_command'][0]
|
|
879
|
+
_get_cusror_from_index = self.get_cursor_from_index
|
|
880
|
+
|
|
881
|
+
if undo_type == 'insert':
|
|
882
|
+
cindex, substring = x_item['redo_command']
|
|
883
|
+
self.cursor = _get_cusror_from_index(cindex)
|
|
884
|
+
self.insert_text(substring, True)
|
|
885
|
+
elif undo_type == 'bkspc':
|
|
886
|
+
self.cursor = _get_cusror_from_index(x_item['redo_command'])
|
|
887
|
+
self.do_backspace(from_undo=True)
|
|
888
|
+
elif undo_type == 'shiftln':
|
|
889
|
+
direction, rows, cursor = x_item['redo_command'][1:]
|
|
890
|
+
self._shift_lines(direction, rows, cursor, True)
|
|
891
|
+
else:
|
|
892
|
+
# delsel
|
|
893
|
+
cindex, scindex = x_item['redo_command']
|
|
894
|
+
self._selection_from = cindex
|
|
895
|
+
self._selection_to = scindex
|
|
896
|
+
self._selection = True
|
|
897
|
+
self.delete_selection(True)
|
|
898
|
+
self.cursor = _get_cusror_from_index(cindex)
|
|
899
|
+
self._undo.append(x_item)
|
|
900
|
+
except IndexError:
|
|
901
|
+
# reached at top of undo list
|
|
902
|
+
pass
|
|
903
|
+
|
|
904
|
+
def do_undo(self):
|
|
905
|
+
'''Do undo operation.
|
|
906
|
+
|
|
907
|
+
.. versionadded:: 1.3.0
|
|
908
|
+
|
|
909
|
+
This action un-does any edits that have been made since the last
|
|
910
|
+
call to reset_undo().
|
|
911
|
+
This function is automatically called when `ctrl+z` keys are pressed.
|
|
912
|
+
'''
|
|
913
|
+
try:
|
|
914
|
+
x_item = self._undo.pop()
|
|
915
|
+
undo_type = x_item['undo_command'][0]
|
|
916
|
+
self.cursor = self.get_cursor_from_index(x_item['undo_command'][1])
|
|
917
|
+
|
|
918
|
+
if undo_type == 'insert':
|
|
919
|
+
cindex, scindex = x_item['undo_command'][1:]
|
|
920
|
+
self._selection_from = cindex
|
|
921
|
+
self._selection_to = scindex
|
|
922
|
+
self._selection = True
|
|
923
|
+
self.delete_selection(True)
|
|
924
|
+
elif undo_type == 'bkspc':
|
|
925
|
+
substring = x_item['undo_command'][2][0]
|
|
926
|
+
mode = x_item['undo_command'][3]
|
|
927
|
+
self.insert_text(substring, True)
|
|
928
|
+
if mode == 'del':
|
|
929
|
+
self.cursor = self.get_cursor_from_index(
|
|
930
|
+
self.cursor_index() - 1)
|
|
931
|
+
elif undo_type == 'shiftln':
|
|
932
|
+
direction, rows, cursor = x_item['undo_command'][1:]
|
|
933
|
+
self._shift_lines(direction, rows, cursor, True)
|
|
934
|
+
else:
|
|
935
|
+
# delsel
|
|
936
|
+
substring = x_item['undo_command'][2:][0]
|
|
937
|
+
self.insert_text(substring, True)
|
|
938
|
+
self._redo.append(x_item)
|
|
939
|
+
self.scroll_x = self.get_max_scroll_x()
|
|
940
|
+
except IndexError:
|
|
941
|
+
# reached at top of undo list
|
|
942
|
+
pass
|
|
943
|
+
|
|
944
|
+
def do_backspace(self, from_undo=False, mode='bkspc'):
|
|
945
|
+
'''Do backspace operation from the current cursor position.
|
|
946
|
+
This action might do several things:
|
|
947
|
+
|
|
948
|
+
- removing the current selection if available.
|
|
949
|
+
- removing the previous char and move the cursor back.
|
|
950
|
+
- do nothing, if we are at the start.
|
|
951
|
+
|
|
952
|
+
'''
|
|
953
|
+
# IME system handles its own backspaces
|
|
954
|
+
if self.readonly or self._ime_composition:
|
|
955
|
+
return
|
|
956
|
+
col, row = self.cursor
|
|
957
|
+
_lines = self._lines
|
|
958
|
+
_lines_flags = self._lines_flags
|
|
959
|
+
text = _lines[row]
|
|
960
|
+
cursor_index = self.cursor_index()
|
|
961
|
+
|
|
962
|
+
if col == 0 and row == 0:
|
|
963
|
+
return
|
|
964
|
+
start = row
|
|
965
|
+
if col == 0:
|
|
966
|
+
if _lines_flags[row] == FL_IS_LINEBREAK:
|
|
967
|
+
substring = u'\n'
|
|
968
|
+
new_text = _lines[row - 1] + text
|
|
969
|
+
else:
|
|
970
|
+
substring = _lines[row - 1][-1] if len(_lines[row - 1]) > 0 \
|
|
971
|
+
else u''
|
|
972
|
+
new_text = _lines[row - 1][:-1] + text
|
|
973
|
+
self._set_line_text(row - 1, new_text)
|
|
974
|
+
self._delete_line(row)
|
|
975
|
+
start = row - 1
|
|
976
|
+
else:
|
|
977
|
+
# ch = text[col-1]
|
|
978
|
+
substring = text[col - 1]
|
|
979
|
+
new_text = text[:col - 1] + text[col:]
|
|
980
|
+
self._set_line_text(row, new_text)
|
|
981
|
+
|
|
982
|
+
# refresh just the current line instead of the whole text
|
|
983
|
+
start, finish, lines, lineflags, len_lines = (
|
|
984
|
+
self._get_line_from_cursor(start, new_text)
|
|
985
|
+
)
|
|
986
|
+
# avoid trigger refresh, leads to issue with
|
|
987
|
+
# keys/text send rapidly through code.
|
|
988
|
+
self._refresh_text_from_property(
|
|
989
|
+
'insert' if col == 0 else 'del', start, finish,
|
|
990
|
+
lines, lineflags, len_lines
|
|
991
|
+
)
|
|
992
|
+
|
|
993
|
+
self.cursor = self.get_cursor_from_index(cursor_index - 1)
|
|
994
|
+
# handle undo and redo
|
|
995
|
+
self._set_unredo_bkspc(
|
|
996
|
+
cursor_index,
|
|
997
|
+
cursor_index - 1,
|
|
998
|
+
substring, from_undo, mode)
|
|
999
|
+
self.scroll_x = self.get_max_scroll_x()
|
|
1000
|
+
|
|
1001
|
+
def _set_unredo_bkspc(self, ol_index, new_index, substring, from_undo,
|
|
1002
|
+
mode):
|
|
1003
|
+
# handle undo and redo for backspace
|
|
1004
|
+
if from_undo:
|
|
1005
|
+
return
|
|
1006
|
+
self._undo.append({
|
|
1007
|
+
'undo_command': ('bkspc', new_index, substring, mode),
|
|
1008
|
+
'redo_command': ol_index})
|
|
1009
|
+
# reset redo when undo is appended to
|
|
1010
|
+
self._redo = []
|
|
1011
|
+
|
|
1012
|
+
_re_whitespace = re.compile(r'\s+')
|
|
1013
|
+
|
|
1014
|
+
def _move_cursor_word_left(self, index=None):
|
|
1015
|
+
pos = index or self.cursor_index()
|
|
1016
|
+
if pos == 0:
|
|
1017
|
+
return self.cursor
|
|
1018
|
+
lines = self._lines
|
|
1019
|
+
col, row = self.get_cursor_from_index(pos)
|
|
1020
|
+
if col == 0:
|
|
1021
|
+
row -= 1
|
|
1022
|
+
col = len(lines[row])
|
|
1023
|
+
|
|
1024
|
+
while True:
|
|
1025
|
+
matches = list(self._re_whitespace.finditer(lines[row], 0, col))
|
|
1026
|
+
if not matches:
|
|
1027
|
+
if col == 0:
|
|
1028
|
+
if row == 0:
|
|
1029
|
+
return 0, 0
|
|
1030
|
+
row -= 1
|
|
1031
|
+
col = len(lines[row])
|
|
1032
|
+
continue
|
|
1033
|
+
return 0, row
|
|
1034
|
+
|
|
1035
|
+
match = matches[-1]
|
|
1036
|
+
mpos = match.end()
|
|
1037
|
+
if mpos == col:
|
|
1038
|
+
if len(matches) > 1:
|
|
1039
|
+
match = matches[-2]
|
|
1040
|
+
mpos = match.end()
|
|
1041
|
+
else:
|
|
1042
|
+
if match.start() == 0:
|
|
1043
|
+
if row == 0:
|
|
1044
|
+
return 0, 0
|
|
1045
|
+
row -= 1
|
|
1046
|
+
col = len(lines[row])
|
|
1047
|
+
continue
|
|
1048
|
+
return 0, row
|
|
1049
|
+
col = mpos
|
|
1050
|
+
return col, row
|
|
1051
|
+
|
|
1052
|
+
def _move_cursor_word_right(self, index=None):
|
|
1053
|
+
pos = index or self.cursor_index()
|
|
1054
|
+
col, row = self.get_cursor_from_index(pos)
|
|
1055
|
+
lines = self._lines
|
|
1056
|
+
mrow = len(lines) - 1
|
|
1057
|
+
if row == mrow and col == len(lines[row]):
|
|
1058
|
+
return col, row
|
|
1059
|
+
if col == len(lines[row]):
|
|
1060
|
+
row += 1
|
|
1061
|
+
col = 0
|
|
1062
|
+
|
|
1063
|
+
while True:
|
|
1064
|
+
matches = list(self._re_whitespace.finditer(lines[row], col))
|
|
1065
|
+
if not matches:
|
|
1066
|
+
if col == len(lines[row]):
|
|
1067
|
+
if row == mrow:
|
|
1068
|
+
return col, row
|
|
1069
|
+
row += 1
|
|
1070
|
+
col = 0
|
|
1071
|
+
continue
|
|
1072
|
+
return len(lines[row]), row
|
|
1073
|
+
|
|
1074
|
+
match = matches[0]
|
|
1075
|
+
mpos = match.start()
|
|
1076
|
+
if mpos == col:
|
|
1077
|
+
if len(matches) > 1:
|
|
1078
|
+
match = matches[1]
|
|
1079
|
+
mpos = match.start()
|
|
1080
|
+
else:
|
|
1081
|
+
if match.end() == len(lines[row]):
|
|
1082
|
+
if row == mrow:
|
|
1083
|
+
return col, row
|
|
1084
|
+
row += 1
|
|
1085
|
+
col = 0
|
|
1086
|
+
continue
|
|
1087
|
+
return len(lines[row]), row
|
|
1088
|
+
col = mpos
|
|
1089
|
+
return col, row
|
|
1090
|
+
|
|
1091
|
+
def _expand_range(self, ifrom, ito=None):
|
|
1092
|
+
if ito is None:
|
|
1093
|
+
ito = ifrom
|
|
1094
|
+
rfrom = self.get_cursor_from_index(ifrom)[1]
|
|
1095
|
+
rtcol, rto = self.get_cursor_from_index(ito)
|
|
1096
|
+
rfrom, rto = self._expand_rows(rfrom, rto + 1 if rtcol else rto)
|
|
1097
|
+
|
|
1098
|
+
return (self.cursor_index((0, rfrom)),
|
|
1099
|
+
self.cursor_index((0, rto)))
|
|
1100
|
+
|
|
1101
|
+
def _expand_rows(self, rfrom, rto=None):
|
|
1102
|
+
if rto is None or rto == rfrom:
|
|
1103
|
+
rto = rfrom + 1
|
|
1104
|
+
lines = self._lines
|
|
1105
|
+
flags = list(reversed(self._lines_flags))
|
|
1106
|
+
while rfrom > 0 and not (flags[rfrom - 1] & FL_IS_NEWLINE):
|
|
1107
|
+
rfrom -= 1
|
|
1108
|
+
rmax = len(lines) - 1
|
|
1109
|
+
while 0 < rto < rmax and not (flags[rto - 1] & FL_IS_NEWLINE):
|
|
1110
|
+
rto += 1
|
|
1111
|
+
return max(0, rfrom), min(rmax, rto)
|
|
1112
|
+
|
|
1113
|
+
def _shift_lines(
|
|
1114
|
+
self, direction, rows=None, old_cursor=None, from_undo=False
|
|
1115
|
+
):
|
|
1116
|
+
if self._selection_callback:
|
|
1117
|
+
if from_undo:
|
|
1118
|
+
self._selection_callback.cancel()
|
|
1119
|
+
else:
|
|
1120
|
+
return
|
|
1121
|
+
|
|
1122
|
+
lines = self._lines
|
|
1123
|
+
flags = list(reversed(self._lines_flags))
|
|
1124
|
+
labels = self._lines_labels
|
|
1125
|
+
rects = self._lines_rects
|
|
1126
|
+
orig_cursor = self.cursor
|
|
1127
|
+
sel = None
|
|
1128
|
+
if old_cursor is not None:
|
|
1129
|
+
self.cursor = old_cursor
|
|
1130
|
+
|
|
1131
|
+
if not rows:
|
|
1132
|
+
sindex = self.selection_from
|
|
1133
|
+
eindex = self.selection_to
|
|
1134
|
+
if (sindex or eindex) and sindex != eindex:
|
|
1135
|
+
sindex, eindex = tuple(sorted((sindex, eindex)))
|
|
1136
|
+
sindex, eindex = self._expand_range(sindex, eindex)
|
|
1137
|
+
else:
|
|
1138
|
+
sindex, eindex = self._expand_range(self.cursor_index())
|
|
1139
|
+
srow = self.get_cursor_from_index(sindex)[1]
|
|
1140
|
+
erow = self.get_cursor_from_index(eindex)[1]
|
|
1141
|
+
sel = sindex, eindex
|
|
1142
|
+
|
|
1143
|
+
if direction < 0 and srow > 0:
|
|
1144
|
+
psrow, perow = self._expand_rows(srow - 1)
|
|
1145
|
+
rows = ((srow, erow), (psrow, perow))
|
|
1146
|
+
elif direction > 0 and erow < len(lines) - 1:
|
|
1147
|
+
psrow, perow = self._expand_rows(erow)
|
|
1148
|
+
rows = ((srow, erow), (psrow, perow))
|
|
1149
|
+
|
|
1150
|
+
else:
|
|
1151
|
+
(srow, erow), (psrow, perow) = rows
|
|
1152
|
+
if direction < 0:
|
|
1153
|
+
m1srow, m1erow = psrow, perow
|
|
1154
|
+
m2srow, m2erow = srow, erow
|
|
1155
|
+
cdiff = psrow - perow
|
|
1156
|
+
xdiff = srow - erow
|
|
1157
|
+
else:
|
|
1158
|
+
m1srow, m1erow = srow, erow
|
|
1159
|
+
m2srow, m2erow = psrow, perow
|
|
1160
|
+
cdiff = perow - psrow
|
|
1161
|
+
xdiff = erow - srow
|
|
1162
|
+
|
|
1163
|
+
self._lines_flags = list(reversed(chain(
|
|
1164
|
+
flags[:m1srow],
|
|
1165
|
+
flags[m2srow:m2erow],
|
|
1166
|
+
flags[m1srow:m1erow],
|
|
1167
|
+
flags[m2erow:],
|
|
1168
|
+
)))
|
|
1169
|
+
self._lines[:] = (
|
|
1170
|
+
lines[:m1srow]
|
|
1171
|
+
+ lines[m2srow:m2erow]
|
|
1172
|
+
+ lines[m1srow:m1erow]
|
|
1173
|
+
+ lines[m2erow:]
|
|
1174
|
+
)
|
|
1175
|
+
self._lines_labels = (
|
|
1176
|
+
labels[:m1srow]
|
|
1177
|
+
+ labels[m2srow:m2erow]
|
|
1178
|
+
+ labels[m1srow:m1erow]
|
|
1179
|
+
+ labels[m2erow:]
|
|
1180
|
+
)
|
|
1181
|
+
self._lines_rects = (
|
|
1182
|
+
rects[:m1srow]
|
|
1183
|
+
+ rects[m2srow:m2erow]
|
|
1184
|
+
+ rects[m1srow:m1erow]
|
|
1185
|
+
+ rects[m2erow:]
|
|
1186
|
+
)
|
|
1187
|
+
self._trigger_update_graphics()
|
|
1188
|
+
csrow = srow + cdiff
|
|
1189
|
+
cerow = erow + cdiff
|
|
1190
|
+
sel = (
|
|
1191
|
+
self.cursor_index((0, csrow)),
|
|
1192
|
+
self.cursor_index((0, cerow))
|
|
1193
|
+
)
|
|
1194
|
+
self.cursor = self.cursor_col, self.cursor_row + cdiff
|
|
1195
|
+
|
|
1196
|
+
if not from_undo:
|
|
1197
|
+
undo_rows = ((srow + cdiff, erow + cdiff),
|
|
1198
|
+
(psrow - xdiff, perow - xdiff))
|
|
1199
|
+
self._undo.append({
|
|
1200
|
+
'undo_command': ('shiftln', direction * -1, undo_rows,
|
|
1201
|
+
self.cursor),
|
|
1202
|
+
'redo_command': ('shiftln', direction, rows, orig_cursor),
|
|
1203
|
+
})
|
|
1204
|
+
self._redo = []
|
|
1205
|
+
|
|
1206
|
+
if sel:
|
|
1207
|
+
def cb(dt):
|
|
1208
|
+
self.select_text(*sel)
|
|
1209
|
+
self._selection_callback = None
|
|
1210
|
+
self._selection_callback = Clock.schedule_once(cb)
|
|
1211
|
+
|
|
1212
|
+
@property
|
|
1213
|
+
def pgmove_speed(self):
|
|
1214
|
+
"""how much vertical distance hitting pg_up or pg_down will move
|
|
1215
|
+
"""
|
|
1216
|
+
return int(
|
|
1217
|
+
self.height
|
|
1218
|
+
/ (self.line_height + self.line_spacing) - 1
|
|
1219
|
+
)
|
|
1220
|
+
|
|
1221
|
+
def _move_cursor_up(self, col, row, control=False, alt=False):
|
|
1222
|
+
if self.multiline and control:
|
|
1223
|
+
self.scroll_y = max(0, self.scroll_y - self.line_height)
|
|
1224
|
+
elif not self.readonly and self.multiline and alt:
|
|
1225
|
+
self._shift_lines(-1)
|
|
1226
|
+
return
|
|
1227
|
+
else:
|
|
1228
|
+
row = max(row - 1, 0)
|
|
1229
|
+
col = min(len(self._lines[row]), col)
|
|
1230
|
+
|
|
1231
|
+
return col, row
|
|
1232
|
+
|
|
1233
|
+
def _move_cursor_down(self, col, row, control, alt):
|
|
1234
|
+
if self.multiline and control:
|
|
1235
|
+
maxy = self.minimum_height - self.height
|
|
1236
|
+
self.scroll_y = max(
|
|
1237
|
+
0,
|
|
1238
|
+
min(maxy, self.scroll_y + self.line_height)
|
|
1239
|
+
)
|
|
1240
|
+
elif not self.readonly and self.multiline and alt:
|
|
1241
|
+
self._shift_lines(1)
|
|
1242
|
+
return
|
|
1243
|
+
else:
|
|
1244
|
+
row = min(row + 1, len(self._lines) - 1)
|
|
1245
|
+
col = min(len(self._lines[row]), col)
|
|
1246
|
+
|
|
1247
|
+
return col, row
|
|
1248
|
+
|
|
1249
|
+
def do_cursor_movement(self, action, control=False, alt=False):
|
|
1250
|
+
'''Move the cursor relative to its current position.
|
|
1251
|
+
Action can be one of :
|
|
1252
|
+
|
|
1253
|
+
- cursor_left: move the cursor to the left
|
|
1254
|
+
- cursor_right: move the cursor to the right
|
|
1255
|
+
- cursor_up: move the cursor on the previous line
|
|
1256
|
+
- cursor_down: move the cursor on the next line
|
|
1257
|
+
- cursor_home: move the cursor at the start of the current line
|
|
1258
|
+
- cursor_end: move the cursor at the end of current line
|
|
1259
|
+
- cursor_pgup: move one "page" before
|
|
1260
|
+
- cursor_pgdown: move one "page" after
|
|
1261
|
+
|
|
1262
|
+
In addition, the behavior of certain actions can be modified:
|
|
1263
|
+
|
|
1264
|
+
- control + cursor_left: move the cursor one word to the left
|
|
1265
|
+
- control + cursor_right: move the cursor one word to the right
|
|
1266
|
+
- control + cursor_up: scroll up one line
|
|
1267
|
+
- control + cursor_down: scroll down one line
|
|
1268
|
+
- control + cursor_home: go to beginning of text
|
|
1269
|
+
- control + cursor_end: go to end of text
|
|
1270
|
+
- alt + cursor_up: shift line(s) up
|
|
1271
|
+
- alt + cursor_down: shift line(s) down
|
|
1272
|
+
|
|
1273
|
+
.. versionchanged:: 1.9.1
|
|
1274
|
+
|
|
1275
|
+
'''
|
|
1276
|
+
if not self._lines:
|
|
1277
|
+
return
|
|
1278
|
+
|
|
1279
|
+
col, row = self.cursor
|
|
1280
|
+
if action == 'cursor_up':
|
|
1281
|
+
result = self._move_cursor_up(col, row, control, alt)
|
|
1282
|
+
if result:
|
|
1283
|
+
col, row = result
|
|
1284
|
+
else:
|
|
1285
|
+
return
|
|
1286
|
+
|
|
1287
|
+
elif action == 'cursor_down':
|
|
1288
|
+
result = self._move_cursor_down(col, row, control, alt)
|
|
1289
|
+
if result:
|
|
1290
|
+
col, row = result
|
|
1291
|
+
else:
|
|
1292
|
+
return
|
|
1293
|
+
|
|
1294
|
+
elif action == 'cursor_home':
|
|
1295
|
+
col = 0
|
|
1296
|
+
if control:
|
|
1297
|
+
row = 0
|
|
1298
|
+
|
|
1299
|
+
elif action == 'cursor_end':
|
|
1300
|
+
if control:
|
|
1301
|
+
row = len(self._lines) - 1
|
|
1302
|
+
col = len(self._lines[row])
|
|
1303
|
+
|
|
1304
|
+
elif action == 'cursor_pgup':
|
|
1305
|
+
row = max(0, row - self.pgmove_speed)
|
|
1306
|
+
col = min(len(self._lines[row]), col)
|
|
1307
|
+
|
|
1308
|
+
elif action == 'cursor_pgdown':
|
|
1309
|
+
row = min(row + self.pgmove_speed, len(self._lines) - 1)
|
|
1310
|
+
col = min(len(self._lines[row]), col)
|
|
1311
|
+
|
|
1312
|
+
elif (
|
|
1313
|
+
self._selection and self._selection_finished
|
|
1314
|
+
and self._selection_from < self._selection_to
|
|
1315
|
+
and action == 'cursor_left'
|
|
1316
|
+
):
|
|
1317
|
+
current_selection_to = self._selection_to
|
|
1318
|
+
while self._selection_from != current_selection_to:
|
|
1319
|
+
current_selection_to -= 1
|
|
1320
|
+
if col:
|
|
1321
|
+
col -= 1
|
|
1322
|
+
else:
|
|
1323
|
+
row -= 1
|
|
1324
|
+
col = len(self._lines[row])
|
|
1325
|
+
|
|
1326
|
+
elif (
|
|
1327
|
+
self._selection and self._selection_finished
|
|
1328
|
+
and self._selection_from > self._selection_to
|
|
1329
|
+
and action == 'cursor_right'
|
|
1330
|
+
):
|
|
1331
|
+
current_selection_to = self._selection_to
|
|
1332
|
+
while self._selection_from != current_selection_to:
|
|
1333
|
+
current_selection_to += 1
|
|
1334
|
+
if len(self._lines[row]) > col:
|
|
1335
|
+
col += 1
|
|
1336
|
+
else:
|
|
1337
|
+
row += 1
|
|
1338
|
+
col = 0
|
|
1339
|
+
|
|
1340
|
+
elif action == 'cursor_left':
|
|
1341
|
+
if not self.password and control:
|
|
1342
|
+
col, row = self._move_cursor_word_left()
|
|
1343
|
+
else:
|
|
1344
|
+
if col == 0:
|
|
1345
|
+
if row:
|
|
1346
|
+
row -= 1
|
|
1347
|
+
col = len(self._lines[row])
|
|
1348
|
+
else:
|
|
1349
|
+
col, row = col - 1, row
|
|
1350
|
+
|
|
1351
|
+
elif action == 'cursor_right':
|
|
1352
|
+
if not self.password and control:
|
|
1353
|
+
col, row = self._move_cursor_word_right()
|
|
1354
|
+
else:
|
|
1355
|
+
if col == len(self._lines[row]):
|
|
1356
|
+
if row < len(self._lines) - 1:
|
|
1357
|
+
col = 0
|
|
1358
|
+
row += 1
|
|
1359
|
+
else:
|
|
1360
|
+
col, row = col + 1, row
|
|
1361
|
+
|
|
1362
|
+
dont_move_cursor = control and action in ['cursor_up', 'cursor_down']
|
|
1363
|
+
if dont_move_cursor:
|
|
1364
|
+
self._trigger_update_graphics()
|
|
1365
|
+
else:
|
|
1366
|
+
self.cursor = col, row
|
|
1367
|
+
|
|
1368
|
+
def get_cursor_from_xy(self, x, y):
|
|
1369
|
+
'''Return the (col, row) of the cursor from an (x, y) position.
|
|
1370
|
+
'''
|
|
1371
|
+
padding_left, padding_top, padding_right, padding_bottom = self.padding
|
|
1372
|
+
|
|
1373
|
+
lines = self._lines
|
|
1374
|
+
dy = self.line_height + self.line_spacing
|
|
1375
|
+
cursor_x = x - self.x
|
|
1376
|
+
scroll_y = self.scroll_y
|
|
1377
|
+
scroll_x = self.scroll_x
|
|
1378
|
+
scroll_y = scroll_y / dy if scroll_y > 0 else 0
|
|
1379
|
+
|
|
1380
|
+
cursor_y = (self.top - padding_top + scroll_y * dy) - y
|
|
1381
|
+
cursor_y = int(boundary(
|
|
1382
|
+
round(cursor_y / dy - 0.5),
|
|
1383
|
+
0,
|
|
1384
|
+
len(lines) - 1
|
|
1385
|
+
))
|
|
1386
|
+
|
|
1387
|
+
get_text_width = self._get_text_width
|
|
1388
|
+
tab_width = self.tab_width
|
|
1389
|
+
label_cached = self._label_cached
|
|
1390
|
+
|
|
1391
|
+
# Offset for horizontal text alignment
|
|
1392
|
+
xoff = 0
|
|
1393
|
+
halign = self.halign
|
|
1394
|
+
base_dir = self.base_direction or self._resolved_base_dir
|
|
1395
|
+
auto_halign_r = halign == 'auto' and base_dir and 'rtl' in base_dir
|
|
1396
|
+
|
|
1397
|
+
if halign == 'center':
|
|
1398
|
+
viewport_width = self.width - padding_left - padding_right
|
|
1399
|
+
xoff = max(
|
|
1400
|
+
0, int((viewport_width - self._get_row_width(cursor_y)) / 2)
|
|
1401
|
+
)
|
|
1402
|
+
|
|
1403
|
+
elif halign == 'right' or auto_halign_r:
|
|
1404
|
+
viewport_width = self.width - padding_left - padding_right
|
|
1405
|
+
xoff = max(
|
|
1406
|
+
0, int(viewport_width - self._get_row_width(cursor_y))
|
|
1407
|
+
)
|
|
1408
|
+
|
|
1409
|
+
for i in range(0, len(lines[cursor_y])):
|
|
1410
|
+
line_y = lines[cursor_y]
|
|
1411
|
+
|
|
1412
|
+
if cursor_x + scroll_x < (
|
|
1413
|
+
xoff
|
|
1414
|
+
+ get_text_width(line_y[:i], tab_width, label_cached)
|
|
1415
|
+
+ get_text_width(line_y[i], tab_width, label_cached) * 0.6
|
|
1416
|
+
+ padding_left
|
|
1417
|
+
):
|
|
1418
|
+
cursor_x = i
|
|
1419
|
+
break
|
|
1420
|
+
else:
|
|
1421
|
+
cursor_x = len(lines[cursor_y])
|
|
1422
|
+
|
|
1423
|
+
return cursor_x, cursor_y
|
|
1424
|
+
|
|
1425
|
+
def get_max_scroll_x(self):
|
|
1426
|
+
'''
|
|
1427
|
+
Return how many pixels it needs to scroll to the right
|
|
1428
|
+
to reveal the remaining content of a text that extends
|
|
1429
|
+
beyond the visible width of a TextInput
|
|
1430
|
+
'''
|
|
1431
|
+
minimum_width = self._get_row_width(0) + self.padding[0] + \
|
|
1432
|
+
self.padding[2]
|
|
1433
|
+
max_scroll_x = max(0, minimum_width - self.width)
|
|
1434
|
+
return max_scroll_x
|
|
1435
|
+
|
|
1436
|
+
#
|
|
1437
|
+
# Selection control
|
|
1438
|
+
#
|
|
1439
|
+
def cancel_selection(self):
|
|
1440
|
+
'''Cancel current selection (if any).
|
|
1441
|
+
'''
|
|
1442
|
+
self._selection_from = self._selection_to = self.cursor_index()
|
|
1443
|
+
self._selection = False
|
|
1444
|
+
self._selection_finished = True
|
|
1445
|
+
self._selection_touch = None
|
|
1446
|
+
self.selection_text = u''
|
|
1447
|
+
self._trigger_update_graphics()
|
|
1448
|
+
|
|
1449
|
+
def delete_selection(self, from_undo=False):
|
|
1450
|
+
'''Delete the current text selection (if any).
|
|
1451
|
+
'''
|
|
1452
|
+
if self.readonly:
|
|
1453
|
+
return
|
|
1454
|
+
self._hide_handles(EventLoop.window)
|
|
1455
|
+
scroll_x = self.scroll_x
|
|
1456
|
+
scroll_y = self.scroll_y
|
|
1457
|
+
cc, cr = self.cursor
|
|
1458
|
+
if not self._selection:
|
|
1459
|
+
return
|
|
1460
|
+
text = self.text
|
|
1461
|
+
a, b = sorted((self._selection_from, self._selection_to))
|
|
1462
|
+
|
|
1463
|
+
start = self.get_cursor_from_index(a)
|
|
1464
|
+
finish = self.get_cursor_from_index(b)
|
|
1465
|
+
cur_line = self._lines[start[1]][:start[0]] +\
|
|
1466
|
+
self._lines[finish[1]][finish[0]:]
|
|
1467
|
+
|
|
1468
|
+
self._set_line_text(start[1], cur_line)
|
|
1469
|
+
start_del, finish_del, lines, lines_flags, len_lines = \
|
|
1470
|
+
self._get_line_from_cursor(start[1], cur_line,
|
|
1471
|
+
lines=(self._lines[:(start[1] + 1)] +
|
|
1472
|
+
self._lines[(finish[1] + 1):]),
|
|
1473
|
+
lines_flags=(
|
|
1474
|
+
self._lines_flags[:(start[1] + 1)] +
|
|
1475
|
+
self._lines_flags[(finish[1] + 1):])
|
|
1476
|
+
)
|
|
1477
|
+
self._refresh_text_from_property('del', start_del,
|
|
1478
|
+
finish_del + (finish[1] - start[1]),
|
|
1479
|
+
lines, lines_flags, len_lines)
|
|
1480
|
+
|
|
1481
|
+
self.scroll_x = scroll_x
|
|
1482
|
+
self.scroll_y = scroll_y
|
|
1483
|
+
# handle undo and redo for delete selection
|
|
1484
|
+
if text[a:b]:
|
|
1485
|
+
self._set_unredo_delsel(a, b, text[a:b], from_undo)
|
|
1486
|
+
self.cancel_selection()
|
|
1487
|
+
self.cursor = self.get_cursor_from_index(a)
|
|
1488
|
+
|
|
1489
|
+
def _set_unredo_delsel(self, a, b, substring, from_undo):
|
|
1490
|
+
# handle undo and redo for backspace
|
|
1491
|
+
if from_undo:
|
|
1492
|
+
return
|
|
1493
|
+
|
|
1494
|
+
self._undo.append({
|
|
1495
|
+
'undo_command': ('delsel', a, substring),
|
|
1496
|
+
'redo_command': (a, b)})
|
|
1497
|
+
# reset redo when undo is appended to
|
|
1498
|
+
self._redo = []
|
|
1499
|
+
|
|
1500
|
+
def _update_selection(self, finished=False):
|
|
1501
|
+
'''Update selection text and order of from/to if finished is True.
|
|
1502
|
+
Can be called multiple times until finished is True.
|
|
1503
|
+
'''
|
|
1504
|
+
a, b = int(self._selection_from), int(self._selection_to)
|
|
1505
|
+
if a > b:
|
|
1506
|
+
a, b = b, a
|
|
1507
|
+
self._selection_finished = finished
|
|
1508
|
+
_selection_text = self.text[a:b]
|
|
1509
|
+
self.selection_text = ("" if not self.allow_copy else
|
|
1510
|
+
((self.password_mask * (b - a)) if
|
|
1511
|
+
self.password else _selection_text))
|
|
1512
|
+
if not finished:
|
|
1513
|
+
self._selection = True
|
|
1514
|
+
else:
|
|
1515
|
+
self._selection = bool(len(_selection_text))
|
|
1516
|
+
self._selection_touch = None
|
|
1517
|
+
if a == 0:
|
|
1518
|
+
# update graphics only on new line
|
|
1519
|
+
# allows smoother scrolling, noticeably
|
|
1520
|
+
# faster when dealing with large text.
|
|
1521
|
+
self._update_graphics_selection()
|
|
1522
|
+
# self._trigger_update_graphics()
|
|
1523
|
+
|
|
1524
|
+
#
|
|
1525
|
+
# Touch control
|
|
1526
|
+
#
|
|
1527
|
+
def long_touch(self, dt):
|
|
1528
|
+
self._long_touch_ev = None
|
|
1529
|
+
if self._selection_to == self._selection_from:
|
|
1530
|
+
pos = self.to_local(*self._touch_down.pos, relative=False)
|
|
1531
|
+
self._show_cut_copy_paste(
|
|
1532
|
+
pos, EventLoop.window, mode='paste')
|
|
1533
|
+
|
|
1534
|
+
def cancel_long_touch_event(self):
|
|
1535
|
+
# schedule long touch for paste
|
|
1536
|
+
if self._long_touch_ev is not None:
|
|
1537
|
+
self._long_touch_ev.cancel()
|
|
1538
|
+
self._long_touch_ev = None
|
|
1539
|
+
|
|
1540
|
+
def _select_word(self, delimiters=u' .,:;!?\'"<>()[]{}'):
|
|
1541
|
+
cindex = self.cursor_index()
|
|
1542
|
+
col = self.cursor_col
|
|
1543
|
+
line = self._lines[self.cursor_row]
|
|
1544
|
+
start = max(0, len(line[:col]) -
|
|
1545
|
+
max(line[:col].rfind(s) for s in delimiters) - 1)
|
|
1546
|
+
end = min((line[col:].find(s) if line[col:].find(s) > -1
|
|
1547
|
+
else (len(line) - col)) for s in delimiters)
|
|
1548
|
+
Clock.schedule_once(lambda dt: self.select_text(cindex - start,
|
|
1549
|
+
cindex + end))
|
|
1550
|
+
|
|
1551
|
+
def on_double_tap(self):
|
|
1552
|
+
'''This event is dispatched when a double tap happens
|
|
1553
|
+
inside TextInput. The default behavior is to select the
|
|
1554
|
+
word around the current cursor position. Override this to provide
|
|
1555
|
+
different behavior. Alternatively, you can bind to this
|
|
1556
|
+
event to provide additional functionality.
|
|
1557
|
+
'''
|
|
1558
|
+
self._select_word()
|
|
1559
|
+
|
|
1560
|
+
def on_triple_tap(self):
|
|
1561
|
+
'''This event is dispatched when a triple tap happens
|
|
1562
|
+
inside TextInput. The default behavior is to select the
|
|
1563
|
+
line around current cursor position. Override this to provide
|
|
1564
|
+
different behavior. Alternatively, you can bind to this
|
|
1565
|
+
event to provide additional functionality.
|
|
1566
|
+
'''
|
|
1567
|
+
ci = self.cursor_index()
|
|
1568
|
+
sindex, eindex = self._expand_range(ci)
|
|
1569
|
+
Clock.schedule_once(lambda dt: self.select_text(sindex, eindex))
|
|
1570
|
+
|
|
1571
|
+
def on_quad_touch(self):
|
|
1572
|
+
'''This event is dispatched when four fingers are touching
|
|
1573
|
+
inside TextInput. The default behavior is to select all text.
|
|
1574
|
+
Override this to provide different behavior. Alternatively,
|
|
1575
|
+
you can bind to this event to provide additional functionality.
|
|
1576
|
+
'''
|
|
1577
|
+
Clock.schedule_once(lambda dt: self.select_all())
|
|
1578
|
+
|
|
1579
|
+
def on_touch_down(self, touch):
|
|
1580
|
+
if self.disabled:
|
|
1581
|
+
return
|
|
1582
|
+
|
|
1583
|
+
touch_pos = touch.pos
|
|
1584
|
+
if not self.collide_point(*touch_pos):
|
|
1585
|
+
return False
|
|
1586
|
+
if super().on_touch_down(touch):
|
|
1587
|
+
return True
|
|
1588
|
+
|
|
1589
|
+
if self.focus:
|
|
1590
|
+
self._trigger_cursor_reset()
|
|
1591
|
+
|
|
1592
|
+
# Check for scroll wheel
|
|
1593
|
+
if 'button' in touch.profile and touch.button.startswith('scroll'):
|
|
1594
|
+
# TODO: implement 'scrollleft' and 'scrollright'
|
|
1595
|
+
scroll_type = touch.button[6:]
|
|
1596
|
+
if scroll_type == 'down':
|
|
1597
|
+
if self.multiline:
|
|
1598
|
+
if self.scroll_y > 0:
|
|
1599
|
+
self.scroll_y = max(0,
|
|
1600
|
+
self.scroll_y - self.line_height *
|
|
1601
|
+
self.lines_to_scroll)
|
|
1602
|
+
self._trigger_update_graphics()
|
|
1603
|
+
else:
|
|
1604
|
+
if self.scroll_x > 0:
|
|
1605
|
+
self.scroll_x = max(0, self.scroll_x -
|
|
1606
|
+
self.line_height)
|
|
1607
|
+
self._trigger_update_graphics()
|
|
1608
|
+
if scroll_type == 'up':
|
|
1609
|
+
if self.multiline:
|
|
1610
|
+
max_scroll_y = max(0, self.minimum_height - self.height)
|
|
1611
|
+
if self.scroll_y < max_scroll_y:
|
|
1612
|
+
self.scroll_y = min(max_scroll_y,
|
|
1613
|
+
self.scroll_y + self.line_height *
|
|
1614
|
+
self.lines_to_scroll)
|
|
1615
|
+
self._trigger_update_graphics()
|
|
1616
|
+
else:
|
|
1617
|
+
max_scroll_x = self.get_max_scroll_x()
|
|
1618
|
+
if self.scroll_x < max_scroll_x:
|
|
1619
|
+
self.scroll_x = min(max_scroll_x, self.scroll_x +
|
|
1620
|
+
self.line_height)
|
|
1621
|
+
self._trigger_update_graphics()
|
|
1622
|
+
return True
|
|
1623
|
+
|
|
1624
|
+
touch.grab(self)
|
|
1625
|
+
self._touch_count += 1
|
|
1626
|
+
if touch.is_double_tap:
|
|
1627
|
+
self.dispatch('on_double_tap')
|
|
1628
|
+
if touch.is_triple_tap:
|
|
1629
|
+
self.dispatch('on_triple_tap')
|
|
1630
|
+
if self._touch_count == 4:
|
|
1631
|
+
self.dispatch('on_quad_touch')
|
|
1632
|
+
|
|
1633
|
+
# stores the touch for later use
|
|
1634
|
+
self._touch_down = touch
|
|
1635
|
+
|
|
1636
|
+
# Is a new touch_down, so previous scroll states needs to be reset
|
|
1637
|
+
self._enable_scroll = True
|
|
1638
|
+
self._have_scrolled = False
|
|
1639
|
+
self._scroll_distance_x = 0
|
|
1640
|
+
self._scroll_distance_y = 0
|
|
1641
|
+
|
|
1642
|
+
self._hide_cut_copy_paste(EventLoop.window)
|
|
1643
|
+
# schedule long touch for paste
|
|
1644
|
+
self._long_touch_ev = Clock.schedule_once(self.long_touch, .5)
|
|
1645
|
+
|
|
1646
|
+
self.cursor = self.get_cursor_from_xy(*touch_pos)
|
|
1647
|
+
if not self.scroll_from_swipe:
|
|
1648
|
+
self._cancel_update_selection(self._touch_down)
|
|
1649
|
+
|
|
1650
|
+
if CutBuffer and 'button' in touch.profile and \
|
|
1651
|
+
touch.button == 'middle':
|
|
1652
|
+
self.insert_text(CutBuffer.get_cutbuffer())
|
|
1653
|
+
return True
|
|
1654
|
+
|
|
1655
|
+
return True
|
|
1656
|
+
|
|
1657
|
+
# cancel/update existing selection after a single tap
|
|
1658
|
+
def _cancel_update_selection(self, touch):
|
|
1659
|
+
if not self._selection_touch:
|
|
1660
|
+
self.cancel_selection()
|
|
1661
|
+
self._selection_touch = touch
|
|
1662
|
+
self._selection_from = self._selection_to = self.cursor_index()
|
|
1663
|
+
self._update_selection()
|
|
1664
|
+
|
|
1665
|
+
def on_touch_move(self, touch):
|
|
1666
|
+
if touch.grab_current is not self:
|
|
1667
|
+
return
|
|
1668
|
+
if not self.focus:
|
|
1669
|
+
touch.ungrab(self)
|
|
1670
|
+
if self._selection_touch is touch:
|
|
1671
|
+
self._selection_touch = None
|
|
1672
|
+
return False
|
|
1673
|
+
|
|
1674
|
+
if self.scroll_from_swipe:
|
|
1675
|
+
self.scroll_text_from_swipe(touch)
|
|
1676
|
+
|
|
1677
|
+
if not self._have_scrolled and self._selection_touch is touch:
|
|
1678
|
+
self.cursor = self.get_cursor_from_xy(touch.x, touch.y)
|
|
1679
|
+
self._selection_to = self.cursor_index()
|
|
1680
|
+
self._update_selection()
|
|
1681
|
+
return True
|
|
1682
|
+
|
|
1683
|
+
def on_touch_up(self, touch):
|
|
1684
|
+
if touch.grab_current is not self:
|
|
1685
|
+
return
|
|
1686
|
+
touch.ungrab(self)
|
|
1687
|
+
self._touch_count -= 1
|
|
1688
|
+
|
|
1689
|
+
self.cancel_long_touch_event()
|
|
1690
|
+
|
|
1691
|
+
if not self.focus:
|
|
1692
|
+
return False
|
|
1693
|
+
|
|
1694
|
+
# types of touch that will have higher priority in being recognized,
|
|
1695
|
+
# compared to single tap
|
|
1696
|
+
prioritized_touch_types = (
|
|
1697
|
+
touch.is_double_tap
|
|
1698
|
+
or touch.is_triple_tap
|
|
1699
|
+
or self._touch_count == 4
|
|
1700
|
+
)
|
|
1701
|
+
|
|
1702
|
+
if not self._have_scrolled and not prioritized_touch_types:
|
|
1703
|
+
# Is a single tap and did not scrolled.
|
|
1704
|
+
# Selection needs to be canceled.
|
|
1705
|
+
self._cancel_update_selection(self._touch_down)
|
|
1706
|
+
|
|
1707
|
+
# show Bubble
|
|
1708
|
+
win = EventLoop.window
|
|
1709
|
+
if self._selection_to != self._selection_from:
|
|
1710
|
+
self._show_cut_copy_paste(touch.pos, win)
|
|
1711
|
+
|
|
1712
|
+
if self._selection_touch is touch:
|
|
1713
|
+
self._selection_to = self.cursor_index()
|
|
1714
|
+
self._update_selection(True)
|
|
1715
|
+
if self.use_handles and self._selection_to == self._selection_from:
|
|
1716
|
+
self._hide_handles()
|
|
1717
|
+
handle_middle = self._handle_middle
|
|
1718
|
+
if handle_middle is None:
|
|
1719
|
+
self._handle_middle = handle_middle = Selector(
|
|
1720
|
+
source=self.handle_image_middle,
|
|
1721
|
+
window=win,
|
|
1722
|
+
target=self,
|
|
1723
|
+
size_hint=(None, None),
|
|
1724
|
+
size=('45dp', '45dp'))
|
|
1725
|
+
handle_middle.bind(on_press=self._handle_pressed,
|
|
1726
|
+
on_touch_move=self._handle_move,
|
|
1727
|
+
on_release=self._handle_released)
|
|
1728
|
+
if not self._handle_middle.parent and self.text:
|
|
1729
|
+
EventLoop.window.add_widget(handle_middle, canvas='after')
|
|
1730
|
+
self._position_handles(mode='middle')
|
|
1731
|
+
return True
|
|
1732
|
+
|
|
1733
|
+
def scroll_text_from_swipe(self, touch):
|
|
1734
|
+
_scroll_timeout = (touch.time_update - touch.time_start) * 1000
|
|
1735
|
+
self._scroll_distance_x += abs(touch.dx)
|
|
1736
|
+
self._scroll_distance_y += abs(touch.dy)
|
|
1737
|
+
if not self._have_scrolled:
|
|
1738
|
+
# To be considered a scroll, touch should travel more than
|
|
1739
|
+
# scroll_distance in less than the scroll_timeout since touch_down
|
|
1740
|
+
if not (
|
|
1741
|
+
_scroll_timeout <= self.scroll_timeout
|
|
1742
|
+
and (
|
|
1743
|
+
(self._scroll_distance_x >= self.scroll_distance)
|
|
1744
|
+
or (self._scroll_distance_y >= self.scroll_distance)
|
|
1745
|
+
)
|
|
1746
|
+
):
|
|
1747
|
+
# Distance isn't enough (yet) to consider it as a scroll
|
|
1748
|
+
if _scroll_timeout <= self.scroll_timeout:
|
|
1749
|
+
# Timeout is not reached, scroll is still enabled.
|
|
1750
|
+
return False
|
|
1751
|
+
else:
|
|
1752
|
+
self._enable_scroll = False
|
|
1753
|
+
self._cancel_update_selection(self._touch_down)
|
|
1754
|
+
return False
|
|
1755
|
+
# We have a scroll!
|
|
1756
|
+
self._have_scrolled = True
|
|
1757
|
+
|
|
1758
|
+
self.cancel_long_touch_event()
|
|
1759
|
+
|
|
1760
|
+
if self.multiline:
|
|
1761
|
+
max_scroll_y = max(0, self.minimum_height - self.height)
|
|
1762
|
+
self.scroll_y = min(
|
|
1763
|
+
max(0, self.scroll_y + touch.dy),
|
|
1764
|
+
max_scroll_y
|
|
1765
|
+
)
|
|
1766
|
+
else:
|
|
1767
|
+
max_scroll_x = self.get_max_scroll_x()
|
|
1768
|
+
self.scroll_x = min(
|
|
1769
|
+
max(0, self.scroll_x - touch.dx),
|
|
1770
|
+
max_scroll_x
|
|
1771
|
+
)
|
|
1772
|
+
self._trigger_update_graphics()
|
|
1773
|
+
self._position_handles()
|
|
1774
|
+
return True
|
|
1775
|
+
|
|
1776
|
+
def _handle_pressed(self, instance):
|
|
1777
|
+
self._hide_cut_copy_paste()
|
|
1778
|
+
from_, to_ = self._selection_from, self.selection_to
|
|
1779
|
+
if from_ > to_:
|
|
1780
|
+
self._selection_from, self._selection_to = to_, from_
|
|
1781
|
+
|
|
1782
|
+
def _handle_released(self, instance):
|
|
1783
|
+
if self._selection_from == self.selection_to:
|
|
1784
|
+
return
|
|
1785
|
+
|
|
1786
|
+
self._update_selection()
|
|
1787
|
+
self._show_cut_copy_paste(
|
|
1788
|
+
(
|
|
1789
|
+
self.x + instance.right
|
|
1790
|
+
if instance is self._handle_left
|
|
1791
|
+
else self.x + instance.x,
|
|
1792
|
+
self.y + instance.top + self.line_height
|
|
1793
|
+
),
|
|
1794
|
+
EventLoop.window
|
|
1795
|
+
)
|
|
1796
|
+
|
|
1797
|
+
def _handle_move(self, instance, touch):
|
|
1798
|
+
if touch.grab_current != instance:
|
|
1799
|
+
return
|
|
1800
|
+
get_cursor = self.get_cursor_from_xy
|
|
1801
|
+
handle_right = self._handle_right
|
|
1802
|
+
handle_left = self._handle_left
|
|
1803
|
+
handle_middle = self._handle_middle
|
|
1804
|
+
|
|
1805
|
+
try:
|
|
1806
|
+
touch.push()
|
|
1807
|
+
touch.apply_transform_2d(self.to_widget)
|
|
1808
|
+
x, y = touch.pos
|
|
1809
|
+
finally:
|
|
1810
|
+
touch.pop()
|
|
1811
|
+
|
|
1812
|
+
cursor = get_cursor(
|
|
1813
|
+
x,
|
|
1814
|
+
y + instance._touch_diff + (self.line_height / 2)
|
|
1815
|
+
)
|
|
1816
|
+
self.cursor = cursor
|
|
1817
|
+
|
|
1818
|
+
if instance != touch.grab_current:
|
|
1819
|
+
return
|
|
1820
|
+
|
|
1821
|
+
if instance == handle_middle:
|
|
1822
|
+
self._position_handles(mode='middle')
|
|
1823
|
+
return
|
|
1824
|
+
|
|
1825
|
+
cindex = self.cursor_index()
|
|
1826
|
+
|
|
1827
|
+
if instance == handle_left:
|
|
1828
|
+
self._selection_from = cindex
|
|
1829
|
+
elif instance == handle_right:
|
|
1830
|
+
self._selection_to = cindex
|
|
1831
|
+
self._update_selection()
|
|
1832
|
+
self._trigger_update_graphics()
|
|
1833
|
+
self._trigger_position_handles()
|
|
1834
|
+
|
|
1835
|
+
def _position_handles(self, *args, **kwargs):
|
|
1836
|
+
if not self.text:
|
|
1837
|
+
return
|
|
1838
|
+
mode = kwargs.get('mode', 'both')
|
|
1839
|
+
|
|
1840
|
+
lh = self.line_height
|
|
1841
|
+
|
|
1842
|
+
handle_middle = self._handle_middle
|
|
1843
|
+
if handle_middle:
|
|
1844
|
+
hp_mid = self.cursor_pos
|
|
1845
|
+
pos = self.to_local(*hp_mid, relative=True)
|
|
1846
|
+
handle_middle.x = pos[0] - handle_middle.width / 2
|
|
1847
|
+
handle_middle.top = max(self.padding[3],
|
|
1848
|
+
min(self.height - self.padding[1],
|
|
1849
|
+
pos[1] - lh))
|
|
1850
|
+
if mode[0] == 'm':
|
|
1851
|
+
return
|
|
1852
|
+
|
|
1853
|
+
group = self.canvas.get_group('selection')
|
|
1854
|
+
if not group:
|
|
1855
|
+
return
|
|
1856
|
+
|
|
1857
|
+
EventLoop.window.remove_widget(self._handle_middle)
|
|
1858
|
+
|
|
1859
|
+
handle_left = self._handle_left
|
|
1860
|
+
if not handle_left:
|
|
1861
|
+
return
|
|
1862
|
+
hp_left = group[2].pos
|
|
1863
|
+
handle_left.pos = self.to_local(*hp_left, relative=True)
|
|
1864
|
+
handle_left.x -= handle_left.width
|
|
1865
|
+
handle_left.y -= handle_left.height
|
|
1866
|
+
|
|
1867
|
+
handle_right = self._handle_right
|
|
1868
|
+
last_rect = group[-1]
|
|
1869
|
+
hp_right = last_rect.pos[0], last_rect.pos[1]
|
|
1870
|
+
x, y = self.to_local(*hp_right, relative=True)
|
|
1871
|
+
handle_right.x = x + last_rect.size[0]
|
|
1872
|
+
handle_right.y = y - handle_right.height
|
|
1873
|
+
|
|
1874
|
+
def _hide_handles(self, win=None):
|
|
1875
|
+
win = win or EventLoop.window
|
|
1876
|
+
if win is None:
|
|
1877
|
+
return
|
|
1878
|
+
win.remove_widget(self._handle_right)
|
|
1879
|
+
win.remove_widget(self._handle_left)
|
|
1880
|
+
win.remove_widget(self._handle_middle)
|
|
1881
|
+
|
|
1882
|
+
def _show_handles(self, dt):
|
|
1883
|
+
if not self.use_handles or not self.text:
|
|
1884
|
+
return
|
|
1885
|
+
|
|
1886
|
+
win = EventLoop.window
|
|
1887
|
+
|
|
1888
|
+
handle_right = self._handle_right
|
|
1889
|
+
handle_left = self._handle_left
|
|
1890
|
+
if self._handle_left is None:
|
|
1891
|
+
self._handle_left = handle_left = Selector(
|
|
1892
|
+
source=self.handle_image_left,
|
|
1893
|
+
target=self,
|
|
1894
|
+
window=win,
|
|
1895
|
+
size_hint=(None, None),
|
|
1896
|
+
size=('45dp', '45dp'))
|
|
1897
|
+
handle_left.bind(on_press=self._handle_pressed,
|
|
1898
|
+
on_touch_move=self._handle_move,
|
|
1899
|
+
on_release=self._handle_released)
|
|
1900
|
+
self._handle_right = handle_right = Selector(
|
|
1901
|
+
source=self.handle_image_right,
|
|
1902
|
+
target=self,
|
|
1903
|
+
window=win,
|
|
1904
|
+
size_hint=(None, None),
|
|
1905
|
+
size=('45dp', '45dp'))
|
|
1906
|
+
handle_right.bind(on_press=self._handle_pressed,
|
|
1907
|
+
on_touch_move=self._handle_move,
|
|
1908
|
+
on_release=self._handle_released)
|
|
1909
|
+
else:
|
|
1910
|
+
if self._handle_left.parent:
|
|
1911
|
+
self._position_handles()
|
|
1912
|
+
return
|
|
1913
|
+
if not self.parent:
|
|
1914
|
+
return
|
|
1915
|
+
|
|
1916
|
+
self._trigger_position_handles()
|
|
1917
|
+
if self.selection_from != self.selection_to:
|
|
1918
|
+
self._handle_left.opacity = self._handle_right.opacity = 0
|
|
1919
|
+
win.add_widget(self._handle_left, canvas='after')
|
|
1920
|
+
win.add_widget(self._handle_right, canvas='after')
|
|
1921
|
+
anim = Animation(opacity=1, d=.4)
|
|
1922
|
+
anim.start(self._handle_right)
|
|
1923
|
+
anim.start(self._handle_left)
|
|
1924
|
+
|
|
1925
|
+
def _show_cut_copy_paste(
|
|
1926
|
+
self, pos, win, parent_changed=False, mode='', pos_in_window=False, *l
|
|
1927
|
+
):
|
|
1928
|
+
"""Show a bubble with cut copy and paste buttons"""
|
|
1929
|
+
if not self.use_bubble:
|
|
1930
|
+
return
|
|
1931
|
+
|
|
1932
|
+
bubble = self._bubble
|
|
1933
|
+
if bubble is None:
|
|
1934
|
+
self._bubble = bubble = TextInputCutCopyPaste(textinput=self)
|
|
1935
|
+
self.fbind('parent', self._show_cut_copy_paste, pos, win, True)
|
|
1936
|
+
|
|
1937
|
+
def hide_(*args):
|
|
1938
|
+
return self._hide_cut_copy_paste(win)
|
|
1939
|
+
|
|
1940
|
+
self.bind(
|
|
1941
|
+
focus=hide_,
|
|
1942
|
+
cursor_pos=hide_,
|
|
1943
|
+
)
|
|
1944
|
+
else:
|
|
1945
|
+
win.remove_widget(bubble)
|
|
1946
|
+
if not self.parent:
|
|
1947
|
+
return
|
|
1948
|
+
if parent_changed:
|
|
1949
|
+
return
|
|
1950
|
+
|
|
1951
|
+
# Search the position from the touch to the window
|
|
1952
|
+
lh, ls = self.line_height, self.line_spacing
|
|
1953
|
+
|
|
1954
|
+
x, y = pos
|
|
1955
|
+
t_pos = (x, y) if pos_in_window else self.to_window(x, y)
|
|
1956
|
+
bubble_size = bubble.size
|
|
1957
|
+
bubble_hw = bubble_size[0] / 2.
|
|
1958
|
+
win_size = win.size
|
|
1959
|
+
bubble_pos = (t_pos[0], t_pos[1] + inch(.25))
|
|
1960
|
+
|
|
1961
|
+
if (bubble_pos[0] - bubble_hw) < 0:
|
|
1962
|
+
# bubble beyond left of window
|
|
1963
|
+
if bubble_pos[1] > (win_size[1] - bubble_size[1]):
|
|
1964
|
+
# bubble above window height
|
|
1965
|
+
bubble_pos = (bubble_hw, (t_pos[1]) - (lh + ls + inch(.25)))
|
|
1966
|
+
bubble.arrow_pos = 'top_left'
|
|
1967
|
+
else:
|
|
1968
|
+
bubble_pos = (bubble_hw, bubble_pos[1])
|
|
1969
|
+
bubble.arrow_pos = 'bottom_left'
|
|
1970
|
+
elif (bubble_pos[0] + bubble_hw) > win_size[0]:
|
|
1971
|
+
# bubble beyond right of window
|
|
1972
|
+
if bubble_pos[1] > (win_size[1] - bubble_size[1]):
|
|
1973
|
+
# bubble above window height
|
|
1974
|
+
bubble_pos = (
|
|
1975
|
+
win_size[0] - bubble_hw,
|
|
1976
|
+
(t_pos[1]) - (lh + ls + inch(.25))
|
|
1977
|
+
)
|
|
1978
|
+
bubble.arrow_pos = 'top_right'
|
|
1979
|
+
else:
|
|
1980
|
+
bubble_pos = (win_size[0] - bubble_hw, bubble_pos[1])
|
|
1981
|
+
bubble.arrow_pos = 'bottom_right'
|
|
1982
|
+
else:
|
|
1983
|
+
if bubble_pos[1] > (win_size[1] - bubble_size[1]):
|
|
1984
|
+
# bubble above window height
|
|
1985
|
+
bubble_pos = (
|
|
1986
|
+
bubble_pos[0],
|
|
1987
|
+
(t_pos[1]) - (lh + ls + inch(.25))
|
|
1988
|
+
)
|
|
1989
|
+
bubble.arrow_pos = 'top_mid'
|
|
1990
|
+
else:
|
|
1991
|
+
bubble.arrow_pos = 'bottom_mid'
|
|
1992
|
+
|
|
1993
|
+
bubble_pos = self.to_widget(*bubble_pos, relative=True)
|
|
1994
|
+
bubble.center_x = bubble_pos[0]
|
|
1995
|
+
if bubble.arrow_pos[0] == 't':
|
|
1996
|
+
bubble.top = bubble_pos[1]
|
|
1997
|
+
else:
|
|
1998
|
+
bubble.y = bubble_pos[1]
|
|
1999
|
+
bubble.mode = mode
|
|
2000
|
+
Animation.cancel_all(bubble)
|
|
2001
|
+
bubble.opacity = 0
|
|
2002
|
+
win.add_widget(bubble, canvas='after')
|
|
2003
|
+
Animation(opacity=1, d=.225).start(bubble)
|
|
2004
|
+
|
|
2005
|
+
def _hide_cut_copy_paste(self, win=None):
|
|
2006
|
+
bubble = self._bubble
|
|
2007
|
+
if not bubble:
|
|
2008
|
+
return
|
|
2009
|
+
|
|
2010
|
+
bubble.hide()
|
|
2011
|
+
|
|
2012
|
+
#
|
|
2013
|
+
# Private
|
|
2014
|
+
#
|
|
2015
|
+
|
|
2016
|
+
@staticmethod
|
|
2017
|
+
def _reload_remove_observer(wr):
|
|
2018
|
+
"""called when the textinput is deleted"""
|
|
2019
|
+
if wr in _textinput_list:
|
|
2020
|
+
_textinput_list.remove(wr)
|
|
2021
|
+
|
|
2022
|
+
def _on_textinput_focused(self, instance, value, *largs):
|
|
2023
|
+
win = EventLoop.window
|
|
2024
|
+
self.cancel_selection()
|
|
2025
|
+
self._hide_cut_copy_paste(win)
|
|
2026
|
+
|
|
2027
|
+
if value:
|
|
2028
|
+
if (
|
|
2029
|
+
not (self.readonly or self.disabled)
|
|
2030
|
+
or _is_desktop
|
|
2031
|
+
and self._keyboard_mode == 'system'
|
|
2032
|
+
):
|
|
2033
|
+
self._trigger_cursor_reset()
|
|
2034
|
+
self._editable = True
|
|
2035
|
+
else:
|
|
2036
|
+
self._editable = False
|
|
2037
|
+
else:
|
|
2038
|
+
self._do_blink_cursor_ev.cancel()
|
|
2039
|
+
self._hide_handles(win)
|
|
2040
|
+
|
|
2041
|
+
def _ensure_clipboard(self):
|
|
2042
|
+
global Clipboard, CutBuffer
|
|
2043
|
+
if not Clipboard:
|
|
2044
|
+
from kivy.core.clipboard import Clipboard, CutBuffer
|
|
2045
|
+
|
|
2046
|
+
def cut(self):
|
|
2047
|
+
''' Copy current selection to clipboard then delete it from TextInput.
|
|
2048
|
+
|
|
2049
|
+
.. versionadded:: 1.8.0
|
|
2050
|
+
|
|
2051
|
+
'''
|
|
2052
|
+
self._cut(self.selection_text)
|
|
2053
|
+
|
|
2054
|
+
def _cut(self, data):
|
|
2055
|
+
self._ensure_clipboard()
|
|
2056
|
+
Clipboard.copy(data)
|
|
2057
|
+
self.delete_selection()
|
|
2058
|
+
|
|
2059
|
+
def copy(self, data=''):
|
|
2060
|
+
''' Copy the value provided in argument `data` into current clipboard.
|
|
2061
|
+
If data is not of type string it will be converted to string.
|
|
2062
|
+
If no data is provided then current selection if present is copied.
|
|
2063
|
+
|
|
2064
|
+
.. versionadded:: 1.8.0
|
|
2065
|
+
|
|
2066
|
+
'''
|
|
2067
|
+
self._ensure_clipboard()
|
|
2068
|
+
if data:
|
|
2069
|
+
return Clipboard.copy(data)
|
|
2070
|
+
if self.selection_text:
|
|
2071
|
+
return Clipboard.copy(self.selection_text)
|
|
2072
|
+
|
|
2073
|
+
def paste(self):
|
|
2074
|
+
''' Insert text from system :class:`~kivy.core.clipboard.Clipboard`
|
|
2075
|
+
into the :class:`~kivy.uix.textinput.TextInput` at current cursor
|
|
2076
|
+
position.
|
|
2077
|
+
|
|
2078
|
+
.. versionadded:: 1.8.0
|
|
2079
|
+
|
|
2080
|
+
'''
|
|
2081
|
+
self._ensure_clipboard()
|
|
2082
|
+
data = Clipboard.paste()
|
|
2083
|
+
self.delete_selection()
|
|
2084
|
+
if not self.multiline:
|
|
2085
|
+
data = data.replace('\n', ' ')
|
|
2086
|
+
self.insert_text(data)
|
|
2087
|
+
|
|
2088
|
+
def _update_cutbuffer(self, *args):
|
|
2089
|
+
CutBuffer.set_cutbuffer(self.selection_text)
|
|
2090
|
+
|
|
2091
|
+
def _get_text_width(self, text, tab_width, _label_cached):
|
|
2092
|
+
"""Return the width of a text, according to the current line options"""
|
|
2093
|
+
kw = self._get_line_options()
|
|
2094
|
+
|
|
2095
|
+
try:
|
|
2096
|
+
cid = u'{}\0{}\0{}'.format(text, self.password, kw)
|
|
2097
|
+
except UnicodeDecodeError:
|
|
2098
|
+
cid = '{}\0{}\0{}'.format(text, self.password, kw)
|
|
2099
|
+
|
|
2100
|
+
width = Cache_get('textinput.width', cid)
|
|
2101
|
+
if width:
|
|
2102
|
+
return width
|
|
2103
|
+
if not _label_cached:
|
|
2104
|
+
_label_cached = self._label_cached
|
|
2105
|
+
text = text.replace('\t', ' ' * tab_width)
|
|
2106
|
+
if not self.password:
|
|
2107
|
+
width = _label_cached.get_extents(text)[0]
|
|
2108
|
+
else:
|
|
2109
|
+
width = _label_cached.get_extents(
|
|
2110
|
+
self.password_mask * len(text))[0]
|
|
2111
|
+
Cache_append('textinput.width', cid, width)
|
|
2112
|
+
return width
|
|
2113
|
+
|
|
2114
|
+
def on_cursor_blink(self, instance, value):
|
|
2115
|
+
"""trigger blink event reset to switch blinking while focused"""
|
|
2116
|
+
self._reset_cursor_blink()
|
|
2117
|
+
|
|
2118
|
+
def _do_blink_cursor(self, dt):
|
|
2119
|
+
if not self.cursor_blink:
|
|
2120
|
+
# ignore event if not triggered,
|
|
2121
|
+
# stop if cursor_blink value changed right now
|
|
2122
|
+
if self._do_blink_cursor_ev.is_triggered:
|
|
2123
|
+
self._do_blink_cursor_ev.cancel()
|
|
2124
|
+
# don't blink, make cursor visible
|
|
2125
|
+
self._cursor_blink = False
|
|
2126
|
+
return
|
|
2127
|
+
|
|
2128
|
+
# Callback for blinking the cursor.
|
|
2129
|
+
self._cursor_blink = not self._cursor_blink
|
|
2130
|
+
|
|
2131
|
+
def _reset_cursor_blink(self, *args):
|
|
2132
|
+
self._do_blink_cursor_ev.cancel()
|
|
2133
|
+
self._cursor_blink = False
|
|
2134
|
+
self._do_blink_cursor_ev()
|
|
2135
|
+
|
|
2136
|
+
def on_cursor(self, instance, value):
|
|
2137
|
+
"""
|
|
2138
|
+
When the cursor is moved, reset cursor blinking to keep it showing,
|
|
2139
|
+
and update all the graphics.
|
|
2140
|
+
"""
|
|
2141
|
+
if self.focus:
|
|
2142
|
+
self._trigger_cursor_reset()
|
|
2143
|
+
self._trigger_update_graphics()
|
|
2144
|
+
|
|
2145
|
+
def _delete_line(self, idx):
|
|
2146
|
+
"""Delete current line, and fix cursor position"""
|
|
2147
|
+
assert idx < len(self._lines)
|
|
2148
|
+
self._lines_flags.pop(idx)
|
|
2149
|
+
self._lines_labels.pop(idx)
|
|
2150
|
+
self._lines.pop(idx)
|
|
2151
|
+
self.cursor = self.cursor
|
|
2152
|
+
|
|
2153
|
+
def _set_line_text(self, line_num, text):
|
|
2154
|
+
"""Set current line with other text than the default one."""
|
|
2155
|
+
self._lines_labels[line_num] = self._create_line_label(text)
|
|
2156
|
+
self._lines[line_num] = text
|
|
2157
|
+
|
|
2158
|
+
def _trigger_refresh_line_options(self, *largs):
|
|
2159
|
+
if self._refresh_line_options_ev is not None:
|
|
2160
|
+
self._refresh_line_options_ev.cancel()
|
|
2161
|
+
else:
|
|
2162
|
+
self._refresh_line_options_ev = Clock.create_trigger(
|
|
2163
|
+
self._refresh_line_options, 0)
|
|
2164
|
+
self._refresh_line_options_ev()
|
|
2165
|
+
|
|
2166
|
+
def _refresh_line_options(self, *largs):
|
|
2167
|
+
self._line_options = None
|
|
2168
|
+
self._get_line_options()
|
|
2169
|
+
self._refresh_text_from_property()
|
|
2170
|
+
self._refresh_hint_text()
|
|
2171
|
+
self.cursor = self.get_cursor_from_index(len(self.text))
|
|
2172
|
+
|
|
2173
|
+
def _trigger_refresh_text(self, *largs):
|
|
2174
|
+
if len(largs) and largs[0] == self:
|
|
2175
|
+
largs = ()
|
|
2176
|
+
if self._refresh_text_from_property_ev is not None:
|
|
2177
|
+
self._refresh_text_from_property_ev.cancel()
|
|
2178
|
+
self._refresh_text_from_property_ev = Clock.schedule_once(
|
|
2179
|
+
lambda dt: self._refresh_text_from_property(*largs))
|
|
2180
|
+
|
|
2181
|
+
def _update_text_options(self, *largs):
|
|
2182
|
+
Cache_remove('textinput.width')
|
|
2183
|
+
self._trigger_refresh_text()
|
|
2184
|
+
|
|
2185
|
+
def _refresh_text_from_trigger(self, dt, *largs):
|
|
2186
|
+
self._refresh_text_from_property(*largs)
|
|
2187
|
+
|
|
2188
|
+
def _refresh_text_from_property(self, *largs):
|
|
2189
|
+
self._refresh_text(self.text, *largs)
|
|
2190
|
+
|
|
2191
|
+
def _refresh_text(self, text, *largs):
|
|
2192
|
+
"""
|
|
2193
|
+
Refresh all the lines from a new text.
|
|
2194
|
+
By using cache in internal functions, this method should be fast.
|
|
2195
|
+
"""
|
|
2196
|
+
mode = 'all'
|
|
2197
|
+
if len(largs) > 1:
|
|
2198
|
+
mode, start, finish, _lines, _lines_flags, len_lines = largs
|
|
2199
|
+
# start = max(0, start)
|
|
2200
|
+
cursor = None
|
|
2201
|
+
else:
|
|
2202
|
+
cursor = self.cursor_index()
|
|
2203
|
+
_lines, self._lines_flags = self._split_smart(text)
|
|
2204
|
+
_lines_labels = []
|
|
2205
|
+
_line_rects = []
|
|
2206
|
+
_create_label = self._create_line_label
|
|
2207
|
+
|
|
2208
|
+
for x in _lines:
|
|
2209
|
+
lbl = _create_label(x)
|
|
2210
|
+
_lines_labels.append(lbl)
|
|
2211
|
+
_line_rects.append(Rectangle(size=lbl.size))
|
|
2212
|
+
|
|
2213
|
+
if mode == 'all':
|
|
2214
|
+
self._lines_labels = _lines_labels
|
|
2215
|
+
self._lines_rects = _line_rects
|
|
2216
|
+
self._lines[:] = _lines
|
|
2217
|
+
elif mode == 'del':
|
|
2218
|
+
if finish > start:
|
|
2219
|
+
self._insert_lines(start, finish + 1, len_lines,
|
|
2220
|
+
_lines_flags, _lines, _lines_labels,
|
|
2221
|
+
_line_rects)
|
|
2222
|
+
elif mode == 'insert':
|
|
2223
|
+
self._insert_lines(start, finish + 1, len_lines, _lines_flags,
|
|
2224
|
+
_lines, _lines_labels, _line_rects)
|
|
2225
|
+
|
|
2226
|
+
min_line_ht = self._label_cached.get_extents('_')[1]
|
|
2227
|
+
# with markup texture can be of height `1`
|
|
2228
|
+
self.line_height = max(_lines_labels[0].height, min_line_ht)
|
|
2229
|
+
# self.line_spacing = 2
|
|
2230
|
+
# now, if the text change, maybe the cursor is not at the same place as
|
|
2231
|
+
# before. so, try to set the cursor on the good place
|
|
2232
|
+
row = self.cursor_row
|
|
2233
|
+
self.cursor = self.get_cursor_from_index(
|
|
2234
|
+
self.cursor_index() if cursor is None else cursor
|
|
2235
|
+
)
|
|
2236
|
+
|
|
2237
|
+
# if we back to a new line, reset the scroll, otherwise, the effect is
|
|
2238
|
+
# ugly
|
|
2239
|
+
if self.cursor_row != row:
|
|
2240
|
+
self.scroll_x = 0
|
|
2241
|
+
# with the new text don't forget to update graphics again
|
|
2242
|
+
self._trigger_update_graphics()
|
|
2243
|
+
|
|
2244
|
+
def _insert_lines(self, start, finish, len_lines, _lines_flags,
|
|
2245
|
+
_lines, _lines_labels, _line_rects):
|
|
2246
|
+
self_lines_flags = self._lines_flags
|
|
2247
|
+
_lins_flags = []
|
|
2248
|
+
_lins_flags.extend(self_lines_flags[:start])
|
|
2249
|
+
if len_lines:
|
|
2250
|
+
# if not inserting at first line then
|
|
2251
|
+
if start:
|
|
2252
|
+
# make sure line flags restored for first line
|
|
2253
|
+
# _split_smart assumes first line to be not a new line
|
|
2254
|
+
_lines_flags[0] = self_lines_flags[start]
|
|
2255
|
+
_lins_flags.extend(_lines_flags)
|
|
2256
|
+
_lins_flags.extend(self_lines_flags[finish:])
|
|
2257
|
+
self._lines_flags = _lins_flags
|
|
2258
|
+
|
|
2259
|
+
_lins_lbls = []
|
|
2260
|
+
_lins_lbls.extend(self._lines_labels[:start])
|
|
2261
|
+
if len_lines:
|
|
2262
|
+
_lins_lbls.extend(_lines_labels)
|
|
2263
|
+
_lins_lbls.extend(self._lines_labels[finish:])
|
|
2264
|
+
self._lines_labels = _lins_lbls
|
|
2265
|
+
|
|
2266
|
+
_lins_rcts = []
|
|
2267
|
+
_lins_rcts.extend(self._lines_rects[:start])
|
|
2268
|
+
if len_lines:
|
|
2269
|
+
_lins_rcts.extend(_line_rects)
|
|
2270
|
+
_lins_rcts.extend(self._lines_rects[finish:])
|
|
2271
|
+
self._lines_rects = _lins_rcts
|
|
2272
|
+
|
|
2273
|
+
_lins = []
|
|
2274
|
+
_lins.extend(self._lines[:start])
|
|
2275
|
+
if len_lines:
|
|
2276
|
+
_lins.extend(_lines)
|
|
2277
|
+
_lins.extend(self._lines[finish:])
|
|
2278
|
+
self._lines[:] = _lins
|
|
2279
|
+
|
|
2280
|
+
def _trigger_update_graphics(self, *largs):
|
|
2281
|
+
self._update_graphics_ev.cancel()
|
|
2282
|
+
self._update_graphics_ev()
|
|
2283
|
+
|
|
2284
|
+
def _update_graphics(self, *largs):
|
|
2285
|
+
"""
|
|
2286
|
+
Update all the graphics according to the current internal values.
|
|
2287
|
+
"""
|
|
2288
|
+
|
|
2289
|
+
# This is a little bit complex, because we have to :
|
|
2290
|
+
# - handle scroll_x
|
|
2291
|
+
# - handle padding
|
|
2292
|
+
# - create rectangle for the lines matching the viewport
|
|
2293
|
+
# - crop the texture coordinates to match the viewport
|
|
2294
|
+
|
|
2295
|
+
# This is the first step of graphics, the second is the selection.
|
|
2296
|
+
|
|
2297
|
+
self.canvas.clear()
|
|
2298
|
+
|
|
2299
|
+
line_height = self.line_height
|
|
2300
|
+
dy = line_height + self.line_spacing
|
|
2301
|
+
|
|
2302
|
+
# adjust view if the cursor is going outside the bounds
|
|
2303
|
+
scroll_x = self.scroll_x
|
|
2304
|
+
scroll_y = self.scroll_y
|
|
2305
|
+
|
|
2306
|
+
# draw labels
|
|
2307
|
+
if (
|
|
2308
|
+
not self._lines
|
|
2309
|
+
or (not self._lines[0] and len(self._lines) == 1)
|
|
2310
|
+
):
|
|
2311
|
+
rects = self._hint_text_rects
|
|
2312
|
+
labels = self._hint_text_labels
|
|
2313
|
+
lines = self._hint_text_lines
|
|
2314
|
+
else:
|
|
2315
|
+
rects = self._lines_rects
|
|
2316
|
+
labels = self._lines_labels
|
|
2317
|
+
lines = self._lines
|
|
2318
|
+
|
|
2319
|
+
padding_left, padding_top, padding_right, padding_bottom = self.padding
|
|
2320
|
+
x = self.x + padding_left
|
|
2321
|
+
y = self.top - padding_top + scroll_y
|
|
2322
|
+
miny = self.y + padding_bottom
|
|
2323
|
+
maxy = self.top - padding_top
|
|
2324
|
+
halign = self.halign
|
|
2325
|
+
base_dir = self.base_direction
|
|
2326
|
+
|
|
2327
|
+
auto_halign_r = halign == 'auto' and base_dir and 'rtl' in base_dir
|
|
2328
|
+
|
|
2329
|
+
fst_visible_ln = None
|
|
2330
|
+
viewport_pos = scroll_x, 0
|
|
2331
|
+
for line_num, value in enumerate(lines):
|
|
2332
|
+
if miny < y < maxy + dy:
|
|
2333
|
+
if fst_visible_ln is None:
|
|
2334
|
+
fst_visible_ln = line_num
|
|
2335
|
+
|
|
2336
|
+
y = self._draw_line(
|
|
2337
|
+
value,
|
|
2338
|
+
line_num,
|
|
2339
|
+
labels[line_num],
|
|
2340
|
+
viewport_pos,
|
|
2341
|
+
line_height,
|
|
2342
|
+
miny,
|
|
2343
|
+
maxy,
|
|
2344
|
+
x,
|
|
2345
|
+
y,
|
|
2346
|
+
base_dir,
|
|
2347
|
+
halign,
|
|
2348
|
+
rects,
|
|
2349
|
+
auto_halign_r,
|
|
2350
|
+
)
|
|
2351
|
+
elif y <= miny:
|
|
2352
|
+
line_num -= 1
|
|
2353
|
+
break
|
|
2354
|
+
|
|
2355
|
+
y -= dy
|
|
2356
|
+
|
|
2357
|
+
if fst_visible_ln is not None:
|
|
2358
|
+
self._visible_lines_range = (fst_visible_ln, line_num + 1)
|
|
2359
|
+
else:
|
|
2360
|
+
self._visible_lines_range = 0, 0
|
|
2361
|
+
|
|
2362
|
+
self._update_graphics_selection()
|
|
2363
|
+
|
|
2364
|
+
def _draw_line(
|
|
2365
|
+
self,
|
|
2366
|
+
value,
|
|
2367
|
+
line_num,
|
|
2368
|
+
texture,
|
|
2369
|
+
viewport_pos,
|
|
2370
|
+
line_height,
|
|
2371
|
+
miny,
|
|
2372
|
+
maxy,
|
|
2373
|
+
x,
|
|
2374
|
+
y,
|
|
2375
|
+
base_dir,
|
|
2376
|
+
halign,
|
|
2377
|
+
rects,
|
|
2378
|
+
auto_halign_r,
|
|
2379
|
+
):
|
|
2380
|
+
size = list(texture.size)
|
|
2381
|
+
texcoords = texture.tex_coords[:]
|
|
2382
|
+
|
|
2383
|
+
# compute coordinate
|
|
2384
|
+
padding_left, padding_top, padding_right, padding_bottom = self.padding
|
|
2385
|
+
viewport_width = self.width - padding_left - padding_right
|
|
2386
|
+
viewport_height = self.height - padding_top - padding_bottom
|
|
2387
|
+
texture_width, texture_height = size
|
|
2388
|
+
original_height, original_width = tch, tcw = texcoords[1:3]
|
|
2389
|
+
|
|
2390
|
+
# adjust size/texcoord according to viewport
|
|
2391
|
+
if viewport_pos:
|
|
2392
|
+
tcx, tcy = viewport_pos
|
|
2393
|
+
tcx = tcx / texture_width * original_width
|
|
2394
|
+
tcy = tcy / texture_height * original_height
|
|
2395
|
+
|
|
2396
|
+
else:
|
|
2397
|
+
tcx, tcy = 0, 0
|
|
2398
|
+
|
|
2399
|
+
if texture_width * (1 - tcx) < viewport_width:
|
|
2400
|
+
tcw = tcw - tcx
|
|
2401
|
+
texture_width = tcw * texture_width
|
|
2402
|
+
elif viewport_width < texture_width:
|
|
2403
|
+
tcw = (viewport_width / texture_width) * tcw
|
|
2404
|
+
texture_width = viewport_width
|
|
2405
|
+
|
|
2406
|
+
if viewport_height < texture_height:
|
|
2407
|
+
tch = (viewport_height / texture_height) * tch
|
|
2408
|
+
texture_height = viewport_height
|
|
2409
|
+
|
|
2410
|
+
# cropping
|
|
2411
|
+
if y > maxy:
|
|
2412
|
+
viewport_height = (maxy - y + line_height)
|
|
2413
|
+
tch = (viewport_height / line_height) * original_height
|
|
2414
|
+
tcy = original_height - tch
|
|
2415
|
+
texture_height = viewport_height
|
|
2416
|
+
if y - line_height < miny:
|
|
2417
|
+
diff = miny - (y - line_height)
|
|
2418
|
+
y += diff
|
|
2419
|
+
viewport_height = line_height - diff
|
|
2420
|
+
tch = (viewport_height / line_height) * original_height
|
|
2421
|
+
texture_height = viewport_height
|
|
2422
|
+
|
|
2423
|
+
if tcw < 0:
|
|
2424
|
+
# nothing to show
|
|
2425
|
+
return y
|
|
2426
|
+
|
|
2427
|
+
top_left_corner = tcx, tcy + tch
|
|
2428
|
+
top_right_corner = tcx + tcw, tcy + tch
|
|
2429
|
+
bottom_right_corner = tcx + tcw, tcy
|
|
2430
|
+
bottom_left_corner = tcx, tcy
|
|
2431
|
+
|
|
2432
|
+
texcoords = (
|
|
2433
|
+
top_left_corner
|
|
2434
|
+
+ top_right_corner
|
|
2435
|
+
+ bottom_right_corner
|
|
2436
|
+
+ bottom_left_corner
|
|
2437
|
+
)
|
|
2438
|
+
|
|
2439
|
+
# Horizontal alignment
|
|
2440
|
+
xoffset = 0
|
|
2441
|
+
if not base_dir:
|
|
2442
|
+
base_dir = self._resolved_base_dir = Label.find_base_direction(value) # noqa
|
|
2443
|
+
if base_dir and halign == 'auto':
|
|
2444
|
+
auto_halign_r = 'rtl' in base_dir
|
|
2445
|
+
if halign == 'center':
|
|
2446
|
+
xoffset = int((viewport_width - texture_width) / 2.)
|
|
2447
|
+
elif halign == 'right' or auto_halign_r:
|
|
2448
|
+
xoffset = max(0, int(viewport_width - texture_width))
|
|
2449
|
+
|
|
2450
|
+
# add rectangle
|
|
2451
|
+
rect = rects[line_num]
|
|
2452
|
+
rect.pos = int(xoffset + x), int(y - line_height)
|
|
2453
|
+
rect.size = texture_width, texture_height
|
|
2454
|
+
rect.texture = texture
|
|
2455
|
+
rect.tex_coords = texcoords
|
|
2456
|
+
# useful to debug rectangle sizes
|
|
2457
|
+
# self.canvas.add(Color(0, .5, 0, .5, mode='rgba'))
|
|
2458
|
+
# self.canvas.add(Rectangle(pos=rect.pos, size=rect.size))
|
|
2459
|
+
# self.canvas.add(Color())
|
|
2460
|
+
self.canvas.add(rect)
|
|
2461
|
+
|
|
2462
|
+
return y
|
|
2463
|
+
|
|
2464
|
+
def _update_graphics_selection(self):
|
|
2465
|
+
if not self._selection:
|
|
2466
|
+
return
|
|
2467
|
+
|
|
2468
|
+
# local references to avoid dot lookups later
|
|
2469
|
+
padding_left, padding_top, padding_right, padding_bottom = self.padding
|
|
2470
|
+
rects = self._lines_rects
|
|
2471
|
+
label_cached = self._label_cached
|
|
2472
|
+
lines = self._lines
|
|
2473
|
+
tab_width = self.tab_width
|
|
2474
|
+
top = self.top
|
|
2475
|
+
get_text_width = self._get_text_width
|
|
2476
|
+
get_cursor_from_index = self.get_cursor_from_index
|
|
2477
|
+
draw_selection = self._draw_selection
|
|
2478
|
+
canvas_add = self.canvas.add
|
|
2479
|
+
selection_color = self.selection_color
|
|
2480
|
+
|
|
2481
|
+
# selection borders
|
|
2482
|
+
a, b = sorted((self._selection_from, self._selection_to))
|
|
2483
|
+
selection_start_col, selection_start_row = get_cursor_from_index(a)
|
|
2484
|
+
selection_end_col, selection_end_row = get_cursor_from_index(b)
|
|
2485
|
+
|
|
2486
|
+
dy = self.line_height + self.line_spacing
|
|
2487
|
+
x = self.x
|
|
2488
|
+
y = top - padding_top + self.scroll_y - selection_start_row * dy
|
|
2489
|
+
width = self.width
|
|
2490
|
+
|
|
2491
|
+
miny = self.y + padding_bottom
|
|
2492
|
+
maxy = top - padding_top + dy
|
|
2493
|
+
|
|
2494
|
+
self.canvas.remove_group('selection')
|
|
2495
|
+
first_visible_line = math.floor(self.scroll_y / dy)
|
|
2496
|
+
last_visible_line = math.ceil((self.scroll_y + maxy - miny) / dy)
|
|
2497
|
+
width_minus_padding = width - (padding_right + padding_left)
|
|
2498
|
+
|
|
2499
|
+
for line_num, rect in enumerate(
|
|
2500
|
+
islice(
|
|
2501
|
+
rects,
|
|
2502
|
+
max(selection_start_row, first_visible_line),
|
|
2503
|
+
min(selection_end_row + 1, last_visible_line - 1),
|
|
2504
|
+
),
|
|
2505
|
+
start=max(selection_start_row, first_visible_line)
|
|
2506
|
+
):
|
|
2507
|
+
draw_selection(
|
|
2508
|
+
rect.pos,
|
|
2509
|
+
rect.size,
|
|
2510
|
+
line_num,
|
|
2511
|
+
(selection_start_col, selection_start_row),
|
|
2512
|
+
(selection_end_col, selection_end_row),
|
|
2513
|
+
lines,
|
|
2514
|
+
get_text_width,
|
|
2515
|
+
tab_width,
|
|
2516
|
+
label_cached,
|
|
2517
|
+
width_minus_padding,
|
|
2518
|
+
padding_left,
|
|
2519
|
+
padding_right,
|
|
2520
|
+
x,
|
|
2521
|
+
canvas_add,
|
|
2522
|
+
selection_color
|
|
2523
|
+
)
|
|
2524
|
+
self._position_handles('both')
|
|
2525
|
+
|
|
2526
|
+
def _draw_selection(
|
|
2527
|
+
self,
|
|
2528
|
+
pos,
|
|
2529
|
+
size,
|
|
2530
|
+
line_num,
|
|
2531
|
+
selection_start,
|
|
2532
|
+
selection_end,
|
|
2533
|
+
lines,
|
|
2534
|
+
get_text_width,
|
|
2535
|
+
tab_width,
|
|
2536
|
+
label_cached,
|
|
2537
|
+
width_minus_padding,
|
|
2538
|
+
padding_left,
|
|
2539
|
+
padding_right,
|
|
2540
|
+
x,
|
|
2541
|
+
canvas_add,
|
|
2542
|
+
selection_color
|
|
2543
|
+
):
|
|
2544
|
+
selection_start_col, selection_start_row = selection_start
|
|
2545
|
+
selection_end_col, selection_end_row = selection_end
|
|
2546
|
+
|
|
2547
|
+
# Draw the current selection on the widget.
|
|
2548
|
+
if not selection_start_row <= line_num <= selection_end_row:
|
|
2549
|
+
return
|
|
2550
|
+
x, y = pos
|
|
2551
|
+
w, h = size
|
|
2552
|
+
beg = x
|
|
2553
|
+
end = x + w
|
|
2554
|
+
|
|
2555
|
+
if line_num == selection_start_row:
|
|
2556
|
+
line = lines[line_num]
|
|
2557
|
+
beg -= self.scroll_x
|
|
2558
|
+
beg += get_text_width(
|
|
2559
|
+
line[:selection_start_col],
|
|
2560
|
+
tab_width,
|
|
2561
|
+
label_cached
|
|
2562
|
+
)
|
|
2563
|
+
|
|
2564
|
+
if line_num == selection_end_row:
|
|
2565
|
+
line = lines[line_num]
|
|
2566
|
+
end = (x - self.scroll_x) + get_text_width(
|
|
2567
|
+
line[:selection_end_col],
|
|
2568
|
+
tab_width,
|
|
2569
|
+
label_cached
|
|
2570
|
+
)
|
|
2571
|
+
|
|
2572
|
+
beg = boundary(beg, x, x + width_minus_padding)
|
|
2573
|
+
end = boundary(end, x, x + width_minus_padding)
|
|
2574
|
+
if beg == end:
|
|
2575
|
+
return
|
|
2576
|
+
|
|
2577
|
+
canvas_add(Color(*selection_color, group='selection'))
|
|
2578
|
+
canvas_add(
|
|
2579
|
+
Rectangle(
|
|
2580
|
+
pos=(beg, y),
|
|
2581
|
+
size=(end - beg, h),
|
|
2582
|
+
group='selection'
|
|
2583
|
+
)
|
|
2584
|
+
)
|
|
2585
|
+
|
|
2586
|
+
def on_size(self, instance, value):
|
|
2587
|
+
# if the size change, we might do invalid scrolling / text split
|
|
2588
|
+
# size the text maybe be put after size_hint have been resolved.
|
|
2589
|
+
self._trigger_refresh_text()
|
|
2590
|
+
self._refresh_hint_text()
|
|
2591
|
+
self.scroll_x = self.scroll_y = 0
|
|
2592
|
+
|
|
2593
|
+
def _get_row_width(self, row):
|
|
2594
|
+
# Get the pixel width of the given row.
|
|
2595
|
+
_labels = self._lines_labels
|
|
2596
|
+
if row < len(_labels):
|
|
2597
|
+
return _labels[row].width
|
|
2598
|
+
return 0
|
|
2599
|
+
|
|
2600
|
+
def _get_cursor_pos(self):
|
|
2601
|
+
# return the current cursor x/y from the row/col
|
|
2602
|
+
dy = self.line_height + self.line_spacing
|
|
2603
|
+
padding_left = self.padding[0]
|
|
2604
|
+
padding_top = self.padding[1]
|
|
2605
|
+
padding_right = self.padding[2]
|
|
2606
|
+
left = self.x + padding_left
|
|
2607
|
+
top = self.top - padding_top
|
|
2608
|
+
y = top + self.scroll_y
|
|
2609
|
+
y -= self.cursor_row * dy
|
|
2610
|
+
|
|
2611
|
+
# Horizontal alignment
|
|
2612
|
+
halign = self.halign
|
|
2613
|
+
viewport_width = self.width - padding_left - padding_right
|
|
2614
|
+
cursor_offset = self.cursor_offset()
|
|
2615
|
+
base_dir = self.base_direction or self._resolved_base_dir
|
|
2616
|
+
auto_halign_r = halign == 'auto' and base_dir and 'rtl' in base_dir
|
|
2617
|
+
if halign == 'center':
|
|
2618
|
+
row_width = self._get_row_width(self.cursor_row)
|
|
2619
|
+
x = (
|
|
2620
|
+
left
|
|
2621
|
+
+ max(0, (viewport_width - row_width) // 2)
|
|
2622
|
+
+ cursor_offset
|
|
2623
|
+
- self.scroll_x
|
|
2624
|
+
)
|
|
2625
|
+
elif halign == 'right' or auto_halign_r:
|
|
2626
|
+
row_width = self._get_row_width(self.cursor_row)
|
|
2627
|
+
x = (
|
|
2628
|
+
left
|
|
2629
|
+
+ max(0, viewport_width - row_width)
|
|
2630
|
+
+ cursor_offset
|
|
2631
|
+
- self.scroll_x
|
|
2632
|
+
)
|
|
2633
|
+
else:
|
|
2634
|
+
x = left + cursor_offset - self.scroll_x
|
|
2635
|
+
|
|
2636
|
+
return x, y
|
|
2637
|
+
|
|
2638
|
+
def _get_cursor_visual_height(self):
|
|
2639
|
+
# Return the height of the cursor's visible part
|
|
2640
|
+
_, cy = map(int, self.cursor_pos)
|
|
2641
|
+
max_y = self.top - self.padding[1]
|
|
2642
|
+
min_y = self.y + self.padding[3]
|
|
2643
|
+
|
|
2644
|
+
lh = self.line_height
|
|
2645
|
+
if cy > max_y:
|
|
2646
|
+
return lh - min(lh, cy - max_y)
|
|
2647
|
+
else:
|
|
2648
|
+
return min(lh, max(0, cy - min_y))
|
|
2649
|
+
|
|
2650
|
+
def _get_cursor_visual_pos(self):
|
|
2651
|
+
# Return the position of the cursor's top visible point
|
|
2652
|
+
cx, cy = map(int, self.cursor_pos)
|
|
2653
|
+
max_y = self.top - self.padding[3]
|
|
2654
|
+
return [cx, min(max_y, cy)]
|
|
2655
|
+
|
|
2656
|
+
def _get_line_options(self):
|
|
2657
|
+
# Get or create line options, to be used for Label creation
|
|
2658
|
+
if self._line_options is None:
|
|
2659
|
+
self._line_options = kw = {
|
|
2660
|
+
'font_size': self.font_size,
|
|
2661
|
+
'font_name': self.font_name,
|
|
2662
|
+
'font_context': self.font_context,
|
|
2663
|
+
'font_family': self.font_family,
|
|
2664
|
+
'text_language': self.text_language,
|
|
2665
|
+
'base_direction': self.base_direction,
|
|
2666
|
+
'anchor_x': 'left',
|
|
2667
|
+
'anchor_y': 'top',
|
|
2668
|
+
'padding_x': 0,
|
|
2669
|
+
'padding_y': 0,
|
|
2670
|
+
'padding': (0, 0)
|
|
2671
|
+
}
|
|
2672
|
+
self._label_cached = Label(**kw)
|
|
2673
|
+
return self._line_options
|
|
2674
|
+
|
|
2675
|
+
def _create_line_label(self, text, hint=False):
|
|
2676
|
+
# Create a label from a text, using line options
|
|
2677
|
+
ntext = text.replace(u'\n', u'').replace(u'\t', u' ' * self.tab_width)
|
|
2678
|
+
|
|
2679
|
+
if self.password and not hint: # Don't replace hint_text with *
|
|
2680
|
+
ntext = self.password_mask * len(ntext)
|
|
2681
|
+
|
|
2682
|
+
kw = self._get_line_options()
|
|
2683
|
+
cid = '%s\0%s' % (ntext, str(kw))
|
|
2684
|
+
texture = Cache_get('textinput.label', cid)
|
|
2685
|
+
|
|
2686
|
+
if texture is None:
|
|
2687
|
+
# FIXME right now, we can't render very long line...
|
|
2688
|
+
# if we move on "VBO" version as fallback, we won't need to
|
|
2689
|
+
# do this. try to find the maximum text we can handle
|
|
2690
|
+
label = None
|
|
2691
|
+
label_len = len(ntext)
|
|
2692
|
+
ld = None
|
|
2693
|
+
|
|
2694
|
+
# check for blank line
|
|
2695
|
+
if not ntext:
|
|
2696
|
+
texture = Texture.create(size=(1, 1))
|
|
2697
|
+
Cache_append('textinput.label', cid, texture)
|
|
2698
|
+
return texture
|
|
2699
|
+
|
|
2700
|
+
while True:
|
|
2701
|
+
try:
|
|
2702
|
+
label = Label(text=ntext[:label_len], **kw)
|
|
2703
|
+
label.refresh()
|
|
2704
|
+
if ld is not None and ld > 2:
|
|
2705
|
+
ld //= 2
|
|
2706
|
+
label_len += ld
|
|
2707
|
+
else:
|
|
2708
|
+
break
|
|
2709
|
+
|
|
2710
|
+
except:
|
|
2711
|
+
# exception happen when we tried to render the text
|
|
2712
|
+
# reduce it...
|
|
2713
|
+
if ld is None:
|
|
2714
|
+
ld = len(ntext)
|
|
2715
|
+
ld //= 2
|
|
2716
|
+
if ld < 2 and label_len:
|
|
2717
|
+
label_len -= 1
|
|
2718
|
+
label_len -= ld
|
|
2719
|
+
continue
|
|
2720
|
+
|
|
2721
|
+
# ok, we found it.
|
|
2722
|
+
texture = label.texture
|
|
2723
|
+
Cache_append('textinput.label', cid, texture)
|
|
2724
|
+
return texture
|
|
2725
|
+
|
|
2726
|
+
_tokenize_delimiters = u' .,:;!?\r\t'
|
|
2727
|
+
|
|
2728
|
+
def _tokenize(self, text):
|
|
2729
|
+
# Tokenize a text string from some delimiters
|
|
2730
|
+
if text is None:
|
|
2731
|
+
return
|
|
2732
|
+
delimiters = self._tokenize_delimiters
|
|
2733
|
+
old_index = 0
|
|
2734
|
+
prev_char = ''
|
|
2735
|
+
for index, char in enumerate(text):
|
|
2736
|
+
if char not in delimiters:
|
|
2737
|
+
if char != u'\n':
|
|
2738
|
+
if index > 0 and (prev_char in delimiters):
|
|
2739
|
+
if old_index < index:
|
|
2740
|
+
yield text[old_index:index]
|
|
2741
|
+
old_index = index
|
|
2742
|
+
else:
|
|
2743
|
+
if old_index < index:
|
|
2744
|
+
yield text[old_index:index]
|
|
2745
|
+
yield text[index:index + 1]
|
|
2746
|
+
old_index = index + 1
|
|
2747
|
+
prev_char = char
|
|
2748
|
+
yield text[old_index:]
|
|
2749
|
+
|
|
2750
|
+
def _split_smart(self, text):
|
|
2751
|
+
"""
|
|
2752
|
+
Do a "smart" split. If not multiline, or if wrap is set,
|
|
2753
|
+
we are not doing smart split, just a split on line break.
|
|
2754
|
+
Otherwise, we are trying to split as soon as possible, to prevent
|
|
2755
|
+
overflow on the widget.
|
|
2756
|
+
"""
|
|
2757
|
+
|
|
2758
|
+
# depend of the options, split the text on line, or word
|
|
2759
|
+
if not self.multiline or not self.do_wrap:
|
|
2760
|
+
lines = text.split(u'\n')
|
|
2761
|
+
lines_flags = [0] + [FL_IS_LINEBREAK] * (len(lines) - 1)
|
|
2762
|
+
return lines, lines_flags
|
|
2763
|
+
|
|
2764
|
+
# no autosize, do wordwrap.
|
|
2765
|
+
x = flags = 0
|
|
2766
|
+
line = []
|
|
2767
|
+
lines = []
|
|
2768
|
+
lines_flags = []
|
|
2769
|
+
_join = u''.join
|
|
2770
|
+
lines_append, lines_flags_append = lines.append, lines_flags.append
|
|
2771
|
+
padding_left = self.padding[0]
|
|
2772
|
+
padding_right = self.padding[2]
|
|
2773
|
+
width = self.width - padding_left - padding_right
|
|
2774
|
+
text_width = self._get_text_width
|
|
2775
|
+
_tab_width, _label_cached = self.tab_width, self._label_cached
|
|
2776
|
+
|
|
2777
|
+
# try to add each word on current line.
|
|
2778
|
+
words_widths = {}
|
|
2779
|
+
for word in self._tokenize(text):
|
|
2780
|
+
is_newline = (word == u'\n')
|
|
2781
|
+
try:
|
|
2782
|
+
w = words_widths[word]
|
|
2783
|
+
except KeyError:
|
|
2784
|
+
w = text_width(word, _tab_width, _label_cached)
|
|
2785
|
+
words_widths[word] = w
|
|
2786
|
+
# if we have more than the width, or if it's a newline,
|
|
2787
|
+
# push the current line, and create a new one
|
|
2788
|
+
if (x + w > width and line) or is_newline:
|
|
2789
|
+
lines_append(_join(line))
|
|
2790
|
+
lines_flags_append(flags)
|
|
2791
|
+
flags = 0
|
|
2792
|
+
line = []
|
|
2793
|
+
x = 0
|
|
2794
|
+
if is_newline:
|
|
2795
|
+
flags |= FL_IS_LINEBREAK
|
|
2796
|
+
elif width >= 1 and w > width:
|
|
2797
|
+
while w > width:
|
|
2798
|
+
split_width = split_pos = 0
|
|
2799
|
+
# split the word
|
|
2800
|
+
for c in word:
|
|
2801
|
+
try:
|
|
2802
|
+
cw = words_widths[c]
|
|
2803
|
+
except KeyError:
|
|
2804
|
+
cw = text_width(c, _tab_width, _label_cached)
|
|
2805
|
+
words_widths[c] = cw
|
|
2806
|
+
if split_width + cw > width:
|
|
2807
|
+
break
|
|
2808
|
+
split_width += cw
|
|
2809
|
+
split_pos += 1
|
|
2810
|
+
if split_width == split_pos == 0:
|
|
2811
|
+
# can't fit the word in, give up
|
|
2812
|
+
break
|
|
2813
|
+
lines_append(word[:split_pos])
|
|
2814
|
+
lines_flags_append(flags)
|
|
2815
|
+
flags = FL_IS_WORDBREAK
|
|
2816
|
+
word = word[split_pos:]
|
|
2817
|
+
w -= split_width
|
|
2818
|
+
x = w
|
|
2819
|
+
line.append(word)
|
|
2820
|
+
else:
|
|
2821
|
+
x += w
|
|
2822
|
+
line.append(word)
|
|
2823
|
+
if line or flags & FL_IS_LINEBREAK:
|
|
2824
|
+
lines_append(_join(line))
|
|
2825
|
+
lines_flags_append(flags)
|
|
2826
|
+
|
|
2827
|
+
return lines, lines_flags
|
|
2828
|
+
|
|
2829
|
+
def _key_down(self, key, repeat=False):
|
|
2830
|
+
displayed_str, internal_str, internal_action, scale = key
|
|
2831
|
+
|
|
2832
|
+
# handle deletion
|
|
2833
|
+
if (
|
|
2834
|
+
self._selection
|
|
2835
|
+
and internal_action in (None, 'del', 'backspace', 'enter')
|
|
2836
|
+
and (internal_action != 'enter' or self.multiline)
|
|
2837
|
+
):
|
|
2838
|
+
self.delete_selection()
|
|
2839
|
+
|
|
2840
|
+
elif internal_action == 'del':
|
|
2841
|
+
# Move cursor one char to the right. If that was successful,
|
|
2842
|
+
# do a backspace (effectively deleting char right of cursor)
|
|
2843
|
+
cursor = self.cursor
|
|
2844
|
+
self.do_cursor_movement('cursor_right')
|
|
2845
|
+
if cursor != self.cursor:
|
|
2846
|
+
self.do_backspace(mode='del')
|
|
2847
|
+
|
|
2848
|
+
elif internal_action == 'backspace':
|
|
2849
|
+
self.do_backspace()
|
|
2850
|
+
|
|
2851
|
+
# handle action keys and text insertion
|
|
2852
|
+
if internal_action is None:
|
|
2853
|
+
self.insert_text(displayed_str)
|
|
2854
|
+
|
|
2855
|
+
elif internal_action in ('shift', 'shift_L', 'shift_R'):
|
|
2856
|
+
if not self._selection:
|
|
2857
|
+
self._selection_from = self._selection_to = self.cursor_index()
|
|
2858
|
+
self._selection = True
|
|
2859
|
+
self._selection_finished = False
|
|
2860
|
+
|
|
2861
|
+
elif internal_action == 'ctrl_L':
|
|
2862
|
+
self._ctrl_l = True
|
|
2863
|
+
|
|
2864
|
+
elif internal_action == 'ctrl_R':
|
|
2865
|
+
self._ctrl_r = True
|
|
2866
|
+
|
|
2867
|
+
elif internal_action == 'alt_L':
|
|
2868
|
+
self._alt_l = True
|
|
2869
|
+
|
|
2870
|
+
elif internal_action == 'alt_R':
|
|
2871
|
+
self._alt_r = True
|
|
2872
|
+
|
|
2873
|
+
elif internal_action.startswith('cursor_'):
|
|
2874
|
+
cc, cr = self.cursor
|
|
2875
|
+
self.do_cursor_movement(
|
|
2876
|
+
internal_action,
|
|
2877
|
+
self._ctrl_l or self._ctrl_r,
|
|
2878
|
+
self._alt_l or self._alt_r
|
|
2879
|
+
)
|
|
2880
|
+
if self._selection and not self._selection_finished:
|
|
2881
|
+
self._selection_to = self.cursor_index()
|
|
2882
|
+
self._update_selection()
|
|
2883
|
+
else:
|
|
2884
|
+
self.cancel_selection()
|
|
2885
|
+
|
|
2886
|
+
elif internal_action == 'enter':
|
|
2887
|
+
if self.multiline:
|
|
2888
|
+
self.insert_text(u'\n')
|
|
2889
|
+
else:
|
|
2890
|
+
self.dispatch('on_text_validate')
|
|
2891
|
+
if self.text_validate_unfocus:
|
|
2892
|
+
self.focus = False
|
|
2893
|
+
|
|
2894
|
+
elif internal_action == 'escape':
|
|
2895
|
+
self.focus = False
|
|
2896
|
+
|
|
2897
|
+
def _key_up(self, key, repeat=False):
|
|
2898
|
+
displayed_str, internal_str, internal_action, scale = key
|
|
2899
|
+
if internal_action in ('shift', 'shift_L', 'shift_R'):
|
|
2900
|
+
if self._selection:
|
|
2901
|
+
self._update_selection(True)
|
|
2902
|
+
elif internal_action == 'ctrl_L':
|
|
2903
|
+
self._ctrl_l = False
|
|
2904
|
+
elif internal_action == 'ctrl_R':
|
|
2905
|
+
self._ctrl_r = False
|
|
2906
|
+
elif internal_action == 'alt_L':
|
|
2907
|
+
self._alt_l = False
|
|
2908
|
+
elif internal_action == 'alt_R':
|
|
2909
|
+
self._alt_r = False
|
|
2910
|
+
|
|
2911
|
+
def keyboard_on_key_down(self, window, keycode, text, modifiers):
|
|
2912
|
+
key, _ = keycode
|
|
2913
|
+
win = EventLoop.window
|
|
2914
|
+
|
|
2915
|
+
# This allows *either* ctrl *or* cmd, but not both.
|
|
2916
|
+
modifiers = set(modifiers) - {'capslock', 'numlock'}
|
|
2917
|
+
is_shortcut = (
|
|
2918
|
+
modifiers == {'ctrl'}
|
|
2919
|
+
or _is_osx and modifiers == {'meta'}
|
|
2920
|
+
)
|
|
2921
|
+
is_interesting_key = key in self.interesting_keys.keys()
|
|
2922
|
+
|
|
2923
|
+
if (
|
|
2924
|
+
not self.write_tab
|
|
2925
|
+
and super().keyboard_on_key_down(window, keycode, text, modifiers)
|
|
2926
|
+
):
|
|
2927
|
+
return True
|
|
2928
|
+
|
|
2929
|
+
if text and is_shortcut and not is_interesting_key:
|
|
2930
|
+
self._handle_shortcut(key)
|
|
2931
|
+
|
|
2932
|
+
elif self._editable and text and not is_interesting_key:
|
|
2933
|
+
self._hide_handles(win)
|
|
2934
|
+
self._hide_cut_copy_paste(win)
|
|
2935
|
+
win.remove_widget(self._handle_middle)
|
|
2936
|
+
|
|
2937
|
+
# check for command modes
|
|
2938
|
+
# we use \x01INFO\x02 to get info from IME on mobiles
|
|
2939
|
+
# pygame seems to pass \x01 as the unicode for ctrl+a
|
|
2940
|
+
# checking for modifiers ensures conflict resolution.
|
|
2941
|
+
|
|
2942
|
+
first_char = ord(text[0])
|
|
2943
|
+
if not modifiers and first_char == 1:
|
|
2944
|
+
self._command_mode = True
|
|
2945
|
+
self._command = ''
|
|
2946
|
+
if not modifiers and first_char == 2:
|
|
2947
|
+
self._command_mode = False
|
|
2948
|
+
self._command = self._command[1:]
|
|
2949
|
+
|
|
2950
|
+
if self._command_mode:
|
|
2951
|
+
self._command += text
|
|
2952
|
+
return
|
|
2953
|
+
|
|
2954
|
+
_command = self._command
|
|
2955
|
+
if _command and first_char == 2:
|
|
2956
|
+
self._handle_command(_command)
|
|
2957
|
+
return
|
|
2958
|
+
|
|
2959
|
+
else:
|
|
2960
|
+
if EventLoop.window.managed_textinput:
|
|
2961
|
+
# we expect to get managed key input via on_textinput
|
|
2962
|
+
return
|
|
2963
|
+
if self._selection:
|
|
2964
|
+
self.delete_selection()
|
|
2965
|
+
self.insert_text(text)
|
|
2966
|
+
# self._recalc_size()
|
|
2967
|
+
return
|
|
2968
|
+
|
|
2969
|
+
if is_interesting_key:
|
|
2970
|
+
self._hide_cut_copy_paste(win)
|
|
2971
|
+
self._hide_handles(win)
|
|
2972
|
+
|
|
2973
|
+
if key == 27: # escape
|
|
2974
|
+
self.focus = False
|
|
2975
|
+
return True
|
|
2976
|
+
elif key == 9: # tab
|
|
2977
|
+
self.delete_selection()
|
|
2978
|
+
self.insert_text(u'\t')
|
|
2979
|
+
return True
|
|
2980
|
+
|
|
2981
|
+
k = self.interesting_keys.get(key)
|
|
2982
|
+
if k:
|
|
2983
|
+
key = (None, None, k, 1)
|
|
2984
|
+
self._key_down(key)
|
|
2985
|
+
|
|
2986
|
+
def _handle_command(self, command):
|
|
2987
|
+
from_undo = True
|
|
2988
|
+
command, data = command.split(':')
|
|
2989
|
+
self._command = ''
|
|
2990
|
+
if self._selection:
|
|
2991
|
+
self.delete_selection()
|
|
2992
|
+
if command == 'DEL':
|
|
2993
|
+
count = int(data)
|
|
2994
|
+
if not count:
|
|
2995
|
+
self.delete_selection(from_undo=True)
|
|
2996
|
+
end = self.cursor_index()
|
|
2997
|
+
self._selection_from = max(end - count, 0)
|
|
2998
|
+
self._selection_to = end
|
|
2999
|
+
self._selection = True
|
|
3000
|
+
self.delete_selection(from_undo=True)
|
|
3001
|
+
return
|
|
3002
|
+
elif command == 'INSERT':
|
|
3003
|
+
self.insert_text(data, from_undo)
|
|
3004
|
+
elif command == 'INSERTN':
|
|
3005
|
+
from_undo = False
|
|
3006
|
+
self.insert_text(data, from_undo)
|
|
3007
|
+
elif command == 'SELWORD':
|
|
3008
|
+
self.dispatch('on_double_tap')
|
|
3009
|
+
elif command == 'SEL':
|
|
3010
|
+
if data == '0':
|
|
3011
|
+
Clock.schedule_once(lambda dt: self.cancel_selection())
|
|
3012
|
+
elif command == 'CURCOL':
|
|
3013
|
+
self.cursor = int(data), self.cursor_row
|
|
3014
|
+
|
|
3015
|
+
def _handle_shortcut(self, key):
|
|
3016
|
+
# actions that can be done in readonly
|
|
3017
|
+
if key == ord('a'): # select all
|
|
3018
|
+
self.select_all()
|
|
3019
|
+
elif key == ord('c'): # copy selection
|
|
3020
|
+
self.copy()
|
|
3021
|
+
|
|
3022
|
+
if not self._editable:
|
|
3023
|
+
return
|
|
3024
|
+
|
|
3025
|
+
# actions that can be done only if editable
|
|
3026
|
+
if key == ord('x'): # cut selection
|
|
3027
|
+
self._cut(self.selection_text)
|
|
3028
|
+
elif key == ord('v'): # paste clipboard content
|
|
3029
|
+
self.paste()
|
|
3030
|
+
elif key == ord('z'): # undo
|
|
3031
|
+
self.do_undo()
|
|
3032
|
+
elif key == ord('r'): # redo
|
|
3033
|
+
self.do_redo()
|
|
3034
|
+
|
|
3035
|
+
def keyboard_on_key_up(self, window, keycode):
|
|
3036
|
+
key = keycode[0]
|
|
3037
|
+
k = self.interesting_keys.get(key)
|
|
3038
|
+
if k:
|
|
3039
|
+
key = (None, None, k, 1)
|
|
3040
|
+
self._key_up(key)
|
|
3041
|
+
|
|
3042
|
+
def keyboard_on_textinput(self, window, text):
|
|
3043
|
+
if self._selection:
|
|
3044
|
+
self.delete_selection()
|
|
3045
|
+
self.insert_text(text, False)
|
|
3046
|
+
|
|
3047
|
+
# current IME composition in progress by the IME system, or '' if nothing
|
|
3048
|
+
_ime_composition = StringProperty('')
|
|
3049
|
+
# cursor position of last IME event
|
|
3050
|
+
_ime_cursor = ListProperty(None, allownone=True)
|
|
3051
|
+
|
|
3052
|
+
def _bind_keyboard(self):
|
|
3053
|
+
super()._bind_keyboard()
|
|
3054
|
+
Window.bind(on_textedit=self.window_on_textedit)
|
|
3055
|
+
|
|
3056
|
+
def _unbind_keyboard(self):
|
|
3057
|
+
super()._unbind_keyboard()
|
|
3058
|
+
Window.unbind(on_textedit=self.window_on_textedit)
|
|
3059
|
+
|
|
3060
|
+
def window_on_textedit(self, window, ime_input):
|
|
3061
|
+
text_lines = self._lines or ['']
|
|
3062
|
+
if self._ime_composition:
|
|
3063
|
+
pcc, pcr = self._ime_cursor
|
|
3064
|
+
text = text_lines[pcr]
|
|
3065
|
+
len_ime = len(self._ime_composition)
|
|
3066
|
+
if text[pcc - len_ime:pcc] == self._ime_composition: # always?
|
|
3067
|
+
remove_old_ime_text = text[:pcc - len_ime] + text[pcc:]
|
|
3068
|
+
ci = self.cursor_index()
|
|
3069
|
+
self._refresh_text_from_property(
|
|
3070
|
+
"insert",
|
|
3071
|
+
*self._get_line_from_cursor(pcr, remove_old_ime_text)
|
|
3072
|
+
)
|
|
3073
|
+
self.cursor = self.get_cursor_from_index(ci - len_ime)
|
|
3074
|
+
|
|
3075
|
+
if ime_input:
|
|
3076
|
+
if self._selection:
|
|
3077
|
+
self.delete_selection()
|
|
3078
|
+
cc, cr = self.cursor
|
|
3079
|
+
text = text_lines[cr]
|
|
3080
|
+
new_text = text[:cc] + ime_input + text[cc:]
|
|
3081
|
+
self._refresh_text_from_property(
|
|
3082
|
+
"insert", *self._get_line_from_cursor(cr, new_text)
|
|
3083
|
+
)
|
|
3084
|
+
self.cursor = self.get_cursor_from_index(
|
|
3085
|
+
self.cursor_index() + len(ime_input)
|
|
3086
|
+
)
|
|
3087
|
+
self._ime_composition = ime_input
|
|
3088
|
+
self._ime_cursor = self.cursor
|
|
3089
|
+
|
|
3090
|
+
def on__hint_text(self, instance, value):
|
|
3091
|
+
self._refresh_hint_text()
|
|
3092
|
+
|
|
3093
|
+
def _refresh_hint_text(self):
|
|
3094
|
+
_lines, self._hint_text_flags = self._split_smart(self.hint_text)
|
|
3095
|
+
_hint_text_labels = []
|
|
3096
|
+
_hint_text_rects = []
|
|
3097
|
+
_create_label = self._create_line_label
|
|
3098
|
+
|
|
3099
|
+
for x in _lines:
|
|
3100
|
+
lbl = _create_label(x, hint=True)
|
|
3101
|
+
_hint_text_labels.append(lbl)
|
|
3102
|
+
_hint_text_rects.append(Rectangle(size=lbl.size))
|
|
3103
|
+
|
|
3104
|
+
self._hint_text_lines[:] = _lines
|
|
3105
|
+
self._hint_text_labels = _hint_text_labels
|
|
3106
|
+
self._hint_text_rects = _hint_text_rects
|
|
3107
|
+
|
|
3108
|
+
# Remember to update graphics
|
|
3109
|
+
self._trigger_update_graphics()
|
|
3110
|
+
|
|
3111
|
+
#
|
|
3112
|
+
# Properties
|
|
3113
|
+
#
|
|
3114
|
+
|
|
3115
|
+
_lines = ListProperty([])
|
|
3116
|
+
_hint_text_lines = ListProperty([])
|
|
3117
|
+
_editable = BooleanProperty(True)
|
|
3118
|
+
_insert_int_pat = re.compile(u'^-?[0-9]*$')
|
|
3119
|
+
_insert_float_pat = re.compile(u'^-?[0-9]*\\.?[0-9]*$')
|
|
3120
|
+
_cursor_blink = BooleanProperty(False)
|
|
3121
|
+
_cursor_visual_pos = AliasProperty(
|
|
3122
|
+
_get_cursor_visual_pos, None, bind=['cursor_pos']
|
|
3123
|
+
)
|
|
3124
|
+
_cursor_visual_height = AliasProperty(
|
|
3125
|
+
_get_cursor_visual_height, None, bind=['cursor_pos']
|
|
3126
|
+
)
|
|
3127
|
+
|
|
3128
|
+
readonly = BooleanProperty(False)
|
|
3129
|
+
'''If True, the user will not be able to change the content of a textinput.
|
|
3130
|
+
|
|
3131
|
+
.. versionadded:: 1.3.0
|
|
3132
|
+
|
|
3133
|
+
:attr:`readonly` is a :class:`~kivy.properties.BooleanProperty` and
|
|
3134
|
+
defaults to False.
|
|
3135
|
+
'''
|
|
3136
|
+
|
|
3137
|
+
text_validate_unfocus = BooleanProperty(True)
|
|
3138
|
+
'''If True, the :meth:`TextInput.on_text_validate` event will unfocus the
|
|
3139
|
+
widget, therefore make it stop listening to the keyboard. When disabled,
|
|
3140
|
+
the :meth:`TextInput.on_text_validate` event can be fired multiple times
|
|
3141
|
+
as the result of TextInput keeping the focus enabled.
|
|
3142
|
+
|
|
3143
|
+
.. versionadded:: 1.10.1
|
|
3144
|
+
|
|
3145
|
+
:attr:`text_validate_unfocus` is
|
|
3146
|
+
a :class:`~kivy.properties.BooleanProperty` and defaults to True.
|
|
3147
|
+
'''
|
|
3148
|
+
|
|
3149
|
+
multiline = BooleanProperty(True)
|
|
3150
|
+
'''If True, the widget will be able show multiple lines of text. If False,
|
|
3151
|
+
the "enter" keypress will defocus the textinput instead of adding a new
|
|
3152
|
+
line.
|
|
3153
|
+
|
|
3154
|
+
:attr:`multiline` is a :class:`~kivy.properties.BooleanProperty` and
|
|
3155
|
+
defaults to True.
|
|
3156
|
+
'''
|
|
3157
|
+
|
|
3158
|
+
do_wrap = BooleanProperty(True)
|
|
3159
|
+
'''If True, and the text is multiline, then lines larger than the width of
|
|
3160
|
+
the widget will wrap around to the next line, avoiding the need for
|
|
3161
|
+
horizontal scrolling. Disabling this option ensure one line is always
|
|
3162
|
+
displayed as one line.
|
|
3163
|
+
|
|
3164
|
+
:attr:`do_wrap` is a :class:`~kivy.properties.BooleanProperty` and defaults
|
|
3165
|
+
to True.
|
|
3166
|
+
|
|
3167
|
+
versionadded:: 2.1.0
|
|
3168
|
+
'''
|
|
3169
|
+
|
|
3170
|
+
password = BooleanProperty(False)
|
|
3171
|
+
'''If True, the widget will display its characters as the character
|
|
3172
|
+
set in :attr:`password_mask`.
|
|
3173
|
+
|
|
3174
|
+
.. versionadded:: 1.2.0
|
|
3175
|
+
|
|
3176
|
+
:attr:`password` is a :class:`~kivy.properties.BooleanProperty` and
|
|
3177
|
+
defaults to False.
|
|
3178
|
+
'''
|
|
3179
|
+
|
|
3180
|
+
password_mask = StringProperty('*')
|
|
3181
|
+
'''Sets the character used to mask the text when :attr:`password` is True.
|
|
3182
|
+
|
|
3183
|
+
.. versionadded:: 1.10.0
|
|
3184
|
+
|
|
3185
|
+
:attr:`password_mask` is a :class:`~kivy.properties.StringProperty` and
|
|
3186
|
+
defaults to `'*'`.
|
|
3187
|
+
'''
|
|
3188
|
+
|
|
3189
|
+
cursor_blink = BooleanProperty(True)
|
|
3190
|
+
'''This property is used to set whether the graphic cursor should blink
|
|
3191
|
+
or not.
|
|
3192
|
+
|
|
3193
|
+
.. versionchanged:: 1.10.1
|
|
3194
|
+
`cursor_blink` has been refactored to enable switching the blinking
|
|
3195
|
+
on/off and the previous behavior has been moved to a private
|
|
3196
|
+
`_cursor_blink` property. The previous default value `False` has been
|
|
3197
|
+
changed to `True`.
|
|
3198
|
+
|
|
3199
|
+
:attr:`cursor_blink` is a :class:`~kivy.properties.BooleanProperty` and
|
|
3200
|
+
defaults to True.
|
|
3201
|
+
'''
|
|
3202
|
+
|
|
3203
|
+
def _get_cursor(self):
|
|
3204
|
+
return self._cursor
|
|
3205
|
+
|
|
3206
|
+
def _set_cursor(self, pos):
|
|
3207
|
+
if not self._lines:
|
|
3208
|
+
self._trigger_refresh_text()
|
|
3209
|
+
return
|
|
3210
|
+
l = self._lines
|
|
3211
|
+
cr = boundary(pos[1], 0, len(l) - 1)
|
|
3212
|
+
cc = boundary(pos[0], 0, len(l[cr]))
|
|
3213
|
+
cursor = cc, cr
|
|
3214
|
+
|
|
3215
|
+
# adjust scrollview to ensure that the cursor will be always inside our
|
|
3216
|
+
# viewport.
|
|
3217
|
+
self._adjust_viewport(cc, cr)
|
|
3218
|
+
|
|
3219
|
+
if self._cursor == cursor:
|
|
3220
|
+
return
|
|
3221
|
+
|
|
3222
|
+
self._cursor = cursor
|
|
3223
|
+
return True
|
|
3224
|
+
|
|
3225
|
+
@triggered(timeout=-1)
|
|
3226
|
+
def _adjust_viewport(self, cc, cr):
|
|
3227
|
+
padding_left = self.padding[0]
|
|
3228
|
+
padding_right = self.padding[2]
|
|
3229
|
+
viewport_width = self.width - padding_left - padding_right
|
|
3230
|
+
sx = self.scroll_x
|
|
3231
|
+
base_dir = self.base_direction or self._resolved_base_dir
|
|
3232
|
+
auto_halign_r = (
|
|
3233
|
+
self.halign == 'auto'
|
|
3234
|
+
and base_dir
|
|
3235
|
+
and 'rtl' in base_dir
|
|
3236
|
+
)
|
|
3237
|
+
|
|
3238
|
+
offset = self.cursor_offset()
|
|
3239
|
+
row_width = self._get_row_width(self.cursor_row)
|
|
3240
|
+
|
|
3241
|
+
# if offset is outside the current bounds, readjust
|
|
3242
|
+
if offset - sx >= viewport_width:
|
|
3243
|
+
self.scroll_x = offset - viewport_width
|
|
3244
|
+
elif offset < sx + 1:
|
|
3245
|
+
self.scroll_x = offset
|
|
3246
|
+
|
|
3247
|
+
# Avoid right/center horizontal alignment issues if the viewport is at
|
|
3248
|
+
# the end of the line, if not multiline.
|
|
3249
|
+
viewport_scroll_x = row_width - viewport_width
|
|
3250
|
+
if (
|
|
3251
|
+
not self.multiline
|
|
3252
|
+
and offset >= viewport_scroll_x
|
|
3253
|
+
and self.scroll_x >= viewport_scroll_x
|
|
3254
|
+
and (
|
|
3255
|
+
self.halign == "center"
|
|
3256
|
+
or self.halign == "right"
|
|
3257
|
+
or auto_halign_r
|
|
3258
|
+
)
|
|
3259
|
+
):
|
|
3260
|
+
self.scroll_x = max(0, viewport_scroll_x)
|
|
3261
|
+
|
|
3262
|
+
# do the same for Y
|
|
3263
|
+
# this algo try to center the cursor as much as possible
|
|
3264
|
+
dy = self.line_height + self.line_spacing
|
|
3265
|
+
offsety = cr * dy
|
|
3266
|
+
|
|
3267
|
+
padding_top = self.padding[1]
|
|
3268
|
+
padding_bottom = self.padding[3]
|
|
3269
|
+
viewport_height = self.height - padding_top - padding_bottom - dy
|
|
3270
|
+
|
|
3271
|
+
sy = self.scroll_y
|
|
3272
|
+
if offsety > viewport_height + sy:
|
|
3273
|
+
self.scroll_y = offsety - viewport_height
|
|
3274
|
+
elif offsety < sy:
|
|
3275
|
+
self.scroll_y = offsety
|
|
3276
|
+
|
|
3277
|
+
cursor = AliasProperty(_get_cursor, _set_cursor)
|
|
3278
|
+
'''Tuple of (col, row) values indicating the current cursor position.
|
|
3279
|
+
You can set a new (col, row) if you want to move the cursor. The scrolling
|
|
3280
|
+
area will be automatically updated to ensure that the cursor is
|
|
3281
|
+
visible inside the viewport.
|
|
3282
|
+
|
|
3283
|
+
:attr:`cursor` is an :class:`~kivy.properties.AliasProperty`.
|
|
3284
|
+
'''
|
|
3285
|
+
|
|
3286
|
+
def _get_cursor_col(self):
|
|
3287
|
+
return self._cursor[0]
|
|
3288
|
+
|
|
3289
|
+
cursor_col = AliasProperty(_get_cursor_col, None, bind=('cursor', ))
|
|
3290
|
+
'''Current column of the cursor.
|
|
3291
|
+
|
|
3292
|
+
:attr:`cursor_col` is an :class:`~kivy.properties.AliasProperty` to
|
|
3293
|
+
cursor[0], read-only.
|
|
3294
|
+
'''
|
|
3295
|
+
|
|
3296
|
+
def _get_cursor_row(self):
|
|
3297
|
+
return self._cursor[1]
|
|
3298
|
+
|
|
3299
|
+
cursor_row = AliasProperty(_get_cursor_row, None, bind=('cursor', ))
|
|
3300
|
+
'''Current row of the cursor.
|
|
3301
|
+
|
|
3302
|
+
:attr:`cursor_row` is an :class:`~kivy.properties.AliasProperty` to
|
|
3303
|
+
cursor[1], read-only.
|
|
3304
|
+
'''
|
|
3305
|
+
|
|
3306
|
+
cursor_pos = AliasProperty(_get_cursor_pos, None,
|
|
3307
|
+
bind=('cursor', 'padding', 'pos', 'size',
|
|
3308
|
+
'focus', 'scroll_x', 'scroll_y',
|
|
3309
|
+
'line_height', 'line_spacing'),
|
|
3310
|
+
cache=False)
|
|
3311
|
+
'''Current position of the cursor, in (x, y).
|
|
3312
|
+
|
|
3313
|
+
:attr:`cursor_pos` is an :class:`~kivy.properties.AliasProperty`,
|
|
3314
|
+
read-only.
|
|
3315
|
+
'''
|
|
3316
|
+
|
|
3317
|
+
cursor_color = ColorProperty([1, 0, 0, 1])
|
|
3318
|
+
'''Current color of the cursor, in (r, g, b, a) format.
|
|
3319
|
+
|
|
3320
|
+
.. versionadded:: 1.9.0
|
|
3321
|
+
|
|
3322
|
+
:attr:`cursor_color` is a :class:`~kivy.properties.ColorProperty` and
|
|
3323
|
+
defaults to [1, 0, 0, 1].
|
|
3324
|
+
|
|
3325
|
+
.. versionchanged:: 2.0.0
|
|
3326
|
+
Changed from :class:`~kivy.properties.ListProperty` to
|
|
3327
|
+
:class:`~kivy.properties.ColorProperty`.
|
|
3328
|
+
'''
|
|
3329
|
+
|
|
3330
|
+
cursor_width = NumericProperty('1sp')
|
|
3331
|
+
'''Current width of the cursor.
|
|
3332
|
+
|
|
3333
|
+
.. versionadded:: 1.10.0
|
|
3334
|
+
|
|
3335
|
+
:attr:`cursor_width` is a :class:`~kivy.properties.NumericProperty` and
|
|
3336
|
+
defaults to '1sp'.
|
|
3337
|
+
'''
|
|
3338
|
+
|
|
3339
|
+
line_height = NumericProperty(1)
|
|
3340
|
+
'''Height of a line. This property is automatically computed from the
|
|
3341
|
+
:attr:`font_name`, :attr:`font_size`. Changing the line_height will have
|
|
3342
|
+
no impact.
|
|
3343
|
+
|
|
3344
|
+
.. note::
|
|
3345
|
+
|
|
3346
|
+
:attr:`line_height` is the height of a single line of text.
|
|
3347
|
+
Use :attr:`minimum_height`, which also includes padding, to
|
|
3348
|
+
get the height required to display the text properly.
|
|
3349
|
+
|
|
3350
|
+
:attr:`line_height` is a :class:`~kivy.properties.NumericProperty`,
|
|
3351
|
+
read-only.
|
|
3352
|
+
'''
|
|
3353
|
+
|
|
3354
|
+
tab_width = NumericProperty(4)
|
|
3355
|
+
'''By default, each tab will be replaced by four spaces on the text
|
|
3356
|
+
input widget. You can set a lower or higher value.
|
|
3357
|
+
|
|
3358
|
+
:attr:`tab_width` is a :class:`~kivy.properties.NumericProperty` and
|
|
3359
|
+
defaults to 4.
|
|
3360
|
+
'''
|
|
3361
|
+
|
|
3362
|
+
padding_x = VariableListProperty([0, 0], length=2, deprecated=True)
|
|
3363
|
+
'''Horizontal padding of the text: [padding_left, padding_right].
|
|
3364
|
+
|
|
3365
|
+
padding_x also accepts a one argument form [padding_horizontal].
|
|
3366
|
+
|
|
3367
|
+
:attr:`padding_x` is a :class:`~kivy.properties.VariableListProperty` and
|
|
3368
|
+
defaults to [0, 0]. This might be changed by the current theme.
|
|
3369
|
+
|
|
3370
|
+
.. deprecated:: 1.7.0
|
|
3371
|
+
Use :attr:`padding` instead.
|
|
3372
|
+
'''
|
|
3373
|
+
|
|
3374
|
+
def on_padding_x(self, instance, value):
|
|
3375
|
+
self.padding[0] = value[0]
|
|
3376
|
+
self.padding[2] = value[1]
|
|
3377
|
+
|
|
3378
|
+
padding_y = VariableListProperty([0, 0], length=2, deprecated=True)
|
|
3379
|
+
'''Vertical padding of the text: [padding_top, padding_bottom].
|
|
3380
|
+
|
|
3381
|
+
padding_y also accepts a one argument form [padding_vertical].
|
|
3382
|
+
|
|
3383
|
+
:attr:`padding_y` is a :class:`~kivy.properties.VariableListProperty` and
|
|
3384
|
+
defaults to [0, 0]. This might be changed by the current theme.
|
|
3385
|
+
|
|
3386
|
+
.. deprecated:: 1.7.0
|
|
3387
|
+
Use :attr:`padding` instead.
|
|
3388
|
+
'''
|
|
3389
|
+
|
|
3390
|
+
def on_padding_y(self, instance, value):
|
|
3391
|
+
self.padding[1] = value[0]
|
|
3392
|
+
self.padding[3] = value[1]
|
|
3393
|
+
|
|
3394
|
+
padding = VariableListProperty([6, 6, 6, 6])
|
|
3395
|
+
'''Padding of the text: [padding_left, padding_top, padding_right,
|
|
3396
|
+
padding_bottom].
|
|
3397
|
+
|
|
3398
|
+
padding also accepts a two argument form [padding_horizontal,
|
|
3399
|
+
padding_vertical] and a one argument form [padding].
|
|
3400
|
+
|
|
3401
|
+
.. versionchanged:: 1.7.0
|
|
3402
|
+
Replaced AliasProperty with VariableListProperty.
|
|
3403
|
+
|
|
3404
|
+
:attr:`padding` is a :class:`~kivy.properties.VariableListProperty` and
|
|
3405
|
+
defaults to [6, 6, 6, 6].
|
|
3406
|
+
'''
|
|
3407
|
+
|
|
3408
|
+
halign = OptionProperty('auto', options=['left', 'center', 'right',
|
|
3409
|
+
'auto'])
|
|
3410
|
+
'''Horizontal alignment of the text.
|
|
3411
|
+
|
|
3412
|
+
:attr:`halign` is an :class:`~kivy.properties.OptionProperty` and
|
|
3413
|
+
defaults to 'auto'. Available options are : auto, left, center and right.
|
|
3414
|
+
Auto will attempt to autodetect horizontal alignment for RTL text (Pango
|
|
3415
|
+
only), otherwise it behaves like `left`.
|
|
3416
|
+
|
|
3417
|
+
.. versionadded:: 1.10.1
|
|
3418
|
+
'''
|
|
3419
|
+
|
|
3420
|
+
scroll_x = NumericProperty(0)
|
|
3421
|
+
'''X scrolling value of the viewport. The scrolling is automatically
|
|
3422
|
+
updated when the cursor is moved or text changed. If there is no
|
|
3423
|
+
user input, the scroll_x and scroll_y properties may be changed.
|
|
3424
|
+
|
|
3425
|
+
:attr:`scroll_x` is a :class:`~kivy.properties.NumericProperty` and
|
|
3426
|
+
defaults to 0.
|
|
3427
|
+
'''
|
|
3428
|
+
|
|
3429
|
+
scroll_y = NumericProperty(0)
|
|
3430
|
+
'''Y scrolling value of the viewport. See :attr:`scroll_x` for more
|
|
3431
|
+
information.
|
|
3432
|
+
|
|
3433
|
+
:attr:`scroll_y` is a :class:`~kivy.properties.NumericProperty` and
|
|
3434
|
+
defaults to 0.
|
|
3435
|
+
'''
|
|
3436
|
+
|
|
3437
|
+
selection_color = ColorProperty([0.1843, 0.6549, 0.8313, .5])
|
|
3438
|
+
'''Current color of the selection, in (r, g, b, a) format.
|
|
3439
|
+
|
|
3440
|
+
.. warning::
|
|
3441
|
+
|
|
3442
|
+
The color should always have an "alpha" component less than 1
|
|
3443
|
+
since the selection is drawn after the text.
|
|
3444
|
+
|
|
3445
|
+
:attr:`selection_color` is a :class:`~kivy.properties.ColorProperty` and
|
|
3446
|
+
defaults to [0.1843, 0.6549, 0.8313, .5].
|
|
3447
|
+
|
|
3448
|
+
.. versionchanged:: 2.0.0
|
|
3449
|
+
Changed from :class:`~kivy.properties.ListProperty` to
|
|
3450
|
+
:class:`~kivy.properties.ColorProperty`.
|
|
3451
|
+
'''
|
|
3452
|
+
|
|
3453
|
+
border = ListProperty([4, 4, 4, 4])
|
|
3454
|
+
'''Border used for :class:`~kivy.graphics.vertex_instructions.BorderImage`
|
|
3455
|
+
graphics instruction. Used with :attr:`background_normal` and
|
|
3456
|
+
:attr:`background_active`. Can be used for a custom background.
|
|
3457
|
+
|
|
3458
|
+
.. versionadded:: 1.4.1
|
|
3459
|
+
|
|
3460
|
+
It must be a list of four values: (bottom, right, top, left). Read the
|
|
3461
|
+
BorderImage instruction for more information about how to use it.
|
|
3462
|
+
|
|
3463
|
+
:attr:`border` is a :class:`~kivy.properties.ListProperty` and defaults
|
|
3464
|
+
to (4, 4, 4, 4).
|
|
3465
|
+
'''
|
|
3466
|
+
|
|
3467
|
+
background_normal = StringProperty(
|
|
3468
|
+
'atlas://data/images/defaulttheme/textinput')
|
|
3469
|
+
'''Background image of the TextInput when it's not in focus.
|
|
3470
|
+
|
|
3471
|
+
.. versionadded:: 1.4.1
|
|
3472
|
+
|
|
3473
|
+
:attr:`background_normal` is a :class:`~kivy.properties.StringProperty` and
|
|
3474
|
+
defaults to 'atlas://data/images/defaulttheme/textinput'.
|
|
3475
|
+
'''
|
|
3476
|
+
|
|
3477
|
+
background_disabled_normal = StringProperty(
|
|
3478
|
+
'atlas://data/images/defaulttheme/textinput_disabled')
|
|
3479
|
+
'''Background image of the TextInput when disabled.
|
|
3480
|
+
|
|
3481
|
+
.. versionadded:: 1.8.0
|
|
3482
|
+
|
|
3483
|
+
:attr:`background_disabled_normal` is a
|
|
3484
|
+
:class:`~kivy.properties.StringProperty` and
|
|
3485
|
+
defaults to 'atlas://data/images/defaulttheme/textinput_disabled'.
|
|
3486
|
+
'''
|
|
3487
|
+
|
|
3488
|
+
background_active = StringProperty(
|
|
3489
|
+
'atlas://data/images/defaulttheme/textinput_active')
|
|
3490
|
+
'''Background image of the TextInput when it's in focus.
|
|
3491
|
+
|
|
3492
|
+
.. versionadded:: 1.4.1
|
|
3493
|
+
|
|
3494
|
+
:attr:`background_active` is a
|
|
3495
|
+
:class:`~kivy.properties.StringProperty` and
|
|
3496
|
+
defaults to 'atlas://data/images/defaulttheme/textinput_active'.
|
|
3497
|
+
'''
|
|
3498
|
+
|
|
3499
|
+
background_color = ColorProperty([1, 1, 1, 1])
|
|
3500
|
+
'''Current color of the background, in (r, g, b, a) format.
|
|
3501
|
+
|
|
3502
|
+
.. versionadded:: 1.2.0
|
|
3503
|
+
|
|
3504
|
+
:attr:`background_color` is a :class:`~kivy.properties.ColorProperty`
|
|
3505
|
+
and defaults to [1, 1, 1, 1] (white).
|
|
3506
|
+
|
|
3507
|
+
.. versionchanged:: 2.0.0
|
|
3508
|
+
Changed from :class:`~kivy.properties.ListProperty` to
|
|
3509
|
+
:class:`~kivy.properties.ColorProperty`.
|
|
3510
|
+
'''
|
|
3511
|
+
|
|
3512
|
+
foreground_color = ColorProperty([0, 0, 0, 1])
|
|
3513
|
+
'''Current color of the foreground, in (r, g, b, a) format.
|
|
3514
|
+
|
|
3515
|
+
.. versionadded:: 1.2.0
|
|
3516
|
+
|
|
3517
|
+
:attr:`foreground_color` is a :class:`~kivy.properties.ColorProperty`
|
|
3518
|
+
and defaults to [0, 0, 0, 1] (black).
|
|
3519
|
+
|
|
3520
|
+
.. versionchanged:: 2.0.0
|
|
3521
|
+
Changed from :class:`~kivy.properties.ListProperty` to
|
|
3522
|
+
:class:`~kivy.properties.ColorProperty`.
|
|
3523
|
+
'''
|
|
3524
|
+
|
|
3525
|
+
disabled_foreground_color = ColorProperty([0, 0, 0, .5])
|
|
3526
|
+
'''Current color of the foreground when disabled, in (r, g, b, a) format.
|
|
3527
|
+
|
|
3528
|
+
.. versionadded:: 1.8.0
|
|
3529
|
+
|
|
3530
|
+
:attr:`disabled_foreground_color` is a
|
|
3531
|
+
:class:`~kivy.properties.ColorProperty` and
|
|
3532
|
+
defaults to [0, 0, 0, 5] (50% transparent black).
|
|
3533
|
+
|
|
3534
|
+
.. versionchanged:: 2.0.0
|
|
3535
|
+
Changed from :class:`~kivy.properties.ListProperty` to
|
|
3536
|
+
:class:`~kivy.properties.ColorProperty`.
|
|
3537
|
+
'''
|
|
3538
|
+
|
|
3539
|
+
use_bubble = BooleanProperty(not _is_desktop)
|
|
3540
|
+
'''Indicates whether the cut/copy/paste bubble is used.
|
|
3541
|
+
|
|
3542
|
+
.. versionadded:: 1.7.0
|
|
3543
|
+
|
|
3544
|
+
:attr:`use_bubble` is a :class:`~kivy.properties.BooleanProperty`
|
|
3545
|
+
and defaults to True on mobile OS's, False on desktop OS's.
|
|
3546
|
+
'''
|
|
3547
|
+
|
|
3548
|
+
use_handles = BooleanProperty(not _is_desktop)
|
|
3549
|
+
'''Indicates whether the selection handles are displayed.
|
|
3550
|
+
|
|
3551
|
+
.. versionadded:: 1.8.0
|
|
3552
|
+
|
|
3553
|
+
:attr:`use_handles` is a :class:`~kivy.properties.BooleanProperty`
|
|
3554
|
+
and defaults to True on mobile OS's, False on desktop OS's.
|
|
3555
|
+
'''
|
|
3556
|
+
|
|
3557
|
+
scroll_from_swipe = BooleanProperty(not _is_desktop)
|
|
3558
|
+
'''Allow to scroll the text using swipe gesture according to
|
|
3559
|
+
:attr:`scroll_timeout` and :attr:`scroll_distance`.
|
|
3560
|
+
|
|
3561
|
+
.. versionadded:: 2.1.0
|
|
3562
|
+
|
|
3563
|
+
:attr:`scroll_from_swipe` is a BooleanProperty and defaults to True on
|
|
3564
|
+
mobile OS’s, False on desktop OS’s.
|
|
3565
|
+
'''
|
|
3566
|
+
|
|
3567
|
+
scroll_distance = NumericProperty(_scroll_distance)
|
|
3568
|
+
'''Minimum distance to move before change from scroll to selection mode, in
|
|
3569
|
+
pixels.
|
|
3570
|
+
It is advisable that you base this value on the dpi of your target device's
|
|
3571
|
+
screen.
|
|
3572
|
+
|
|
3573
|
+
.. versionadded:: 2.1.0
|
|
3574
|
+
|
|
3575
|
+
:attr:`scroll_distance` is a NumericProperty and defaults to 20 pixels.
|
|
3576
|
+
'''
|
|
3577
|
+
|
|
3578
|
+
scroll_timeout = NumericProperty(_scroll_timeout)
|
|
3579
|
+
'''Timeout allowed to trigger the :attr:`scroll_distance`, in milliseconds.
|
|
3580
|
+
If the user has not moved :attr:`scroll_distance` within the timeout, the
|
|
3581
|
+
scrolling will be disabled, and the selection mode will start.
|
|
3582
|
+
|
|
3583
|
+
.. versionadded:: 2.1.0
|
|
3584
|
+
|
|
3585
|
+
:attr:`scroll_timeout` is a NumericProperty and defaults to 250
|
|
3586
|
+
milliseconds.
|
|
3587
|
+
'''
|
|
3588
|
+
|
|
3589
|
+
def get_sel_from(self):
|
|
3590
|
+
return self._selection_from
|
|
3591
|
+
|
|
3592
|
+
selection_from = AliasProperty(get_sel_from, None)
|
|
3593
|
+
'''If a selection is in progress or complete, this property will represent
|
|
3594
|
+
the cursor index where the selection started.
|
|
3595
|
+
|
|
3596
|
+
.. versionchanged:: 1.4.0
|
|
3597
|
+
:attr:`selection_from` is an :class:`~kivy.properties.AliasProperty`
|
|
3598
|
+
and defaults to None, readonly.
|
|
3599
|
+
'''
|
|
3600
|
+
|
|
3601
|
+
def get_sel_to(self):
|
|
3602
|
+
return self._selection_to
|
|
3603
|
+
|
|
3604
|
+
selection_to = AliasProperty(get_sel_to, None)
|
|
3605
|
+
'''If a selection is in progress or complete, this property will represent
|
|
3606
|
+
the cursor index where the selection started.
|
|
3607
|
+
|
|
3608
|
+
.. versionchanged:: 1.4.0
|
|
3609
|
+
:attr:`selection_to` is an :class:`~kivy.properties.AliasProperty` and
|
|
3610
|
+
defaults to None, readonly.
|
|
3611
|
+
'''
|
|
3612
|
+
|
|
3613
|
+
selection_text = StringProperty(u'')
|
|
3614
|
+
'''Current content selection.
|
|
3615
|
+
|
|
3616
|
+
:attr:`selection_text` is a :class:`~kivy.properties.StringProperty`
|
|
3617
|
+
and defaults to '', readonly.
|
|
3618
|
+
'''
|
|
3619
|
+
|
|
3620
|
+
def on_selection_text(self, instance, value):
|
|
3621
|
+
if value:
|
|
3622
|
+
if self.use_handles:
|
|
3623
|
+
self._trigger_show_handles()
|
|
3624
|
+
if CutBuffer and not self.password:
|
|
3625
|
+
self._trigger_update_cutbuffer()
|
|
3626
|
+
|
|
3627
|
+
def _get_text(self):
|
|
3628
|
+
flags = self._lines_flags
|
|
3629
|
+
lines = self._lines
|
|
3630
|
+
len_lines = len(lines)
|
|
3631
|
+
less_flags = len(flags) < len_lines
|
|
3632
|
+
if less_flags:
|
|
3633
|
+
flags.append(1)
|
|
3634
|
+
text = ''.join(
|
|
3635
|
+
('\n' if (flags[i] & FL_IS_LINEBREAK) else '') + lines[i]
|
|
3636
|
+
for i in range(len_lines)
|
|
3637
|
+
)
|
|
3638
|
+
if less_flags:
|
|
3639
|
+
flags.pop()
|
|
3640
|
+
return text
|
|
3641
|
+
|
|
3642
|
+
def _set_text(self, text):
|
|
3643
|
+
if isinstance(text, bytes):
|
|
3644
|
+
text = text.decode('utf8')
|
|
3645
|
+
if self.replace_crlf:
|
|
3646
|
+
text = text.replace(u'\r\n', u'\n')
|
|
3647
|
+
if self.text != text:
|
|
3648
|
+
self._refresh_text(text)
|
|
3649
|
+
self.cursor = self.get_cursor_from_index(len(text))
|
|
3650
|
+
|
|
3651
|
+
text = AliasProperty(_get_text, _set_text, bind=('_lines',), cache=True)
|
|
3652
|
+
'''Text of the widget.
|
|
3653
|
+
|
|
3654
|
+
Creation of a simple hello world::
|
|
3655
|
+
|
|
3656
|
+
widget = TextInput(text='Hello world')
|
|
3657
|
+
|
|
3658
|
+
If you want to create the widget with an unicode string, use::
|
|
3659
|
+
|
|
3660
|
+
widget = TextInput(text=u'My unicode string')
|
|
3661
|
+
|
|
3662
|
+
:attr:`text` is an :class:`~kivy.properties.AliasProperty`.
|
|
3663
|
+
'''
|
|
3664
|
+
|
|
3665
|
+
font_name = StringProperty(DEFAULT_FONT)
|
|
3666
|
+
'''Filename of the font to use. The path can be absolute or relative.
|
|
3667
|
+
Relative paths are resolved by the :func:`~kivy.resources.resource_find`
|
|
3668
|
+
function.
|
|
3669
|
+
|
|
3670
|
+
.. warning::
|
|
3671
|
+
|
|
3672
|
+
Depending on your text provider, the font file may be ignored. However,
|
|
3673
|
+
you can mostly use this without problems.
|
|
3674
|
+
|
|
3675
|
+
If the font used lacks the glyphs for the particular language/symbols
|
|
3676
|
+
you are using, you will see '[]' blank box characters instead of the
|
|
3677
|
+
actual glyphs. The solution is to use a font that has the glyphs you
|
|
3678
|
+
need to display. For example, to display |unicodechar|, use a font like
|
|
3679
|
+
freesans.ttf that has the glyph.
|
|
3680
|
+
|
|
3681
|
+
.. |unicodechar| image:: images/unicode-char.png
|
|
3682
|
+
|
|
3683
|
+
:attr:`font_name` is a :class:`~kivy.properties.StringProperty` and
|
|
3684
|
+
defaults to 'Roboto'. This value is taken
|
|
3685
|
+
from :class:`~kivy.config.Config`.
|
|
3686
|
+
'''
|
|
3687
|
+
|
|
3688
|
+
font_size = NumericProperty('15sp')
|
|
3689
|
+
'''Font size of the text in pixels.
|
|
3690
|
+
|
|
3691
|
+
:attr:`font_size` is a :class:`~kivy.properties.NumericProperty` and
|
|
3692
|
+
defaults to 15 :attr:`~kivy.metrics.sp`.
|
|
3693
|
+
'''
|
|
3694
|
+
|
|
3695
|
+
font_context = StringProperty(None, allownone=True)
|
|
3696
|
+
'''Font context. `None` means the font is used in isolation, so you are
|
|
3697
|
+
guaranteed to be drawing with the TTF file resolved by :attr:`font_name`.
|
|
3698
|
+
Specifying a value here will load the font file into a named context,
|
|
3699
|
+
enabling fallback between all fonts in the same context. If a font
|
|
3700
|
+
context is set, you are not guaranteed that rendering will actually use
|
|
3701
|
+
the specified TTF file for all glyphs (Pango will pick the one it
|
|
3702
|
+
thinks is best).
|
|
3703
|
+
|
|
3704
|
+
If Kivy is linked against a system-wide installation of FontConfig,
|
|
3705
|
+
you can load the system fonts by specifying a font context starting
|
|
3706
|
+
with the special string `system://`. This will load the system
|
|
3707
|
+
fontconfig configuration, and add your application-specific fonts on
|
|
3708
|
+
top of it (this imposes a significant risk of family name collision,
|
|
3709
|
+
Pango may not use your custom font file, but pick one from the system)
|
|
3710
|
+
|
|
3711
|
+
.. note::
|
|
3712
|
+
This feature requires the Pango text provider.
|
|
3713
|
+
|
|
3714
|
+
.. versionadded:: 1.10.1
|
|
3715
|
+
|
|
3716
|
+
:attr:`font_context` is a :class:`~kivy.properties.StringProperty` and
|
|
3717
|
+
defaults to None.
|
|
3718
|
+
'''
|
|
3719
|
+
|
|
3720
|
+
font_family = StringProperty(None, allownone=True)
|
|
3721
|
+
'''Font family, this is only applicable when using :attr:`font_context`
|
|
3722
|
+
option. The specified font family will be requested, but note that it may
|
|
3723
|
+
not be available, or there could be multiple fonts registered with the
|
|
3724
|
+
same family. The value can be a family name (string) available in the
|
|
3725
|
+
font context (for example a system font in a `system://` context, or a
|
|
3726
|
+
custom font file added using :class:`kivy.core.text.FontContextManager`).
|
|
3727
|
+
If set to `None`, font selection is controlled by the :attr:`font_name`
|
|
3728
|
+
setting.
|
|
3729
|
+
|
|
3730
|
+
.. note::
|
|
3731
|
+
If using :attr:`font_name` to reference a custom font file, you
|
|
3732
|
+
should leave this as `None`. The family name is managed automatically
|
|
3733
|
+
in this case.
|
|
3734
|
+
|
|
3735
|
+
.. note::
|
|
3736
|
+
This feature requires the Pango text provider.
|
|
3737
|
+
|
|
3738
|
+
.. versionadded:: 1.10.1
|
|
3739
|
+
|
|
3740
|
+
:attr:`font_family` is a :class:`~kivy.properties.StringProperty` and
|
|
3741
|
+
defaults to None.
|
|
3742
|
+
'''
|
|
3743
|
+
|
|
3744
|
+
base_direction = OptionProperty(
|
|
3745
|
+
None,
|
|
3746
|
+
options=['ltr', 'rtl', 'weak_rtl', 'weak_ltr', None],
|
|
3747
|
+
allownone=True
|
|
3748
|
+
)
|
|
3749
|
+
'''Base direction of text, this impacts horizontal alignment when
|
|
3750
|
+
:attr:`halign` is `auto` (the default). Available options are: None,
|
|
3751
|
+
"ltr" (left to right), "rtl" (right to left) plus "weak_ltr" and
|
|
3752
|
+
"weak_rtl".
|
|
3753
|
+
|
|
3754
|
+
.. note::
|
|
3755
|
+
This feature requires the Pango text provider.
|
|
3756
|
+
|
|
3757
|
+
.. note::
|
|
3758
|
+
Weak modes are currently not implemented in Kivy text layout, and
|
|
3759
|
+
have the same effect as setting strong mode.
|
|
3760
|
+
|
|
3761
|
+
.. versionadded:: 1.10.1
|
|
3762
|
+
|
|
3763
|
+
:attr:`base_direction` is an :class:`~kivy.properties.OptionProperty` and
|
|
3764
|
+
defaults to None (autodetect RTL if possible, otherwise LTR).
|
|
3765
|
+
'''
|
|
3766
|
+
|
|
3767
|
+
text_language = StringProperty(None, allownone=True)
|
|
3768
|
+
'''Language of the text, if None Pango will determine it from locale.
|
|
3769
|
+
This is an RFC-3066 format language tag (as a string), for example
|
|
3770
|
+
"en_US", "zh_CN", "fr" or "ja". This can impact font selection, metrics
|
|
3771
|
+
and rendering. For example, the same bytes of text can look different
|
|
3772
|
+
for `ur` and `ar` languages, though both use Arabic script.
|
|
3773
|
+
|
|
3774
|
+
.. note::
|
|
3775
|
+
This feature requires the Pango text provider.
|
|
3776
|
+
|
|
3777
|
+
.. versionadded:: 1.10.1
|
|
3778
|
+
|
|
3779
|
+
:attr:`text_language` is a :class:`~kivy.properties.StringProperty` and
|
|
3780
|
+
defaults to None.
|
|
3781
|
+
'''
|
|
3782
|
+
|
|
3783
|
+
_hint_text = StringProperty('')
|
|
3784
|
+
|
|
3785
|
+
def _set_hint_text(self, value):
|
|
3786
|
+
if isinstance(value, bytes):
|
|
3787
|
+
value = value.decode('utf8')
|
|
3788
|
+
self._hint_text = value
|
|
3789
|
+
|
|
3790
|
+
def _get_hint_text(self):
|
|
3791
|
+
return self._hint_text
|
|
3792
|
+
|
|
3793
|
+
hint_text = AliasProperty(
|
|
3794
|
+
_get_hint_text, _set_hint_text, bind=('_hint_text', ))
|
|
3795
|
+
'''Hint text of the widget, shown if text is ''.
|
|
3796
|
+
|
|
3797
|
+
.. versionadded:: 1.6.0
|
|
3798
|
+
|
|
3799
|
+
.. versionchanged:: 1.10.0
|
|
3800
|
+
The property is now an AliasProperty and byte values are decoded to
|
|
3801
|
+
strings. The hint text will stay visible when the widget is focused.
|
|
3802
|
+
|
|
3803
|
+
:attr:`hint_text` a :class:`~kivy.properties.AliasProperty` and defaults
|
|
3804
|
+
to ''.
|
|
3805
|
+
'''
|
|
3806
|
+
|
|
3807
|
+
hint_text_color = ColorProperty([0.5, 0.5, 0.5, 1.0])
|
|
3808
|
+
'''Current color of the hint_text text, in (r, g, b, a) format.
|
|
3809
|
+
|
|
3810
|
+
.. versionadded:: 1.6.0
|
|
3811
|
+
|
|
3812
|
+
:attr:`hint_text_color` is a :class:`~kivy.properties.ColorProperty` and
|
|
3813
|
+
defaults to [0.5, 0.5, 0.5, 1.0] (grey).
|
|
3814
|
+
|
|
3815
|
+
.. versionchanged:: 2.0.0
|
|
3816
|
+
Changed from :class:`~kivy.properties.ListProperty` to
|
|
3817
|
+
:class:`~kivy.properties.ColorProperty`.
|
|
3818
|
+
'''
|
|
3819
|
+
|
|
3820
|
+
auto_indent = BooleanProperty(False)
|
|
3821
|
+
'''Automatically indent multiline text.
|
|
3822
|
+
|
|
3823
|
+
.. versionadded:: 1.7.0
|
|
3824
|
+
|
|
3825
|
+
:attr:`auto_indent` is a :class:`~kivy.properties.BooleanProperty` and
|
|
3826
|
+
defaults to False.
|
|
3827
|
+
'''
|
|
3828
|
+
|
|
3829
|
+
replace_crlf = BooleanProperty(True)
|
|
3830
|
+
'''Automatically replace CRLF with LF.
|
|
3831
|
+
|
|
3832
|
+
.. versionadded:: 1.9.1
|
|
3833
|
+
|
|
3834
|
+
:attr:`replace_crlf` is a :class:`~kivy.properties.BooleanProperty` and
|
|
3835
|
+
defaults to True.
|
|
3836
|
+
'''
|
|
3837
|
+
|
|
3838
|
+
allow_copy = BooleanProperty(True)
|
|
3839
|
+
'''Decides whether to allow copying the text.
|
|
3840
|
+
|
|
3841
|
+
.. versionadded:: 1.8.0
|
|
3842
|
+
|
|
3843
|
+
:attr:`allow_copy` is a :class:`~kivy.properties.BooleanProperty` and
|
|
3844
|
+
defaults to True.
|
|
3845
|
+
'''
|
|
3846
|
+
|
|
3847
|
+
def _get_min_height(self):
|
|
3848
|
+
return (
|
|
3849
|
+
len(self._lines) * (self.line_height + self.line_spacing)
|
|
3850
|
+
+ self.padding[1]
|
|
3851
|
+
+ self.padding[3]
|
|
3852
|
+
)
|
|
3853
|
+
|
|
3854
|
+
minimum_height = AliasProperty(
|
|
3855
|
+
_get_min_height,
|
|
3856
|
+
bind=(
|
|
3857
|
+
'_lines', 'line_spacing', 'padding', 'font_size', 'font_name',
|
|
3858
|
+
'password', 'font_context', 'hint_text', 'line_height'
|
|
3859
|
+
),
|
|
3860
|
+
cache=True
|
|
3861
|
+
)
|
|
3862
|
+
'''Minimum height of the content inside the TextInput.
|
|
3863
|
+
|
|
3864
|
+
.. versionadded:: 1.8.0
|
|
3865
|
+
|
|
3866
|
+
:attr:`minimum_height` is a readonly
|
|
3867
|
+
:class:`~kivy.properties.AliasProperty`.
|
|
3868
|
+
|
|
3869
|
+
.. warning::
|
|
3870
|
+
:attr:`minimum_width` is calculated based on :attr:`width` therefore
|
|
3871
|
+
code like this will lead to an infinite loop::
|
|
3872
|
+
|
|
3873
|
+
<FancyTextInput>:
|
|
3874
|
+
height: self.minimum_height
|
|
3875
|
+
width: self.height
|
|
3876
|
+
'''
|
|
3877
|
+
|
|
3878
|
+
line_spacing = NumericProperty(0)
|
|
3879
|
+
'''Space taken up between the lines.
|
|
3880
|
+
|
|
3881
|
+
.. versionadded:: 1.8.0
|
|
3882
|
+
|
|
3883
|
+
:attr:`line_spacing` is a :class:`~kivy.properties.NumericProperty` and
|
|
3884
|
+
defaults to 0.
|
|
3885
|
+
'''
|
|
3886
|
+
|
|
3887
|
+
lines_to_scroll = BoundedNumericProperty(3, min=1)
|
|
3888
|
+
'''Set how many lines will be scrolled at once when using the mouse scroll
|
|
3889
|
+
wheel.
|
|
3890
|
+
|
|
3891
|
+
.. versionadded:: 2.2.0
|
|
3892
|
+
|
|
3893
|
+
:attr:`lines_to_scroll is a
|
|
3894
|
+
:class:`~kivy.properties.BoundedNumericProperty` and defaults to 3, the
|
|
3895
|
+
minimum is 1.
|
|
3896
|
+
'''
|
|
3897
|
+
|
|
3898
|
+
input_filter = ObjectProperty(None, allownone=True)
|
|
3899
|
+
''' Filters the input according to the specified mode, if not None. If
|
|
3900
|
+
None, no filtering is applied.
|
|
3901
|
+
|
|
3902
|
+
.. versionadded:: 1.9.0
|
|
3903
|
+
|
|
3904
|
+
:attr:`input_filter` is an :class:`~kivy.properties.ObjectProperty` and
|
|
3905
|
+
defaults to `None`. Can be one of `None`, `'int'` (string), or `'float'`
|
|
3906
|
+
(string), or a callable. If it is `'int'`, it will only accept numbers.
|
|
3907
|
+
If it is `'float'` it will also accept a single period. Finally, if it is
|
|
3908
|
+
a callable it will be called with two parameters; the string to be added
|
|
3909
|
+
and a bool indicating whether the string is a result of undo (True). The
|
|
3910
|
+
callable should return a new substring that will be used instead.
|
|
3911
|
+
'''
|
|
3912
|
+
|
|
3913
|
+
handle_image_middle = StringProperty(
|
|
3914
|
+
'atlas://data/images/defaulttheme/selector_middle')
|
|
3915
|
+
'''Image used to display the middle handle on the TextInput for cursor
|
|
3916
|
+
positioning.
|
|
3917
|
+
|
|
3918
|
+
.. versionadded:: 1.8.0
|
|
3919
|
+
|
|
3920
|
+
:attr:`handle_image_middle` is a :class:`~kivy.properties.StringProperty`
|
|
3921
|
+
and defaults to 'atlas://data/images/defaulttheme/selector_middle'.
|
|
3922
|
+
'''
|
|
3923
|
+
|
|
3924
|
+
def on_handle_image_middle(self, instance, value):
|
|
3925
|
+
if self._handle_middle:
|
|
3926
|
+
self._handle_middle.source = value
|
|
3927
|
+
|
|
3928
|
+
handle_image_left = StringProperty(
|
|
3929
|
+
'atlas://data/images/defaulttheme/selector_left')
|
|
3930
|
+
'''Image used to display the Left handle on the TextInput for selection.
|
|
3931
|
+
|
|
3932
|
+
.. versionadded:: 1.8.0
|
|
3933
|
+
|
|
3934
|
+
:attr:`handle_image_left` is a :class:`~kivy.properties.StringProperty` and
|
|
3935
|
+
defaults to 'atlas://data/images/defaulttheme/selector_left'.
|
|
3936
|
+
'''
|
|
3937
|
+
|
|
3938
|
+
def on_handle_image_left(self, instance, value):
|
|
3939
|
+
if self._handle_left:
|
|
3940
|
+
self._handle_left.source = value
|
|
3941
|
+
|
|
3942
|
+
handle_image_right = StringProperty(
|
|
3943
|
+
'atlas://data/images/defaulttheme/selector_right')
|
|
3944
|
+
'''Image used to display the Right handle on the TextInput for selection.
|
|
3945
|
+
|
|
3946
|
+
.. versionadded:: 1.8.0
|
|
3947
|
+
|
|
3948
|
+
:attr:`handle_image_right` is a
|
|
3949
|
+
:class:`~kivy.properties.StringProperty` and defaults to
|
|
3950
|
+
'atlas://data/images/defaulttheme/selector_right'.
|
|
3951
|
+
'''
|
|
3952
|
+
|
|
3953
|
+
def on_handle_image_right(self, instance, value):
|
|
3954
|
+
if self._handle_right:
|
|
3955
|
+
self._handle_right.source = value
|
|
3956
|
+
|
|
3957
|
+
write_tab = BooleanProperty(True)
|
|
3958
|
+
'''Whether the tab key should move focus to the next widget or if it should
|
|
3959
|
+
enter a tab in the :class:`TextInput`. If `True` a tab will be written,
|
|
3960
|
+
otherwise, focus will move to the next widget.
|
|
3961
|
+
|
|
3962
|
+
.. versionadded:: 1.9.0
|
|
3963
|
+
|
|
3964
|
+
:attr:`write_tab` is a :class:`~kivy.properties.BooleanProperty` and
|
|
3965
|
+
defaults to `True`.
|
|
3966
|
+
'''
|
|
3967
|
+
|
|
3968
|
+
|
|
3969
|
+
if __name__ == '__main__':
|
|
3970
|
+
from textwrap import dedent
|
|
3971
|
+
from kivy.app import App
|
|
3972
|
+
from kivy.uix.boxlayout import BoxLayout
|
|
3973
|
+
from kivy.lang import Builder
|
|
3974
|
+
|
|
3975
|
+
KV = dedent(r'''
|
|
3976
|
+
#:set font_size '20dp'
|
|
3977
|
+
|
|
3978
|
+
BoxLayout:
|
|
3979
|
+
orientation: 'vertical'
|
|
3980
|
+
padding: '20dp'
|
|
3981
|
+
spacing: '10dp'
|
|
3982
|
+
TextInput:
|
|
3983
|
+
font_size: font_size
|
|
3984
|
+
size_hint_y: None
|
|
3985
|
+
height: self.minimum_height
|
|
3986
|
+
multiline: False
|
|
3987
|
+
text: 'monoline'
|
|
3988
|
+
|
|
3989
|
+
TextInput:
|
|
3990
|
+
size_hint_y: None
|
|
3991
|
+
font_size: font_size
|
|
3992
|
+
height: self.minimum_height
|
|
3993
|
+
multiline: False
|
|
3994
|
+
password: True
|
|
3995
|
+
password_mask: '•'
|
|
3996
|
+
text: 'password'
|
|
3997
|
+
|
|
3998
|
+
TextInput:
|
|
3999
|
+
font_size: font_size
|
|
4000
|
+
size_hint_y: None
|
|
4001
|
+
height: self.minimum_height
|
|
4002
|
+
multiline: False
|
|
4003
|
+
readonly: True
|
|
4004
|
+
text: 'readonly'
|
|
4005
|
+
|
|
4006
|
+
TextInput:
|
|
4007
|
+
font_size: font_size
|
|
4008
|
+
size_hint_y: None
|
|
4009
|
+
height: self.minimum_height
|
|
4010
|
+
multiline: False
|
|
4011
|
+
disabled: True
|
|
4012
|
+
text: 'disabled'
|
|
4013
|
+
|
|
4014
|
+
TextInput:
|
|
4015
|
+
font_size: font_size
|
|
4016
|
+
hint_text: 'normal with hint text'
|
|
4017
|
+
|
|
4018
|
+
TextInput:
|
|
4019
|
+
font_size: font_size
|
|
4020
|
+
text: 'default'
|
|
4021
|
+
|
|
4022
|
+
TextInput:
|
|
4023
|
+
font_size: font_size
|
|
4024
|
+
text: 'bubble & handles'
|
|
4025
|
+
use_bubble: True
|
|
4026
|
+
use_handles: True
|
|
4027
|
+
|
|
4028
|
+
TextInput:
|
|
4029
|
+
font_size: font_size
|
|
4030
|
+
text: 'no wrap'
|
|
4031
|
+
do_wrap: False
|
|
4032
|
+
|
|
4033
|
+
TextInput:
|
|
4034
|
+
font_size: font_size
|
|
4035
|
+
text: 'multiline\nreadonly'
|
|
4036
|
+
disabled: app.time % 5 < 2.5
|
|
4037
|
+
''')
|
|
4038
|
+
|
|
4039
|
+
class TextInputApp(App):
|
|
4040
|
+
time = NumericProperty()
|
|
4041
|
+
|
|
4042
|
+
def build(self):
|
|
4043
|
+
Clock.schedule_interval(self.update_time, 0)
|
|
4044
|
+
return Builder.load_string(KV)
|
|
4045
|
+
|
|
4046
|
+
def update_time(self, dt):
|
|
4047
|
+
self.time += dt
|
|
4048
|
+
|
|
4049
|
+
TextInputApp().run()
|