├── .gitignore ├── LICENSE ├── README ├── input_string.yyp ├── objects └── ob_input_string_example │ ├── Create_0.gml │ ├── Draw_0.gml │ ├── Other_63.gml │ ├── Step_0.gml │ └── ob_input_string_example.yy ├── rooms └── rm_input_string_example │ └── rm_input_string_example.yy └── scripts ├── input_string ├── input_string.gml └── input_string.yy └── input_string_async ├── input_string_async.gml └── input_string_async.yy /.gitignore: -------------------------------------------------------------------------------- 1 | options/* 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 alynne o. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | input-string 2 | 3 | Multiplatform GMS2 text entry utility 4 | Robust alternative to keyboard_string 5 | 6 | Frontend Features 7 | - Reliable getters and setters 8 | - Callback for text submission 9 | - Easier async dialog handling 10 | - Realtime array search filter 11 | 12 | Backend Features 13 | - Safe handling at string length limit 14 | - Filter errant keyboard control codes 15 | - Normalized Backspace repeat behavior 16 | - Prevent Android off-by-one text bugs 17 | - Prevent dropping any user keystrokes 18 | - Prevent input falling through dialog 19 | - Maintained value for keyboard_string 20 | - Steamworks virtual onscreen keyboard 21 | 22 | Quick Start 23 | 1 input_string_callback_set(aFunction) 24 | 2 Once a frame, do input_string_tick() 25 | 3 Upon focus, input_string_set(oldVal) 26 | 4 Focused, newVal = input_string_get() 27 | 28 | input_string_get() 29 | function: Get managed text 30 | returned: String 31 | 32 | input_string_set([string]) 33 | function: Set managed text 34 | argument: String (Optional) 35 | returned: None 36 | 37 | input_string_callback_set([function]) 38 | function: Set submission callback 39 | argument: Function (Optional) 40 | returned: None 41 | 42 | input_string_search_set([array]) 43 | function: Set search corpus 44 | argument: Array (Optional) 45 | returned: None 46 | 47 | input_string_search_results() 48 | function: Get search results 49 | returned: Array of indexes 50 | 51 | input_string_force_submit() 52 | function: Force submission callback 53 | returned: None 54 | 55 | input_string_max_length_set(value) 56 | function: Set string limit 57 | argument: Value (Integer) 58 | returned: None 59 | 60 | input_string_platform_hint() 61 | function: Get entry method hint 62 | returned: String (Hint) 63 | possible: keyboard, virtual, async 64 | 65 | input_string_submit_get() 66 | function: Momentary return status 67 | returned: Boolean 68 | 69 | input_string_keyboard_show([type]) 70 | function: Show virtual keyboard 71 | argument: Keyboard type (Optional) 72 | returned: Undefined or Boolean 73 | 74 | input_string_keyboard_hide() 75 | function: Hide virtual keyboard 76 | returned: None 77 | 78 | input_string_tick() 79 | function: Update state 80 | in event: Begin Step (Optional) 81 | returned: None 82 | 83 | input_string_async_active() 84 | function: Get dialog status 85 | returned: Boolean 86 | 87 | input_string_async_get([caption]) 88 | function: Issue dialog prompt 89 | requires: input_​string_​async_​event 90 | argument: Caption (String, Optional) 91 | returned: None 92 | 93 | input_string_async_event() 94 | function: Manage dialog submission 95 | in event: Dialog Async (Optional) 96 | returned: None 97 | 98 | For configuration options, see scripts 99 | 100 | s/o @JujuAdams @tabularelf @nkrapivin 101 | Community: discord.gg/8krYCqr 102 | 103 | @offalynne, 2022 104 | MIT licensed, use as you please 105 | -------------------------------------------------------------------------------- /input_string.yyp: -------------------------------------------------------------------------------- 1 | { 2 | "resources": [ 3 | {"id":{"name":"rm_input_string_example","path":"rooms/rm_input_string_example/rm_input_string_example.yy",},"order":1,}, 4 | {"id":{"name":"ob_input_string_example","path":"objects/ob_input_string_example/ob_input_string_example.yy",},"order":0,}, 5 | {"id":{"name":"input_string_async","path":"scripts/input_string_async/input_string_async.yy",},"order":3,}, 6 | {"id":{"name":"input_string","path":"scripts/input_string/input_string.yy",},"order":2,}, 7 | ], 8 | "isDnDProject": false, 9 | "isEcma": false, 10 | "RoomOrderNodes": [ 11 | {"roomId":{"name":"rm_input_string_example","path":"rooms/rm_input_string_example/rm_input_string_example.yy",},}, 12 | ], 13 | "Folders": [ 14 | {"folderPath":"folders/tests.yy","order":1,"resourceVersion":"1.0","name":"tests","tags":[],"resourceType":"GMFolder",}, 15 | ], 16 | "resourceVersion": "1.4", 17 | "name": "input_string", 18 | "tags": [], 19 | "resourceType": "GMProject", 20 | } -------------------------------------------------------------------------------- /objects/ob_input_string_example/Create_0.gml: -------------------------------------------------------------------------------- 1 | /// @description Test setup 2 | 3 | long_string = ""; 4 | 5 | //feather disable once all 6 | repeat((__input_string()).__max_length) 7 | { 8 | // Fill with garbage 9 | long_string += chr(ord("A") + irandom(25)); 10 | } 11 | 12 | callback = function() 13 | { 14 | var _string = input_string_get(); 15 | self.submission_test = "Callback test @" + string(current_time) + ": " + _string; 16 | // input_string_set(); // Clear on submission 17 | }; 18 | 19 | submission_test = ""; 20 | input_string_callback_set(callback); 21 | 22 | // Initial ticking state. Set to true for user ease, 23 | // but suggest testing at false to be comprehensive. 24 | ticking = true; 25 | 26 | // Search test 27 | search_test_index = -1; 28 | search_test_list = [ 29 | ["Aardvark", "Aardwolf", "Abyssinian", "Abyssinian Guinea Pig", "Acadian Flycatcher", "Achrioptera Manga", "Ackie Monitor", "Addax", "Adelie Penguin", "Admiral Butterfly", "Aesculapian Snake", "Affenpinscher", "Afghan Hound", "African Bullfrog", "African Bush Elephant", "African Civet", "African Clawed Frog", "African Fish Eagle", "African Forest Elephant", "African Golden Cat", "African Grey Parrot", "African Jacana", "African Palm Civet", "African Penguin", "African Sugarcane Borer", "African Tree Toad", "African Wild Dog", "Africanized bee (killer bee)", "Agama Lizard", "Agkistrodon Contortrix", "Agouti", "Aidi", "Ainu", "Airedale Terrier", "Airedoodle", "Akbash", "Akita", "Akita Shepherd", "Alabai (Central Asian Shepherd)", "Alaskan Husky", "Alaskan Klee Kai", "Alaskan Malamute", "Alaskan Pollock", "Alaskan Shepherd", "Albacore Tuna", "Albatross"], 30 | ["White-Eyed Vireo", "White-Faced Capuchin", "White-shouldered House Moth", "White-tail deer", "White-Tailed Eagle", "Whitetail Deer", "Whiting", "Whoodle", "Whooping Crane", "Wild Boar", "Wildebeest", "Willow Flycatcher", "Willow Warbler", "Winter Moth", "Wire Fox Terrier", "Wirehaired Pointing Griffon", "Wirehaired Vizsla", "Wiwaxia", "Wolf", "Wolf Eel", "Wolf Snake", "Wolf Spider", "Wolffish", "Wolverine", "Woma Python", "Wombat", "Wood Bison", "Wood Frog", "Wood Tick", "Wood Turtle", "Woodlouse", "Woodlouse Spider", "Woodpecker", "Woodrat", "Wool Carder Bee", "Woolly Aphids", "Woolly Bear Caterpillar", "Woolly Mammoth", "Woolly Monkey", "Woolly Rhinoceros", "Worm", "Worm Snake", "Wrasse", "Writing Spider", "Wrought Iron Butterflyfish", "Wryneck", "Wyandotte Chicken", "Wyoming Toad", "X-Ray Tetra", "Xeme (Sabine’s Gull)", "Xenacanthus", "Xenoceratops"], 31 | ]; -------------------------------------------------------------------------------- /objects/ob_input_string_example/Draw_0.gml: -------------------------------------------------------------------------------- 1 | /// @description Test readout 2 | 3 | draw_set_halign(fa_left); 4 | draw_text(10, 30, "GM version " + GM_runtime_version); 5 | draw_text(10, 10, "Last built " + date_time_string(GM_build_date)); 6 | 7 | draw_set_halign(fa_right); 8 | draw_text(room_width - 10, 10, "\"" + input_string_platform_hint() + "\" source hint "); 9 | draw_text(room_width - 10, 30, string(string_length(keyboard_string)) + " string length "); 10 | draw_text(room_width - 10, 50, "\"" + string_replace_all(keyboard_string, "\n", " ") + "\" keyboard_string "); 11 | draw_text(room_width - 10, 70, "\"" + input_string_get() + "\" input_string_get() "); 12 | draw_text(room_width - 10, 90, string(keyboard_virtual_status()) + " keyboard_virtual_status()"); 13 | draw_text(room_width - 10, 110, string(keyboard_virtual_height()) + " keyboard_virtual_height()"); 14 | draw_text(room_width - 10, 130, string(input_string_submit_get()) + " input_string_submit_get()"); 15 | 16 | draw_set_halign(fa_center); 17 | draw_text(room_width * .50, 10, "Tick " + (ticking? "On" : "Off")); 18 | draw_text(room_width * .50, 30, submission_test); 19 | 20 | // Search results 21 | if (search_test_index >= 0) 22 | { 23 | var _search = input_string_search_results(); 24 | draw_text(room_width / 2, room_height - 50, "Found " + string(array_length(_search)) + " of " + string(array_length(search_test_list[search_test_index]))); 25 | 26 | _search = string(_search); 27 | _search = string_copy(_search, 2, string_length(_search) - 3); 28 | draw_text(room_width / 2, room_height - 30, _search); 29 | } 30 | 31 | // Test button labels 32 | if (input_string_platform_hint() != "virtual") draw_set_color(c_gray); 33 | draw_text(room_width * .33, 200, "Show OSK"); 34 | draw_text(room_width * .66, 200, "Hide OSK"); 35 | 36 | draw_set_color(c_white); 37 | draw_text(room_width * .25, 400, "Fill String" ); 38 | draw_text(room_width * .50, 400, "Clear String"); 39 | draw_text(room_width * .75, 400, "Set Async" ); 40 | 41 | 42 | -------------------------------------------------------------------------------- /objects/ob_input_string_example/Other_63.gml: -------------------------------------------------------------------------------- 1 | input_string_dialog_async_event(); -------------------------------------------------------------------------------- /objects/ob_input_string_example/Step_0.gml: -------------------------------------------------------------------------------- 1 | /// @description Test cycle 2 | 3 | if (mouse_check_button_released(mb_any) && !input_string_async_active()) 4 | { 5 | // Pointer buttons 6 | var _x = device_mouse_x(0); 7 | if (device_mouse_y(0) < 300) 8 | { 9 | if (_x < (room_width/2)) 10 | { 11 | // Show OSK 12 | input_string_keyboard_show(); 13 | } 14 | else 15 | { 16 | // Hide OSK 17 | input_string_keyboard_hide(); 18 | } 19 | } 20 | else 21 | { 22 | switch((_x <= 0)? 0 : _x div (room_width/3)) 23 | { 24 | // Primary tests 25 | case 0: input_string_set(long_string); break; // Fill String 26 | case 1: input_string_set(); break; // Clear String 27 | case 2: input_string_async_get("Test Caption"); break; // Set Async 28 | } 29 | } 30 | } 31 | 32 | var _gamepad_index = (os_type == os_ios); 33 | 34 | // Gamepad test 35 | if (gamepad_button_check_pressed(_gamepad_index, gp_face3)) input_string_keyboard_show(); 36 | if (gamepad_button_check_pressed(_gamepad_index, gp_face4)) input_string_set(long_string); 37 | if (gamepad_button_check_pressed(_gamepad_index, gp_shoulderrb)) input_string_set(); 38 | if (gamepad_button_check_pressed(_gamepad_index, gp_start)) input_string_async_get("Test Caption"); 39 | 40 | // Secondary tests 41 | if (keyboard_check_pressed(vk_f1) || gamepad_button_check_pressed(_gamepad_index, gp_padu)) 42 | { 43 | // Append, force tick 44 | input_string_add(" add test"); 45 | input_string_tick(); 46 | } 47 | 48 | if (keyboard_check_pressed(vk_f2) || gamepad_button_check_pressed(_gamepad_index, gp_padd)) 49 | { 50 | // Manual submission 51 | input_string_force_submit(); 52 | } 53 | 54 | if (keyboard_check_pressed(vk_f3) || gamepad_button_check_pressed(_gamepad_index, gp_padl)) 55 | { 56 | // Toggle tick 57 | ticking = !ticking; 58 | } 59 | 60 | if (keyboard_check_pressed(vk_f4) || gamepad_button_check_pressed(_gamepad_index, gp_padr)) 61 | { 62 | // Set max-length 63 | input_string___max_length_set(32); 64 | } 65 | 66 | if (keyboard_check_pressed(vk_f5) || keyboard_check_pressed(vk_f6) || gamepad_button_check_pressed(_gamepad_index, gp_shoulderl) || gamepad_button_check_pressed(_gamepad_index, gp_shoulderr)) 67 | { 68 | if (keyboard_check_pressed(vk_f5) || gamepad_button_check_pressed(_gamepad_index, gp_shoulderl)) search_test_index = 0; 69 | if (keyboard_check_pressed(vk_f6) || gamepad_button_check_pressed(_gamepad_index, gp_shoulderr)) search_test_index = 1; 70 | 71 | input_string_search_set(search_test_list[search_test_index]); 72 | } 73 | 74 | if (keyboard_check_pressed(vk_f7) || gamepad_button_check_pressed(_gamepad_index, gp_shoulderlb)) 75 | { 76 | search_test_index = -1; 77 | input_string_search_set(undefined); 78 | } 79 | 80 | 81 | if (ticking) 82 | { 83 | input_string_tick(); 84 | } -------------------------------------------------------------------------------- /objects/ob_input_string_example/ob_input_string_example.yy: -------------------------------------------------------------------------------- 1 | { 2 | "spriteId": null, 3 | "solid": false, 4 | "visible": true, 5 | "spriteMaskId": null, 6 | "persistent": false, 7 | "parentObjectId": null, 8 | "physicsObject": false, 9 | "physicsSensor": false, 10 | "physicsShape": 1, 11 | "physicsGroup": 1, 12 | "physicsDensity": 0.5, 13 | "physicsRestitution": 0.1, 14 | "physicsLinearDamping": 0.1, 15 | "physicsAngularDamping": 0.1, 16 | "physicsFriction": 0.2, 17 | "physicsStartAwake": true, 18 | "physicsKinematic": false, 19 | "physicsShapePoints": [], 20 | "eventList": [ 21 | {"isDnD":false,"eventNum":0,"eventType":3,"collisionObjectId":null,"resourceVersion":"1.0","name":"","tags":[],"resourceType":"GMEvent",}, 22 | {"isDnD":false,"eventNum":0,"eventType":8,"collisionObjectId":null,"resourceVersion":"1.0","name":"","tags":[],"resourceType":"GMEvent",}, 23 | {"isDnD":false,"eventNum":0,"eventType":0,"collisionObjectId":null,"resourceVersion":"1.0","name":"","tags":[],"resourceType":"GMEvent",}, 24 | {"isDnD":false,"eventNum":63,"eventType":7,"collisionObjectId":null,"resourceVersion":"1.0","name":"","tags":[],"resourceType":"GMEvent",}, 25 | ], 26 | "properties": [], 27 | "overriddenProperties": [], 28 | "parent": { 29 | "name": "tests", 30 | "path": "folders/tests.yy", 31 | }, 32 | "resourceVersion": "1.0", 33 | "name": "ob_input_string_example", 34 | "tags": [], 35 | "resourceType": "GMObject", 36 | } -------------------------------------------------------------------------------- /rooms/rm_input_string_example/rm_input_string_example.yy: -------------------------------------------------------------------------------- 1 | { 2 | "isDnd": false, 3 | "volume": 1.0, 4 | "parentRoom": null, 5 | "views": [ 6 | {"inherit":false,"visible":false,"xview":0,"yview":0,"wview":1366,"hview":768,"xport":0,"yport":0,"wport":1366,"hport":768,"hborder":32,"vborder":32,"hspeed":-1,"vspeed":-1,"objectId":null,}, 7 | {"inherit":false,"visible":false,"xview":0,"yview":0,"wview":1366,"hview":768,"xport":0,"yport":0,"wport":1366,"hport":768,"hborder":32,"vborder":32,"hspeed":-1,"vspeed":-1,"objectId":null,}, 8 | {"inherit":false,"visible":false,"xview":0,"yview":0,"wview":1366,"hview":768,"xport":0,"yport":0,"wport":1366,"hport":768,"hborder":32,"vborder":32,"hspeed":-1,"vspeed":-1,"objectId":null,}, 9 | {"inherit":false,"visible":false,"xview":0,"yview":0,"wview":1366,"hview":768,"xport":0,"yport":0,"wport":1366,"hport":768,"hborder":32,"vborder":32,"hspeed":-1,"vspeed":-1,"objectId":null,}, 10 | {"inherit":false,"visible":false,"xview":0,"yview":0,"wview":1366,"hview":768,"xport":0,"yport":0,"wport":1366,"hport":768,"hborder":32,"vborder":32,"hspeed":-1,"vspeed":-1,"objectId":null,}, 11 | {"inherit":false,"visible":false,"xview":0,"yview":0,"wview":1366,"hview":768,"xport":0,"yport":0,"wport":1366,"hport":768,"hborder":32,"vborder":32,"hspeed":-1,"vspeed":-1,"objectId":null,}, 12 | {"inherit":false,"visible":false,"xview":0,"yview":0,"wview":1366,"hview":768,"xport":0,"yport":0,"wport":1366,"hport":768,"hborder":32,"vborder":32,"hspeed":-1,"vspeed":-1,"objectId":null,}, 13 | {"inherit":false,"visible":false,"xview":0,"yview":0,"wview":1366,"hview":768,"xport":0,"yport":0,"wport":1366,"hport":768,"hborder":32,"vborder":32,"hspeed":-1,"vspeed":-1,"objectId":null,}, 14 | ], 15 | "layers": [ 16 | {"instances":[ 17 | {"properties":[],"isDnd":false,"objectId":{"name":"ob_input_string_example","path":"objects/ob_input_string_example/ob_input_string_example.yy",},"inheritCode":false,"hasCreationCode":false,"colour":4294967295,"rotation":0.0,"scaleX":1.0,"scaleY":1.0,"imageIndex":0,"imageSpeed":1.0,"inheritedItemId":null,"frozen":false,"ignore":false,"inheritItemSettings":false,"x":32.0,"y":32.0,"resourceVersion":"1.0","name":"inst_1397F825","tags":[],"resourceType":"GMRInstance",}, 18 | ],"visible":true,"depth":0,"userdefinedDepth":false,"inheritLayerDepth":false,"inheritLayerSettings":false,"gridX":32,"gridY":32,"layers":[],"hierarchyFrozen":false,"resourceVersion":"1.0","name":"Instances","tags":[],"resourceType":"GMRInstanceLayer",}, 19 | {"spriteId":null,"colour":4278190080,"x":0,"y":0,"htiled":false,"vtiled":false,"hspeed":0.0,"vspeed":0.0,"stretch":false,"animationFPS":15.0,"animationSpeedType":0,"userdefinedAnimFPS":false,"visible":true,"depth":100,"userdefinedDepth":false,"inheritLayerDepth":false,"inheritLayerSettings":false,"gridX":32,"gridY":32,"layers":[],"hierarchyFrozen":false,"resourceVersion":"1.0","name":"Background","tags":[],"resourceType":"GMRBackgroundLayer",}, 20 | ], 21 | "inheritLayers": false, 22 | "creationCodeFile": "", 23 | "inheritCode": false, 24 | "instanceCreationOrder": [ 25 | {"name":"inst_1397F825","path":"rooms/rm_input_string_example/rm_input_string_example.yy",}, 26 | ], 27 | "inheritCreationOrder": false, 28 | "sequenceId": null, 29 | "roomSettings": { 30 | "inheritRoomSettings": false, 31 | "Width": 1366, 32 | "Height": 768, 33 | "persistent": false, 34 | }, 35 | "viewSettings": { 36 | "inheritViewSettings": false, 37 | "enableViews": false, 38 | "clearViewBackground": false, 39 | "clearDisplayBuffer": true, 40 | }, 41 | "physicsSettings": { 42 | "inheritPhysicsSettings": false, 43 | "PhysicsWorld": false, 44 | "PhysicsWorldGravityX": 0.0, 45 | "PhysicsWorldGravityY": 10.0, 46 | "PhysicsWorldPixToMetres": 0.1, 47 | }, 48 | "parent": { 49 | "name": "tests", 50 | "path": "folders/tests.yy", 51 | }, 52 | "resourceVersion": "1.0", 53 | "name": "rm_input_string_example", 54 | "tags": [], 55 | "resourceType": "GMRoom", 56 | } -------------------------------------------------------------------------------- /scripts/input_string/input_string.gml: -------------------------------------------------------------------------------- 1 | // input-string library feather disable all 2 | 3 | function __input_string() 4 | { 5 | // Self initialize 6 | static instance = new (function() constructor { 7 | 8 | 9 | #region Configuration 10 | 11 | __auto_closevkb = true; // Whether the 'Return' key closes the virtual keyboard 12 | __auto_submit = true; // Whether the 'Return' key fires a submission callback 13 | __auto_trim = true; // Whether submit trims leading and trailing whitespace 14 | 15 | __allow_case = false; // Whether searches are performed with case sensitivity 16 | __allow_empty = false; // Whether a blank field submission is treated as valid 17 | __allow_newline = false; // Whether to allow newline characters or swap to space 18 | 19 | __max_length = 1000; // Maximum text entry string length. Do not exceed 1024 20 | __search_timeout = 200; // Minimum millisecond delay between consecutive search 21 | 22 | #endregion 23 | 24 | 25 | #region Initialization 26 | 27 | __value = ""; 28 | __predialog = ""; 29 | 30 | __search_list = []; 31 | __result_list = []; 32 | 33 | __delete_duration = 0; 34 | __tick_last = 0; 35 | __search_last = 0; 36 | 37 | __callback = undefined; 38 | __async_id = undefined; 39 | 40 | __async_submit = false; 41 | __just_ticked = false; 42 | __just_set = false; 43 | __search_queue = false; 44 | 45 | __on_windows = (os_type == os_windows); 46 | __on_android = (os_type == os_android); 47 | __on_ios = ((os_type == os_ios) || (os_type == os_tvos)); 48 | __on_mobile = (__on_android || __on_ios); 49 | __on_playstation = ((os_type == os_ps4) || (os_type == os_ps5)); 50 | __on_xbox = ((os_type == os_xboxone) || (os_type == os_xboxseriesxs)); 51 | __on_console = ((os_type == os_switch) || __on_playstation || __on_xbox); 52 | __on_unix_native = ((os_browser == browser_not_a_browser) && ((os_type == os_macosx) || (os_type == os_linux))); 53 | __on_mobile_web = ((os_browser != browser_not_a_browser) && ((os_type != os_macosx) && (os_type != os_linux) && (os_type != os_windows) && (os_type != os_operagx))); 54 | 55 | // GDK hard-limit for all entry methods 56 | if (__on_xbox) __max_length = min(__max_length, 256); 57 | 58 | #endregion 59 | 60 | 61 | #region Detect features 62 | 63 | var _feature_report = ""; 64 | 65 | __use_steam = false; 66 | if (extension_exists("Steamworks")) 67 | { 68 | try 69 | { 70 | __use_steam = is_bool(steam_utils_is_steam_running_on_steam_deck()); 71 | if (__use_steam) _feature_report += " Using Steamworks extension."; 72 | } 73 | catch(_error) 74 | { 75 | // No Steamworks support 76 | _feature_report += " Steamworks extension version unsupported."; 77 | } 78 | } 79 | 80 | __use_trim = false; 81 | try 82 | { 83 | if (string_trim(" z ") == "z") __use_trim = true; 84 | } 85 | catch(_error) 86 | { 87 | // No `string_trim` support 88 | _feature_report += " Not using native string trim."; 89 | } 90 | 91 | // Set platform hint 92 | if (__on_console || __on_mobile_web) 93 | { 94 | // 'async' (dialog) on console 95 | __platform_hint = "async"; 96 | } 97 | else if (__on_mobile) 98 | { 99 | // 'virtual' (OSK) on mobile native 100 | __platform_hint = "virtual"; 101 | 102 | if (__on_android) 103 | { 104 | var _map = os_get_info(); 105 | if (ds_exists(_map, ds_type_map)) 106 | { 107 | // Android on Chromebook form factor (ARC) test via Google 108 | // matches(".+_cheets|cheets_.+") 109 | var _device = string(_map[? "DEVICE"]); 110 | var _match = string_pos("_cheets", _device); 111 | if ((_match > 1) || ((_match > 0) && (_match < (string_length(_device) - 6)))) 112 | { 113 | // 'keyboard' (hardware) on Android Chromebook 114 | __platform_hint = "keyboard"; 115 | } 116 | 117 | ds_map_destroy(_map) 118 | } 119 | } 120 | } 121 | else 122 | { 123 | __platform_hint = "keyboard"; 124 | 125 | if (__use_steam) 126 | { 127 | // 'virtual' (OSK) on Steam Deck 128 | if (steam_utils_is_steam_running_on_steam_deck()) __platform_hint = "virtual"; 129 | } 130 | } 131 | 132 | _feature_report += " Platform hint is \"" + __platform_hint + "\"."; 133 | 134 | show_debug_message("Input String:" + _feature_report); 135 | 136 | #endregion 137 | 138 | 139 | #region Utilities 140 | 141 | if (__use_trim) 142 | { 143 | __trim = string_trim; 144 | } 145 | else 146 | { 147 | __trim = function(_string) 148 | { 149 | var _char = 0; 150 | var _right = string_length(_string); 151 | var _left = 1; 152 | 153 | repeat (_right) 154 | { 155 | // Offset left 156 | _char = ord(string_char_at(_string, _left)); 157 | if ((_char > 8) && (_char < 14) || (_char == 32)) _left++; else break; 158 | } 159 | 160 | repeat (_right - _left) 161 | { 162 | // Offset right 163 | _char = ord(string_char_at(_string, _right)); 164 | if ((_char > 8) && (_char < 14) || (_char == 32)) _right--; else break; 165 | } 166 | 167 | return string_copy(_string, _left, _right - _left + 1); 168 | }; 169 | } 170 | 171 | __set = function(_string) 172 | { 173 | _string = string(_string); 174 | 175 | if (!__allow_newline) 176 | { 177 | // Filter carriage return and newline 178 | _string = string_replace_all(_string, chr(13), ""); 179 | _string = string_replace_all(_string, chr(10), " "); 180 | } 181 | 182 | // Filter delete character (fixes Windows and Mac quirk) 183 | _string = string_replace_all(_string, chr(127), ""); 184 | 185 | // Enforce length 186 | var _max = __max_length + (__on_android? 1 : 0); 187 | _string = string_copy(_string, 1, _max); 188 | 189 | // Left pad one space (fixes Android quirk on first character) 190 | var _trim = (string_char_at(_string, 1) == " "); 191 | if (__on_android && !_trim) 192 | { 193 | // Set leading space 194 | _string = " " + _string; 195 | _trim = true; 196 | } 197 | 198 | // Update internal value 199 | if ((keyboard_string != _string) && ((__tick_last > (current_time - (delta_time div 1000) - 2)) || __just_ticked)) 200 | { 201 | // Close keyboard on overflow (fixes iOS string setting quirk) 202 | if (__on_ios && (string_length(keyboard_string) > _max)) keyboard_virtual_hide(); 203 | 204 | // Set inbuilt value if necessary 205 | keyboard_string = _string; 206 | } 207 | 208 | // Strip leading space 209 | if (__on_android && _trim) _string = string_delete(_string, 1, 1); 210 | 211 | // Search on change 212 | if (!__search_queue && (__value != _string) && (array_length(__search_list) != 0)) __search_queue = true; 213 | 214 | // Set internal value 215 | __value = _string; 216 | 217 | __just_ticked = false; 218 | }; 219 | 220 | __submit = function() 221 | { 222 | if (__auto_trim) __set(__trim(__value)); 223 | 224 | if ((__callback != undefined) && ((__value != "") || __allow_empty)) 225 | { 226 | if (is_method(__callback)) 227 | { 228 | // Execute method 229 | __callback(); 230 | } 231 | else if (is_numeric(__callback) && script_exists(__callback)) 232 | { 233 | // Execute script 234 | script_execute(__callback); 235 | } 236 | else 237 | { 238 | // Invalid callback 239 | show_error("Input String Error: Callback set to an illegal value (typeof=" + typeof(__callback) + ")", false); 240 | } 241 | } 242 | }; 243 | 244 | __search_set = function(_array) 245 | { 246 | // Clear 247 | var _was_empty = (array_length(__search_list) == 0); 248 | array_resize(__search_list, 0); 249 | 250 | // Stringify 251 | var _i = 0; 252 | repeat(array_length(_array)) 253 | { 254 | __search_list[_i] = string(_array[_i]); 255 | ++_i; 256 | } 257 | 258 | // Search 259 | if (!__search_queue && !(_was_empty && (array_length(__search_list) == 0))) __search_queue = true; 260 | }; 261 | 262 | __search = function() 263 | { 264 | if (__search_queue && (__search_last < (current_time - __search_timeout))) 265 | { 266 | __search_queue = false; 267 | __search_last = current_time; 268 | 269 | array_resize(__result_list, 0); 270 | if (__trim(__value) == "") return __result_list; 271 | 272 | var _find = __value; 273 | var _i = 0; 274 | if (!__allow_case) 275 | { 276 | // Any case 277 | _find = string_lower(_find); 278 | repeat(array_length(__search_list)) 279 | { 280 | if (string_pos(_find, string_lower(__search_list[_i])) > 0) array_push(__result_list, __search_list[_i]); 281 | ++_i; 282 | } 283 | } 284 | else 285 | { 286 | // Match case 287 | repeat(array_length(__search_list)) 288 | { 289 | if (string_pos(_find, __search_list[_i]) > 0) array_push(__result_list, __search_list[_i]); 290 | ++_i; 291 | } 292 | } 293 | } 294 | 295 | return __result_list; 296 | }; 297 | 298 | __submit_get = function() 299 | { 300 | if (__async_id == undefined) 301 | { 302 | // Handle virtual keyboard submission 303 | if ((ord(keyboard_lastchar) == 10) && (string_length(keyboard_string) > (string_length(__value) - (__on_android? 1 : 0)))) 304 | { 305 | // Mobile virtual keyboard submission 306 | return true; 307 | } 308 | else if (__on_xbox && !__just_set) 309 | { 310 | // Xbox virtual keyboard submission 311 | return (keyboard_string != __value); 312 | } 313 | else if (__on_android && keyboard_check_pressed(10)) 314 | { 315 | // Android virtual keyboard submission 316 | return true; 317 | } 318 | else 319 | { 320 | // Virtual or hardware keyboard submission 321 | return keyboard_check_pressed(vk_enter); 322 | } 323 | } 324 | 325 | return false; 326 | }; 327 | 328 | __keyboard_show = function(_kbv_type) 329 | { 330 | // Note platform suitability 331 | if ((__platform_hint != "virtual") && !__use_steam) show_debug_message("Input String Warning: Onscreen keyboard is not suitable for use on the current platform"); 332 | if (__platform_hint == "async") show_debug_message("Input String Warning: Consider using async dialog for modal text input instead"); 333 | 334 | if (__use_steam) 335 | { 336 | switch (_kbv_type) 337 | { 338 | case kbv_type_email: _kbv_type = steam_floating_gamepad_text_input_mode_email; break; 339 | case kbv_type_numbers: _kbv_type = steam_floating_gamepad_text_input_mode_numeric; break; 340 | default: _kbv_type = steam_floating_gamepad_text_input_mode_single_line; break; 341 | } 342 | 343 | return steam_show_floating_gamepad_text_input(_kbv_type, 0, 0, 0, 0); 344 | } 345 | else if (__on_android || (!keyboard_virtual_status() && !__on_xbox)) 346 | { 347 | keyboard_virtual_show(_kbv_type, kbv_returnkey_default, kbv_autocapitalize_sentences, false); 348 | } 349 | 350 | return undefined; 351 | } 352 | 353 | __keyboard_hide = function() 354 | { 355 | if (__on_android || keyboard_virtual_status()) 356 | { 357 | keyboard_virtual_hide(); 358 | } 359 | else if (__use_steam) 360 | { 361 | return steam_dismiss_floating_gamepad_text_input(); 362 | } 363 | 364 | return undefined; 365 | }; 366 | 367 | __tick = function() 368 | { 369 | if (__tick_last <= (current_time - (delta_time div 1000) - 2)) 370 | { 371 | __just_ticked = true; 372 | __set(__value); 373 | } 374 | 375 | _virtual_submit = false; 376 | if (!__on_playstation && !__just_set && (__async_id == undefined)) 377 | { 378 | // Manage text input 379 | var _string = keyboard_string; 380 | if ((_string == "") && (string_length(__value) > 1)) 381 | { 382 | // Revert internal string when in overflow state 383 | _string = ""; 384 | } 385 | 386 | _virtual_submit = __submit_get(); 387 | if (__auto_closevkb && _virtual_submit) 388 | { 389 | // Close virtual keyboard on submission 390 | __keyboard_hide(); 391 | } 392 | 393 | if (_string != "") 394 | { 395 | // Backspace key repeat (fixes lack-of on native Mac and Linux) 396 | if (__on_unix_native) 397 | { 398 | if (__delete_duration > 0) 399 | { 400 | if (keyboard_check_pressed(vk_control) || keyboard_check_pressed(vk_shift) || keyboard_check_pressed(vk_alt)) 401 | { 402 | keyboard_clear(vk_backspace); 403 | } 404 | 405 | // Repeat on hold, normalized against Windows. Timed in microseconds 406 | var _repeat_rate = 33000; 407 | if (!keyboard_check(vk_backspace)) 408 | { 409 | __delete_duration = 0; 410 | } 411 | else if ((__delete_duration > 500000) && ((__delete_duration mod _repeat_rate) > ((__delete_duration + delta_time) mod _repeat_rate))) 412 | { 413 | //Assumes LTR 414 | _string = string_copy(_string, 1, string_length(_string) - 1); 415 | } 416 | } 417 | 418 | if (keyboard_check(vk_backspace)) __delete_duration += delta_time; 419 | } 420 | } 421 | 422 | __set(_string); 423 | } 424 | 425 | if (__auto_submit && !__async_submit && (_virtual_submit || (!__on_playstation && keyboard_check_pressed(vk_enter)))) __submit(); 426 | 427 | __async_submit = false; 428 | __just_set = false; 429 | __tick_last = current_time; 430 | }; 431 | 432 | #endregion 433 | 434 | 435 | })(); return instance; 436 | } 437 | 438 | function input_string_max_length_set(_max_length) 439 | { 440 | if (!is_numeric(_max_length) || (_max_length < 0) || (_max_length > 1024)) 441 | { 442 | show_error 443 | ( 444 | "Input String Error: Invalid value provided for max length: \"" 445 | + string(_max_length) 446 | + "\". Expected a value between 0 and 1024", 447 | true 448 | ); 449 | 450 | return; 451 | } 452 | 453 | with (__input_string()) 454 | { 455 | __max_length = _max_length; 456 | 457 | // Respect hard-limit on Xbox GDK 458 | if (__on_xbox) __max_length = max(__max_length, 256); 459 | 460 | __set(string_copy(__value, 0, __max_length)); 461 | } 462 | } 463 | 464 | function input_string_callback_set(_callback) 465 | { 466 | if !(is_undefined(_callback) || is_method(_callback) || (is_numeric(_callback) && !script_exists(_callback))) 467 | { 468 | show_error 469 | ( 470 | "Input String Error: Invalid value provided as callback: \"" 471 | + string(_callback) 472 | + "\". Expected a function or method.", 473 | true 474 | ); 475 | 476 | return; 477 | } 478 | 479 | (__input_string()).__callback = _callback; 480 | } 481 | 482 | function input_string_keyboard_show(_keyboard_type = kbv_type_default) 483 | { 484 | return (__input_string()).__keyboard_show(_keyboard_type); 485 | } 486 | 487 | function input_string_set(_string = "") 488 | { 489 | with (__input_string()) 490 | { 491 | if (__on_ios) 492 | { 493 | // Close virtual keyboard if string is manually set (fixes iOS setting quirk) 494 | keyboard_virtual_hide(); 495 | } 496 | 497 | __just_set = true; 498 | __set(_string); 499 | } 500 | } 501 | 502 | function input_string_add(_string) 503 | { 504 | input_string_set((__input_string()).__value + string(_string)); 505 | } 506 | 507 | function input_string_search_set(_array) 508 | { 509 | // Coallesce 510 | _array = _array ?? []; 511 | 512 | if (!is_array(_array)) 513 | { 514 | // Stringify 515 | _array = string(_array); 516 | 517 | // Wrap 518 | _array = [_array]; 519 | } 520 | 521 | (__input_string()).__search_set(_array); 522 | } 523 | 524 | function input_string_tick() { return (__input_string()).__tick(); } 525 | function input_string_submit_get() { return (__input_string()).__submit_get(); } 526 | function input_string_force_submit() { return (__input_string()).__submit(); } 527 | function input_string_keyboard_hide() { return (__input_string()).__keyboard_hide(); } 528 | function input_string_search_results() { return (__input_string()).__search(); } 529 | function input_string_platform_hint() { return (__input_string()).__platform_hint; } 530 | function input_string_get() { return (__input_string()).__value; } 531 | -------------------------------------------------------------------------------- /scripts/input_string/input_string.yy: -------------------------------------------------------------------------------- 1 | { 2 | "isDnD": false, 3 | "isCompatibility": false, 4 | "parent": { 5 | "name": "input_string", 6 | "path": "input_string.yyp", 7 | }, 8 | "resourceVersion": "1.0", 9 | "name": "input_string", 10 | "tags": [], 11 | "resourceType": "GMScript", 12 | } -------------------------------------------------------------------------------- /scripts/input_string_async/input_string_async.gml: -------------------------------------------------------------------------------- 1 | // input-string library feather disable all 2 | 3 | function input_string_async_get(_prompt, _string = undefined) 4 | { 5 | static _warning = false; 6 | with (__input_string()) 7 | { 8 | _string = _string ?? __value; 9 | if (__async_id != undefined) 10 | { 11 | // Do not request the input modal when it is already open 12 | show_debug_message("Input String Warning: Dialog prompt refused. Awaiting callback ID \"" + string(__async_id) + "\""); 13 | return false; 14 | } 15 | else 16 | { 17 | if (!_warning) 18 | { 19 | // Note platform suitability 20 | if (__platform_hint != "async") show_debug_message("Input String Warning: Async dialog is not suitable for use on the current platform"); 21 | if (__platform_hint == "virtual") show_debug_message("Input String Warning: Consider showing the virtual keyboard for non-modal text input instead"); 22 | _warning = true; 23 | } 24 | 25 | // Hide lingering overlay on dialog prompt open (Fixes mobile keyboard focus quirk) 26 | if (__on_mobile) keyboard_virtual_hide(); 27 | 28 | if (_string != "") 29 | { 30 | var _console_limit = 0; 31 | switch(os_type) 32 | { 33 | // Enforce dialog character limit per platform 34 | case os_xboxone: case os_xboxseriesxs: _console_limit = 256; break; 35 | case os_switch: _console_limit = 500; break; 36 | case os_ps4: case os_ps5: _console_limit = 1024; break; 37 | } 38 | 39 | if (_console_limit < string_length(_string)) 40 | { 41 | show_debug_message("Input String Warning: Platform dialog has a limit of " + string(_console_limit) + " characters"); 42 | _string = string_copy(_string, 1, _console_limit); 43 | } 44 | 45 | if (string_length(_string) > __max_length) 46 | { 47 | // Enforce configured character limit 48 | show_debug_message("Input String Warning: Truncating string to " + string(__max_length) + " characters"); 49 | _string = string_copy(_string, 1, __max_length); 50 | } 51 | } 52 | 53 | __predialog = __value; 54 | __async_id = get_string_async(_prompt, _string); 55 | 56 | return true; 57 | } 58 | } 59 | } 60 | 61 | // input-string feather disable all 62 | 63 | function input_string_dialog_async_event() 64 | { 65 | if (string_pos("__YYInternalObject__", object_get_name(object_index)) > 0) 66 | { 67 | // Object event only 68 | show_error("Input String Error: Async dialog used in invalid context (outside an object async event)", true); 69 | } 70 | 71 | if (event_number != ((os_browser == browser_not_a_browser)? ev_dialog_async : 0)) 72 | { 73 | // Async dialog event only 74 | show_error 75 | ( 76 | "Input String Error: Async dialog used in invalid event " 77 | + object_get_name(object_index) + ", " 78 | + "Event " + string(event_type) + ", " 79 | + "no. " + string(event_number) + ") ", 80 | true 81 | ); 82 | 83 | return; 84 | } 85 | 86 | with (__input_string()) 87 | { 88 | if ((__async_id != undefined) && (async_load != -1) && (async_load[? "id"] == __async_id)) 89 | { 90 | // Confirm Async 91 | var _result = async_load[? "result"]; 92 | if ((async_load[? "status"] != true) || (_result == undefined)) 93 | { 94 | // Set empty 95 | _result = ""; 96 | } 97 | else 98 | { 99 | _result = string(_result); 100 | } 101 | 102 | if ((async_load[? "status"] != true) || (!__allow_empty && (_result == ""))) 103 | { 104 | // Revert empty 105 | _result = __predialog; 106 | } 107 | else 108 | { 109 | __async_submit = true; 110 | } 111 | 112 | __set(_result); 113 | __async_id = undefined; 114 | 115 | if (__async_submit) __submit(); 116 | } 117 | } 118 | } 119 | 120 | function input_string_async_active(){ return ((__input_string()).__async_id != undefined); } 121 | -------------------------------------------------------------------------------- /scripts/input_string_async/input_string_async.yy: -------------------------------------------------------------------------------- 1 | { 2 | "isDnD": false, 3 | "isCompatibility": false, 4 | "parent": { 5 | "name": "input_string", 6 | "path": "input_string.yyp", 7 | }, 8 | "resourceVersion": "1.0", 9 | "name": "input_string_async", 10 | "tags": [], 11 | "resourceType": "GMScript", 12 | } --------------------------------------------------------------------------------