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: