├── .github └── workflows │ └── pythonpackage.yml ├── .gitignore ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── pygame_vkeyboard ├── DejaVuSans.ttf ├── __init__.py ├── examples │ ├── __init__.py │ ├── azerty.py │ ├── numeric.py │ ├── resize.py │ └── textinput.py ├── vkeyboard.py ├── vkeys.py ├── vrenderers.py └── vtextinput.py ├── screenshot ├── vkeyboard_azerty.png ├── vkeyboard_numeric.gif └── vkeyboard_textinput.gif ├── setup.py └── tests └── test_examples.py /.github/workflows/pythonpackage.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: Set up Python 13 | uses: actions/setup-python@v1 14 | with: 15 | python-version: '3.x' 16 | - name: Install SDL 17 | run: sudo apt update && sudo apt install -y libsdl-dev libsdl-image1.2-dev libsdl-mixer1.2-dev libsdl-ttf2.0-dev libsmpeg-dev libportmidi-dev libavformat-dev libswscale-dev 18 | - name: Install Python tools 19 | run: python -m pip install --upgrade pip && python -m pip install -U setuptools wheel twine pygame 20 | - name: Build sdist and wheel 21 | run: python setup.py sdist bdist_wheel 22 | - name: Publish to PyPi 23 | env: 24 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 25 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 26 | run: twine check dist/* && twine upload --skip-existing dist/* 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.pyc 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | /example.py -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include LICENSE.txt 3 | include pygame_vkeyboard/*.ttf 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pygame-vkeyboard 2 | 3 | [![Python package](https://github.com/Faylixe/pygame_vkeyboard/workflows/Python%20package/badge.svg?branch=master)](https://github.com/Faylixe/pygame_vkeyboard/actions) [![PyPI version](https://badge.fury.io/py/pygame-vkeyboard.svg)](https://badge.fury.io/py/pygame-vkeyboard) [![PyPI downloads](https://img.shields.io/pypi/dm/pygame-vkeyboard?color=purple)](https://pypi.org/project/pygame-vkeyboard) 4 | 5 | Visual keyboard for Pygame engine. Aims to be easy to use as highly customizable as well. 6 | 7 |
8 | 9 | 10 | 12 | 14 | 16 | 17 |
11 | 13 | 15 |
18 |
19 | 20 | ## Install 21 | 22 | ```bash 23 | pip install pygame-vkeyboard 24 | ``` 25 | 26 | ## Basic usage 27 | 28 | ``VKeyboard`` only require a pygame surface to be displayed on and a text consumer function, as in the following example : 29 | 30 | ```python 31 | from pygame_vkeyboard import * 32 | 33 | # Initializes your window object or surface your want 34 | # vkeyboard to be displayed on top of. 35 | surface = ... 36 | 37 | def consumer(text): 38 | print('Current text : %s' % text) 39 | 40 | # Initializes and activates vkeyboard 41 | layout = VKeyboardLayout(VKeyboardLayout.AZERTY) 42 | keyboard = VKeyboard(surface, consumer, layout) 43 | ``` 44 | 45 | The keyboard has the following optional parameters: 46 | 47 | - **show_text**: display a text bar with the current text 48 | - **renderer** : define a custom renderer (see chapter below) 49 | - **special_char_layout**: define a custom layout for special characters 50 | - **joystick_navigation**: enable navigation using a joystick 51 | 52 | ## Event management 53 | 54 | A ``VKeyboard`` object handles the following pygame event : 55 | 56 | - **MOUSEBUTTONDOWN** 57 | - **MOUSEBUTTONUP** 58 | - **FINGERDOWN** 59 | - **FINGERUP** 60 | - **KEYDOWN** 61 | - **KEYUP** 62 | - **JOYHATMOTION** 63 | - **JOYBUTTONDOWN** 64 | - **JOYBUTTONUP** 65 | 66 | In order to process those events, keyboard instance event handling method should be called like in the following example: 67 | 68 | ```python 69 | while True: 70 | 71 | events = pygame.event.get() 72 | 73 | # Update internal variables 74 | keyboard.update(events) 75 | 76 | # Draw the keyboard 77 | keyboard.draw(surface) 78 | 79 | # 80 | # Perform other tasks here 81 | # 82 | 83 | # Update the display 84 | pygame.display.flip() 85 | ``` 86 | 87 | It will update key state accordingly as the keyboard buffer as well. 88 | The buffer modification will be notified through the keyboard text consumer function. 89 | 90 | The **global performances can be improved avoiding to flip the entire display** 91 | at each loop by using the ``pygame.display.update()`` function. 92 | 93 | ```python 94 | while True: 95 | 96 | # Draw the keyboard 97 | rects = keyboard.draw(surface) 98 | 99 | # Update only the dirty rectangles of the display 100 | pygame.display.update(rects) 101 | ``` 102 | 103 | **Note:** the ``surface`` parameter of the ``draw()`` method is optional, it is used to clear/hide the keyboard when it is necessary and may be mandatory if the surface has changed. 104 | 105 | ## Customize layout 106 | 107 | The keyboard layout is the model that indicates keys are displayed and how they are dispatched 108 | across the keyboard space. It consists in a ``VKeyboardLayout`` object which is built using list of string, 109 | each string corresponding to a keyboard key row. ``VkeyboardLayout`` constructor signature is defined as following : 110 | 111 | ```python 112 | def __init__(self, model, key_size=None, padding=5, height_ratio=None, allow_uppercase=True, allow_special_chars=True, allow_space=True) 113 | ``` 114 | 115 | If the **key_size** or **height_ratio** parameters are not provided, they will be computed dynamically regarding of 116 | the target surface the keyboard will be rendered into (**height_ratio** is 50% by default). 117 | 118 | In order to only display a numerical ``Vkeyboard`` for example, you can use a custom layout like this : 119 | 120 | ```python 121 | model = ['123', '456', '789', '0'] 122 | layout = VKeyboardLayout(model) 123 | ``` 124 | 125 | ## Custom rendering using VKeyboardRenderer 126 | 127 | If you want to customize keyboard rendering you could provide a ``VKeyboardRenderer`` instance at ``VKeyboard``construction. 128 | 129 | ```python 130 | keyboard = VKeyboard(surface, consumer, layout, renderer=VKeyboardRenderer.DARK) 131 | ``` 132 | 133 | Here is the list of default renderers provided with ``pygame-vkeyboard``: 134 | 135 | - VKeyboardRenderer.DEFAULT 136 | - VKeyboardRenderer.DARK 137 | 138 | A custom ``VKeyboardRenderer`` can be built using following constructor : 139 | 140 | ```python 141 | renderer = VKeyboardRenderer( 142 | # Key font name/path. 143 | 'arial', 144 | # Text color for key and text box (one per state: released, pressed). 145 | ((0, 0, 0), (255, 255, 255)), 146 | # Text box cursor color. 147 | (0, 0, 0), 148 | # Color to highlight the selected key. 149 | (20, 200, 98), 150 | # Keyboard background color. 151 | (50, 50, 50), 152 | # Key background color (one per state, as for the text color). 153 | ((255, 255, 255), (0, 0, 0)), 154 | # Text input background color. 155 | (220, 220, 220), 156 | # Optional special key text color (one per state, as for the text color). 157 | ((0, 250, 0), (255, 255, 255)), 158 | # Optional special key background color (one per state, as for the text color). 159 | ((255, 255, 255), (0, 0, 0)), 160 | ) 161 | ``` 162 | 163 | Please note that the default renderer implementation require a unicode font. 164 | 165 | You can also create your own renderer. Just override ``VKeyboardRenderer``class and override any of the following methods : 166 | 167 | - **draw_background(surface)**: Draws the background of the keyboard. 168 | - **draw_text(surface, text)**: Draws the text of the text input box. 169 | - **draw_cursor(surface, cursor)**: Draws the cursor of the text input box. 170 | - **draw_character_key(surface, key, special=False)**: Draws a key based on character value. 171 | - **draw_space_key(surface, key)**: Draws space bar. 172 | - **draw_back_key(surface, key)**: Draws back key. 173 | - **draw_uppercase_key(surface, key)**: Draw uppercase switch key. 174 | - **draw_special_char_key(surface, key)**: Draw special character switch key. 175 | 176 | ## Getting/Setting data 177 | 178 | Several information can be retrieved from the keyboard: 179 | 180 | ```python 181 | keyboard = VKeyboard(...) 182 | 183 | # Get a pygame.Rect object in which the keyboard is included. 184 | keyboard.get_rect() 185 | 186 | # Get the current text. 187 | keyboard.get_text() 188 | 189 | # Set the current text (clear the existing one). 190 | keyboard.set_text("Hello world!") 191 | 192 | # Enable the keyboard, it will be displayed on next keyboard.draw() call. 193 | keyboard.enable() 194 | 195 | # Return True if the keyboard is enabled (thus displayed at screen). 196 | keyboard.is_enabled() 197 | 198 | # Disable and hide the keyboard (keyboard.update() and keyboard.draw() have no effect). 199 | keyboard.disable() 200 | ``` 201 | 202 | ## Run examples 203 | 204 | Several examples are provided with the **pygame_vkeyboard** library. 205 | To run the examples, simply execute these commands in a terminal: 206 | 207 | ```bash 208 | python -m pygame_vkeyboard.examples.azerty 209 | python -m pygame_vkeyboard.examples.numeric 210 | python -m pygame_vkeyboard.examples.textinput 211 | python -m pygame_vkeyboard.examples.resize 212 | ``` 213 | 214 | ## Contributing 215 | 216 | If you develop you own renderer please share it ! I will keep a collection of rendering class in this repository. 217 | Don't hesitate to report bug, feedback, suggestion into the repository issues section. 218 | -------------------------------------------------------------------------------- /pygame_vkeyboard/DejaVuSans.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Faylixe/pygame-vkeyboard/3385a8d5a5290c0a622d830c1383c7f0258becc5/pygame_vkeyboard/DejaVuSans.ttf -------------------------------------------------------------------------------- /pygame_vkeyboard/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf8 3 | 4 | """Visual keyboard highly customizable for pygame.""" 5 | 6 | from .vkeyboard import VKeyboardLayout, VKeyboard 7 | from .vrenderers import VKeyboardRenderer 8 | 9 | __version__ = '2.0.9' 10 | -------------------------------------------------------------------------------- /pygame_vkeyboard/examples/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf8 3 | 4 | """ Example package. """ 5 | -------------------------------------------------------------------------------- /pygame_vkeyboard/examples/azerty.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf8 3 | 4 | """Simple keyboard usage using AZERTY layout.""" 5 | 6 | # pylint: disable=import-error 7 | import pygame 8 | import pygame_vkeyboard as vkboard 9 | # pylint: enable=import-error 10 | 11 | 12 | def on_key_event(text): 13 | """ Print the current text. """ 14 | print('Current text:', text) 15 | 16 | 17 | def main(test=False): 18 | """ Main program. 19 | 20 | :param test: Indicate function is being tested 21 | :type test: bool 22 | :return: None 23 | """ 24 | 25 | # Init pygame 26 | pygame.init() 27 | screen = pygame.display.set_mode((500, 400)) 28 | 29 | # Create keyboard 30 | layout = vkboard.VKeyboardLayout(vkboard.VKeyboardLayout.AZERTY, height_ratio=1) 31 | keyboard = vkboard.VKeyboard(screen, on_key_event, layout) 32 | 33 | clock = pygame.time.Clock() 34 | 35 | # Main loop 36 | while True: 37 | clock.tick(100) # Ensure not exceed 100 FPS 38 | 39 | for event in pygame.event.get(): 40 | keyboard.on_event(event) 41 | if event.type == pygame.QUIT: 42 | print("Average FPS: ", clock.get_fps()) 43 | exit() 44 | 45 | # Flip the entire surface 46 | pygame.display.flip() 47 | 48 | # At first loop returns 49 | if test: 50 | break 51 | 52 | 53 | if __name__ == '__main__': 54 | main() 55 | -------------------------------------------------------------------------------- /pygame_vkeyboard/examples/numeric.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf8 3 | 4 | """Simple keyboard usage using custom numeric layout.""" 5 | 6 | # pylint: disable=import-error 7 | import pygame 8 | import pygame_vkeyboard as vkboard 9 | # pylint: enable=import-error 10 | 11 | 12 | def on_key_event(text): 13 | """ Print the current text. """ 14 | print('Current text:', text) 15 | 16 | 17 | def main(test=False): 18 | """ Main program. 19 | 20 | :param test: Indicate function is being tested 21 | :type test: bool 22 | :return: None 23 | """ 24 | 25 | # Init pygame 26 | pygame.init() 27 | screen = pygame.display.set_mode((400, 400)) 28 | 29 | # Create keyboard 30 | model = ['123', '456', '789', '*0#'] 31 | layout = vkboard.VKeyboardLayout(model, 32 | key_size=30, 33 | padding = 15, 34 | height_ratio=0.8, 35 | allow_uppercase=False, 36 | allow_special_chars=False, 37 | allow_space=False) 38 | keyboard = vkboard.VKeyboard(screen, on_key_event, layout, 39 | joystick_navigation=True) 40 | 41 | # Main loop 42 | while True: 43 | 44 | events = pygame.event.get() 45 | 46 | for event in events: 47 | if event.type == pygame.QUIT: 48 | exit() 49 | 50 | keyboard.update(events) 51 | rects = keyboard.draw(screen) 52 | 53 | # Flip only the updated area 54 | pygame.display.update(rects) 55 | 56 | # At first loop returns 57 | if test: 58 | break 59 | 60 | 61 | if __name__ == '__main__': 62 | main() 63 | -------------------------------------------------------------------------------- /pygame_vkeyboard/examples/resize.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf8 3 | 4 | """Simple keyboard usage using QWERTY layout and input text.""" 5 | 6 | # pylint: disable=import-error 7 | import pygame 8 | import pygame_vkeyboard as vkboard 9 | # pylint: enable=import-error 10 | 11 | 12 | def on_key_event(text): 13 | """ Print the current text. """ 14 | print('Current text:', text) 15 | 16 | 17 | def main(test=False): 18 | """ Main program. 19 | 20 | :param test: Indicate function is being tested 21 | :type test: bool 22 | :return: None 23 | """ 24 | 25 | # Init pygame 26 | pygame.init() 27 | screen = pygame.display.set_mode((500, 300), pygame.RESIZABLE) 28 | screen.fill((100, 100, 100)) 29 | 30 | # Create keyboard 31 | layout = vkboard.VKeyboardLayout(vkboard.VKeyboardLayout.QWERTY, 32 | allow_special_chars=False, 33 | allow_space=False) 34 | keyboard = vkboard.VKeyboard(screen, 35 | on_key_event, 36 | layout, 37 | renderer=vkboard.VKeyboardRenderer.DARK, 38 | show_text=True, 39 | joystick_navigation=True) 40 | 41 | clock = pygame.time.Clock() 42 | 43 | # Main loop 44 | while True: 45 | clock.tick(100) # Ensure not exceed 100 FPS 46 | 47 | events = pygame.event.get() 48 | 49 | for event in events: 50 | if event.type == pygame.QUIT: 51 | print("Average FPS: ", clock.get_fps()) 52 | exit() 53 | if event.type == pygame.VIDEORESIZE: 54 | screen.fill((100, 100, 100)) 55 | 56 | keyboard.update(events) 57 | rects = keyboard.draw(screen) 58 | 59 | # Flip only the updated area 60 | pygame.display.update(rects) 61 | 62 | # At first loop returns 63 | if test: 64 | break 65 | 66 | 67 | if __name__ == '__main__': 68 | main() 69 | -------------------------------------------------------------------------------- /pygame_vkeyboard/examples/textinput.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf8 3 | 4 | """Simple keyboard usage using QWERTY layout and input text.""" 5 | 6 | # pylint: disable=import-error 7 | import pygame 8 | import pygame_vkeyboard as vkboard 9 | # pylint: enable=import-error 10 | 11 | 12 | def on_key_event(text): 13 | """ Print the current text. """ 14 | print('Current text:', text) 15 | 16 | 17 | def main(test=False): 18 | """ Main program. 19 | 20 | :param test: Indicate function is being tested 21 | :type test: bool 22 | :return: None 23 | """ 24 | 25 | # Init pygame 26 | pygame.init() 27 | screen = pygame.display.set_mode((300, 400)) 28 | screen.fill((20, 100, 100)) 29 | 30 | # Create keyboard 31 | layout = vkboard.VKeyboardLayout(vkboard.VKeyboardLayout.QWERTY) 32 | keyboard = vkboard.VKeyboard(screen, 33 | on_key_event, 34 | layout, 35 | renderer=vkboard.VKeyboardRenderer.DARK, 36 | show_text=True, 37 | joystick_navigation=True) 38 | 39 | clock = pygame.time.Clock() 40 | 41 | # Main loop 42 | while True: 43 | clock.tick(100) # Ensure not exceed 100 FPS 44 | 45 | events = pygame.event.get() 46 | 47 | for event in events: 48 | if event.type == pygame.QUIT: 49 | print("Average FPS: ", clock.get_fps()) 50 | exit() 51 | 52 | keyboard.update(events) 53 | rects = keyboard.draw(screen) 54 | 55 | # Flip only the updated area 56 | pygame.display.update(rects) 57 | 58 | # At first loop returns 59 | if test: 60 | break 61 | 62 | 63 | if __name__ == '__main__': 64 | main() 65 | -------------------------------------------------------------------------------- /pygame_vkeyboard/vkeyboard.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf8 3 | 4 | """ 5 | Visual keyboard for Pygame engine. Aims to be easy to use 6 | as highly customizable as well. 7 | 8 | ``VKeyboard`` only require a pygame surface to be displayed 9 | on and a text consumer function, as in the following example : 10 | 11 | ```python 12 | from pygame_vkeyboard import VKeyboard, VKeyboardLayout 13 | 14 | # Initializes your window object or surface your want 15 | # vkeyboard to be displayed on top of. 16 | surface = ... 17 | 18 | def consume(text): 19 | print(repr('Current text : %s' % text)) 20 | 21 | # Initializes and activates vkeyboard 22 | layout = VKeyboardLayout(VKeyboardLayout.AZERTY) 23 | keyboard = VKeyboard(window, consumer, layout) 24 | keyboard.enable() 25 | ``` 26 | """ 27 | 28 | import logging 29 | import pygame # pylint: disable=import-error 30 | 31 | from . import vkeys 32 | from .vrenderers import VKeyboardRenderer 33 | from .vtextinput import VTextInput, VBackground 34 | 35 | 36 | LOGGER = logging.getLogger(__name__) 37 | 38 | # Joystick controls 39 | JOYHAT_UP = (0, 1) 40 | JOYHAT_LEFT = (-1, 0) 41 | JOYHAT_RIGHT = (1, 0) 42 | JOYHAT_DOWN = (0, -1) 43 | 44 | 45 | def synchronize_layouts(surface_size, *layouts): 46 | """Synchronizes given layouts by normalizing height by using 47 | max height of given layouts to avoid transistion dirty effects. 48 | 49 | Parameters 50 | ---------- 51 | surface_size: 52 | Target surface size on which layout will be displayed. 53 | layouts: 54 | All layouts to synchronize 55 | """ 56 | for layout in layouts: 57 | layout.configure_bound(surface_size) 58 | 59 | layout_ref = min(layouts, key=lambda l: l.key_size) 60 | 61 | for layout in layouts: 62 | if layout.key_size != layout_ref.key_size: 63 | logging.warning( 64 | 'Normalizing layout%s key size to %spx (from layout%s)', 65 | layouts.index(layout), 66 | layout_ref.key_size, 67 | layouts.index(layout_ref)) 68 | layout.key_size = layout_ref.key_size 69 | if layout.size != layout_ref.size: 70 | logging.warning( 71 | 'Normalizing layout%s size to %s*%spx (from layout%s)', 72 | layouts.index(layout), 73 | layout_ref.size[0], layout_ref.size[1], 74 | layouts.index(layout_ref)) 75 | 76 | # Compute all internal values of the layout 77 | layout.set_size(layout_ref.size, surface_size) 78 | 79 | 80 | class VKeyRow(object): 81 | """A VKeyRow defines a keyboard row which is composed of a list of 82 | VKey. 83 | 84 | This class aims to be created internally after parsing a keyboard 85 | layout model. It is used to optimize collision detection, by first 86 | checking row collision, then internal row key detection. 87 | """ 88 | 89 | def __init__(self): 90 | """Default row constructor. """ 91 | self.keys = [] 92 | self.height = 0 93 | self.position = (0, 0) 94 | self.space = None 95 | 96 | def add_key(self, key, first=False): 97 | """Adds the given key to this row. 98 | 99 | Parameters 100 | ---------- 101 | key: 102 | Key to be added to this row. 103 | first: 104 | Flag that indicates if key is added at the beginning or at the end. 105 | """ 106 | if first: 107 | self.keys = [key] + self.keys 108 | else: 109 | self.keys.append(key) 110 | if isinstance(key, vkeys.VSpaceKey): 111 | self.space = key 112 | 113 | def set_size(self, position, size, padding): 114 | """Row size setter. The size correspond to the row height, since the 115 | row width is constraint to the surface width the associated keyboard 116 | belongs. Once size is settled, the size for each child keys is 117 | associated. 118 | 119 | Parameters 120 | ---------- 121 | position: 122 | Position of this row. 123 | size: 124 | Size of the row (height) 125 | padding: 126 | Padding between key. 127 | """ 128 | self.height = size 129 | self.position = position 130 | x = position[0] 131 | for key in self.keys: 132 | key.set_size(size, size) 133 | key.set_position(x, position[1]) 134 | x += padding + key.rect.width 135 | 136 | def __len__(self): 137 | """len() operator overload. 138 | 139 | Returns 140 | ------- 141 | len: 142 | Number of keys thi row contains. 143 | """ 144 | return len(self.keys) 145 | 146 | 147 | class VKeyboardLayout(object): 148 | """Keyboard layout class. 149 | 150 | A keyboard layout is built using layout model which consists in an 151 | list of supported character. Such list item as simple string containing 152 | characters assigned to a row. 153 | 154 | An erasing key is inserted automatically to the first row. 155 | 156 | If `allow_uppercase` flag is `True`, then an upper case key will be 157 | inserted at the beginning of the second row. 158 | 159 | If `allow_special_chars` flag is `True`, then an special 160 | characters / number key will be inserted at the beginning of the third row. 161 | Pressing this key will switch the associated keyboard current layout. 162 | """ 163 | 164 | # AZERTY Layout. 165 | AZERTY = ['1234567890', 'azertyuiop', 'qsdfghjklm', 'wxcvbn'] 166 | 167 | # QWERTY Layout. 168 | QWERTY = ['1234567890', 'qwertyuiop', 'asdfghjkl', 'zxcvbnm'] 169 | 170 | # Number only layout. 171 | NUMBER = ['123', '456', '789', '0'] 172 | 173 | # TODO : Insert special characters layout which include number. 174 | SPECIAL = [u'&é"\'(§è!çà)', u'°_-^$¨*ù`%£', u',;:=?.@+<>#', u'€[]{}/\\|'] 175 | 176 | def __init__(self, 177 | model, 178 | key_size=None, 179 | padding=5, 180 | height_ratio=None, 181 | allow_uppercase=True, 182 | allow_special_chars=True, 183 | allow_space=True): 184 | """Default constructor. Initializes layout rows. 185 | 186 | Parameters 187 | ---------- 188 | model: 189 | Layout model to use. 190 | key_size: 191 | Size of the key, dynamically computed if not specified. 192 | padding: 193 | Padding between key (work horizontally as vertically). 194 | height_ratio: 195 | Ratio (0.2 to 1) of the surface height to recover, dynamically 196 | computed if not specified. 197 | allow_uppercase: 198 | Boolean flag that indicates usage of upper case switching key. 199 | allow_special_chars: 200 | Boolean flag that indicates usage of special char switching key. 201 | allow_space: 202 | Boolean flag that indicates usage of space bar. 203 | 204 | Raises 205 | ------ 206 | ValueError 207 | If the layout model is empty. 208 | """ 209 | self.position = None 210 | self.size = None 211 | self.rows = [] 212 | self.sprites = pygame.sprite.LayeredDirty() 213 | self.padding = padding 214 | self.height_ratio = height_ratio 215 | self.selection = None 216 | self.allow_space = allow_space 217 | self.allow_uppercase = allow_uppercase 218 | self.allow_special_chars = allow_special_chars 219 | for model_row in model: 220 | row = VKeyRow() 221 | for value in model_row: 222 | key = vkeys.VKey(value) 223 | row.add_key(key) 224 | self.sprites.add(key, layer=1) 225 | self.rows.append(row) 226 | self.max_length = len(max(self.rows, key=len)) 227 | if self.max_length == 0: 228 | raise ValueError('Empty layout model provided') 229 | if height_ratio is not None and (height_ratio < 0.2 or height_ratio > 1): 230 | raise ValueError('Surface height ratio shall be from 0.2 to 1') 231 | 232 | self._key_size = key_size 233 | self._key_size_computed = None 234 | 235 | @property 236 | def key_size(self): 237 | return self._key_size or self._key_size_computed 238 | 239 | @key_size.setter 240 | def key_size(self, key_size): 241 | self._key_size = key_size 242 | 243 | def hide(self): 244 | """Hide all keys.""" 245 | for sprite in self.sprites: 246 | sprite.visible = 0 247 | 248 | def show(self): 249 | """Show all keys.""" 250 | for sprite in self.sprites: 251 | sprite.visible = 1 252 | 253 | def configure_renderer(self, renderer): 254 | """Configure the keys with the given renderer. 255 | 256 | Parameters 257 | ---------- 258 | renderer: 259 | Renderer instance this layout uses. 260 | """ 261 | for key in self.sprites.get_sprites_from_layer(1): 262 | key.renderer = renderer 263 | 264 | def configure_special_keys(self, keyboard): 265 | """Configures specials key if needed. 266 | 267 | Parameters 268 | ---------- 269 | keyboard: 270 | Keyboard instance this layout belong. 271 | """ 272 | special_row = VKeyRow() 273 | max_length = self.max_length 274 | i = len(self.rows) - 1 275 | current_row = self.rows[i] 276 | 277 | # Create special keys list 278 | special_keys = [vkeys.VBackKey()] 279 | if self.allow_uppercase: 280 | special_keys.append( 281 | vkeys.VUppercaseKey(keyboard.on_uppercase, keyboard)) 282 | if self.allow_special_chars: 283 | special_keys.append( 284 | vkeys.VSpecialCharKey(keyboard.on_special_char, keyboard)) 285 | self.sprites.add(*special_keys, layer=1) 286 | 287 | # Dispatch special keys in the layout 288 | while special_keys: 289 | first = False 290 | while special_keys and len(current_row) < max_length: 291 | current_row.add_key(special_keys.pop(0), first=first) 292 | first = not first 293 | if i > 0: 294 | i -= 1 295 | current_row = self.rows[i] 296 | else: 297 | break 298 | if self.allow_space: 299 | space_length = len(current_row) - len(special_keys) 300 | special_row.add_key(vkeys.VSpaceKey(space_length)) 301 | self.sprites.add(special_row.space, layer=1) 302 | first = True 303 | 304 | # Adding left to the special bar. 305 | while special_keys: 306 | special_row.add_key(special_keys.pop(0), first=first) 307 | first = not first 308 | 309 | if special_row: 310 | self.rows.append(special_row) 311 | 312 | def configure_bound(self, surface_size): 313 | """Compute keyboard bound regarding of this layout. If `key_size` is 314 | `None`, then it will compute it regarding of the given surface_size. 315 | 316 | Parameters 317 | ---------- 318 | surface_size: 319 | Size of the surface this layout will be rendered on. 320 | """ 321 | nb_rows = len(self.rows) 322 | if self._key_size is None: 323 | self._key_size_computed = int( 324 | (surface_size[0] - (self.padding * (self.max_length + 1))) 325 | / self.max_length) 326 | 327 | height = self.key_size * nb_rows + self.padding * (nb_rows + 1) 328 | if height > surface_size[1] * (self.height_ratio or 0.5): 329 | self._key_size_computed = int((surface_size[1] * (self.height_ratio or 0.5) 330 | - (self.padding * (nb_rows + 1))) / nb_rows) 331 | height = self.key_size * nb_rows + self.padding * (nb_rows + 1) 332 | if self._key_size: 333 | self._key_size = self._key_size_computed 334 | LOGGER.warning('Computed layout height outbound target surface,' 335 | ' reducing key_size to %spx', self.key_size) 336 | elif self.height_ratio is not None: 337 | height = surface_size[1] * self.height_ratio 338 | self.set_size((surface_size[0], int(height)), surface_size) 339 | 340 | def set_size(self, size, surface_size): 341 | """Sets the size of this layout, and updates 342 | position, and rows accordingly. 343 | 344 | Parameters 345 | ---------- 346 | size: 347 | Size of this layout. 348 | surface_size: 349 | Target surface size on which layout will be displayed. 350 | """ 351 | self.size = size 352 | self.position = (0, surface_size[1] - self.size[1]) 353 | 354 | y = self.position[1] + (self.size[1] - len(self.rows) * self.key_size 355 | - (len(self.rows) + 1) * self.padding) // 2 356 | y += self.padding 357 | for row in self.rows: 358 | nb_keys = len(row) 359 | width = (nb_keys * self.key_size) + ((nb_keys + 1) * self.padding) 360 | x = (surface_size[0] - width) // 2 + self.padding 361 | if row.space: 362 | x -= ((row.space.length - 1) * self.key_size) / 2 363 | row.set_size((x, y), self.key_size, self.padding) 364 | y += self.padding + self.key_size 365 | 366 | def set_uppercase(self, uppercase): 367 | """Sets layout uppercase state. 368 | 369 | Parameters 370 | ---------- 371 | uppercase: 372 | True if uppercase, False otherwise. 373 | """ 374 | for key in self.sprites.get_sprites_from_layer(1): 375 | key.set_uppercase(uppercase) 376 | 377 | def get_key(self, value): 378 | """Retrieve if any key with the given value 379 | 380 | Parameters 381 | ---------- 382 | value: 383 | Value to find among keys. 384 | 385 | Returns 386 | ------- 387 | key: 388 | The located key if any with the given value, None otherwise. 389 | """ 390 | for key in self.sprites.get_sprites_from_layer(1): 391 | if key.value == value: 392 | return key 393 | return None 394 | 395 | def get_key_at(self, position): 396 | """Retrieve if any key is located at the given position 397 | 398 | Parameters 399 | ---------- 400 | position: 401 | Position to check key at. 402 | 403 | Returns 404 | ------- 405 | key: 406 | The located key if any at the given position, None otherwise. 407 | """ 408 | for sprite in self.sprites.get_sprites_at(position): 409 | if isinstance(sprite, vkeys.VKey): 410 | return sprite 411 | return None 412 | 413 | def get_key_closest(self, key, loop_row=True, loop_col=True): 414 | """Retrieve the keys closest to the given one. It returns 415 | a dictionary with closest keys and their position relative 416 | to the given key (diff row, diff column). 417 | 418 | Parameters 419 | ---------- 420 | key: 421 | The key to locate. 422 | loop_row: 423 | Loop to the start/end of the row when right/left overflow. 424 | loop_col: 425 | Loop to the start/end of the column when bottom/top overflow. 426 | 427 | Returns 428 | ------- 429 | keys_dict: 430 | The located keys on the left, top, right, bottom. 431 | """ 432 | center = (0, 0) 433 | left = (0, -1) 434 | top = (-1, 0) 435 | right = (0, 1) 436 | bottom = (1, 0) 437 | keys_dict = {center: key, 438 | left: None, top: None, right: None, bottom: None} 439 | 440 | for row_index, r in enumerate(self.rows): 441 | for key_index, k in enumerate(r.keys): 442 | if key == k: 443 | if key_index - 1 >= 0 or loop_row: 444 | keys_dict[left] = r.keys[(key_index - 1) % len(r.keys)] 445 | if key_index + 1 < len(r.keys) or loop_row: 446 | keys_dict[right] = r.keys[(key_index + 1) % len(r.keys)] 447 | 448 | if row_index - 1 >= 0 or loop_col: 449 | prev_row = self.rows[(row_index - 1) % len(self.rows)] 450 | keys_dict[top] = prev_row\ 451 | .keys[min(key_index, len(prev_row) - 1)] 452 | if row_index + 1 < len(self.rows) or loop_col: 453 | next_row = self.rows[(row_index + 1) % len(self.rows)] 454 | keys_dict[bottom] = next_row\ 455 | .keys[min(key_index, len(next_row) - 1)] 456 | return keys_dict 457 | 458 | 459 | class VKeyboard(object): 460 | """ 461 | Virtual Keyboard class. 462 | 463 | A virtual keyboard consists in a VKeyboardLayout that acts as 464 | the keyboard model and a VKeyboardRenderer which is in charge 465 | of drawing keyboard component to screen. 466 | """ 467 | 468 | def __init__(self, 469 | surface, 470 | text_consumer, 471 | main_layout, 472 | show_text=False, 473 | joystick_navigation=False, 474 | renderer=VKeyboardRenderer.DEFAULT, 475 | special_char_layout=None): 476 | """ Default constructor. 477 | 478 | Parameters 479 | ---------- 480 | surface: 481 | Surface this keyboard will be displayed at. 482 | text_consumer: 483 | Consumer that process text for each update. 484 | main_layout: 485 | First displayed layout of this keyboard. 486 | show_text: 487 | Display the current text in a text box. 488 | joystick_navigation: 489 | Handle joystick events and arrow keys. 490 | renderer: 491 | Keyboard renderer instance, using VKeyboardRenderer.DEFAULT 492 | if not specified. 493 | special_char_layout: 494 | Alternative layout to use, using VKeyboardLayout.SPECIAL 495 | if not specified. 496 | """ 497 | self.surface = surface 498 | self.text_consumer = text_consumer 499 | self.renderer = renderer 500 | self.state = 1 # Enabled by default 501 | self.last_pressed = None 502 | self.uppercase = False 503 | self.special_char = False 504 | self.joystick_navigation = joystick_navigation 505 | 506 | # Setup background as a DirtySprite 507 | self.eraser = None 508 | self.background = VBackground(self.surface.get_rect().size, 509 | self.renderer) 510 | 511 | # Setup the layouts 512 | self.layout = main_layout 513 | self.layouts = [main_layout] 514 | if self.layout.allow_special_chars: 515 | if not special_char_layout: 516 | special_char_layout = VKeyboardLayout( 517 | VKeyboardLayout.SPECIAL, 518 | key_size=self.layout.key_size, 519 | padding=self.layout.padding, 520 | height_ratio=self.layout.height_ratio, 521 | allow_uppercase=self.layout.allow_uppercase, 522 | allow_special_chars=self.layout.allow_special_chars, 523 | allow_space=self.layout.allow_space) 524 | self.layouts.append(special_char_layout) 525 | 526 | for layout in self.layouts: 527 | layout.configure_special_keys(self) 528 | layout.configure_renderer(self.renderer) 529 | layout.sprites.add(self.background, layer=0) 530 | 531 | # Setup the text input box 532 | self.show_text = show_text 533 | self.input = VTextInput((0, 0), (10, 10), renderer=self.renderer) 534 | 535 | self.set_size(*surface.get_size()) 536 | 537 | if self.show_text: 538 | self.input.enable() 539 | 540 | # Setup de joystick 541 | if self.joystick_navigation: 542 | if not pygame.joystick.get_init(): 543 | pygame.joystick.init() 544 | for i in range(pygame.joystick.get_count()): 545 | pygame.joystick.Joystick(i).init() 546 | 547 | def set_layout(self, layout): 548 | """Sets the layout this keyboard work with. 549 | Keyboard is invalidate by this action and redraw itself. 550 | 551 | Parameters 552 | ---------- 553 | layout: 554 | Layout to set. 555 | """ 556 | self.layout.hide() 557 | self.layout = layout 558 | self.layout.show() 559 | 560 | def set_eraser(self, surface): 561 | """Setup the surface used to hide/clear the keyboard. 562 | """ 563 | self.eraser = surface.copy() 564 | for layout in self.layouts: 565 | layout.sprites.clear(surface, self.eraser) 566 | 567 | def set_size(self, width, height): 568 | """Resize the keyboard according to the surface size and the parameters 569 | of the layout(s). 570 | 571 | Parameters 572 | ---------- 573 | width: 574 | Keyboard width. 575 | height: 576 | Keyboard height. 577 | """ 578 | synchronize_layouts((width, height), *self.layouts) 579 | self.background.set_rect(*self.layout.position + self.layout.size) 580 | 581 | for layout in self.layouts: 582 | if layout.sprites.get_clip() != self.background.rect: 583 | # Changing the clipping area will force update of all 584 | # sprites without using "dirty mechanism" 585 | layout.sprites.set_clip(self.background.rect) 586 | 587 | self.input.set_line_rect(self.layout.position[0], 588 | self.layout.position[1] - self.layout.key_size, 589 | self.layout.size[0], 590 | self.layout.key_size) 591 | 592 | def set_text(self, text): 593 | """Set the current text in the internal buffer. 594 | 595 | Parameters 596 | ---------- 597 | text: 598 | Text to be set. 599 | """ 600 | self.input.set_text(text) 601 | 602 | def get_text(self): 603 | """Return the current text of the internal buffer.""" 604 | return self.input.text 605 | 606 | def enable(self): 607 | """Set this keyboard as active.""" 608 | self.state = 1 609 | self.layout.show() 610 | if self.show_text: 611 | self.input.enable() 612 | 613 | def is_enabled(self): 614 | """Return True if this keyboard is active.""" 615 | return self.state == 1 616 | 617 | def disable(self): 618 | """Set this keyboard as non active.""" 619 | self.state = 0 620 | self.layout.hide() 621 | self.input.disable() 622 | 623 | def get_rect(self): 624 | """Return keyboard rect.""" 625 | rect = self.background.rect 626 | if self.show_text: 627 | rect = rect.union(self.input.get_rect()) 628 | return rect 629 | 630 | def draw(self, surface=None, force=False): 631 | """Draw the virtual keyboard. 632 | 633 | This method is optimized to be called at each loop of the 634 | main application. It uses DirtySprite to update only parts 635 | of the screen that need to be refreshed. 636 | 637 | The first call to this method will setup the "eraser" surface that 638 | will be used to redraw dirty parts of the screen. 639 | 640 | The `force` parameter shall be used if the surface has been redrawn: 641 | it reset the eraser and redraw all sprites. 642 | 643 | Parameters 644 | ---------- 645 | surface: 646 | Surface this keyboard will be displayed at. 647 | force: 648 | Force the drawing of the entire surface (time consuming). 649 | 650 | Returns 651 | ------- 652 | rects: 653 | List of updated area. 654 | """ 655 | surface = surface or self.surface 656 | 657 | # Check if surface has been resized 658 | if self.eraser and surface.get_rect() != self.eraser.get_rect(): 659 | force = True # To force creating new eraser 660 | self.set_size(*surface.get_size()) 661 | 662 | # Setup eraser 663 | if not self.eraser or force: 664 | self.set_eraser(surface) 665 | 666 | rects = self.layout.sprites.draw(surface) 667 | rects += self.input.draw(surface, force) 668 | 669 | if force: 670 | self.layout.sprites.repaint_rect(self.background.rect) 671 | return rects 672 | 673 | def update(self, events): 674 | """Pygame events processing callback method. 675 | 676 | Parameters 677 | ---------- 678 | events: 679 | List of events to process. 680 | """ 681 | if self.state == 1: 682 | self.layout.sprites.update(events) 683 | self.input.update(events) 684 | 685 | for event in events: 686 | if event.type == pygame.MOUSEBUTTONDOWN\ 687 | and event.button in (1, 2, 3): 688 | # Don't consider the mouse wheel (button 4 & 5) 689 | key = self.layout.get_key_at(event.pos) 690 | if key: 691 | self.on_key_down(key) 692 | self.on_select(0, 0, key) 693 | elif self.input.get_rect().collidepoint(event.pos)\ 694 | and self.layout.selection: 695 | self.layout.selection.set_selected(0) 696 | self.layout.selection = None 697 | self.input.set_selected(1) 698 | elif event.type == pygame.FINGERDOWN: 699 | display_size = pygame.display.get_surface().get_size() 700 | finger_pos = (event.x * display_size[0], event.y * display_size[1]) 701 | key = self.layout.get_key_at(finger_pos) 702 | if key: 703 | self.on_key_down(key) 704 | self.on_select(0, 0, key) 705 | elif self.input.get_rect().collidepoint(finger_pos)\ 706 | and self.layout.selection: 707 | self.layout.selection.set_selected(0) 708 | self.layout.selection = None 709 | self.input.set_selected(1) 710 | elif event.type == pygame.KEYDOWN: 711 | key = self.layout.get_key(event.unicode or event.key) 712 | if key: 713 | self.on_key_down(key) 714 | self.on_select(0, 0, key) 715 | elif event.key == pygame.K_LEFT and not self.input.selected: 716 | self.on_select(0, -1) 717 | elif event.key == pygame.K_UP: 718 | self.on_select(-1, 0) 719 | elif event.key == pygame.K_RIGHT and\ 720 | not self.input.selected: 721 | self.on_select(0, 1) 722 | elif event.key == pygame.K_DOWN: 723 | self.on_select(1, 0) 724 | elif event.key == pygame.K_RETURN and self.layout.selection: 725 | self.on_key_down(self.layout.selection) 726 | elif event.type == pygame.JOYHATMOTION: 727 | if event.value == JOYHAT_LEFT and\ 728 | not self.input.selected: 729 | self.on_select(0, -1) 730 | elif event.value == JOYHAT_UP: 731 | self.on_select(-1, 0) 732 | elif event.value == JOYHAT_RIGHT and\ 733 | not self.input.selected: 734 | self.on_select(0, 1) 735 | elif event.value == JOYHAT_DOWN: 736 | self.on_select(1, 0) 737 | elif event.type == pygame.JOYBUTTONDOWN\ 738 | and event.button == 0 and self.layout.selection: 739 | # Select button pressed 740 | self.on_key_down(self.layout.selection) 741 | 742 | def on_select(self, increment_row, increment_col, key=None): 743 | """"Change the currently selected key. 744 | 745 | Parameters 746 | ---------- 747 | increment_row: 748 | Of how many rows to move (-1 to go up, 1 to go down). 749 | increment_col: 750 | Of how many columns to move (-1 to go left, 1 to go right). 751 | key: 752 | The key to start from, by default the currently selected one. 753 | """ 754 | if self.joystick_navigation: 755 | if not self.layout.selection: 756 | if key: 757 | self.layout.selection = key 758 | elif increment_row > 0: 759 | self.layout.selection = self.layout.rows[0].keys[0] 760 | else: 761 | self.layout.selection = self.layout.rows[-1].keys[0] 762 | self.layout.selection.set_selected(1) 763 | self.input.set_selected(0) 764 | return 765 | 766 | closest = self.layout.get_key_closest( 767 | key or self.layout.selection, 768 | loop_col=not self.input.is_enabled()) 769 | 770 | self.layout.selection.set_selected(0) 771 | self.layout.selection = None 772 | if closest[(increment_row, increment_col)]: 773 | self.layout.selection = closest[(increment_row, increment_col)] 774 | self.layout.selection.set_selected(1) 775 | else: 776 | self.input.set_selected(1) 777 | 778 | def on_event(self, event): 779 | """Deprecated method, only for backward compatibility.""" 780 | self.update((event,)) 781 | self.draw() 782 | 783 | def on_uppercase(self): 784 | """Uppercase key press handler.""" 785 | self.uppercase = not self.uppercase 786 | for layout in self.layouts: 787 | layout.set_uppercase(self.uppercase) 788 | 789 | def on_special_char(self): 790 | """Special char key press handler.""" 791 | if len(self.layouts) > 1: 792 | self.special_char = not self.special_char 793 | if self.special_char: 794 | self.set_layout(self.layouts[1]) 795 | else: 796 | self.set_layout(self.layouts[0]) 797 | 798 | def on_key_down(self, key): 799 | """Process key down event by pressing the given key. 800 | 801 | Parameters 802 | ---------- 803 | key: 804 | Key that receives the key down event. 805 | """ 806 | if isinstance(key, vkeys.VBackKey): 807 | self.input.delete_at_cursor() 808 | else: 809 | text = key.update_buffer('') 810 | if text: 811 | self.input.add_at_cursor(text) 812 | 813 | if not isinstance(key, vkeys.VActionKey): 814 | self.text_consumer(self.input.text) 815 | -------------------------------------------------------------------------------- /pygame_vkeyboard/vkeys.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf8 3 | 4 | """ 5 | Module for keys definitions. It contains the following classes: 6 | 7 | - `VKey` : base class for all keys 8 | - `VSpaceKey` : *space* key definition 9 | - `VBackKey` : *delete* key definition 10 | 11 | - `VActionKey` : base class for key without effect on text 12 | - `VUppercaseKey` : *shift* key definition 13 | - `VSpecialCharKey`: change the keyboard layout 14 | """ 15 | 16 | import pygame # pylint: disable=import-error 17 | 18 | 19 | class VKey(pygame.sprite.DirtySprite): 20 | """ 21 | Simple key holder class. 22 | 23 | Holds key information (its value), as its state, size / position. 24 | State attributes with their default values: 25 | 26 | pressed = 0 27 | If set to 0, the key is released. 28 | If set to 1, the key is pressed. 29 | 30 | selected = 0 31 | If set to 0, the key is selectable but not selected. 32 | If set to 1, the key is selected. 33 | """ 34 | 35 | def __init__(self, value, symbol=None): 36 | """Default key constructor. 37 | 38 | Parameters 39 | ---------- 40 | value: 41 | Value of this key. 42 | symbol: 43 | Visual representation of the key displayed to the screen 44 | (equal to the value if not given). 45 | """ 46 | super(VKey, self).__init__() 47 | self.pressed = 0 48 | self.selected = 0 49 | self.value = value 50 | self.symbol = symbol 51 | self.rect = pygame.Rect((0, 0), (10, 10)) 52 | self.image = pygame.Surface(self.rect.size, pygame.SRCALPHA, 32) 53 | self.renderer = None 54 | self.pressed_key = None 55 | 56 | def __str__(self): 57 | """Key representation when using str() or print()""" 58 | if self.symbol: 59 | return self.symbol 60 | return self.value 61 | 62 | def set_position(self, x, y): 63 | """Set the key position. 64 | 65 | Parameters 66 | ---------- 67 | x: 68 | Position x. 69 | y: 70 | Position y. 71 | """ 72 | if self.rect.topleft != (x, y): 73 | self.rect.topleft = (x, y) 74 | self.dirty = 1 75 | 76 | def set_size(self, width, height): 77 | """Set the key size. 78 | 79 | Parameters 80 | ---------- 81 | width: 82 | Background width. 83 | height: 84 | Background height. 85 | """ 86 | if self.rect.size != (width, height): 87 | self.rect.size = (width, height) 88 | self.image = pygame.Surface(self.rect.size, pygame.SRCALPHA, 32) 89 | self.renderer.draw_key(self.image, self) 90 | self.dirty = 1 91 | 92 | def set_uppercase(self, uppercase): 93 | """Set key uppercase state and redraws it. 94 | 95 | Parameters 96 | ---------- 97 | uppercase: 98 | True if uppercase, False otherwise. 99 | """ 100 | if uppercase: 101 | new_value = self.value.upper() 102 | else: 103 | new_value = self.value.lower() 104 | if new_value != self.value: 105 | self.value = new_value 106 | self.renderer.draw_key(self.image, self) 107 | self.dirty = 1 108 | 109 | def set_pressed(self, state): 110 | """Set the key pressed state (1 for pressed 0 for released) 111 | and redraws it. 112 | 113 | Parameters 114 | ---------- 115 | state: 116 | New key state. 117 | """ 118 | if self.pressed != int(state): 119 | self.pressed = int(state) 120 | self.renderer.draw_key(self.image, self) 121 | self.dirty = 1 122 | 123 | def set_selected(self, state): 124 | """Set the key selection state (1 for selected else 0) 125 | and redraws it. 126 | 127 | Parameters 128 | ---------- 129 | state: 130 | New key state. 131 | """ 132 | if self.selected != int(state): 133 | self.selected = int(state) 134 | self.renderer.draw_key(self.image, self) 135 | self.dirty = 1 136 | 137 | def update(self, events): 138 | """Pygame events processing callback method. 139 | 140 | Parameters 141 | ---------- 142 | events: 143 | List of events to process. 144 | """ 145 | for event in events: 146 | if event.type == pygame.MOUSEBUTTONDOWN\ 147 | and event.button in (1, 2, 3): 148 | # Don't consider the mouse wheel (button 4 & 5): 149 | if self.rect.collidepoint(event.pos): 150 | self.set_pressed(1) 151 | elif event.type == pygame.MOUSEBUTTONUP\ 152 | and event.button in (1, 2, 3): 153 | # Don't consider the mouse wheel (button 4 & 5): 154 | self.set_pressed(0) 155 | elif event.type == pygame.FINGERDOWN: 156 | display_size = pygame.display.get_surface().get_size() 157 | finger_pos = (event.x * display_size[0], event.y * display_size[1]) 158 | if self.rect.collidepoint(finger_pos): 159 | self.set_pressed(1) 160 | elif event.type == pygame.FINGERUP: 161 | self.set_pressed(0) 162 | elif event.type == pygame.KEYDOWN: 163 | if event.unicode and event.unicode == self.value: 164 | self.set_pressed(1) 165 | self.pressed_key = event.key 166 | elif event.key == self.value: 167 | self.set_pressed(1) 168 | self.pressed_key = event.key 169 | elif event.key == pygame.K_RETURN and self.selected: 170 | self.set_pressed(1) 171 | self.pressed_key = event.key 172 | elif event.type == pygame.KEYUP and self.pressed_key is not None: 173 | self.set_pressed(0) 174 | self.pressed_key = None 175 | elif event.type == pygame.JOYBUTTONDOWN and event.button == 0\ 176 | and self.selected: # Select button pressed 177 | self.set_pressed(1) 178 | elif event.type == pygame.JOYBUTTONUP and event.button == 0\ 179 | and self.selected: # Select button released 180 | self.set_pressed(0) 181 | 182 | def update_buffer(self, string): 183 | """Text update method. 184 | 185 | Aims to be called internally when a key collision has been detected. 186 | Updates and returns the given buffer using this key value. 187 | 188 | Parameters 189 | ---------- 190 | string: 191 | Buffer to be updated. 192 | 193 | Returns 194 | ------- 195 | string: 196 | Updated buffer value. 197 | """ 198 | return string + self.value 199 | 200 | 201 | class VSpaceKey(VKey): 202 | """Custom key for spacebar. """ 203 | 204 | def __init__(self, length): 205 | """Default constructor. 206 | 207 | Parameters 208 | ---------- 209 | length: 210 | Key length. 211 | """ 212 | VKey.__init__(self, ' ', u'space') 213 | self.length = length 214 | 215 | def set_uppercase(self, uppercase): 216 | """Nothing to do on upper case action.""" 217 | pass 218 | 219 | def set_size(self, width, height): 220 | """Sets the size of this key. 221 | 222 | Parameters 223 | ---------- 224 | width: 225 | Background width. 226 | height: 227 | Background height. 228 | """ 229 | super(VSpaceKey, self).set_size(width * self.length, height) 230 | 231 | 232 | class VBackKey(VKey): 233 | """Custom key for back. """ 234 | 235 | def __init__(self): 236 | """Default constructor. """ 237 | VKey.__init__(self, u'\x7f', u'\u2190') 238 | 239 | def set_uppercase(self, uppercase): 240 | """Nothing to do on upper case action.""" 241 | pass 242 | 243 | def update_buffer(self, string): 244 | """Text update method. Removes last character. 245 | 246 | Parameters 247 | ---------- 248 | string: 249 | Buffer to be updated. 250 | 251 | Returns 252 | ------- 253 | string: 254 | Updated buffer value. 255 | """ 256 | return string[:-1] 257 | 258 | 259 | class VActionKey(VKey): 260 | """ 261 | A VActionKey is a key that trigger an action 262 | rather than updating the buffer when pressed. 263 | """ 264 | 265 | def __init__(self, action, state_holder, symbol, activated_symbol): 266 | """Default constructor. 267 | 268 | Parameters 269 | ---------- 270 | action: 271 | Delegate action called when this key is pressed. 272 | state_holder: 273 | Holder for this key state (activated or not). 274 | """ 275 | super(VActionKey, self).__init__('', symbol) 276 | self.action = action 277 | self.state_holder = state_holder 278 | self.activated_symbol = activated_symbol 279 | self.activated = False 280 | 281 | def __str__(self): 282 | """Key representation when using str() or print()""" 283 | if self.is_activated(): 284 | self.activated = True 285 | return self.activated_symbol 286 | return self.symbol 287 | 288 | def update(self, events): 289 | """Check if state holder has changed.""" 290 | super(VActionKey, self).update(events) 291 | if self.activated != self.is_activated() and not self.dirty: 292 | self.activated = self.is_activated() 293 | self.renderer.draw_key(self.image, self) 294 | self.dirty = 1 295 | 296 | def set_uppercase(self, uppercase): 297 | """Nothing to do on upper case action.""" 298 | pass 299 | 300 | def set_pressed(self, state): 301 | """Nothing to do on upper case action.""" 302 | prev_state = self.pressed 303 | super(VActionKey, self).set_pressed(state) 304 | if prev_state != self.pressed and self.pressed == 0: 305 | # The key is getting unpressed 306 | self.action() 307 | 308 | def is_activated(self): 309 | """Indicates if this key is activated. 310 | 311 | Returns 312 | ------- 313 | is_activated: bool 314 | True if activated, False otherwise. 315 | """ 316 | raise NotImplementedError( 317 | "Method 'is_activated' have to be overwritten") 318 | 319 | def update_buffer(self, string): 320 | """Do not update text but trigger the delegate action. 321 | 322 | Parameters 323 | ---------- 324 | string: 325 | Not used, just to match parent interface. 326 | 327 | Returns 328 | ------- 329 | string: 330 | Buffer provided as parameter. 331 | """ 332 | return string 333 | 334 | 335 | class VUppercaseKey(VActionKey): 336 | """Action key for the uppercase switch. """ 337 | 338 | def __init__(self, action, state_holder): 339 | super(VUppercaseKey, self).__init__(action, 340 | state_holder, 341 | u'\u21e7', 342 | u'\u21ea') 343 | self.value = pygame.K_LSHIFT 344 | 345 | def is_activated(self): 346 | """Indicates if this key is activated. 347 | 348 | Returns 349 | ------- 350 | is_activated: bool 351 | True if activated, False otherwise. 352 | """ 353 | return self.state_holder.uppercase 354 | 355 | 356 | class VSpecialCharKey(VActionKey): 357 | """Action key for the special char switch. """ 358 | 359 | def __init__(self, action, state_holder): 360 | super(VSpecialCharKey, self).__init__(action, 361 | state_holder, 362 | u'#', 363 | u'Ab') 364 | 365 | def is_activated(self): 366 | """Indicates if this key is activated. 367 | 368 | Returns 369 | ------- 370 | is_activated: bool 371 | True if activated, False otherwise. 372 | """ 373 | return self.state_holder.special_char 374 | -------------------------------------------------------------------------------- /pygame_vkeyboard/vrenderers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf8 3 | 4 | """ 5 | Renderer for the keyboard and the text box used to display 6 | the current text. 7 | """ 8 | 9 | import os.path as osp 10 | import pygame # pylint: disable=import-error 11 | 12 | from . import vkeys 13 | 14 | 15 | def fit_font(font_name, max_height): 16 | """Set the size of the font to fit the given height. 17 | 18 | This function uses the binary search algorithm to go faster 19 | than a one-by-one try. 20 | 21 | Parameters 22 | ---------- 23 | font_name: 24 | Path to font file for rendering key. 25 | max_height: 26 | Height to fit. 27 | """ 28 | font = pygame.font.Font(font_name, 1) 29 | 30 | # Ensure a large panel of characters heights 31 | text = "?/|!()§&@0123456789azertyuiopqsdfghjklmwxcvbnAZERTYUIOPQSDFGHJKLMWXCVBN" # noqa 32 | 33 | start = 1 34 | end = max_height * 2 35 | 36 | while start < end: 37 | k = (start + end) // 2 38 | font = pygame.font.Font(font_name, k) 39 | height = font.size(text)[1] 40 | if height > max_height: 41 | end = k 42 | else: 43 | start = k + 1 44 | if start < end: 45 | # Run garbage collector, to avoid opening too many files 46 | del font 47 | 48 | return font 49 | 50 | 51 | def draw_round_rect(surface, color, rect, radius=0.1, width=0): 52 | """Draw a rounded rectangle. 53 | 54 | Parameters 55 | ---------- 56 | surface: 57 | Surface to draw on. 58 | color: 59 | RGBA tuple color to draw with, the alpha value is optional. 60 | rect: 61 | Rectangle to draw, position and dimensions. 62 | radius: 63 | Used for drawing rectangle with rounded corners. The supported range is 64 | [0, 1] with 0 representing a rectangle without rounded corners. 65 | width: 66 | Line thickness (0 to fill the rectangle). 67 | """ 68 | rect = pygame.Rect(rect) 69 | if len(color) == 4: 70 | alpha = color[-1] 71 | color = color[:3] + (0,) 72 | else: 73 | alpha = 255 74 | color += (0,) 75 | 76 | shape = pygame.Surface(rect.size, pygame.SRCALPHA) 77 | 78 | circle = pygame.Surface([min(rect.size) * 3] * 2, pygame.SRCALPHA) 79 | if width > 0: 80 | pygame.draw.arc(circle, (0, 0, 0), circle.get_rect(), 81 | 1.571, 3.1415, width * 8) 82 | else: 83 | pygame.draw.ellipse(circle, (0, 0, 0), circle.get_rect(), 0) 84 | circle = pygame.transform.smoothscale(circle, 85 | [int(min(rect.size) * radius)] * 2) 86 | 87 | i = 1 88 | shape_rect = shape.get_rect() 89 | for pos in ('topleft', 'topright', 'bottomleft', 'bottomright'): 90 | r = circle.get_rect(**{pos: getattr(shape_rect, pos)}) 91 | shape.blit(circle, r) 92 | if width > 0: 93 | circle = pygame.transform.rotate(circle, -i * 90) 94 | i += 1 95 | 96 | hrect = shape_rect.inflate(0, -circle.get_height() + 1) 97 | vrect = shape_rect.inflate(-circle.get_width() + 1, 0) 98 | if width > 0: 99 | hrect.width = width 100 | vrect.height = width 101 | shape.fill((0, 0, 0), hrect) 102 | shape.fill((0, 0, 0), vrect) 103 | hrect.right = shape_rect.right 104 | vrect.bottom = shape_rect.bottom 105 | shape.fill((0, 0, 0), hrect) 106 | shape.fill((0, 0, 0), vrect) 107 | else: 108 | shape.fill((0, 0, 0), hrect) 109 | shape.fill((0, 0, 0), vrect) 110 | 111 | shape.fill(color, special_flags=pygame.BLEND_RGBA_MAX) 112 | shape.fill((255, 255, 255, alpha), special_flags=pygame.BLEND_RGBA_MIN) 113 | 114 | return surface.blit(shape, rect) 115 | 116 | 117 | class VKeyboardRenderer(object): 118 | """ 119 | A VKeyboardRenderer is in charge of keyboard rendering. 120 | 121 | It handles keyboard rendering properties such as color or padding, 122 | and provides several rendering methods. 123 | 124 | .. note:: 125 | A DEFAULT and DARK styles are available as class attribute. 126 | """ 127 | 128 | DEFAULT = None 129 | DARK = None 130 | 131 | def __init__(self, 132 | font_name, 133 | text_color, 134 | cursor_color, 135 | selection_color, 136 | background_color, 137 | background_key_color, 138 | background_input_color, 139 | text_special_key_color=None, 140 | background_special_key_color=None): 141 | """VKeyboardStyle default constructor. 142 | 143 | Some parameters take a list of color tuples, one per state. 144 | The states are: (released, pressed) 145 | 146 | Parameters 147 | ---------- 148 | font_name: 149 | Path to font file for rendering key. 150 | text_color: 151 | List of RGB tuples for text color (one tuple per state). 152 | cursor_color: 153 | RGB tuple for cursor color of text input. 154 | selection_color: 155 | RGB tuple for selected key color. 156 | background_color: 157 | RGB tuple for background colo r for text. 158 | background_key_color: 159 | List of RGB tuples for key background color (one tuple per state). 160 | background_input_color: 161 | RGB tuple for background color of the text input. 162 | text_special_key_color: 163 | List of RGB tuples for special key text color (one tuple per state). 164 | background_special_key_color: 165 | List of RGB tuples for special key background color (one tuple per 166 | state). 167 | """ 168 | self.font = None 169 | self.font_height = None 170 | self.font_input = None 171 | self.font_input_height = None 172 | self.font_name = font_name 173 | self.text_color = text_color 174 | self.cursor_color = cursor_color 175 | self.selection_color = selection_color 176 | self.background_color = background_color 177 | self.background_key_color = background_key_color 178 | self.background_input_color = background_input_color 179 | 180 | self.text_special_key_color = text_special_key_color 181 | self.background_special_key_color = background_special_key_color 182 | 183 | def get_text_width(self, text): 184 | """Return the width of the given text in the text input box. 185 | 186 | Parameters 187 | ---------- 188 | text: 189 | Text to evaluate. 190 | """ 191 | return self.font_input.size(text)[0] 192 | 193 | def truncate(self, text, max_width, start=0, nearest=False): 194 | """Truncate the given text in order to fit the maximum 195 | given width. 196 | 197 | This function uses the binary search algorithm to go faster 198 | than a one-by-one try. 199 | 200 | Parameters 201 | ---------- 202 | text: 203 | Text to split. 204 | max_width: 205 | Maximum authorized width of the text (according to font). 206 | start: 207 | Index for searching the text part with correct width. 208 | nearest: 209 | If True, the returned text can have a width higher than 210 | the ``max_width`` to reduce abs(max_width - width). 211 | 212 | Returns 213 | ------- 214 | (part, width): 215 | Truncated text and its rendered width. 216 | """ 217 | width = 0 218 | end = len(text) 219 | 220 | if end < start: 221 | return text, self.get_text_width(text) 222 | 223 | while start < end: 224 | k = (start + end) // 2 225 | new_width = self.get_text_width(text[:k+1]) 226 | if new_width > max_width: 227 | end = k 228 | else: 229 | width = new_width 230 | start = k + 1 231 | 232 | if nearest: 233 | next_width = self.get_text_width(text[:start+1]) 234 | if abs(max_width - next_width) < abs(max_width - width): 235 | return text[:start+1], next_width 236 | 237 | return text[:start], width 238 | 239 | def draw_background(self, surface): 240 | """Default drawing method for background. 241 | 242 | Background is drawn as a simple rectangle filled using this 243 | style background color attribute. 244 | 245 | Parameters 246 | ---------- 247 | surface: 248 | Surface background should be drawn in. 249 | """ 250 | surface.fill(self.background_color) 251 | 252 | def draw_cursor(self, surface, cursor): 253 | """Default drawing method for cursor of the text input box. 254 | 255 | Cursor is drawn as a simple rectangle filled using the 256 | cursor color attribute. 257 | 258 | Parameters 259 | ---------- 260 | surface: 261 | Surface representing the cursor. 262 | cursor: 263 | Cursor object. 264 | """ 265 | if cursor.selected: 266 | surface.fill(self.selection_color) 267 | else: 268 | surface.fill(self.cursor_color) 269 | 270 | def draw_text(self, surface, text): 271 | """Default drawing method for text input box. 272 | 273 | Draw the text. 274 | 275 | Parameters 276 | ---------- 277 | surface: 278 | Surface on which the text is drawn. 279 | text: 280 | Target text to be drawn. 281 | """ 282 | if self.font_input_height != surface.get_height(): 283 | # Resize font to fit the surface 284 | self.font_input = fit_font(self.font_name, surface.get_height()) 285 | self.font_input_height = surface.get_height() 286 | 287 | surface.fill(self.background_input_color) 288 | surface.blit(self.font_input.render(text, 1, 289 | self.text_color[0]), (0, 0)) 290 | 291 | def draw_key(self, surface, key): 292 | """Default drawing method for key. 293 | 294 | Draw the key accordingly to it type. 295 | 296 | Parameters 297 | ---------- 298 | surface: 299 | Surface background should be drawn in. 300 | key: 301 | Target key to be drawn. 302 | """ 303 | if isinstance(key, vkeys.VSpaceKey): 304 | self.draw_space_key(surface, key) 305 | elif isinstance(key, vkeys.VBackKey): 306 | self.draw_back_key(surface, key) 307 | elif isinstance(key, vkeys.VUppercaseKey): 308 | self.draw_uppercase_key(surface, key) 309 | elif isinstance(key, vkeys.VSpecialCharKey): 310 | self.draw_special_char_key(surface, key) 311 | else: 312 | self.draw_character_key(surface, key) 313 | 314 | def draw_character_key(self, surface, key, special=False): 315 | """Default drawing method for key. 316 | 317 | Key is drawn as a simple rectangle filled using this 318 | cell style background color attribute. Key value is printed 319 | into drawn cell using internal font. 320 | 321 | Parameters 322 | ---------- 323 | surface: 324 | Surface key background should be drawn in. 325 | key: 326 | Target key to be drawn. 327 | special: 328 | Boolean flag that indicates if the drawn key should use 329 | special background color if available. 330 | """ 331 | rect = surface.get_rect().inflate(-2, -2) 332 | if self.font_height != rect.height: 333 | # Resize font to fit the surface 334 | self.font = fit_font(self.font_name, rect.height) 335 | self.font_height = rect.height 336 | 337 | background_color = self.background_key_color[key.pressed] 338 | if special and self.background_special_key_color: 339 | background_color = self.background_special_key_color[key.pressed] 340 | 341 | text_color = self.text_color[key.pressed] 342 | if special and self.text_special_key_color and not key.pressed: 343 | # Key is not pressed, color according to activated state 344 | state = getattr(key, 'activated', key.pressed) 345 | text_color = self.text_special_key_color[state] 346 | 347 | surface.fill(self.background_color) 348 | draw_round_rect(surface, background_color, rect, 0.4) 349 | if key.selected: 350 | draw_round_rect(surface, self.selection_color, surface.get_rect(), 0.4, 1) 351 | 352 | text = self.font.render(str(key), 1, text_color) 353 | x = (key.rect.width - text.get_width()) // 2 354 | y = (key.rect.height - text.get_height()) // 2 355 | surface.blit(text, (x, y)) 356 | 357 | def draw_space_key(self, surface, key): 358 | """Default drawing method space key. 359 | 360 | Key is drawn as a simple rectangle filled using this 361 | cell style background color attribute. Key value is printed 362 | into drawn cell using internal font. 363 | 364 | Parameters 365 | ---------- 366 | surface: 367 | Surface background should be drawn in. 368 | key: 369 | Target key to be drawn. 370 | """ 371 | self.draw_character_key(surface, key, False) 372 | 373 | def draw_back_key(self, surface, key): 374 | """Default drawing method for back key. Drawn as character key. 375 | 376 | Parameters 377 | ---------- 378 | surface: 379 | Surface background should be drawn in. 380 | key: 381 | Target key to be drawn. 382 | """ 383 | self.draw_character_key(surface, key, True) 384 | 385 | def draw_uppercase_key(self, surface, key): 386 | """Default drawing method for uppercase key. Drawn as character key. 387 | 388 | Parameters 389 | ---------- 390 | surface: 391 | Surface background should be drawn in. 392 | key: 393 | Target key to be drawn. 394 | """ 395 | self.draw_character_key(surface, key, True) 396 | 397 | def draw_special_char_key(self, surface, key): 398 | """Default drawing method for special char key. 399 | Drawn as character key. 400 | 401 | Parameters 402 | ---------- 403 | surface: 404 | Surface background should be drawn in. 405 | key: 406 | Target key to be drawn. 407 | """ 408 | self.draw_character_key(surface, key, True) 409 | 410 | 411 | # Default keyboard renderer 412 | VKeyboardRenderer.DEFAULT = VKeyboardRenderer( 413 | osp.join(osp.dirname(__file__), 'DejaVuSans.ttf'), 414 | text_color=((0, 0, 0), (255, 255, 255)), 415 | cursor_color=(0, 0, 0), 416 | selection_color=(0, 0, 200), 417 | background_color=(255, 255, 255), 418 | background_key_color=((255, 255, 255), (0, 0, 0)), 419 | background_input_color=(220, 220, 220), 420 | background_special_key_color=((180, 180, 180), (0, 0, 0)) 421 | ) 422 | 423 | VKeyboardRenderer.DARK = VKeyboardRenderer( 424 | osp.join(osp.dirname(__file__), 'DejaVuSans.ttf'), 425 | text_color=((182, 183, 184), (255, 255, 255)), 426 | cursor_color=(255, 255, 255), 427 | selection_color=(124, 183, 62), 428 | background_color=(0, 0, 0), 429 | background_key_color=((59, 56, 54), (47, 48, 51)), 430 | background_input_color=(80, 80, 80), 431 | text_special_key_color=((182, 183, 184), (124, 183, 62)), 432 | background_special_key_color=((35, 33, 30), (47, 48, 51)) 433 | ) 434 | -------------------------------------------------------------------------------- /pygame_vkeyboard/vtextinput.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf8 3 | 4 | """ 5 | Text box to display the current text. The mouse events are 6 | supported to move the cursor at the desired place. 7 | """ 8 | 9 | import pygame # pylint: disable=import-error 10 | 11 | from .vrenderers import VKeyboardRenderer 12 | 13 | 14 | class VBackground(pygame.sprite.DirtySprite): 15 | """Background of the text input box. It is used to create 16 | borders by making its size a litle bit larger than the 17 | lines width and the sum of lines heights. 18 | """ 19 | 20 | def __init__(self, size, renderer): 21 | """Default constructor. 22 | 23 | Parameters 24 | ---------- 25 | size: 26 | Size tuple (width, height) of the background. 27 | renderer: 28 | Renderer used to draw the background. 29 | """ 30 | super(VBackground, self).__init__() 31 | self.renderer = renderer 32 | self.image = pygame.Surface(size, pygame.SRCALPHA, 32) 33 | self.rect = pygame.Rect((0, 0), size) 34 | 35 | self.renderer.draw_background(self.image) 36 | 37 | def set_rect(self, x, y, width, height): 38 | """Set the background absolute position and size. 39 | 40 | Parameters 41 | ---------- 42 | x: 43 | Position x. 44 | y: 45 | Position y. 46 | width: 47 | Background width. 48 | height: 49 | Background height. 50 | """ 51 | if self.rect.topleft != (x, y): 52 | self.rect.topleft = (x, y) 53 | self.dirty = 1 54 | if self.rect.size != (width, height): 55 | self.rect.size = (width, height) 56 | self.image = pygame.Surface((width, height), pygame.SRCALPHA, 32) 57 | self.renderer.draw_background(self.image) 58 | self.dirty = 1 59 | 60 | 61 | class VCursor(pygame.sprite.DirtySprite): 62 | """Handles the cursor. 63 | 64 | The ``index`` represente the absolute position which is the number 65 | of characters before it, all lines included. 66 | """ 67 | 68 | def __init__(self, size, renderer): 69 | """Default constructor. 70 | 71 | Parameters 72 | ---------- 73 | size: 74 | Size tuple (width, height) of the cursor. 75 | renderer: 76 | Renderer used to draw the cursor. 77 | """ 78 | super(VCursor, self).__init__() 79 | self.renderer = renderer 80 | self.image = pygame.Surface(size, pygame.SRCALPHA, 32) 81 | self.rect = self.image.get_rect() 82 | self.index = 0 83 | self.selected = 0 84 | 85 | # Blink management 86 | self.clock = pygame.time.Clock() 87 | self.switch_ms = 400 88 | self.switch_counter = 0 89 | 90 | self.renderer.draw_cursor(self.image, self) 91 | 92 | def set_position(self, position): 93 | """Set the cursor absolute position. 94 | 95 | Parameters 96 | ---------- 97 | position: 98 | Position tuple (x, y). 99 | """ 100 | if self.rect.topleft != position: 101 | self.rect.topleft = position 102 | self.dirty = 1 103 | 104 | def set_size(self, width, height): 105 | """Set the cursor size. 106 | 107 | Parameters 108 | ---------- 109 | width: 110 | Background width. 111 | height: 112 | Background height. 113 | """ 114 | if self.rect.size != (width, height): 115 | self.rect.size = (width, height) 116 | self.image = pygame.Surface(self.rect.size, pygame.SRCALPHA, 32) 117 | self.renderer.draw_cursor(self.image, self) 118 | self.dirty = 1 119 | 120 | def set_index(self, index): 121 | """Move the cursor at the given index. 122 | 123 | Parameters 124 | ---------- 125 | index: 126 | Absolute (sum all lines) cursor index. 127 | """ 128 | if index != self.index: 129 | self.index = index 130 | self.dirty = 1 131 | 132 | def set_selected(self, state): 133 | """Set the key selection state (1 for selected else 0) 134 | and redraws it. 135 | 136 | Parameters 137 | ---------- 138 | state: 139 | New key state. 140 | """ 141 | if self.selected != int(state): 142 | self.selected = int(state) 143 | self.renderer.draw_cursor(self.image, self) 144 | self.dirty = 1 145 | 146 | def update(self, events): 147 | """Toggle visibility of the cursor.""" 148 | self.clock.tick() 149 | self.switch_counter += self.clock.get_time() 150 | if self.switch_counter >= self.switch_ms: 151 | self.switch_counter %= self.switch_ms 152 | self.visible = int(not self.visible) 153 | self.dirty = 1 154 | 155 | 156 | class VLine(pygame.sprite.DirtySprite): 157 | """Handles a line of text. A line can be fed until the text 158 | width reaches the line width. 159 | 160 | By default, when the line is empty, its ``visible`` attribute 161 | is set to 0 to hide the line. 162 | """ 163 | 164 | def __init__(self, size, renderer, always_visible=False): 165 | """Default constructor. 166 | 167 | Parameters 168 | ---------- 169 | size: 170 | Size tuple (width, height) of the line. 171 | renderer: 172 | Renderer used to draw the line. 173 | always_visible: 174 | If True, the line will be never hidden even if it is empty. 175 | """ 176 | super(VLine, self).__init__() 177 | self.renderer = renderer 178 | self.image = pygame.Surface(size, pygame.SRCALPHA, 32) 179 | self.rect = pygame.Rect((0, 0), size) 180 | self.text = '' 181 | self.full = False 182 | self.always_visible = always_visible 183 | 184 | self.renderer.draw_text(self.image, '') 185 | 186 | def __len__(self): 187 | """Return the number of characters in the line.""" 188 | return len(self.text) 189 | 190 | def set_position(self, position): 191 | """Set the line absolute position. 192 | 193 | Parameters 194 | ---------- 195 | position: 196 | Position tuple (x, y). 197 | """ 198 | if self.rect.topleft != position: 199 | self.rect.topleft = position 200 | self.dirty = 1 201 | 202 | def set_size(self, width, height): 203 | """Set the line size. 204 | 205 | Parameters 206 | ---------- 207 | width: 208 | Background width. 209 | height: 210 | Background height. 211 | """ 212 | if self.rect.size != (width, height): 213 | self.rect.size = (width, height) 214 | self.image = pygame.Surface(self.rect.size, pygame.SRCALPHA, 32) 215 | self.renderer.draw_text(self.image, self.text) 216 | self.dirty = 1 217 | 218 | def clear(self): 219 | """Clear the current text.""" 220 | if self.text: 221 | self.text = '' 222 | self.full = False 223 | self.renderer.draw_text(self.image, '') 224 | if not self.always_visible: 225 | self.visible = 0 226 | else: 227 | self.dirty = 1 228 | return self.text 229 | 230 | def feed(self, text): 231 | """Feed the line with the given text. The current text is 232 | cleared if an empty string is given. 233 | 234 | Parameters 235 | ---------- 236 | text: 237 | Text to feed in. 238 | 239 | Returns 240 | ------- 241 | remain: 242 | Return the remaining text if the line is full. 243 | """ 244 | if not text: 245 | return self.clear() 246 | elif self.text: 247 | if text.startswith(self.text): 248 | if self.full: 249 | return text[len(self.text):] 250 | else: 251 | self.text = '' 252 | 253 | self.text, _ = self.renderer.truncate(text, 254 | self.rect.width, 255 | len(self.text)) 256 | if text[len(self.text):]: 257 | self.full = True 258 | else: 259 | self.full = False 260 | self.dirty = 1 261 | self.visible = 1 # Show line 262 | self.renderer.draw_text(self.image, self.text) 263 | return text[len(self.text):] 264 | 265 | 266 | class VTextInput(object): 267 | """Handles the text input box. 268 | """ 269 | 270 | def __init__(self, 271 | position, 272 | size, 273 | border=2, 274 | renderer=VKeyboardRenderer.DEFAULT): 275 | """Default constructor. 276 | 277 | Parameters 278 | ---------- 279 | position: 280 | Position tuple (x, y) 281 | size: 282 | Size tuple (width, height) of the text input. 283 | border: 284 | Border thickness. 285 | renderer: 286 | Text input renderer instance, using VTextInputRenderer.DEFAULT 287 | if not specified. 288 | """ 289 | self.state = 0 290 | self.selected = 0 291 | self.position = position 292 | self.size = size # One ligne size 293 | self.text = '' 294 | self.text_margin = border 295 | self.renderer = renderer 296 | 297 | # Define background sprites 298 | self.eraser = None 299 | self.background = VBackground(size, renderer) 300 | self.sprites = pygame.sprite.LayeredDirty(self.background) 301 | 302 | # Initialize first line 303 | line = VLine((self.size[0] - 2 * self.text_margin, 304 | self.size[1]), renderer, True) 305 | self.sprites.add(line, layer=1) 306 | 307 | # Initialize cursor 308 | self.cursor = VCursor((2, self.size[1] - self.text_margin * 2), renderer) 309 | self.sprites.add(self.cursor, layer=2) 310 | 311 | self.disable() 312 | 313 | self.set_line_rect(*self.position + self.size) 314 | 315 | def set_eraser(self, surface): 316 | """Setup the surface used to hide/clear the text input. 317 | """ 318 | self.eraser = surface.copy() 319 | self.sprites.clear(surface, self.eraser) 320 | 321 | def enable(self): 322 | """Set this text input as active.""" 323 | self.state = 1 324 | self.cursor.visible = 1 325 | self.background.visible = 1 326 | self.sprites.get_sprites_from_layer(1)[0].visible = 1 327 | 328 | def is_enabled(self): 329 | """Return True if this keyboard is active.""" 330 | return self.state == 1 331 | 332 | def disable(self): 333 | """Set this text input as non active.""" 334 | self.state = 0 # Disabled by default 335 | self.cursor.visible = 0 336 | self.background.visible = 0 337 | for line in self.sprites.get_sprites_from_layer(1): 338 | line.visible = 0 339 | 340 | def set_selected(self, state): 341 | """Set the input box selection state (1 for selected else 0) 342 | and redraws it. 343 | 344 | Parameters 345 | ---------- 346 | state: 347 | New key state. 348 | """ 349 | self.selected = int(state) 350 | self.cursor.set_selected(state) 351 | 352 | def get_rect(self): 353 | """Return text input rect.""" 354 | return self.background.rect 355 | 356 | def set_line_rect(self, x, y, width, height): 357 | """Set the input text (one line) absolute position and size. 358 | 359 | Parameters 360 | ---------- 361 | x: 362 | Position x. 363 | y: 364 | Position y. 365 | width: 366 | Background width. 367 | height: 368 | Background height. 369 | """ 370 | self.size = (width, height) 371 | self.position = (x, y) 372 | self.background.set_rect(self.position[0], 373 | self.position[1] - 2 * self.text_margin, 374 | width, 375 | height + 2 * self.text_margin) 376 | 377 | for line in self.sprites.get_sprites_from_layer(1): 378 | line.clear() 379 | line.set_position((self.position[0] + self.text_margin, 380 | self.position[1] - self.text_margin)) 381 | line.set_size(self.size[0] - 2 * self.text_margin, self.size[1]) 382 | self.update_lines() 383 | 384 | self.cursor.set_position((self.position[0] + self.text_margin, 385 | self.position[1])) 386 | self.cursor.set_size(2, self.size[1] - self.text_margin * 2) 387 | 388 | # Setup new the surface where to draw 389 | clip_rect = pygame.Rect(self.position[0], 0, 390 | self.background.rect.width, 391 | self.background.rect.bottom) 392 | if self.sprites.get_clip() != clip_rect: 393 | # Changing the clipping area will force update of all 394 | # sprites without using "dirty mechanism" 395 | self.sprites.set_clip(clip_rect) 396 | 397 | def draw(self, surface, force): 398 | """Draw the text input box. 399 | 400 | Parameters 401 | ---------- 402 | surface: 403 | Surface on which the VTextInput is drawn. 404 | force: 405 | Force the drawing of the entire surface (time consuming). 406 | """ 407 | # Check if surface has been resized 408 | if self.eraser and surface.get_rect() != self.eraser.get_rect(): 409 | force = True # To force creating new eraser 410 | 411 | # Setup eraser 412 | if not self.eraser or force: 413 | self.set_eraser(surface) 414 | 415 | if force: 416 | self.sprites.repaint_rect(self.background.rect) 417 | return self.sprites.draw(surface) 418 | 419 | def update(self, events): 420 | """Pygame events processing callback method. 421 | 422 | Parameters 423 | ---------- 424 | events: 425 | List of events to process. 426 | """ 427 | if self.state == 1: 428 | self.sprites.update(events) 429 | for event in events: 430 | if event.type == pygame.KEYUP and self.cursor.selected: 431 | if event.key == pygame.K_LEFT: 432 | self.increment_cursor(-1) 433 | elif event.key == pygame.K_RIGHT: 434 | self.increment_cursor(1) 435 | elif event.key == pygame.K_HOME: 436 | self.cursor.index = 0 437 | self.increment_cursor(0) 438 | elif event.key == pygame.K_END: 439 | self.cursor.index = 0 440 | self.increment_cursor(len(self.text)) 441 | if event.type == pygame.MOUSEBUTTONDOWN\ 442 | and event.button in (1, 2, 3): 443 | # Don't consider the mouse wheel (button 4 & 5): 444 | self.set_cursor(event.pos) 445 | if event.type == pygame.FINGERDOWN: 446 | display_size = pygame.display.get_surface().get_size() 447 | finger_pos = (event.x * display_size[0], event.y * display_size[1]) 448 | self.set_cursor(finger_pos) 449 | 450 | def update_lines(self): 451 | """Update lines content with the current text.""" 452 | if self.state == 1: 453 | remain = self.text 454 | 455 | # Update existing line with text 456 | for line in self.sprites.get_sprites_from_layer(1): 457 | remain = line.feed(remain) 458 | 459 | # Create new lines if necessary 460 | while remain: 461 | line = VLine((self.size[0] - 2 * self.text_margin, 462 | self.size[1]), self.renderer) 463 | self.sprites.add(line, layer=1) 464 | remain = line.feed(remain) 465 | 466 | # Update lines positions 467 | i = 0 468 | for line in reversed(self.sprites.get_sprites_from_layer(1)): 469 | if line.visible: 470 | x = self.position[0] + self.text_margin 471 | y = self.position[1] - i * self.size[1] - self.text_margin 472 | line.set_position((x, y)) 473 | i += 1 474 | 475 | self.background.set_rect(self.position[0], 476 | line.rect.y - self.text_margin, 477 | self.size[0], 478 | i * self.size[1] + 2 * self.text_margin) 479 | 480 | def set_text(self, text): 481 | """Overwrite the current text with the given one. The cursor is 482 | moved at the end of the text. 483 | 484 | Parameters 485 | ---------- 486 | text: 487 | New text. 488 | """ 489 | self.text = text 490 | self.update_lines() 491 | self.cursor.index = 0 492 | self.increment_cursor(len(text)) 493 | 494 | def add_at_cursor(self, text): 495 | """Add a text whereever the cursor is currently located. 496 | 497 | Parameters 498 | ---------- 499 | text: 500 | Single char or text to append. 501 | """ 502 | if self.cursor.index < len(self.text): 503 | # Inserting in the text 504 | prefix = self.text[:self.cursor.index] 505 | suffix = self.text[self.cursor.index:] 506 | self.text = prefix + text + suffix 507 | else: 508 | self.text += text 509 | self.update_lines() 510 | self.increment_cursor(1) 511 | 512 | def delete_at_cursor(self): 513 | """Delete a character before the cursor position.""" 514 | if self.cursor.index == 0: 515 | return 516 | prefix = self.text[:self.cursor.index - 1] 517 | suffix = self.text[self.cursor.index:] 518 | self.text = prefix + suffix 519 | self.update_lines() 520 | self.increment_cursor(-1) 521 | 522 | def increment_cursor(self, step): 523 | """Move the cursor of one or more steps (but not beyond the 524 | text length). 525 | 526 | Parameters 527 | ---------- 528 | step: 529 | From how many characters the cursor shall move. 530 | """ 531 | pos = max(0, self.cursor.index + step) 532 | self.cursor.set_index(min(pos, len(self.text))) 533 | 534 | # Update cursor position 535 | chars_counter = 0 536 | for line in self.sprites.get_sprites_from_layer(1): 537 | if chars_counter + len(line) >= self.cursor.index: 538 | idx = self.cursor.index - chars_counter 539 | x = self.text_margin + self.renderer.get_text_width( 540 | line.text[:idx]) 541 | self.cursor.set_position((x, line.rect.y + self.text_margin)) 542 | break 543 | chars_counter += len(line) 544 | 545 | def set_cursor(self, position): 546 | """Move cursor to char nearest position (x, y). 547 | 548 | Parameters 549 | ---------- 550 | position: 551 | Absolute position (x, y) on the surface. 552 | """ 553 | for collide in self.sprites.get_sprites_at(position): 554 | if isinstance(collide, VLine): 555 | text, width = self.renderer.truncate( 556 | collide.text, 557 | position[0] - collide.rect.left, 558 | nearest=True) 559 | self.cursor.set_position(( 560 | width + self.text_margin, 561 | collide.rect.y + self.text_margin)) 562 | chars_counter = 0 563 | for line in self.sprites.get_sprites_from_layer(1): 564 | if line == collide: 565 | self.cursor.set_index(chars_counter + len(text)) 566 | return 567 | chars_counter += len(line) 568 | -------------------------------------------------------------------------------- /screenshot/vkeyboard_azerty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Faylixe/pygame-vkeyboard/3385a8d5a5290c0a622d830c1383c7f0258becc5/screenshot/vkeyboard_azerty.png -------------------------------------------------------------------------------- /screenshot/vkeyboard_numeric.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Faylixe/pygame-vkeyboard/3385a8d5a5290c0a622d830c1383c7f0258becc5/screenshot/vkeyboard_numeric.gif -------------------------------------------------------------------------------- /screenshot/vkeyboard_textinput.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Faylixe/pygame-vkeyboard/3385a8d5a5290c0a622d830c1383c7f0258becc5/screenshot/vkeyboard_textinput.gif -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf8 3 | 4 | """ Distribution script. """ 5 | 6 | import sys 7 | from os.path import abspath, dirname, join 8 | from setuptools import setup, find_packages 9 | if sys.version_info[0] == 2: 10 | from io import open # Support for encoding on Python 2.x 11 | 12 | import pygame_vkeyboard 13 | 14 | here = dirname(abspath(__file__)) 15 | with open(join(here, 'README.md'), 'r', encoding='utf-8') as stream: 16 | readme = stream.read() 17 | 18 | setup( 19 | name='pygame-vkeyboard', 20 | version=pygame_vkeyboard.__version__, 21 | description=pygame_vkeyboard.__doc__, 22 | long_description=readme, 23 | long_description_content_type='text/markdown', 24 | author='Felix Voituret, Antoine Rousseaux', 25 | author_email='felix@voituret.fr, anxuae-prog@yahoo.fr', 26 | url='https://github.com/Faylixe/pygame-vkeyboard', 27 | download_url='https://pypi.org/project/pygame-vkeyboard/#files', 28 | license='Apache License 2.0', 29 | keywords=['pygame', 'keyboard'], 30 | classifiers=[ 31 | 'Programming Language :: Python :: 2', 32 | 'Programming Language :: Python :: 3', 33 | 'Topic :: Software Development :: Libraries :: pygame' 34 | ], 35 | packages=find_packages(), 36 | include_package_data=True, 37 | python_requires='>=2.7', 38 | install_requires=[ 39 | 'pygame', 40 | ], 41 | setup_requires=[ 42 | 'setuptools>=41.0.1', 43 | 'wheel>=0.33.4' 44 | ], 45 | options={ 46 | 'bdist_wheel': 47 | {'universal': True} # Support Python 2.x 48 | }, 49 | ) 50 | -------------------------------------------------------------------------------- /tests/test_examples.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf8 3 | 4 | from pygame_vkeyboard.examples import azerty, numeric, textinput 5 | 6 | 7 | def test_example_azerty(): 8 | """Run the ASERTY example.""" 9 | azerty.main(test=True) 10 | 11 | 12 | def test_example_numeric(): 13 | """Run the NUMERIC example.""" 14 | numeric.main(test=True) 15 | 16 | 17 | def test_example_textinput(): 18 | """Run the TEXTINPUT example.""" 19 | textinput.main(test=True) 20 | --------------------------------------------------------------------------------