diff --git a/.efrocachemap b/.efrocachemap index c77a4af6..9785c6fd 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -4064,53 +4064,53 @@ "build/assets/windows/Win32/ucrtbased.dll": "2def5335207d41b21b9823f6805997f1", "build/assets/windows/Win32/vc_redist.x86.exe": "b08a55e2e77623fe657bea24f223a3ae", "build/assets/windows/Win32/vcruntime140d.dll": "865b2af4d1e26a1a8073c89acb06e599", - "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "44dec65bbb43c2424334cce255b55836", - "build/prefab/full/linux_arm64_gui/release/ballisticakit": "55489d3d62fd081b83c4df871e40ad27", - "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "21530b0be2f54d1c457a8c2ca5bfb480", - "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "a3d058738fc7891bc1d0139654b5fc26", - "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "8d4b813a4955b6574b4e0e6b413ad7cf", - "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "7c618d9dac85afc6a7be8c7927693e81", - "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "2b2ddfe86feb7e701d472264c5d7ea83", - "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "63f59ed473e1f954786284d6988c1a2b", - "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "b703788a1e3aef102349db7968b2dd99", - "build/prefab/full/mac_arm64_gui/release/ballisticakit": "dd4ea149455ddf77db357bbcf92622ac", - "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "d0651d0ed865c44f45a0e86a93fdf46b", - "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "65f43cfa50bf3fb198ccdacb5ac7dfe4", - "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "a2b3388c4deec4e980a0268b0757ac3a", - "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "a60d7430b9ba3cf71bc9ffe5944026fc", - "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "d08625da75aba13159ea4e649b87eff9", - "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "c265fede38b25b8257ae1e6acb1d8036", - "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "74f47c43f480f60732b43a0f9b80f76d", - "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "c9b4d66d5ce318e5cecb7412771fbfba", - "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "fe6ad3d4cdaadd56326f5e616588b3fb", - "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "772769bf8f5c49031782f68eaa49c0d3", - "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "a075beb846859a3bee6b4fc1c4d9369b", - "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "208a67fb7e7b942988e8520f9570138e", - "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "a075beb846859a3bee6b4fc1c4d9369b", - "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "208a67fb7e7b942988e8520f9570138e", - "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "2a98d808b017ddac714d2f266d443394", - "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "24c7f0248a8f59e5349db9c040e6bd4f", - "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "2a98d808b017ddac714d2f266d443394", - "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "24c7f0248a8f59e5349db9c040e6bd4f", - "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "e8f8be3a0ba00a2ecb8956c2459107ec", - "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "741e277f99d48437a5a1b9dacef107ee", - "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "e8f8be3a0ba00a2ecb8956c2459107ec", - "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "741e277f99d48437a5a1b9dacef107ee", - "build/prefab/lib/mac_x86_64_gui/debug/libballisticaplus.a": "3db2e9a04f23052f3a14390a0f7ba00e", - "build/prefab/lib/mac_x86_64_gui/release/libballisticaplus.a": "15cf0e78e70d952c14c4b5e9ad6ef749", - "build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "a0b27fbfca2dd7404a20997fbfa10a7f", - "build/prefab/lib/mac_x86_64_server/release/libballisticaplus.a": "15cf0e78e70d952c14c4b5e9ad6ef749", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "25d06d95141284fff10db4a55ed481eb", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "708878d75d73b8510b354b2f353da621", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "b8b36fd481e253e83b3cf90734a7d627", - "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "17cb96c46a7e1763fdfbc6f48199f547", - "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "ae3b27deef1240beb1b32a17a46b7d90", - "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "0e2c5cec39ac27d42cb5cd635da996bd", - "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "6b278874c0c0526494bd94aad4a817ae", - "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "55d3224895c30042caca26e6d77e406b", + "build/prefab/full/linux_arm64_gui/debug/ballisticakit": "e8a40affea4d63bc4ca150adfeb973db", + "build/prefab/full/linux_arm64_gui/release/ballisticakit": "fb00781c9574d0ca777eb5ced699dc18", + "build/prefab/full/linux_arm64_server/debug/dist/ballisticakit_headless": "d53b0c174af88e8a483896fdfd411579", + "build/prefab/full/linux_arm64_server/release/dist/ballisticakit_headless": "bb6fd9371937ebe3f25d253d3046269e", + "build/prefab/full/linux_x86_64_gui/debug/ballisticakit": "f71e0d97424b7bf4b1ad57876ca4f01f", + "build/prefab/full/linux_x86_64_gui/release/ballisticakit": "6c2ba3735f33f1fd3804bd89a71c75bc", + "build/prefab/full/linux_x86_64_server/debug/dist/ballisticakit_headless": "cff1c33bb6725fb06020a256ac8e6b16", + "build/prefab/full/linux_x86_64_server/release/dist/ballisticakit_headless": "d9d6dbe7c5396a3adb8468fbecbf2b8c", + "build/prefab/full/mac_arm64_gui/debug/ballisticakit": "2dcddcc4a939a290fd8974d575fa3761", + "build/prefab/full/mac_arm64_gui/release/ballisticakit": "cd7f47495207a86d2f72ea0749b69db8", + "build/prefab/full/mac_arm64_server/debug/dist/ballisticakit_headless": "e223f88277dafee40cd0e631ee4fa02d", + "build/prefab/full/mac_arm64_server/release/dist/ballisticakit_headless": "1b8a5eac67370bed2a29f81525fbc6eb", + "build/prefab/full/mac_x86_64_gui/debug/ballisticakit": "91debaa22c5ce91586cfdad646a72afe", + "build/prefab/full/mac_x86_64_gui/release/ballisticakit": "84c8c568e4daeef372806cdc96f4ed48", + "build/prefab/full/mac_x86_64_server/debug/dist/ballisticakit_headless": "ebf0e8e22d53790b85ceb9f5c507db35", + "build/prefab/full/mac_x86_64_server/release/dist/ballisticakit_headless": "ba136eb04e177e264d459443bcde879f", + "build/prefab/full/windows_x86_gui/debug/BallisticaKit.exe": "fdc1b63de28ebd369b78b2bbeb3c6290", + "build/prefab/full/windows_x86_gui/release/BallisticaKit.exe": "126e2eec53a5f7a00d95039ad506b9b4", + "build/prefab/full/windows_x86_server/debug/dist/BallisticaKitHeadless.exe": "7db3922abdbf3045ef814f235d514cdb", + "build/prefab/full/windows_x86_server/release/dist/BallisticaKitHeadless.exe": "b5a1ff440599ccf09bd543d64a1d7aba", + "build/prefab/lib/linux_arm64_gui/debug/libballisticaplus.a": "1e786451b0abe1451f17b908c2d8abb3", + "build/prefab/lib/linux_arm64_gui/release/libballisticaplus.a": "80db3e75458db18efe1657f8ce686996", + "build/prefab/lib/linux_arm64_server/debug/libballisticaplus.a": "1e786451b0abe1451f17b908c2d8abb3", + "build/prefab/lib/linux_arm64_server/release/libballisticaplus.a": "80db3e75458db18efe1657f8ce686996", + "build/prefab/lib/linux_x86_64_gui/debug/libballisticaplus.a": "a29bc1b96b2422dce9154ef8a404a8e6", + "build/prefab/lib/linux_x86_64_gui/release/libballisticaplus.a": "c36dc72d78f9df240ae9f640dee470c2", + "build/prefab/lib/linux_x86_64_server/debug/libballisticaplus.a": "a29bc1b96b2422dce9154ef8a404a8e6", + "build/prefab/lib/linux_x86_64_server/release/libballisticaplus.a": "c36dc72d78f9df240ae9f640dee470c2", + "build/prefab/lib/mac_arm64_gui/debug/libballisticaplus.a": "8f3b213feb6207ab470a7318de8e5cc9", + "build/prefab/lib/mac_arm64_gui/release/libballisticaplus.a": "1b24bf754c66d424be3f3809b2523c61", + "build/prefab/lib/mac_arm64_server/debug/libballisticaplus.a": "8f3b213feb6207ab470a7318de8e5cc9", + "build/prefab/lib/mac_arm64_server/release/libballisticaplus.a": "1b24bf754c66d424be3f3809b2523c61", + "build/prefab/lib/mac_x86_64_gui/debug/libballisticaplus.a": "93ff05ad705918cf0c47e5946021a2e5", + "build/prefab/lib/mac_x86_64_gui/release/libballisticaplus.a": "61ee5217ce8cfc2d471ffc5d1d8d4ad8", + "build/prefab/lib/mac_x86_64_server/debug/libballisticaplus.a": "d57603910e6c8d153b05a5515ec99ab2", + "build/prefab/lib/mac_x86_64_server/release/libballisticaplus.a": "61ee5217ce8cfc2d471ffc5d1d8d4ad8", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.lib": "2312b4a091f482d64c22f71ca971efe8", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitGenericPlus.pdb": "0d5cb925a29f8185d615d6b954674c6b", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.lib": "eb53d93675b81c631d43f2283b59a9ae", + "build/prefab/lib/windows/Debug_Win32/BallisticaKitHeadlessPlus.pdb": "f15e465066504b1bfadc6fef97bfa6d4", + "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.lib": "bad42242aa1c98b5e6c7318da8b0fa01", + "build/prefab/lib/windows/Release_Win32/BallisticaKitGenericPlus.pdb": "55366eb80cbd1b22c2636d29b780b6f6", + "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.lib": "c6870a6302ae49bac39f53b390797d8f", + "build/prefab/lib/windows/Release_Win32/BallisticaKitHeadlessPlus.pdb": "29ff7769db971f92b49917c3d61de3ec", "src/assets/ba_data/python/babase/_mgen/__init__.py": "f885fed7f2ed98ff2ba271f9dbe3391c", "src/assets/ba_data/python/babase/_mgen/enums.py": "f8cd3af311ac63147882590123b78318", - "src/ballistica/base/mgen/pyembed/binding_base.inc": "ad347097a38e0d7ede9eb6dec6a80ee9", + "src/ballistica/base/mgen/pyembed/binding_base.inc": "c81b2b1f3a14b4cd20a7b93416fe893a", "src/ballistica/base/mgen/pyembed/binding_base_app.inc": "b67add3e1346f63491bf3450148e60d4", "src/ballistica/classic/mgen/pyembed/binding_classic.inc": "3ceb412513963f0818ab39c58bf292e3", "src/ballistica/core/mgen/pyembed/binding_core.inc": "9d0a3c9636138e35284923e0c8311c69", @@ -4118,5 +4118,5 @@ "src/ballistica/core/mgen/python_modules_monolithic.h": "fb967ed1c7db0c77d8deb4f00a7103c5", "src/ballistica/scene_v1/mgen/pyembed/binding_scene_v1.inc": "d80f970053099b3044204bfe29ddefce", "src/ballistica/template_fs/mgen/pyembed/binding_template_fs.inc": "44a45492db057bf7f7158c3b0fa11f0f", - "src/ballistica/ui_v1/mgen/pyembed/binding_ui_v1.inc": "6e982bd0dda68e8b821f5b5cc18ce1d5" + "src/ballistica/ui_v1/mgen/pyembed/binding_ui_v1.inc": "8f4c2070174bdc2fbf735180394d7b3a" } \ No newline at end of file diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml index f50f339b..bfdb3300 100644 --- a/.idea/dictionaries/ericf.xml +++ b/.idea/dictionaries/ericf.xml @@ -2825,6 +2825,7 @@ steelseries stgdict stickman + stname storable storagename storagenames diff --git a/CHANGELOG.md b/CHANGELOG.md index fbbc62da..7b68b826 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,21 @@ -### 1.7.28 (build 21329, api 8, 2023-09-10) +### 1.7.28 (build 21337, api 8, 2023-09-11) - Renamed Console to DevConsole, and added an option under advanced settings to - always show an ugly 'dev' button onscreen which can be used to toggle it. The + always show a 'dev' button onscreen which can be used to toggle it. The backtick key still works also for anyone with a keyboard. I plan to add more functionality besides just the Python console to the dev-console, and perhaps improve the Python console a bit too (add support for on-screen keyboards, etc.) - The in-app Python console text is now sized up on phone and tablet devices, and is generally a bit larger everywhere. +- Cleaned up onscreen keyboard support and generalized it to make it possible to + support other things besides widgets and to make it easier to implement on + other platforms. +- Added onscreen keyboard support to the in-app Python console and added an Exec + button to allow execing it without a return key on a keyboard. The cloud + console is probably still a better way to go for most people but this makes at + least simple things possible without an internet connection for most Android + users. - Added some high level functionality for copying and deleting feature-sets to the `spinoff` tool. For example, to create your own `poo` feature-set based on the existing `template_fs` one, do `tools/spinoff fset-copy template_fs poo`. @@ -24,10 +32,10 @@ significantly faster & more efficient. - Updated internal Python builds for Apple & iOS to 3.11.5, and updated a few dependent libraries as well (OpenSSL bumped from 3.0.8 to 3.0.10, etc.). -- Cleaned up the `babase.quit()` mechanism a bit. The default for the 'soft' arg - is now true, so a raw `babase.quit()` should now be a good citizen on mobile +- Cleaned up the `babase.quit()` mechanism. The default for the 'soft' arg is + now true, so a vanilla `babase.quit()` should now be a good citizen on mobile platforms. Also added the `g_base->QuitApp()` call which gives the C++ layer - an equivalent to the Python call. + a high level equivalent to the Python call. - (build 21326) Fixed an uninitialized variable that could cause V1 networking to fail in some builds/runs (thanks Rikko for the heads-up). - (build 21327) Fixed an issue that could cause the app to pause for 3 seconds diff --git a/ballisticakit-cmake/.idea/dictionaries/ericf.xml b/ballisticakit-cmake/.idea/dictionaries/ericf.xml index 599a6c23..8bf0d316 100644 --- a/ballisticakit-cmake/.idea/dictionaries/ericf.xml +++ b/ballisticakit-cmake/.idea/dictionaries/ericf.xml @@ -1680,6 +1680,7 @@ stepnum stepsize stgdict + stname storagenames storecmd stot diff --git a/src/assets/.asset_manifest_public.json b/src/assets/.asset_manifest_public.json index 11b08f42..75eff2e4 100644 --- a/src/assets/.asset_manifest_public.json +++ b/src/assets/.asset_manifest_public.json @@ -28,6 +28,7 @@ "ba_data/python/babase/__pycache__/_plugin.cpython-311.opt-1.pyc", "ba_data/python/babase/__pycache__/_stringedit.cpython-311.opt-1.pyc", "ba_data/python/babase/__pycache__/_text.cpython-311.opt-1.pyc", + "ba_data/python/babase/__pycache__/_ui.cpython-311.opt-1.pyc", "ba_data/python/babase/__pycache__/_workspace.cpython-311.opt-1.pyc", "ba_data/python/babase/__pycache__/modutils.cpython-311.opt-1.pyc", "ba_data/python/babase/_accountv2.py", @@ -60,6 +61,7 @@ "ba_data/python/babase/_plugin.py", "ba_data/python/babase/_stringedit.py", "ba_data/python/babase/_text.py", + "ba_data/python/babase/_ui.py", "ba_data/python/babase/_workspace.py", "ba_data/python/babase/modutils.py", "ba_data/python/baclassic/__init__.py", diff --git a/src/assets/Makefile b/src/assets/Makefile index 2442d832..77353683 100644 --- a/src/assets/Makefile +++ b/src/assets/Makefile @@ -188,6 +188,7 @@ SCRIPT_TARGETS_PY_PUBLIC = \ $(BUILD_DIR)/ba_data/python/babase/_plugin.py \ $(BUILD_DIR)/ba_data/python/babase/_stringedit.py \ $(BUILD_DIR)/ba_data/python/babase/_text.py \ + $(BUILD_DIR)/ba_data/python/babase/_ui.py \ $(BUILD_DIR)/ba_data/python/babase/_workspace.py \ $(BUILD_DIR)/ba_data/python/babase/modutils.py \ $(BUILD_DIR)/ba_data/python/baclassic/__init__.py \ @@ -460,6 +461,7 @@ SCRIPT_TARGETS_PYC_PUBLIC = \ $(BUILD_DIR)/ba_data/python/babase/__pycache__/_plugin.cpython-311.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/babase/__pycache__/_stringedit.cpython-311.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/babase/__pycache__/_text.cpython-311.opt-1.pyc \ + $(BUILD_DIR)/ba_data/python/babase/__pycache__/_ui.cpython-311.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/babase/__pycache__/_workspace.cpython-311.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/babase/__pycache__/modutils.cpython-311.opt-1.pyc \ $(BUILD_DIR)/ba_data/python/baclassic/__pycache__/__init__.cpython-311.opt-1.pyc \ diff --git a/src/assets/ba_data/python/babase/__init__.py b/src/assets/ba_data/python/babase/__init__.py index 1b5520b3..9f79a21f 100644 --- a/src/assets/ba_data/python/babase/__init__.py +++ b/src/assets/ba_data/python/babase/__init__.py @@ -160,6 +160,7 @@ from babase._math import normalized_color, is_point_in_box, vec3validate from babase._meta import MetadataSubsystem from babase._net import get_ip_address_type, DEFAULT_REQUEST_TIMEOUT_SECONDS from babase._plugin import PluginSpec, Plugin, PluginSubsystem +from babase._stringedit import StringEditAdapter, StringEditSubsystem from babase._text import timestring _babase.app = app = App() @@ -294,6 +295,8 @@ __all__ = [ 'SimpleSound', 'SpecialChar', 'storagename', + 'StringEditAdapter', + 'StringEditSubsystem', 'TeamNotFoundError', 'timestring', 'UIScale', diff --git a/src/assets/ba_data/python/babase/_hooks.py b/src/assets/ba_data/python/babase/_hooks.py index 9d726c9f..5013b956 100644 --- a/src/assets/ba_data/python/babase/_hooks.py +++ b/src/assets/ba_data/python/babase/_hooks.py @@ -20,7 +20,7 @@ from typing import TYPE_CHECKING import _babase if TYPE_CHECKING: - pass + from babase._stringedit import StringEditAdapter def reset_to_main_menu() -> None: @@ -64,14 +64,6 @@ def open_url_with_webbrowser_module(url: str) -> None: _babase.screenmessage(Lstr(resource='errorText'), color=(1, 0, 0)) -def connecting_to_party_message() -> None: - from babase._language import Lstr - - _babase.screenmessage( - Lstr(resource='internal.connectingToPartyText'), color=(1, 1, 1) - ) - - def rejecting_invite_already_in_party_message() -> None: from babase._language import Lstr @@ -372,3 +364,11 @@ def show_client_too_old_error() -> None: ), color=(1, 0, 0), ) + + +def string_edit_adapter_can_be_replaced(adapter: StringEditAdapter) -> bool: + """Return whether a StringEditAdapter can be replaced.""" + from babase._stringedit import StringEditAdapter + + assert isinstance(adapter, StringEditAdapter) + return adapter.can_be_replaced() diff --git a/src/assets/ba_data/python/babase/_plugin.py b/src/assets/ba_data/python/babase/_plugin.py index 23550900..7711328e 100644 --- a/src/assets/ba_data/python/babase/_plugin.py +++ b/src/assets/ba_data/python/babase/_plugin.py @@ -197,6 +197,17 @@ class PluginSubsystem(AppSubsystem): _error.print_exception('Error in plugin on_app_shutdown()') + def on_app_shutdown_complete(self) -> None: + for plugin in self.active_plugins: + try: + plugin.on_app_shutdown_complete() + except Exception: + from babase import _error + + _error.print_exception( + 'Error in plugin on_app_shutdown_complete()' + ) + def load_plugins(self) -> None: """(internal)""" @@ -325,6 +336,9 @@ class Plugin: def on_app_shutdown(self) -> None: """Called when the app is beginning the shutdown process.""" + def on_app_shutdown_complete(self) -> None: + """Called when the app has completed the shutdown process.""" + def has_settings_ui(self) -> bool: """Called to ask if we have settings UI we can show.""" return False diff --git a/src/assets/ba_data/python/babase/_stringedit.py b/src/assets/ba_data/python/babase/_stringedit.py index c7b1482a..915199d3 100644 --- a/src/assets/ba_data/python/babase/_stringedit.py +++ b/src/assets/ba_data/python/babase/_stringedit.py @@ -8,8 +8,13 @@ own ui toolkits. from __future__ import annotations +import time +import logging +import weakref from typing import TYPE_CHECKING, final +from efro.util import empty_weakref + import _babase if TYPE_CHECKING: @@ -20,29 +25,103 @@ class StringEditSubsystem: """Full string-edit state for the app.""" def __init__(self) -> None: - pass - # print('HELLO FROM STRING EDIT') + self.active_adapter = empty_weakref(StringEditAdapter) -class StringEdit: +class StringEditAdapter: """Represents a string editing operation on some object. Editable objects such as text widgets or in-app-consoles can subclass this to make their contents editable on all platforms. + + There can only be one string-edit at a time for the app. New + StringEdits will attempt to register themselves as the globally + active one in their constructor, but this may not succeed. When + creating a StringEditAdapter, always check its 'is_valid()' value after + creating it. If this is False, it was not able to set itself as + the global active one and should be discarded. """ - def __init__(self, initial_text: str) -> None: - pass + def __init__( + self, + description: str, + initial_text: str, + max_length: int | None, + screen_space_center: tuple[float, float] | None, + ) -> None: + if not _babase.in_logic_thread(): + raise RuntimeError('This must be called from the logic thread.') + + self.create_time = time.monotonic() + + # Note: these attr names are hard-coded in C++ code so don't + # change them willy-nilly. + self.description = description + self.initial_text = initial_text + self.max_length = max_length + self.screen_space_center = screen_space_center + + # Attempt to register ourself as the active edit. + subsys = _babase.app.stringedit + current_edit = subsys.active_adapter() + if current_edit is None or current_edit.can_be_replaced(): + subsys.active_adapter = weakref.ref(self) + + @final + def can_be_replaced(self) -> bool: + """Return whether this adapter can be replaced by a new one. + + This is mainly a safeguard to allow adapters whose drivers have + gone away without calling apply or cancel to time out and be + replaced with new ones. + """ + if not _babase.in_logic_thread(): + raise RuntimeError('This must be called from the logic thread.') + + # Allow ourself to be replaced after a bit. + if time.monotonic() - self.create_time > 5.0: + if _babase.do_once(): + logging.warning( + 'StringEditAdapter can_be_replaced() check for %s' + ' yielding True due to timeout; ideally this should' + ' not be possible as the StringEditAdapter driver' + ' should be blocking anything else from kicking off' + ' new edits.', + self, + ) + return True + + # We also are always considered replaceable if we're not the + # active global adapter. + current_edit = _babase.app.stringedit.active_adapter() + if current_edit is not self: + return True + + return False @final def apply(self, new_text: str) -> None: """Should be called by the owner when editing is complete. Note that in some cases this call may be a no-op (such as if - this StringEdit is no longer the globally active one). + this StringEditAdapter is no longer the globally active one). """ if not _babase.in_logic_thread(): raise RuntimeError('This must be called from the logic thread.') + + # Make sure whoever is feeding this adapter is honoring max-length. + if self.max_length is not None and len(new_text) > self.max_length: + logging.warning( + 'apply() on %s was passed a string of length %d,' + ' but adapter max_length is %d; this should not happen' + ' (will truncate).', + self, + len(new_text), + self.max_length, + stack_info=True, + ) + new_text = new_text[: self.max_length] + self._do_apply(new_text) @final diff --git a/src/assets/ba_data/python/babase/_ui.py b/src/assets/ba_data/python/babase/_ui.py new file mode 100644 index 00000000..6f6b0595 --- /dev/null +++ b/src/assets/ba_data/python/babase/_ui.py @@ -0,0 +1,32 @@ +# Released under the MIT License. See LICENSE for details. +# +"""UI related bits of babase.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +from babase._stringedit import StringEditAdapter +import _babase + +if TYPE_CHECKING: + pass + + +class DevConsoleStringEditAdapter(StringEditAdapter): + """Allows editing dev-console text.""" + + def __init__(self) -> None: + description = 'Dev Console Input' + initial_text = _babase.get_dev_console_input_text() + max_length = None + screen_space_center = None + super().__init__( + description, initial_text, max_length, screen_space_center + ) + + def _do_apply(self, new_text: str) -> None: + _babase.set_dev_console_input_text(new_text) + _babase.dev_console_input_adapter_finish() + + def _do_cancel(self) -> None: + _babase.dev_console_input_adapter_finish() diff --git a/src/assets/ba_data/python/baenv.py b/src/assets/ba_data/python/baenv.py index 89695cc6..31e6c7d9 100644 --- a/src/assets/ba_data/python/baenv.py +++ b/src/assets/ba_data/python/baenv.py @@ -52,7 +52,7 @@ if TYPE_CHECKING: # Build number and version of the ballistica binary we expect to be # using. -TARGET_BALLISTICA_BUILD = 21329 +TARGET_BALLISTICA_BUILD = 21337 TARGET_BALLISTICA_VERSION = '1.7.28' diff --git a/src/assets/ba_data/python/bauiv1/_uitypes.py b/src/assets/ba_data/python/bauiv1/_uitypes.py index b97ed2a3..5cbd14e2 100644 --- a/src/assets/ba_data/python/bauiv1/_uitypes.py +++ b/src/assets/ba_data/python/bauiv1/_uitypes.py @@ -241,3 +241,35 @@ def ui_upkeep() -> None: else: remainingchecks.append(check) ui.cleanupchecks = remainingchecks + + +class TextWidgetStringEditAdapter(babase.StringEditAdapter): + """A StringEditAdapter subclass for editing our text widgets.""" + + def __init__(self, text_widget: bauiv1.Widget) -> None: + self.widget = text_widget + + # Ugly hacks to pull values from widgets. Really need to clean + # up that api. + description: Any = _bauiv1.textwidget(query_description=text_widget) + assert isinstance(description, str) + initial_text: Any = _bauiv1.textwidget(query=text_widget) + assert isinstance(initial_text, str) + max_length: Any = _bauiv1.textwidget(query_max_chars=text_widget) + assert isinstance(max_length, int) + + screen_space_center = text_widget.get_screen_space_center() + + super().__init__( + description, initial_text, max_length, screen_space_center + ) + + def _do_apply(self, new_text: str) -> None: + if self.widget: + _bauiv1.textwidget( + edit=self.widget, text=new_text, adapter_finished=True + ) + + def _do_cancel(self) -> None: + if self.widget: + _bauiv1.textwidget(edit=self.widget, adapter_finished=True) diff --git a/src/assets/ba_data/python/bauiv1/onscreenkeyboard.py b/src/assets/ba_data/python/bauiv1/onscreenkeyboard.py index a8e6098f..30a168c3 100644 --- a/src/assets/ba_data/python/bauiv1/onscreenkeyboard.py +++ b/src/assets/ba_data/python/bauiv1/onscreenkeyboard.py @@ -15,27 +15,28 @@ import _bauiv1 from bauiv1._uitypes import Window if TYPE_CHECKING: + from babase import StringEditAdapter + import bauiv1 as bui class OnScreenKeyboardWindow(Window): """Simple built-in on-screen keyboard.""" - def __init__(self, textwidget: bui.Widget, label: str, max_chars: int): - self._target_text = textwidget + def __init__(self, adapter: StringEditAdapter): + self._adapter = adapter self._width = 700 self._height = 400 assert babase.app.classic is not None uiscale = babase.app.ui_v1.uiscale top_extra = 20 if uiscale is babase.UIScale.SMALL else 0 + super().__init__( root_widget=_bauiv1.containerwidget( parent=_bauiv1.get_special_widget('overlay_stack'), size=(self._width, self._height + top_extra), transition='in_scale', - scale_origin_stack_offset=( - self._target_text.get_screen_space_center() - ), + scale_origin_stack_offset=adapter.screen_space_center, scale=( 2.0 if uiscale is babase.UIScale.SMALL @@ -69,7 +70,7 @@ class OnScreenKeyboardWindow(Window): position=(self._width * 0.5, self._height - 41), size=(0, 0), scale=0.95, - text=label, + text=adapter.description, maxwidth=self._width - 140, color=babase.app.ui_v1.title_color, h_align='center', @@ -79,8 +80,8 @@ class OnScreenKeyboardWindow(Window): self._text_field = _bauiv1.textwidget( parent=self._root_widget, position=(70, self._height - 116), - max_chars=max_chars, - text=cast(str, _bauiv1.textwidget(query=self._target_text)), + max_chars=adapter.max_length, + text=adapter.initial_text, on_return_press_call=self._done, autoselect=True, size=(self._width - 140, 55), @@ -436,13 +437,12 @@ class OnScreenKeyboardWindow(Window): self._refresh() def _cancel(self) -> None: + self._adapter.cancel() _bauiv1.getsound('swish').play() _bauiv1.containerwidget(edit=self._root_widget, transition='out_scale') def _done(self) -> None: _bauiv1.containerwidget(edit=self._root_widget, transition='out_scale') - if self._target_text: - _bauiv1.textwidget( - edit=self._target_text, - text=cast(str, _bauiv1.textwidget(query=self._text_field)), - ) + self._adapter.apply( + cast(str, _bauiv1.textwidget(query=self._text_field)) + ) diff --git a/src/ballistica/base/base.cc b/src/ballistica/base/base.cc index 30b74748..c66ee2a8 100644 --- a/src/ballistica/base/base.cc +++ b/src/ballistica/base/base.cc @@ -152,18 +152,8 @@ auto BaseFeatureSet::IsBaseCompletelyImported() -> bool { void BaseFeatureSet::OnAssetsAvailable() { assert(InLogicThread()); - assert(console_ == nullptr); - // Spin up the in-app console. - if (!g_core->HeadlessMode()) { - console_ = new DevConsole(); - - // Print any messages that have built up. - if (!console_startup_messages_.empty()) { - console_->Print(console_startup_messages_); - console_startup_messages_.clear(); - } - } + ui->OnAssetsAvailable(); } void BaseFeatureSet::StartApp() { @@ -576,21 +566,8 @@ void BaseFeatureSet::DoV1CloudLog(const std::string& msg) { plus()->DirectSendV1CloudLogs(logprefix, logsuffix, false, nullptr); } -void BaseFeatureSet::PushConsolePrintCall(const std::string& msg) { - // Completely ignore this stuff in headless mode. - if (g_core->HeadlessMode()) { - return; - } - // If our event loop AND console are up and running, ship it off to - // be printed. Otherwise store it for the console to grab when it's ready. - if (auto* event_loop = logic->event_loop()) { - if (console_ != nullptr) { - event_loop->PushCall([this, msg] { console_->Print(msg); }); - return; - } - } - // Didn't send a print; store for later. - console_startup_messages_ += msg; +void BaseFeatureSet::PushDevConsolePrintCall(const std::string& msg) { + ui->PushDevConsolePrintCall(msg); } PyObject* BaseFeatureSet::GetPyExceptionType(PyExcType exctype) { diff --git a/src/ballistica/base/base.h b/src/ballistica/base/base.h index dba6f777..173ed382 100644 --- a/src/ballistica/base/base.h +++ b/src/ballistica/base/base.h @@ -701,7 +701,7 @@ class BaseFeatureSet : public FeatureSetNativeComponent, -> PyObject* override; auto FeatureSetFromData(PyObject* obj) -> FeatureSetNativeComponent* override; void DoV1CloudLog(const std::string& msg) override; - void PushConsolePrintCall(const std::string& msg) override; + void PushDevConsolePrintCall(const std::string& msg) override; auto GetPyExceptionType(PyExcType exctype) -> PyObject* override; auto PrintPythonStackTrace() -> bool override; auto GetPyLString(PyObject* obj) -> std::string override; @@ -754,7 +754,6 @@ class BaseFeatureSet : public FeatureSetNativeComponent, Utils* const utils; // Variable subsystems. - auto* console() const { return console_; } auto* app_mode() const { return app_mode_; } auto* stress_test() const { return stress_test_; } void set_app_mode(AppMode* mode); @@ -779,13 +778,11 @@ class BaseFeatureSet : public FeatureSetNativeComponent, void PrintContextUnavailable_(); AppMode* app_mode_; - DevConsole* console_{}; PlusSoftInterface* plus_soft_{}; ClassicSoftInterface* classic_soft_{}; UIV1SoftInterface* ui_v1_soft_{}; StressTest* stress_test_; - std::string console_startup_messages_; std::mutex shutdown_suppress_lock_; bool shutdown_suppress_disallowed_{}; int shutdown_suppress_count_{}; diff --git a/src/ballistica/base/input/input.cc b/src/ballistica/base/input/input.cc index 71d3876d..ec0fa986 100644 --- a/src/ballistica/base/input/input.cc +++ b/src/ballistica/base/input/input.cc @@ -813,8 +813,8 @@ void Input::PushTextInputEvent(const std::string& text) { if (IsInputLocked()) { return; } - if (g_base && g_base->console() != nullptr - && g_base->console()->HandleTextEditing(text)) { + if (g_base && g_base->ui->dev_console() != nullptr + && g_base->ui->dev_console()->HandleTextEditing(text)) { return; } g_base->ui->SendWidgetMessage(WidgetMessage( @@ -966,9 +966,10 @@ void Input::HandleKeyPress(const SDL_Keysym* keysym) { } // Let the console intercept stuff if it wants at this point. - if (g_base && g_base->console() != nullptr - && g_base->console()->HandleKeyPress(keysym)) { - return; + if (auto* console = g_base->ui->dev_console()) { + if (console->HandleKeyPress(keysym)) { + return; + } } // Ctrl-V or Cmd-V sends paste commands to any interested text fields. @@ -1089,8 +1090,8 @@ void Input::HandleKeyRelease(const SDL_Keysym* keysym) { keys_held_.erase(keysym->sym); - if (g_base->console() != nullptr) { - g_base->console()->HandleKeyRelease(keysym); + if (g_base->ui->dev_console() != nullptr) { + g_base->ui->dev_console()->HandleKeyRelease(keysym); } if (keyboard_input_) { diff --git a/src/ballistica/base/logic/logic.cc b/src/ballistica/base/logic/logic.cc index 825ac8a0..6ac51aed 100644 --- a/src/ballistica/base/logic/logic.cc +++ b/src/ballistica/base/logic/logic.cc @@ -164,7 +164,7 @@ void Logic::OnInitialAppModeSet() { // We want any sort of raw Python input to only start accepting commands // once we've got an initial app-mode set. Generally said commands will // assume we're running in that mode and will fail if run before it is set. - if (auto* console = g_base->console()) { + if (auto* console = g_base->ui->dev_console()) { console->EnableInput(); } if (g_base->stdio_console) { diff --git a/src/ballistica/base/platform/base_platform.cc b/src/ballistica/base/platform/base_platform.cc index ba74c3c6..c8537f0d 100644 --- a/src/ballistica/base/platform/base_platform.cc +++ b/src/ballistica/base/platform/base_platform.cc @@ -329,4 +329,50 @@ auto BasePlatform::CanBackQuit() -> bool { return false; } void BasePlatform::DoBackQuit() {} void BasePlatform::DoSoftQuit() {} +auto BasePlatform::HaveStringEditor() -> bool { return false; } + +void BasePlatform::InvokeStringEditor(PyObject* string_edit_adapter) { + BA_PRECONDITION(HaveStringEditor()); + BA_PRECONDITION(g_base->InLogicThread()); + + // We assume there's a single one of these at a time. Hold on to it. + string_edit_adapter_.Acquire(string_edit_adapter); + + // Pull values from Python and ship them along to our platform + // implementation. + auto desc = string_edit_adapter_.GetAttr("description").ValueAsString(); + auto initial_text = + string_edit_adapter_.GetAttr("initial_text").ValueAsString(); + auto max_length = + string_edit_adapter_.GetAttr("max_length").ValueAsOptionalInt(); + // TODO(ericf): pass along screen_space_center if its ever useful. + + g_base->platform->DoInvokeStringEditor(desc, initial_text, max_length); +} + +/// Should be called by platform StringEditor to apply a value. +void BasePlatform::StringEditorApply(const std::string& val) { + BA_PRECONDITION(HaveStringEditor()); + BA_PRECONDITION(g_base->InLogicThread()); + BA_PRECONDITION(string_edit_adapter_.Exists()); + auto args = PythonRef::Stolen(Py_BuildValue("(s)", val.c_str())); + string_edit_adapter_.GetAttr("apply").Call(args); + string_edit_adapter_.Release(); +} + +/// Should be called by platform StringEditor to signify a cancel. +void BasePlatform::StringEditorCancel() { + BA_PRECONDITION(HaveStringEditor()); + BA_PRECONDITION(g_base->InLogicThread()); + BA_PRECONDITION(string_edit_adapter_.Exists()); + string_edit_adapter_.GetAttr("cancel").Call(); + string_edit_adapter_.Release(); +} + +void BasePlatform::DoInvokeStringEditor(const std::string& title, + const std::string& value, + std::optional max_chars) { + Log(LogLevel::kError, "FIXME: DoInvokeStringEditor() unimplemented"); +} + } // namespace ballistica::base diff --git a/src/ballistica/base/platform/base_platform.h b/src/ballistica/base/platform/base_platform.h index 0b049737..49b44bc9 100644 --- a/src/ballistica/base/platform/base_platform.h +++ b/src/ballistica/base/platform/base_platform.h @@ -4,6 +4,7 @@ #define BALLISTICA_BASE_PLATFORM_BASE_PLATFORM_H_ #include "ballistica/base/base.h" +#include "ballistica/shared/python/python_ref.h" namespace ballistica::base { @@ -104,13 +105,36 @@ class BasePlatform { bool active); #pragma mark MISC -------------------------------------------------------------- + /// Do we define a platform-specific string editor? This is something like + /// a text view popup which allows the use of default OS input methods + /// such as on-screen-keyboards. + virtual auto HaveStringEditor() -> bool; + + /// Trigger a string edit for the provided StringEditAdapter Python obj. + /// This should only be called once the edit-adapter has been verified as + /// being the globally active one. Must be called from the logic thread. + void InvokeStringEditor(PyObject* string_edit_adapter); + /// Open the provided URL in a browser or whatnot. void OpenURL(const std::string& url); /// Get the most up-to-date cursor position. void GetCursorPosition(float* x, float* y); + /// Should be called by platform StringEditor to apply a value. + /// Must be called in the logic thread. + void StringEditorApply(const std::string& val); + + /// Should be called by platform StringEditor to signify a cancel. + /// Must be called in the logic thread. + void StringEditorCancel(); + protected: + /// Pop up a text edit dialog. + virtual void DoInvokeStringEditor(const std::string& title, + const std::string& value, + std::optional max_chars); + /// Open the provided URL in a browser or whatnot. virtual void DoOpenURL(const std::string& url); @@ -121,11 +145,12 @@ class BasePlatform { virtual ~BasePlatform(); private: - /// Called after our singleton has been instantiated. - /// Any construction functionality requiring virtual functions resolving to - /// their final class versions can go here. + /// Called after our singleton has been instantiated. Any construction + /// functionality requiring virtual functions resolving to their final + /// class versions can go here. virtual void PostInit(); + PythonRef string_edit_adapter_{}; bool ran_base_post_init_{}; std::string public_device_uuid_; }; diff --git a/src/ballistica/base/python/base_python.cc b/src/ballistica/base/python/base_python.cc index 75ab439a..1e579f60 100644 --- a/src/ballistica/base/python/base_python.cc +++ b/src/ballistica/base/python/base_python.cc @@ -580,4 +580,25 @@ auto BasePython::DoOnce() -> bool { return true; } +auto BasePython::CanPyStringEditAdapterBeReplaced(PyObject* o) -> bool { + assert(g_base->InLogicThread()); + + auto args = PythonRef::Stolen(Py_BuildValue("(O)", o)); + auto result = g_base->python->objs() + .Get(BasePython::ObjID::kStringEditAdapterCanBeReplacedCall) + .Call(args); + if (!result.Exists()) { + Log(LogLevel::kError, "Error getting StringEdit valid state."); + return false; + } + if (result.Get() == Py_True) { + return true; + } + if (result.Get() == Py_False) { + return false; + } + Log(LogLevel::kError, "Got unexpected value for StringEdit valid."); + return false; +} + } // namespace ballistica::base diff --git a/src/ballistica/base/python/base_python.h b/src/ballistica/base/python/base_python.h index e38a0f8e..c4934031 100644 --- a/src/ballistica/base/python/base_python.h +++ b/src/ballistica/base/python/base_python.h @@ -41,7 +41,6 @@ class BasePython { kSetConfigFullscreenOnCall, kSetConfigFullscreenOffCall, kNotSignedInScreenMessageCall, - kConnectingToPartyMessageCall, kRejectingInviteAlreadyInPartyMessageCall, kConnectionFailedMessageCall, kTemporarilyUnavailableMessageCall, @@ -104,6 +103,8 @@ class BasePython { kEnvOnNativeModuleImportCall, kOnMainThreadStartAppCall, kAppPushApplyAppConfigCall, + kStringEditAdapterCanBeReplacedCall, + kDevConsoleStringEditAdapterClass, kLast // Sentinel; must be at end. }; @@ -149,6 +150,8 @@ class BasePython { static auto IsPyEnum_InputType(PyObject* obj) -> bool; static auto GetPyEnum_InputType(PyObject* obj) -> InputType; + auto CanPyStringEditAdapterBeReplaced(PyObject* o) -> bool; + auto IsPyLString(PyObject* o) -> bool; auto GetPyLString(PyObject* o) -> std::string; auto GetPyLStrings(PyObject* o) -> std::vector; diff --git a/src/ballistica/base/python/methods/python_methods_app.cc b/src/ballistica/base/python/methods/python_methods_app.cc index 14b0aa8f..567fdfd4 100644 --- a/src/ballistica/base/python/methods/python_methods_app.cc +++ b/src/ballistica/base/python/methods/python_methods_app.cc @@ -9,6 +9,7 @@ #include "ballistica/base/python/base_python.h" #include "ballistica/base/python/support/python_context_call_runnable.h" #include "ballistica/base/support/stress_test.h" +#include "ballistica/base/ui/dev_console.h" #include "ballistica/base/ui/ui.h" #include "ballistica/core/platform/core_platform.h" #include "ballistica/shared/foundation/event_loop.h" @@ -1516,8 +1517,7 @@ static PyMethodDef PyShutdownSuppressEndDef = { "(internal)\n", }; -// ------------------------ shutdown_suppress_count -// ------------------------------ +// ----------------------- shutdown_suppress_count ----------------------------- static auto PyShutdownSuppressCount(PyObject* self) -> PyObject* { BA_PYTHON_TRY; @@ -1537,6 +1537,79 @@ static PyMethodDef PyShutdownSuppressCountDef = { "(internal)\n", }; +// --------------------- get_dev_console_input_text ---------------------------- + +static auto PyGetDevConsoleInputText(PyObject* self) -> PyObject* { + BA_PYTHON_TRY; + BA_PRECONDITION(g_base->InLogicThread()); + auto* console = g_base->ui->dev_console(); + BA_PRECONDITION(console); + return PyUnicode_FromString(console->input_string().c_str()); + BA_PYTHON_CATCH; +} + +static PyMethodDef PyGetDevConsoleInputTextDef = { + "get_dev_console_input_text", // name + (PyCFunction)PyGetDevConsoleInputText, // method + METH_NOARGS, // flags + + "get_dev_console_input_text() -> str\n" + "\n" + "(internal)\n", +}; + +// --------------------- set_dev_console_input_text ---------------------------- + +static auto PySetDevConsoleInputText(PyObject* self, PyObject* args, + PyObject* keywds) -> PyObject* { + BA_PYTHON_TRY; + BA_PRECONDITION(g_base->InLogicThread()); + auto* console = g_base->ui->dev_console(); + BA_PRECONDITION(console); + + const char* val; + static const char* kwlist[] = {"val", nullptr}; + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", + const_cast(kwlist), &val)) { + return nullptr; + } + console->set_input_string(val); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PySetDevConsoleInputTextDef = { + "set_dev_console_input_text", // name + (PyCFunction)PySetDevConsoleInputText, // method + METH_VARARGS | METH_KEYWORDS, // flags + + "set_dev_console_input_text(val: str) -> None\n" + "\n" + "(internal)\n", +}; + +// ------------------ dev_console_input_adapter_finish ------------------------- + +static auto PyDevConsoleInputAdapterFinish(PyObject* self) -> PyObject* { + BA_PYTHON_TRY; + BA_PRECONDITION(g_base->InLogicThread()); + auto* console = g_base->ui->dev_console(); + BA_PRECONDITION(console); + console->InputAdapterFinish(); + Py_RETURN_NONE; + BA_PYTHON_CATCH; +} + +static PyMethodDef PyDevConsoleInputAdapterFinishDef = { + "dev_console_input_adapter_finish", // name + (PyCFunction)PyDevConsoleInputAdapterFinish, // method + METH_NOARGS, // flags + + "dev_console_input_adapter_finish() -> None\n" + "\n" + "(internal)\n", +}; + // ----------------------------------------------------------------------------- auto PythonMethodsApp::GetMethods() -> std::vector { @@ -1589,6 +1662,9 @@ auto PythonMethodsApp::GetMethods() -> std::vector { PyShutdownSuppressBeginDef, PyShutdownSuppressEndDef, PyShutdownSuppressCountDef, + PyGetDevConsoleInputTextDef, + PySetDevConsoleInputTextDef, + PyDevConsoleInputAdapterFinishDef, }; } diff --git a/src/ballistica/base/ui/dev_console.cc b/src/ballistica/base/ui/dev_console.cc index 628c3f1a..53ae57f9 100644 --- a/src/ballistica/base/ui/dev_console.cc +++ b/src/ballistica/base/ui/dev_console.cc @@ -8,6 +8,8 @@ #include "ballistica/base/graphics/text/text_graphics.h" #include "ballistica/base/input/input.h" #include "ballistica/base/logic/logic.h" +#include "ballistica/base/platform/base_platform.h" +#include "ballistica/base/python/base_python.h" #include "ballistica/base/support/context.h" #include "ballistica/base/ui/ui.h" #include "ballistica/core/core.h" @@ -27,11 +29,117 @@ const int kDevConsoleStringBreakUpSize = 1950; const int kDevConsoleActivateKey1 = SDLK_BACKQUOTE; const int kDevConsoleActivateKey2 = SDLK_F2; +const double kTransitionSeconds = 0.1; + +enum class DevButtonAttach_ { kLeft, kCenter, kRight }; + +class DevConsole::Button_ { + public: + template + Button_(const std::string& label, float text_scale, DevButtonAttach_ attach, + float x, float y, float width, float height, const F& lambda) + : label{label}, + attach{attach}, + x{x}, + y{y}, + width{width}, + height{height}, + call{NewLambdaRunnable(lambda)}, + text_scale{text_scale} {} + std::string label; + DevButtonAttach_ attach; + float x; + float y; + float width; + float height; + bool pressed{}; + Object::Ref call; + TextGroup text_group; + bool text_group_built_{}; + float text_scale; + + auto InUs(float mx, float my) -> bool { + mx -= XOffs(); + return (mx >= x && mx <= (x + width) && my >= y && my <= (y + height)); + } + auto XOffs() -> float { + switch (attach) { + case DevButtonAttach_::kLeft: + return 0.0f; + case DevButtonAttach_::kRight: + return g_base->graphics->screen_virtual_width(); + case DevButtonAttach_::kCenter: + return g_base->graphics->screen_virtual_width() * 0.5f; + } + assert(false); + return 0.0f; + } + + auto HandleMouseDown(float mx, float my) -> bool { + if (InUs(mx, my)) { + pressed = true; + return true; + } + return false; + } + + void HandleMouseUp(float mx, float my) { + pressed = false; + if (InUs(mx, my)) { + if (call.Exists()) { + call.Get()->Run(); + } + } + } + + void Draw(RenderPass* pass, float bottom) { + if (!text_group_built_) { + text_group.set_text(label, TextMesh::HAlign::kCenter, + TextMesh::VAlign::kCenter); + } + SimpleComponent c(pass); + c.SetTransparent(true); + if (pressed) { + c.SetColor(0.8f, 0.7f, 0.8f, 1.0f); + } else { + c.SetColor(0.25f, 0.2f, 0.3f, 1.0f); + } + { + auto xf = c.ScopedTransform(); + c.Translate(x + XOffs() + width * 0.5f, y + bottom + height * 0.5f, + kDevConsoleZDepth); + // Draw our backing. + { + auto xf = c.ScopedTransform(); + c.Scale(width, height); + c.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kImage1x1)); + } + // Draw our text. + if (pressed) { + c.SetColor(0.0f, 0.0f, 0.0f, 1.0f); + } else { + c.SetColor(0.8f, 0.7f, 0.8f, 1.0f); + } + c.SetFlatness(1.0f); + int elem_count = text_group.GetElementCount(); + for (int e = 0; e < elem_count; e++) { + c.SetTexture(text_group.GetElementTexture(e)); + { + auto xf = c.ScopedTransform(); + float sc{0.6f * text_scale}; + c.Scale(sc, sc, 1.0f); + c.DrawMesh(text_group.GetElementMesh(e)); + } + } + } + c.Submit(); + } +}; + class DevConsole::Line_ { public: - Line_(std::string s_in, millisecs_t c) - : creation_time(c), s(std::move(s_in)) {} - millisecs_t creation_time; + Line_(std::string s_in, double c) : creation_time(c), s(std::move(s_in)) {} + double creation_time; std::string s; auto GetText() -> TextGroup& { if (!s_mesh_.Exists()) { @@ -59,10 +167,123 @@ DevConsole::DevConsole() { title_text_group_.set_text(title); built_text_group_.set_text("Built: " __DATE__ " " __TIME__); prompt_text_group_.set_text(">"); + + // NOTE: Once we can adjust UI scale on the fly we'll have to update + // this to recalc accordingly. + float bs = PythonConsoleBaseScale_(); + buttons_.emplace_back("Exec", 0.75f * bs, DevButtonAttach_::kRight, + -33.0f * bs, 15.95f * bs, 32.0f * bs, 13.0f * bs, + [this] { Exec(); }); + + // buttons_.emplace_back("TestButton", 1.0f, DevButtonAttach_::kLeft, 100.0f, + // 100.0f, 100.0f, 30.0f, [] { printf("B1 PRESSED!\n"); + // }); + + // buttons_.emplace_back("TestButton2", 1.0f, DevButtonAttach_::kCenter, + // -50.0f, + // 120.0f, 100.0f, 30.0f, [] { printf("B2 PRESSED!\n"); + // }); + + // buttons_.emplace_back("TestButton3", 0.8f, DevButtonAttach_::kRight, + // -200.0f, + // 140.0f, 100.0f, 30.0f, [] { printf("B3 PRESSED!\n"); + // }); } DevConsole::~DevConsole() = default; +auto DevConsole::HandleMouseDown(int button, float x, float y) -> bool { + assert(g_base->InLogicThread()); + + if (state_ == State_::kInactive) { + return false; + } + float bottom{Bottom_()}; + + // Pass to any buttons (in bottom-local space). + if (button == 1) { + for (auto&& button : buttons_) { + if (button.HandleMouseDown(x, y - bottom)) { + return true; + } + } + } + + if (y < bottom) { + return false; + } + + if (button == 1) { + python_console_pressed_ = true; + } + + return true; +} + +void DevConsole::HandleMouseUp(int button, float x, float y) { + assert(g_base->InLogicThread()); + float bottom{Bottom_()}; + + if (button == 1) { + for (auto&& button : buttons_) { + button.HandleMouseUp(x, y - bottom); + } + } + + if (button == 1 && python_console_pressed_) { + python_console_pressed_ = false; + if (y > bottom) { + // If we're not getting fed keyboard events and have a string editor + // available, invoke it. + if (!g_base->ui->UIHasDirectKeyboardInput() + && g_base->platform->HaveStringEditor()) { + InvokeStringEditor_(); + } + } + } +} + +void DevConsole::InvokeStringEditor_() { + // If there's already a valid edit-adapter attached to us, do nothing. + if (string_edit_adapter_.Exists() + && !g_base->python->CanPyStringEditAdapterBeReplaced( + string_edit_adapter_.Get())) { + return; + } + + // Create a Python StringEditAdapter for this widget, passing ourself as + // the sole arg. + auto result = g_base->python->objs() + .Get(BasePython::ObjID::kDevConsoleStringEditAdapterClass) + .Call(); + if (!result.Exists()) { + Log(LogLevel::kError, "Error invoking string edit dialog."); + return; + } + + // If this new one is already marked replacable, it means it wasn't able + // to register as the active one, so we can ignore it. + if (g_base->python->CanPyStringEditAdapterBeReplaced(result.Get())) { + return; + } + + // Ok looks like we're good; store the adapter as our active one. + string_edit_adapter_ = result; + + g_base->platform->InvokeStringEditor(string_edit_adapter_.Get()); +} + +void DevConsole::set_input_string(const std::string& val) { + assert(g_base->InLogicThread()); + input_string_ = val; + input_text_dirty_ = true; +} + +void DevConsole::InputAdapterFinish() { + assert(g_base->InLogicThread()); + string_edit_adapter_.Release(); +} + auto DevConsole::HandleKeyPress(const SDL_Keysym* keysym) -> bool { assert(g_base->InLogicThread()); @@ -73,7 +294,7 @@ auto DevConsole::HandleKeyPress(const SDL_Keysym* keysym) -> bool { if (!g_buildconfig.demo_build() && !g_buildconfig.arcade_build()) { // (reset input so characters don't continue walking and stuff) g_base->input->ResetHoldStates(); - if (auto console = g_base->console()) { + if (auto console = g_base->ui->dev_console()) { console->ToggleState(); } } @@ -83,7 +304,7 @@ auto DevConsole::HandleKeyPress(const SDL_Keysym* keysym) -> bool { break; } - if (state_ == State::kInactive) { + if (state_ == State_::kInactive) { return false; } @@ -129,23 +350,7 @@ auto DevConsole::HandleKeyPress(const SDL_Keysym* keysym) -> bool { } case SDLK_KP_ENTER: case SDLK_RETURN: { - if (!input_enabled_) { - Log(LogLevel::kWarning, "Console input is not allowed yet."); - break; - } - input_history_position_ = 0; - if (input_string_ == "clear") { - last_line_.clear(); - lines_.clear(); - } else { - SubmitCommand_(input_string_); - } - input_history_.push_front(input_string_); - if (input_history_.size() > 100) { - input_history_.pop_back(); - } - input_string_.resize(0); - input_text_dirty_ = true; + Exec(); break; } default: { @@ -168,9 +373,30 @@ auto DevConsole::HandleKeyPress(const SDL_Keysym* keysym) -> bool { return true; } +void DevConsole::Exec() { + BA_PRECONDITION(g_base->InLogicThread()); + if (!input_enabled_) { + Log(LogLevel::kWarning, "Console input is not allowed yet."); + return; + } + input_history_position_ = 0; + if (input_string_ == "clear") { + last_line_.clear(); + lines_.clear(); + } else { + SubmitCommand_(input_string_); + } + input_history_.push_front(input_string_); + if (input_history_.size() > 100) { + input_history_.pop_back(); + } + input_string_.resize(0); + input_text_dirty_ = true; +} + void DevConsole::SubmitCommand_(const std::string& command) { assert(g_base); - g_base->logic->event_loop()->PushCall([command] { + g_base->logic->event_loop()->PushCall([command, this] { // These are always run in whichever context is 'visible'. ScopedSetContext ssc(g_base->app_mode()->GetForegroundContext()); PythonCommand cmd(command, ""); @@ -180,7 +406,7 @@ void DevConsole::SubmitCommand_(const std::string& command) { if (cmd.CanEval()) { auto obj = cmd.Eval(true, nullptr, nullptr); if (obj.Exists() && obj.Get() != Py_None) { - g_base->console()->Print(obj.Repr() + "\n"); + Print(obj.Repr() + "\n"); } } else { // Not eval-able; just exec it. @@ -196,36 +422,36 @@ void DevConsole::EnableInput() { void DevConsole::Dismiss() { assert(g_base->InLogicThread()); - if (state_ == State::kInactive) { + if (state_ == State_::kInactive) { return; } state_prev_ = state_; - state_ = State::kInactive; - transition_start_ = g_core->GetAppTimeMillisecs(); + state_ = State_::kInactive; + transition_start_ = g_base->logic->display_time(); } void DevConsole::ToggleState() { assert(g_base->InLogicThread()); state_prev_ = state_; switch (state_) { - case State::kInactive: - state_ = State::kMini; + case State_::kInactive: + state_ = State_::kMini; break; - case State::kMini: - state_ = State::kFull; + case State_::kMini: + state_ = State_::kFull; break; - case State::kFull: - state_ = State::kInactive; + case State_::kFull: + state_ = State_::kInactive; break; } g_base->audio->PlaySound(g_base->assets->SysSound(SysSoundID::kBlip)); - transition_start_ = g_core->GetAppTimeMillisecs(); + transition_start_ = g_base->logic->display_time(); } auto DevConsole::HandleTextEditing(const std::string& text) -> bool { assert(g_base->InLogicThread()); - if (state_ == State::kInactive) { + if (state_ == State_::kInactive) { return false; } @@ -246,7 +472,7 @@ auto DevConsole::HandleKeyRelease(const SDL_Keysym* keysym) -> bool { } // Otherwise absorb *all* key-ups when we're active. - return state_ != State::kInactive; + return state_ != State_::kInactive; } void DevConsole::Print(const std::string& s_in) { @@ -259,7 +485,7 @@ void DevConsole::Print(const std::string& s_in) { // Spit out all completed lines and keep the last one as lastline. for (size_t i = 0; i < broken_up.size() - 1; i++) { - lines_.emplace_back(broken_up[i], g_core->GetAppTimeMillisecs()); + lines_.emplace_back(broken_up[i], g_base->logic->display_time()); if (lines_.size() > kDevConsoleLineLimit) { lines_.pop_front(); } @@ -268,191 +494,209 @@ void DevConsole::Print(const std::string& s_in) { last_line_mesh_dirty_ = true; } -void DevConsole::Draw(RenderPass* pass) { - millisecs_t transition_ticks = 100; - float bs = PythonConsoleBaseScale(); - if ((transition_start_ != 0) - && (state_ != State::kInactive - || ((g_core->GetAppTimeMillisecs() - transition_start_) - < transition_ticks))) { - float ratio = - (static_cast(g_core->GetAppTimeMillisecs() - transition_start_) - / static_cast(transition_ticks)); - float bottom; - float mini_size = 90.0f * bs; - if (state_ == State::kMini) { - bottom = pass->virtual_height() - mini_size; - } else { - bottom = - pass->virtual_height() - pass->virtual_height() * kDevConsoleSize; - } - if (g_core->GetAppTimeMillisecs() - transition_start_ < transition_ticks) { - float from_height; - if (state_prev_ == State::kMini) { - from_height = pass->virtual_height() - mini_size; - } else if (state_prev_ == State::kFull) { - from_height = - pass->virtual_height() - pass->virtual_height() * kDevConsoleSize; - } else { - from_height = pass->virtual_height(); - } - float to_height; - if (state_ == State::kMini) { - to_height = pass->virtual_height() - mini_size; - } else if (state_ == State::kFull) { - to_height = - pass->virtual_height() - pass->virtual_height() * kDevConsoleSize; - } else { - to_height = pass->virtual_height(); - } - bottom = to_height * ratio + from_height * (1.0 - ratio); - } - { - bg_mesh_.SetPositionAndSize(0, bottom, kDevConsoleZDepth, - pass->virtual_width(), - (pass->virtual_height() - bottom)); - stripe_mesh_.SetPositionAndSize(0, bottom + 15.0f * bs, kDevConsoleZDepth, - pass->virtual_width(), 15.0f * bs); - shadow_mesh_.SetPositionAndSize(0, bottom - 7.0f * bs, kDevConsoleZDepth, - pass->virtual_width(), 7.0f * bs); - SimpleComponent c(pass); - c.SetTransparent(true); - c.SetColor(0, 0, 0.1f, 0.9f); - c.DrawMesh(&bg_mesh_); - c.Submit(); - c.SetColor(1.0f, 1.0f, 1.0f, 0.1f); - c.DrawMesh(&stripe_mesh_); - c.Submit(); - c.SetColor(0, 0, 0, 0.1f); - c.DrawMesh(&shadow_mesh_); - c.Submit(); - } - if (input_text_dirty_) { - input_text_group_.set_text(input_string_); - input_text_dirty_ = false; - last_input_text_change_time_ = pass->frame_def()->real_time(); - } - { - SimpleComponent c(pass); - c.SetTransparent(true); - c.SetColor(0.5f, 0.5f, 0.7f, 1.0f); - int elem_count = built_text_group_.GetElementCount(); - for (int e = 0; e < elem_count; e++) { - c.SetTexture(built_text_group_.GetElementTexture(e)); - { - auto xf = c.ScopedTransform(); - c.Translate(pass->virtual_width() - 115.0f * bs, bottom + 4.0f, - kDevConsoleZDepth); - c.Scale(0.35f * bs, 0.35f * bs, 1.0f); - c.DrawMesh(built_text_group_.GetElementMesh(e)); - } - } - elem_count = title_text_group_.GetElementCount(); - for (int e = 0; e < elem_count; e++) { - c.SetTexture(title_text_group_.GetElementTexture(e)); - { - auto xf = c.ScopedTransform(); - c.Translate(10.0f * bs, bottom + 4.0f, kDevConsoleZDepth); - c.Scale(0.35f * bs, 0.35f * bs, 1.0f); - c.DrawMesh(title_text_group_.GetElementMesh(e)); - } - } - elem_count = prompt_text_group_.GetElementCount(); - for (int e = 0; e < elem_count; e++) { - c.SetTexture(prompt_text_group_.GetElementTexture(e)); - c.SetColor(1, 1, 1, 1); - { - auto xf = c.ScopedTransform(); - c.Translate(5.0f * bs, bottom + 14.5f * bs, kDevConsoleZDepth); - c.Scale(0.5f * bs, 0.5f * bs, 1.0f); - c.DrawMesh(prompt_text_group_.GetElementMesh(e)); - } - } - elem_count = input_text_group_.GetElementCount(); - for (int e = 0; e < elem_count; e++) { - c.SetTexture(input_text_group_.GetElementTexture(e)); - { - auto xf = c.ScopedTransform(); - c.Translate(15.0f * bs, bottom + 14.5f * bs, kDevConsoleZDepth); - c.Scale(0.5f * bs, 0.5f * bs, 1.0f); - c.DrawMesh(input_text_group_.GetElementMesh(e)); - } - } - c.Submit(); - } +auto DevConsole::Bottom_() const -> float { + float bs = PythonConsoleBaseScale_(); + float vw = g_base->graphics->screen_virtual_width(); + float vh = g_base->graphics->screen_virtual_height(); - // Carat. - millisecs_t real_time = pass->frame_def()->real_time(); - if (real_time % 200 < 100 - || (real_time - last_input_text_change_time_ < 100)) { - SimpleComponent c(pass); - c.SetTransparent(true); - c.SetColor(1, 1, 1, 0.7f); + float ratio = + (g_base->logic->display_time() - transition_start_) / kTransitionSeconds; + float bottom; + float mini_size = 90.0f * bs; + if (state_ == State_::kMini) { + bottom = vh - mini_size; + } else { + bottom = vh - vh * kDevConsoleSize; + } + if (g_base->logic->display_time() - transition_start_ < kTransitionSeconds) { + float from_height; + if (state_prev_ == State_::kMini) { + from_height = vh - mini_size; + } else if (state_prev_ == State_::kFull) { + from_height = vh - vh * kDevConsoleSize; + } else { + from_height = vh; + } + float to_height; + if (state_ == State_::kMini) { + to_height = vh - mini_size; + } else if (state_ == State_::kFull) { + to_height = vh - vh * kDevConsoleSize; + } else { + to_height = vh; + } + bottom = to_height * ratio + from_height * (1.0 - ratio); + } + return bottom; +} + +void DevConsole::Draw(RenderPass* pass) { + float bs = PythonConsoleBaseScale_(); + + // If we're not yet transitioning in for the first time OR have + // completed transitioning out, do nothing. + if (transition_start_ <= 0.0 + || state_ == State_::kInactive + && ((g_base->logic->display_time() - transition_start_) + >= kTransitionSeconds)) { + return; + } + + float bottom = Bottom_(); + { + bg_mesh_.SetPositionAndSize(0, bottom, kDevConsoleZDepth, + pass->virtual_width(), + (pass->virtual_height() - bottom)); + stripe_mesh_.SetPositionAndSize(0, bottom + 15.0f * bs, kDevConsoleZDepth, + pass->virtual_width(), 15.0f * bs); + shadow_mesh_.SetPositionAndSize(0, bottom - 7.0f * bs, kDevConsoleZDepth, + pass->virtual_width(), 7.0f * bs); + SimpleComponent c(pass); + c.SetTransparent(true); + c.SetColor(0, 0, 0.1f, 0.9f); + c.DrawMesh(&bg_mesh_); + c.Submit(); + c.SetColor(1.0f, 1.0f, 1.0f, 0.1f); + c.DrawMesh(&stripe_mesh_); + c.Submit(); + c.SetColor(0, 0, 0, 0.1f); + c.DrawMesh(&shadow_mesh_); + c.Submit(); + } + if (input_text_dirty_) { + input_text_group_.set_text(input_string_); + input_text_dirty_ = false; + last_input_text_change_time_ = pass->frame_def()->real_time(); + } + { + SimpleComponent c(pass); + c.SetFlatness(1.0f); + c.SetTransparent(true); + c.SetColor(0.5f, 0.5f, 0.7f, 0.8f); + int elem_count = built_text_group_.GetElementCount(); + for (int e = 0; e < elem_count; e++) { + c.SetTexture(built_text_group_.GetElementTexture(e)); { auto xf = c.ScopedTransform(); - c.Translate( - (19.0f - + g_base->text_graphics->GetStringWidth(input_string_) * 0.5f) - * bs, - bottom + 22.5f * bs, kDevConsoleZDepth); - c.Scale(6.0f * bs, 12.0f * bs, 1.0f); - c.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kImage1x1)); + c.Translate(pass->virtual_width() - 115.0f * bs, bottom + 4.0f, + kDevConsoleZDepth); + c.Scale(0.35f * bs, 0.35f * bs, 1.0f); + c.DrawMesh(built_text_group_.GetElementMesh(e)); } - c.Submit(); } - - // Draw output lines. - { - float draw_scale = 0.6f; - float v_inc = 18.0f; - SimpleComponent c(pass); - c.SetTransparent(true); + elem_count = title_text_group_.GetElementCount(); + for (int e = 0; e < elem_count; e++) { + c.SetTexture(title_text_group_.GetElementTexture(e)); + { + auto xf = c.ScopedTransform(); + c.Translate(10.0f * bs, bottom + 4.0f, kDevConsoleZDepth); + c.Scale(0.35f * bs, 0.35f * bs, 1.0f); + c.DrawMesh(title_text_group_.GetElementMesh(e)); + } + } + elem_count = prompt_text_group_.GetElementCount(); + for (int e = 0; e < elem_count; e++) { + c.SetTexture(prompt_text_group_.GetElementTexture(e)); c.SetColor(1, 1, 1, 1); - float h = 0.5f - * (g_base->graphics->screen_virtual_width() - - (kDevConsoleStringBreakUpSize * draw_scale)); - float v = bottom + 32.0f * bs; - if (!last_line_.empty()) { - if (last_line_mesh_dirty_) { - if (!last_line_mesh_group_.Exists()) { - last_line_mesh_group_ = Object::New(); - } - last_line_mesh_group_->set_text(last_line_); - last_line_mesh_dirty_ = false; - } - int elem_count = last_line_mesh_group_->GetElementCount(); - for (int e = 0; e < elem_count; e++) { - c.SetTexture(last_line_mesh_group_->GetElementTexture(e)); - { - auto xf = c.ScopedTransform(); - c.Translate(h, v + 2, kDevConsoleZDepth); - c.Scale(draw_scale, draw_scale); - c.DrawMesh(last_line_mesh_group_->GetElementMesh(e)); - } - } - v += v_inc; + { + auto xf = c.ScopedTransform(); + c.Translate(5.0f * bs, bottom + 14.5f * bs, kDevConsoleZDepth); + c.Scale(0.5f * bs, 0.5f * bs, 1.0f); + c.DrawMesh(prompt_text_group_.GetElementMesh(e)); } - for (auto i = lines_.rbegin(); i != lines_.rend(); i++) { - int elem_count = i->GetText().GetElementCount(); - for (int e = 0; e < elem_count; e++) { - c.SetTexture(i->GetText().GetElementTexture(e)); - { - auto xf = c.ScopedTransform(); - c.Translate(h, v + 2, kDevConsoleZDepth); - c.Scale(draw_scale, draw_scale); - c.DrawMesh(i->GetText().GetElementMesh(e)); - } + } + elem_count = input_text_group_.GetElementCount(); + for (int e = 0; e < elem_count; e++) { + c.SetTexture(input_text_group_.GetElementTexture(e)); + { + auto xf = c.ScopedTransform(); + c.Translate(15.0f * bs, bottom + 14.5f * bs, kDevConsoleZDepth); + c.Scale(0.5f * bs, 0.5f * bs, 1.0f); + c.DrawMesh(input_text_group_.GetElementMesh(e)); + } + } + c.Submit(); + } + + // Carat. + millisecs_t real_time = pass->frame_def()->real_time(); + if (real_time % 200 < 100 + || (real_time - last_input_text_change_time_ < 100)) { + SimpleComponent c(pass); + c.SetTransparent(true); + c.SetColor(1, 1, 1, 0.7f); + { + auto xf = c.ScopedTransform(); + c.Translate( + (19.0f + g_base->text_graphics->GetStringWidth(input_string_) * 0.5f) + * bs, + bottom + 22.5f * bs, kDevConsoleZDepth); + c.Scale(6.0f * bs, 12.0f * bs, 1.0f); + c.DrawMeshAsset(g_base->assets->SysMesh(SysMeshID::kImage1x1)); + } + c.Submit(); + } + + // Draw output lines. + { + float draw_scale = 0.6f; + float v_inc = 18.0f; + SimpleComponent c(pass); + c.SetTransparent(true); + c.SetColor(1, 1, 1, 1); + c.SetFlatness(1.0f); + float h = 0.5f + * (g_base->graphics->screen_virtual_width() + - (kDevConsoleStringBreakUpSize * draw_scale)); + float v = bottom + 32.0f * bs; + if (!last_line_.empty()) { + if (last_line_mesh_dirty_) { + if (!last_line_mesh_group_.Exists()) { + last_line_mesh_group_ = Object::New(); } - v += v_inc; - if (v > pass->virtual_height() + 14) { - break; + last_line_mesh_group_->set_text(last_line_); + last_line_mesh_dirty_ = false; + } + int elem_count = last_line_mesh_group_->GetElementCount(); + for (int e = 0; e < elem_count; e++) { + c.SetTexture(last_line_mesh_group_->GetElementTexture(e)); + { + auto xf = c.ScopedTransform(); + c.Translate(h, v + 2, kDevConsoleZDepth); + c.Scale(draw_scale, draw_scale); + c.DrawMesh(last_line_mesh_group_->GetElementMesh(e)); } } - c.Submit(); + v += v_inc; + } + for (auto i = lines_.rbegin(); i != lines_.rend(); i++) { + int elem_count = i->GetText().GetElementCount(); + for (int e = 0; e < elem_count; e++) { + c.SetTexture(i->GetText().GetElementTexture(e)); + { + auto xf = c.ScopedTransform(); + c.Translate(h, v + 2, kDevConsoleZDepth); + c.Scale(draw_scale, draw_scale); + c.DrawMesh(i->GetText().GetElementMesh(e)); + } + } + v += v_inc; + if (v > pass->virtual_height() + v_inc) { + break; + } + } + c.Submit(); + } + + // Buttons. + { + for (auto&& button : buttons_) { + button.Draw(pass, bottom); } } } -auto DevConsole::PythonConsoleBaseScale() -> float { + +auto DevConsole::PythonConsoleBaseScale_() const -> float { switch (g_base->ui->scale()) { case UIScale::kLarge: return 1.5f; diff --git a/src/ballistica/base/ui/dev_console.h b/src/ballistica/base/ui/dev_console.h index 19617679..a017551e 100644 --- a/src/ballistica/base/ui/dev_console.h +++ b/src/ballistica/base/ui/dev_console.h @@ -9,6 +9,7 @@ #include "ballistica/base/graphics/renderer/renderer.h" #include "ballistica/shared/foundation/object.h" +#include "ballistica/shared/python/python_ref.h" namespace ballistica::base { @@ -16,7 +17,7 @@ class DevConsole { public: DevConsole(); ~DevConsole(); - auto IsActive() const -> bool { return (state_ != State::kInactive); } + auto IsActive() const -> bool { return (state_ != State_::kInactive); } auto HandleTextEditing(const std::string& text) -> bool; auto HandleKeyPress(const SDL_Keysym* keysym) -> bool; auto HandleKeyRelease(const SDL_Keysym* keysym) -> bool; @@ -35,12 +36,26 @@ class DevConsole { /// Called when the console should start accepting Python command input. void EnableInput(); - private: - class Line_; + auto input_string() const { + assert(g_base->InLogicThread()); + return input_string_; + } + void set_input_string(const std::string& val); - auto PythonConsoleBaseScale() -> float; + void InputAdapterFinish(); + + auto HandleMouseDown(int button, float x, float y) -> bool; + void HandleMouseUp(int button, float x, float y); + void Exec(); + + private: + class Button_; + class Line_; + enum class State_ { kInactive, kMini, kFull }; + auto Bottom_() const -> float; + auto PythonConsoleBaseScale_() const -> float; void SubmitCommand_(const std::string& command); - enum class State { kInactive, kMini, kFull }; + void InvokeStringEditor_(); ImageMesh bg_mesh_; ImageMesh stripe_mesh_; ImageMesh shadow_mesh_; @@ -50,9 +65,9 @@ class DevConsole { TextGroup input_text_group_; millisecs_t last_input_text_change_time_{}; bool input_text_dirty_{true}; - millisecs_t transition_start_{}; - State state_{State::kInactive}; - State state_prev_{State::kInactive}; + double transition_start_{}; + State_ state_{State_::kInactive}; + State_ state_prev_{State_::kInactive}; bool input_enabled_{}; std::string input_string_; std::list input_history_; @@ -61,6 +76,9 @@ class DevConsole { std::string last_line_; Object::Ref last_line_mesh_group_; bool last_line_mesh_dirty_{true}; + bool python_console_pressed_{}; + PythonRef string_edit_adapter_; + std::list buttons_; }; } // namespace ballistica::base diff --git a/src/ballistica/base/ui/ui.cc b/src/ballistica/base/ui/ui.cc index fc17e74e..0eafba90 100644 --- a/src/ballistica/base/ui/ui.cc +++ b/src/ballistica/base/ui/ui.cc @@ -125,14 +125,23 @@ auto UI::HandleMouseDown(int button, float x, float y, bool double_click) -> bool { bool handled{}; - if (show_dev_console_button_ && button == 1) { + // Dev console button. + if (show_dev_console_button_) { float vx = g_base->graphics->screen_virtual_width(); float vy = g_base->graphics->screen_virtual_height(); if (InDevConsoleButton_(x, y)) { - dev_console_button_pressed_ = true; + if (button == 1) { + dev_console_button_pressed_ = true; + } + handled = true; } } + // Dev console itself. + if (!handled && dev_console_ && dev_console_->IsActive()) { + handled = dev_console_->HandleMouseDown(button, x, y); + } + if (!handled && g_base->HaveUIV1()) { handled = g_base->ui_v1()->HandleLegacyRootUIMouseDown(x, y); } @@ -151,10 +160,13 @@ void UI::HandleMouseUp(int button, float x, float y) { SendWidgetMessage( WidgetMessage(WidgetMessage::Type::kMouseUp, nullptr, x, y)); - if (dev_console_button_pressed_) { + if (dev_console_) { + dev_console_->HandleMouseUp(button, x, y); + } + if (dev_console_button_pressed_ && button == 1) { if (InDevConsoleButton_(x, y)) { - if (auto* console = g_base->console()) { - console->ToggleState(); + if (dev_console_) { + dev_console_->ToggleState(); } } dev_console_button_pressed_ = false; @@ -165,6 +177,26 @@ void UI::HandleMouseUp(int button, float x, float y) { } } +auto UI::UIHasDirectKeyboardInput() const -> bool { + // Currently limiting this to desktop operating systems, but should + // generalize this, as situations such as tablets with hardware keyboards + // should behave similarly to desktop PCs. Though perhaps we should make + // this optional everywhere (or language dependent) since direct keyboard + // input might not work well for some languages even on desktops. + if (g_buildconfig.ostype_macos() || g_buildconfig.ostype_windows() + || g_buildconfig.ostype_linux()) { + // Return true if we've got a keyboard attached and either it or nothing + // is active. + auto* ui_input_device = g_base->ui->GetUIInputDevice(); + if (auto* keyboard = g_base->ui->GetUIInputDevice()) { + if (ui_input_device == keyboard || ui_input_device == nullptr) { + return true; + } + } + } + return false; +} + void UI::HandleMouseMotion(float x, float y) { SendWidgetMessage( WidgetMessage(WidgetMessage::Type::kMouseMove, nullptr, x, y)); @@ -353,8 +385,8 @@ void UI::Draw(FrameDef* frame_def) { void UI::DrawDev(FrameDef* frame_def) { // Draw dev console. - if (g_base->console()) { - g_base->console()->Draw(frame_def->overlay_pass()); + if (dev_console_) { + dev_console_->Draw(frame_def->overlay_pass()); } // Draw dev console button. @@ -440,10 +472,10 @@ void UI::ShowURL(const std::string& url) { } void UI::ConfirmQuit() { - g_base->logic->event_loop()->PushCall([] { + g_base->logic->event_loop()->PushCall([this] { // If the in-app console is active, dismiss it. - if (g_base->console() != nullptr && g_base->console()->IsActive()) { - g_base->console()->Dismiss(); + if (dev_console_ != nullptr && dev_console_->IsActive()) { + dev_console_->Dismiss(); } assert(g_base->InLogicThread()); @@ -467,4 +499,37 @@ void UI::ConfirmQuit() { }); } +void UI::PushDevConsolePrintCall(const std::string& msg) { + // Completely ignore this stuff in headless mode. + if (g_core->HeadlessMode()) { + return; + } + // If our event loop AND console are up and running, ship it off to + // be printed. Otherwise store it for the console to grab when it's ready. + if (auto* event_loop = g_base->logic->event_loop()) { + if (dev_console_ != nullptr) { + event_loop->PushCall([this, msg] { dev_console_->Print(msg); }); + return; + } + } + // Didn't send a print; store for later. + dev_console_startup_messages_ += msg; +} + +void UI::OnAssetsAvailable() { + assert(g_base->InLogicThread()); + + // Spin up the dev console. + if (!g_core->HeadlessMode()) { + assert(dev_console_ == nullptr); + dev_console_ = new DevConsole(); + + // Print any messages that have built up. + if (!dev_console_startup_messages_.empty()) { + dev_console_->Print(dev_console_startup_messages_); + dev_console_startup_messages_.clear(); + } + } +} + } // namespace ballistica::base diff --git a/src/ballistica/base/ui/ui.h b/src/ballistica/base/ui/ui.h index ca3d6631..94c7eb30 100644 --- a/src/ballistica/base/ui/ui.h +++ b/src/ballistica/base/ui/ui.h @@ -32,6 +32,8 @@ class UI { void OnScreenSizeChange(); void StepDisplayTime(); + void OnAssetsAvailable(); + void LanguageChanged(); /// Reset all UI to a default state. Generally should be called when @@ -79,6 +81,13 @@ class UI { /// Return the input-device that currently owns the UI; otherwise nullptr. auto GetUIInputDevice() const -> InputDevice*; + /// Return true if there is a full desktop-style hardware keyboard + /// attached and the active UI InputDevice is set to it or not set. This + /// also may take language or user preferences into account. Editable text + /// elements can use this to opt in to accepting key events directly + /// instead of popping up a string edit dialog. + auto UIHasDirectKeyboardInput() const -> bool; + /// Schedule a back button press. Can be called from any thread. void PushBackButtonCall(InputDevice* input_device); @@ -98,11 +107,18 @@ class UI { /// device (nullptr to specify none). Can be called from any thread. void PushMainMenuPressCall(InputDevice* device); + auto* dev_console() const { return dev_console_; } + + void PushDevConsolePrintCall(const std::string& msg); + private: void MainMenuPress_(InputDevice* device); auto DevConsoleButtonSize_() const -> float; auto InDevConsoleButton_(float x, float y) const -> bool; void DrawDevConsoleButton_(FrameDef* frame_def); + + DevConsole* dev_console_{}; + std::string dev_console_startup_messages_; Object::WeakRef ui_input_device_; millisecs_t last_input_device_use_time_{}; millisecs_t last_widget_input_reject_err_sound_time_{}; diff --git a/src/ballistica/core/platform/core_platform.cc b/src/ballistica/core/platform/core_platform.cc index 8bdcf9d9..3468fc75 100644 --- a/src/ballistica/core/platform/core_platform.cc +++ b/src/ballistica/core/platform/core_platform.cc @@ -667,11 +667,6 @@ auto CorePlatform::HaveLeaderboard(const std::string& game, return false; } -void CorePlatform::EditText(const std::string& title, const std::string& value, - int max_chars) { - Log(LogLevel::kError, "FIXME: EditText() unimplemented"); -} - void CorePlatform::ShowOnlineScoreUI(const std::string& show, const std::string& game, const std::string& game_version) { diff --git a/src/ballistica/core/platform/core_platform.h b/src/ballistica/core/platform/core_platform.h index e476ca28..94b9b669 100644 --- a/src/ballistica/core/platform/core_platform.h +++ b/src/ballistica/core/platform/core_platform.h @@ -403,10 +403,6 @@ class CorePlatform { static void SleepMillisecs(millisecs_t ms); - /// Pop up a text edit dialog. - virtual void EditText(const std::string& title, const std::string& value, - int max_chars); - /// Given a C++ symbol, attempt to return a pretty one. virtual auto DemangleCXXSymbol(const std::string& s) -> std::string; diff --git a/src/ballistica/core/support/base_soft.h b/src/ballistica/core/support/base_soft.h index 22852448..85b6cf23 100644 --- a/src/ballistica/core/support/base_soft.h +++ b/src/ballistica/core/support/base_soft.h @@ -33,7 +33,7 @@ class BaseSoftInterface { virtual auto FeatureSetFromData(PyObject* obj) -> FeatureSetNativeComponent* = 0; virtual void DoV1CloudLog(const std::string& msg) = 0; - virtual void PushConsolePrintCall(const std::string& msg) = 0; + virtual void PushDevConsolePrintCall(const std::string& msg) = 0; virtual auto GetPyExceptionType(PyExcType exctype) -> PyObject* = 0; virtual auto PrintPythonStackTrace() -> bool = 0; virtual auto GetPyLString(PyObject* obj) -> std::string = 0; diff --git a/src/ballistica/shared/ballistica.cc b/src/ballistica/shared/ballistica.cc index eeab6728..1033b62f 100644 --- a/src/ballistica/shared/ballistica.cc +++ b/src/ballistica/shared/ballistica.cc @@ -39,7 +39,7 @@ auto main(int argc, char** argv) -> int { namespace ballistica { // These are set automatically via script; don't modify them here. -const int kEngineBuildNumber = 21329; +const int kEngineBuildNumber = 21337; const char* kEngineVersion = "1.7.28"; const int kEngineApiVersion = 8; diff --git a/src/ballistica/shared/foundation/logging.cc b/src/ballistica/shared/foundation/logging.cc index 83cdfcc2..e8fc9f33 100644 --- a/src/ballistica/shared/foundation/logging.cc +++ b/src/ballistica/shared/foundation/logging.cc @@ -24,7 +24,7 @@ void Logging::DisplayLog(const std::string& name, LogLevel level, const std::string& msg) { // Print to the in-app console (with a newline added). if (g_base_soft) { - g_base_soft->PushConsolePrintCall(msg + "\n"); + g_base_soft->PushDevConsolePrintCall(msg + "\n"); } // Ship to platform-specific display mechanisms (android log, etc). diff --git a/src/ballistica/shared/python/python_ref.cc b/src/ballistica/shared/python/python_ref.cc index 91d4b70a..133ec6d5 100644 --- a/src/ballistica/shared/python/python_ref.cc +++ b/src/ballistica/shared/python/python_ref.cc @@ -162,6 +162,15 @@ auto PythonRef::ValueAsString() const -> std::string { return Python::GetPyString(obj_); } +auto PythonRef::ValueAsOptionalInt() const -> std::optional { + assert(Python::HaveGIL()); + ThrowIfUnset(); + if (obj_ == Py_None) { + return {}; + } + return Python::GetPyInt(obj_); +} + void PythonRef::ThrowIfUnset() const { if (!obj_) { throw Exception("PythonRef is unset.", PyExcType::kValue); diff --git a/src/ballistica/shared/python/python_ref.h b/src/ballistica/shared/python/python_ref.h index 9c5cc0e0..e0628fda 100644 --- a/src/ballistica/shared/python/python_ref.h +++ b/src/ballistica/shared/python/python_ref.h @@ -171,6 +171,7 @@ class PythonRef { -> std::optional>; auto ValueAsInt() const -> int64_t; + auto ValueAsOptionalInt() const -> std::optional; /// Returns whether the underlying PyObject is callable. /// Throws an exception if unset. diff --git a/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc b/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc index 50542ee6..dfebded5 100644 --- a/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc +++ b/src/ballistica/ui_v1/python/methods/python_methods_ui_v1.cc @@ -1869,6 +1869,9 @@ static auto PyTextWidget(PyObject* self, PyObject* args, PyObject* keywds) PyObject* always_show_carat_obj = Py_None; PyObject* extra_touch_border_scale_obj = Py_None; PyObject* res_scale_obj = Py_None; + PyObject* query_max_chars_obj = Py_None; + PyObject* query_description_obj = Py_None; + PyObject* adapter_finished_obj = Py_None; static const char* kwlist[] = {"edit", "parent", @@ -1905,9 +1908,12 @@ static auto PyTextWidget(PyObject* self, PyObject* args, PyObject* keywds) "big", "extra_touch_border_scale", "res_scale", + "query_max_chars", + "query_description", + "adapter_finished", nullptr}; if (!PyArg_ParseTupleAndKeywords( - args, keywds, "|OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO", + args, keywds, "|OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO", const_cast(kwlist), &edit_obj, &parent_obj, &size_obj, &pos_obj, &text_obj, &v_align_obj, &h_align_obj, &editable_obj, &padding_obj, &on_return_press_call_obj, &on_activate_call_obj, @@ -1917,7 +1923,8 @@ static auto PyTextWidget(PyObject* self, PyObject* args, PyObject* keywds) &transition_delay_obj, &maxwidth_obj, &max_height_obj, &flatness_obj, &shadow_obj, &autoselect_obj, &rotate_obj, &enabled_obj, &force_internal_editing_obj, &always_show_carat_obj, &big_obj, - &extra_touch_border_scale_obj, &res_scale_obj)) + &extra_touch_border_scale_obj, &res_scale_obj, &query_max_chars_obj, + &query_description_obj, &adapter_finished_obj)) return nullptr; if (!g_base->CurrentContext().IsEmpty()) { @@ -1934,6 +1941,24 @@ static auto PyTextWidget(PyObject* self, PyObject* args, PyObject* keywds) } return PyUnicode_FromString(widget->text_raw().c_str()); } + if (query_max_chars_obj != Py_None) { + widget = + dynamic_cast(UIV1Python::GetPyWidget(query_max_chars_obj)); + if (!widget.Exists()) { + throw Exception("Invalid or nonexistent widget.", + PyExcType::kWidgetNotFound); + } + return PyLong_FromLong(widget->max_chars()); + } + if (query_description_obj != Py_None) { + widget = dynamic_cast( + UIV1Python::GetPyWidget(query_description_obj)); + if (!widget.Exists()) { + throw Exception("Invalid or nonexistent widget.", + PyExcType::kWidgetNotFound); + } + return PyUnicode_FromString(widget->description().c_str()); + } if (edit_obj != Py_None) { widget = dynamic_cast(UIV1Python::GetPyWidget(edit_obj)); if (!widget.Exists()) { @@ -2095,6 +2120,13 @@ static auto PyTextWidget(PyObject* self, PyObject* args, PyObject* keywds) if (res_scale_obj != Py_None) { widget->set_res_scale(Python::GetPyFloat(res_scale_obj)); } + if (adapter_finished_obj != Py_None) { + if (adapter_finished_obj == Py_True) { + widget->AdapterFinished(); + } else { + throw Exception("Unexpected value for adapter_finished"); + } + } // if making a new widget add it at the end if (edit_obj == Py_None) { @@ -2144,7 +2176,10 @@ static PyMethodDef PyTextWidgetDef = { " always_show_carat: bool | None = None,\n" " big: bool | None = None,\n" " extra_touch_border_scale: float | None = None,\n" - " res_scale: float | None = None)\n" + " res_scale: float | None = None," + " query_max_chars: bauiv1.Widget | None = None,\n" + " query_description: bauiv1.Widget | None = None,\n" + " adapter_finished: bool | None = None)\n" " -> bauiv1.Widget\n" "\n" "Create or edit a text widget.\n" @@ -2757,7 +2792,7 @@ static auto PyConsolePrint(PyObject* self, PyObject* args) -> PyObject* { throw Exception(); } const char* c = PyUnicode_AsUTF8(str_obj); - g_base->PushConsolePrintCall(c); + g_base->PushDevConsolePrintCall(c); Py_DECREF(str_obj); } } diff --git a/src/ballistica/ui_v1/python/ui_v1_python.cc b/src/ballistica/ui_v1/python/ui_v1_python.cc index 415124a8..0fb2343b 100644 --- a/src/ballistica/ui_v1/python/ui_v1_python.cc +++ b/src/ballistica/ui_v1/python/ui_v1_python.cc @@ -101,7 +101,22 @@ void UIV1Python::HandleDeviceMenuPress(base::InputDevice* device) { } } -void UIV1Python::LaunchStringEdit(TextWidget* w) { +void UIV1Python::InvokeStringEditor(PyObject* string_edit_adapter_instance) { + BA_PRECONDITION(g_base->InLogicThread()); + BA_PRECONDITION(string_edit_adapter_instance); + + base::ScopedSetContext ssc(nullptr); + g_base->audio->PlaySound(g_base->assets->SysSound(base::SysSoundID::kSwish)); + + // Schedule this in the next cycle to be safe. + PythonRef args(Py_BuildValue("(O)", string_edit_adapter_instance), + PythonRef::kSteal); + Object::New( + objs().Get(ObjID::kOnScreenKeyboardClass)) + ->Schedule(args); +} + +void UIV1Python::LaunchStringEditOld(TextWidget* w) { assert(g_base->InLogicThread()); BA_PRECONDITION(w); diff --git a/src/ballistica/ui_v1/python/ui_v1_python.h b/src/ballistica/ui_v1/python/ui_v1_python.h index a0ea33ff..4e1bff21 100644 --- a/src/ballistica/ui_v1/python/ui_v1_python.h +++ b/src/ballistica/ui_v1/python/ui_v1_python.h @@ -17,7 +17,8 @@ class UIV1Python { void AddPythonClasses(PyObject* module); void ImportPythonObjs(); - void LaunchStringEdit(TextWidget* w); + void LaunchStringEditOld(TextWidget* w); + void InvokeStringEditor(PyObject* string_edit_adapter_instance); void HandleDeviceMenuPress(base::InputDevice* device); void ShowURL(const std::string& url); @@ -37,6 +38,7 @@ class UIV1Python { kQuitWindowCall, kDeviceMenuPressCall, kShowURLWindowCall, + kTextWidgetStringEditAdapterClass, kLast // Sentinel; must be at end. }; diff --git a/src/ballistica/ui_v1/widget/text_widget.cc b/src/ballistica/ui_v1/widget/text_widget.cc index eec34327..bf87b833 100644 --- a/src/ballistica/ui_v1/widget/text_widget.cc +++ b/src/ballistica/ui_v1/widget/text_widget.cc @@ -10,12 +10,17 @@ #include "ballistica/base/input/device/keyboard_input.h" #include "ballistica/base/input/input.h" #include "ballistica/base/logic/logic.h" +#include "ballistica/base/platform/base_platform.h" +#include "ballistica/base/python/base_python.h" #include "ballistica/base/python/support/python_context_call.h" #include "ballistica/base/ui/ui.h" #include "ballistica/core/core.h" #include "ballistica/shared/foundation/event_loop.h" +#include "ballistica/shared/foundation/logging.h" +#include "ballistica/shared/foundation/types.h" #include "ballistica/shared/generic/utils.h" #include "ballistica/shared/python/python.h" +#include "ballistica/shared/python/python_sys.h" #include "ballistica/ui_v1/python/ui_v1_python.h" #include "ballistica/ui_v1/widget/container_widget.h" @@ -23,25 +28,16 @@ namespace ballistica::ui_v1 { const float kClearMargin{13.0f}; -// bool TextWidget::always_use_internal_keyboard_{false}; - -// FIXME: Move this to g_ui or something; not a global. -Object::WeakRef TextWidget::android_string_edit_widget_; -TextWidget* TextWidget::GetAndroidStringEditWidget() { - assert(g_base->InLogicThread()); - return android_string_edit_widget_.Get(); -} - TextWidget::TextWidget() { // We always show our clear button except for in android when we don't // have a touchscreen (android-tv type situations). + // // FIXME - should generalize this to any controller-only situation. if (g_buildconfig.ostype_android()) { if (g_base->input->touch_input() == nullptr) { do_clear_button_ = false; } } - birth_time_millisecs_ = static_cast(g_base->logic->display_time() * 1000.0); } @@ -114,8 +110,8 @@ void TextWidget::Draw(base::RenderPass* pass, bool draw_transparent) { // Center-scale. { - // We should really be scaling our bounds and things, - // but for now lets just do a hacky overall scale. + // We should really be scaling our bounds and things, but for now lets + // just do a hacky overall scale. base::EmptyComponent c(pass); c.SetTransparent(true); c.PushTransform(); @@ -296,7 +292,7 @@ void TextWidget::Draw(base::RenderPass* pass, bool draw_transparent) { } // Apply subs/resources to get our actual text if need be. - UpdateTranslation(); + UpdateTranslation_(); if (!text_group_.Exists()) { text_group_ = Object::New(); @@ -386,7 +382,7 @@ void TextWidget::Draw(base::RenderPass* pass, bool draw_transparent) { // Draw the carat. if (IsHierarchySelected() || always_show_carat_) { bool show_cursor = true; - if (ShouldUseStringEditDialog()) { + if (ShouldUseStringEditor_()) { show_cursor = false; } if (show_cursor @@ -520,70 +516,6 @@ auto TextWidget::GetHeight() -> float { return height_; } -auto TextWidget::ShouldUseStringEditDialog() const -> bool { - if (g_core->HeadlessMode()) { - // Shouldn't really get here, but if we do, keep things simple. - return false; - } - if (force_internal_editing_) { - // Obscure cases such as the text-widget *on* our built-in on-screen - // keyboard. - return false; - } - if (g_ui_v1->always_use_internal_on_screen_keyboard()) { - return true; - } - - // On most platforms we always want to use an edit dialog. On desktop, - // however, we use inline editing *if* the current UI input-device is the - // mouse or keyboard. For anything else, like game controllers, we bust - // out the dialog. - if (g_buildconfig.ostype_macos() || g_buildconfig.ostype_windows() - || g_buildconfig.ostype_linux()) { - auto* ui_input_device = g_base->ui->GetUIInputDevice(); - return !(ui_input_device == nullptr - || ui_input_device == g_base->input->keyboard_input()); - } else { - return true; - } -} - -void TextWidget::InvokeStringEditDialog() { - bool use_internal_dialog{true}; - - // In VR we always use our own dialog. - if (g_core->IsVRMode()) { - use_internal_dialog = true; - } else { - // on Android, use the Android keyboard *unless* the user want to use - // our built-in one. - if (!g_ui_v1->always_use_internal_on_screen_keyboard()) { - if (g_buildconfig.ostype_android()) { - use_internal_dialog = false; - // Store ourself as the current text-widget and kick off an edit. - android_string_edit_widget_ = this; - g_core->main_event_loop()->PushCall( - [name = description_, value = text_raw_, max_chars = max_chars_] { - static millisecs_t last_edit_time = 0; - millisecs_t t = g_core->GetAppTimeMillisecs(); - - // Ignore if too close together (in case second request comes - // in before first takes effect). - if (t - last_edit_time < 1000) { - return; - } - last_edit_time = t; - assert(g_core->InMainThread()); - g_core->platform->EditText(name, value, max_chars); - }); - } - } - } - if (use_internal_dialog) { - g_ui_v1->python->LaunchStringEdit(this); - } -} - void TextWidget::Activate() { last_activate_time_millisecs_ = static_cast(g_base->logic->display_time() * 1000.0); @@ -594,11 +526,82 @@ void TextWidget::Activate() { call->ScheduleWeak(); } - if (editable_ && ShouldUseStringEditDialog()) { - InvokeStringEditDialog(); + // Bring up an editor if applicable. + if (editable_ && ShouldUseStringEditor_()) { + InvokeStringEditor_(); } } +auto TextWidget::ShouldUseStringEditor_() const -> bool { + if (g_core->HeadlessMode()) { + BA_LOG_ONCE( + LogLevel::kError, + "ShouldUseStringEditDialog_ called in headless; should not happen."); + return false; + } + + // Obscure cases such as the text-widget *on* our built-in on-screen + // editor (obviously it should itself not pop up an editor). + if (force_internal_editing_) { + return false; + } + + // If the user wants to use our widget-based keyboard, always say yes + // here. + if (g_ui_v1->always_use_internal_on_screen_keyboard()) { + return true; + } + + // If we can take direct key events, no string-editor needed. + return !g_base->ui->UIHasDirectKeyboardInput(); +} + +void TextWidget::InvokeStringEditor_() { + assert(g_base->InLogicThread()); + + // If there's already a valid edit attached to us, do nothing. + if (string_edit_adapter_.Exists() + && !g_base->python->CanPyStringEditAdapterBeReplaced( + string_edit_adapter_.Get())) { + return; + } + + // Create a Python StringEditAdapter for this widget, passing ourself as + // the sole arg. + auto args = PythonRef::Stolen(Py_BuildValue("(O)", BorrowPyRef())); + auto result = g_ui_v1->python->objs() + .Get(UIV1Python::ObjID::kTextWidgetStringEditAdapterClass) + .Call(args); + if (!result.Exists()) { + Log(LogLevel::kError, "Error invoking string edit dialog."); + return; + } + + // If this new one is already marked replacable, it means it wasn't able + // to register as the active one, so we can ignore it. + if (g_base->python->CanPyStringEditAdapterBeReplaced(result.Get())) { + return; + } + + // Ok looks like we're good; store the adapter and hand it over + // to whoever will be driving it. + string_edit_adapter_ = result; + + // Use the platform string-editor if we have one unless the user + // explicitly wants us to use our own. + if (g_base->platform->HaveStringEditor() + && !g_ui_v1->always_use_internal_on_screen_keyboard()) { + g_base->platform->InvokeStringEditor(string_edit_adapter_.Get()); + } else { + g_ui_v1->python->InvokeStringEditor(string_edit_adapter_.Get()); + } +} + +void TextWidget::AdapterFinished() { + BA_PRECONDITION(g_base->InLogicThread()); + string_edit_adapter_.Release(); +} + auto TextWidget::HandleMessage(const base::WidgetMessage& m) -> bool { if (g_core->HeadlessMode()) { return false; @@ -619,17 +622,17 @@ auto TextWidget::HandleMessage(const base::WidgetMessage& m) -> bool { } // If we're doing inline editing, handle clipboard paste. - if (editable() && !ShouldUseStringEditDialog() + if (editable() && !ShouldUseStringEditor_() && m.type == base::WidgetMessage::Type::kPaste) { if (g_core->platform->ClipboardIsSupported()) { if (g_core->platform->ClipboardHasText()) { // Just enter it char by char as if we had typed it... - AddCharsToText(g_core->platform->ClipboardGetText()); + AddCharsToText_(g_core->platform->ClipboardGetText()); } } } // If we're doing inline editing, handle some key events. - if (m.has_keysym && !ShouldUseStringEditDialog()) { + if (m.has_keysym && !ShouldUseStringEditor_()) { last_carat_change_time_millisecs_ = static_cast(g_base->logic->display_time() * 1000.0); @@ -795,12 +798,12 @@ auto TextWidget::HandleMessage(const base::WidgetMessage& m) -> bool { case base::WidgetMessage::Type::kTextInput: { // If we're using an edit dialog, any attempted text input just kicks us // over to that. - if (editable() && ShouldUseStringEditDialog()) { - InvokeStringEditDialog(); + if (editable() && ShouldUseStringEditor_()) { + InvokeStringEditor_(); } else { // Otherwise apply the text directly. if (editable() && m.sval != nullptr) { - AddCharsToText(*m.sval); + AddCharsToText_(*m.sval); return true; } } @@ -810,8 +813,8 @@ auto TextWidget::HandleMessage(const base::WidgetMessage& m) -> bool { if (!IsSelectable()) { return false; } - float x{ScaleAdjustedX(m.fval1)}; - float y{ScaleAdjustedY(m.fval2)}; + float x{ScaleAdjustedX_(m.fval1)}; + float y{ScaleAdjustedY_(m.fval2)}; bool claimed = (m.fval3 > 0.0f); if (claimed) { mouse_over_ = clear_mouse_over_ = false; @@ -829,8 +832,8 @@ auto TextWidget::HandleMessage(const base::WidgetMessage& m) -> bool { if (!IsSelectable()) { return false; } - float x{ScaleAdjustedX(m.fval1)}; - float y{ScaleAdjustedY(m.fval2)}; + float x{ScaleAdjustedX_(m.fval1)}; + float y{ScaleAdjustedY_(m.fval2)}; auto click_count = static_cast(m.fval3); @@ -874,8 +877,8 @@ auto TextWidget::HandleMessage(const base::WidgetMessage& m) -> bool { } } case base::WidgetMessage::Type::kMouseUp: { - float x{ScaleAdjustedX(m.fval1)}; - float y{ScaleAdjustedY(m.fval2)}; + float x{ScaleAdjustedX_(m.fval1)}; + float y{ScaleAdjustedY_(m.fval2)}; bool claimed = (m.fval3 > 0.0f); if (clear_pressed_ && !claimed && editable() @@ -903,12 +906,12 @@ auto TextWidget::HandleMessage(const base::WidgetMessage& m) -> bool { && (y < (height_ + top_overlap)) && !claimed) { Activate(); pressed_activate_ = false; - } else if (editable_ && ShouldUseStringEditDialog() + } else if (editable_ && ShouldUseStringEditor_() && (x >= (-left_overlap)) && (x < (width_ + right_overlap)) && (y >= (-bottom_overlap)) && (y < (height_ + top_overlap)) && !claimed) { // With dialog-editing, a click/tap brings up our editor. - InvokeStringEditDialog(); + InvokeStringEditor_(); } // Pressed buttons always claim mouse-ups presented to them. @@ -922,19 +925,19 @@ auto TextWidget::HandleMessage(const base::WidgetMessage& m) -> bool { return false; } -auto TextWidget::ScaleAdjustedX(float x) -> float { +auto TextWidget::ScaleAdjustedX_(float x) -> float { // Account for our center_scale_ value. float offsx = x - width_ * 0.5f; return width_ * 0.5f + offsx / center_scale_; } -auto TextWidget::ScaleAdjustedY(float y) -> float { +auto TextWidget::ScaleAdjustedY_(float y) -> float { // Account for our center_scale_ value. float offsy = y - height_ * 0.5f; return height_ * 0.5f + offsy / center_scale_; } -void TextWidget::AddCharsToText(const std::string& addchars) { +void TextWidget::AddCharsToText_(const std::string& addchars) { assert(editable()); std::vector unichars = Utils::UnicodeFromUTF8(text_raw_, "jcjwf8f"); int len = static_cast(unichars.size()); @@ -954,7 +957,7 @@ void TextWidget::AddCharsToText(const std::string& addchars) { text_translation_dirty_ = true; } -void TextWidget::UpdateTranslation() { +void TextWidget::UpdateTranslation_() { // Apply subs/resources to get our actual text if need be. if (text_translation_dirty_) { // We don't run translations on user-editable text. @@ -970,7 +973,7 @@ void TextWidget::UpdateTranslation() { } auto TextWidget::GetTextWidth() -> float { - UpdateTranslation(); + UpdateTranslation_(); // Should we cache this? return g_base->text_graphics->GetStringWidth(text_translated_, big_); diff --git a/src/ballistica/ui_v1/widget/text_widget.h b/src/ballistica/ui_v1/widget/text_widget.h index 8be95ede..2c5a3dcc 100644 --- a/src/ballistica/ui_v1/widget/text_widget.h +++ b/src/ballistica/ui_v1/widget/text_widget.h @@ -5,6 +5,7 @@ #include +#include "ballistica/shared/python/python_ref.h" #include "ballistica/ui_v1/widget/widget.h" namespace ballistica::ui_v1 { @@ -72,6 +73,7 @@ class TextWidget : public Widget { void set_res_scale(float res_scale); auto GetTextWidth() -> float; void OnLanguageChange() override; + void AdapterFinished(); static TextWidget* GetAndroidStringEditWidget(); @@ -86,14 +88,12 @@ class TextWidget : public Widget { } private: - auto ScaleAdjustedX(float x) -> float; - auto ScaleAdjustedY(float y) -> float; - void AddCharsToText(const std::string& addchars); - auto ShouldUseStringEditDialog() const -> bool; - void InvokeStringEditDialog(); - void UpdateTranslation(); - // static bool always_use_internal_keyboard_; - static Object::WeakRef android_string_edit_widget_; + auto ScaleAdjustedX_(float x) -> float; + auto ScaleAdjustedY_(float y) -> float; + void AddCharsToText_(const std::string& addchars); + auto ShouldUseStringEditor_() const -> bool; + void InvokeStringEditor_(); + void UpdateTranslation_(); float res_scale_{1.0f}; bool enabled_{true}; millisecs_t birth_time_millisecs_{}; @@ -150,9 +150,10 @@ class TextWidget : public Widget { millisecs_t last_activate_time_millisecs_{}; millisecs_t last_carat_change_time_millisecs_{}; - // we keep these at the bottom so they're torn down first.. + // We keep these at the bottom so they're torn down first. Object::Ref on_return_press_call_; Object::Ref on_activate_call_; + PythonRef string_edit_adapter_; }; } // namespace ballistica::ui_v1 diff --git a/src/meta/babasemeta/pyembed/binding_base.py b/src/meta/babasemeta/pyembed/binding_base.py index f1351065..0b85ad93 100644 --- a/src/meta/babasemeta/pyembed/binding_base.py +++ b/src/meta/babasemeta/pyembed/binding_base.py @@ -15,6 +15,7 @@ from babase import ( _env, _error, _general, + _ui, ) # The C++ layer looks for this variable: @@ -23,7 +24,6 @@ values = [ _hooks.set_config_fullscreen_on, # kSetConfigFullscreenOnCall _hooks.set_config_fullscreen_off, # kSetConfigFullscreenOffCall _hooks.not_signed_in_screen_message, # kNotSignedInScreenMessageCall - _hooks.connecting_to_party_message, # kConnectingToPartyMessageCall _hooks.rejecting_invite_already_in_party_message, # kRejectingInviteAlreadyInPartyMessageCall _hooks.connection_failed_message, # kConnectionFailedMessageCall _hooks.temporarily_unavailable_message, # kTemporarilyUnavailableMessageCall @@ -52,6 +52,7 @@ values = [ _hooks.remove_in_game_ads_message, # kRemoveInGameAdsMessageCall _hooks.do_quit, # kQuitCall _hooks.show_post_purchase_message, # kShowPostPurchaseMessageCall + _hooks.string_edit_adapter_can_be_replaced, # kStringEditAdapterCanBeReplacedCall _language.Lstr, # kLStrClass _general.Call, # kCallClass _apputils.garbage_collect_session_end, # kGarbageCollectSessionEndCall @@ -80,4 +81,5 @@ values = [ _hooks.open_url_with_webbrowser_module, # kOpenURLWithWebBrowserModuleCall _env.on_native_module_import, # kEnvOnNativeModuleImportCall _env.on_main_thread_start_app, # kOnMainThreadStartAppCall + _ui.DevConsoleStringEditAdapter, # kDevConsoleStringEditAdapterClass ] diff --git a/src/meta/bauiv1meta/pyembed/binding_ui_v1.py b/src/meta/bauiv1meta/pyembed/binding_ui_v1.py index cc7e6d3c..015d75ea 100644 --- a/src/meta/bauiv1meta/pyembed/binding_ui_v1.py +++ b/src/meta/bauiv1meta/pyembed/binding_ui_v1.py @@ -6,6 +6,7 @@ from __future__ import annotations import bauiv1.onscreenkeyboard from bauiv1 import _hooks +from bauiv1._uitypes import TextWidgetStringEditAdapter # The C++ layer looks for this variable: values = [ @@ -21,4 +22,5 @@ values = [ _hooks.quit_window, # kQuitWindowCall _hooks.device_menu_press, # kDeviceMenuPressCall _hooks.show_url_window, # kShowURLWindowCall + TextWidgetStringEditAdapter, # kTextWidgetStringEditAdapterClass ] diff --git a/tools/efro/util.py b/tools/efro/util.py index c00916c3..e5e7e4a9 100644 --- a/tools/efro/util.py +++ b/tools/efro/util.py @@ -39,6 +39,12 @@ class _EmptyObj: pass +# A dead weak-ref should be immutable, right? So we can create exactly +# one and return it for all cases that need an empty weak-ref. +_g_empty_weak_ref = weakref.ref(_EmptyObj()) +assert _g_empty_weak_ref() is None + + # TODO: kill this and just use efro.call.tpartial if TYPE_CHECKING: Call = Call @@ -148,8 +154,11 @@ def empty_weakref(objtype: type[T]) -> weakref.ref[T]: # At runtime, all weakrefs are the same; our type arg is just # for the static type checker. del objtype # Unused. + # Just create an object and let it die. Is there a cleaner way to do this? - return weakref.ref(_EmptyObj()) # type: ignore + # return weakref.ref(_EmptyObj()) # type: ignore + + return _g_empty_weak_ref # type: ignore def data_size_str(bytecount: int) -> str: