dev console now supports on-screen keyboard input

This commit is contained in:
Eric 2023-09-11 15:16:48 -07:00
parent f23365726b
commit 7579225a3d
No known key found for this signature in database
GPG Key ID: 89C93F0F8D6D5A98
42 changed files with 1212 additions and 479 deletions

92
.efrocachemap generated
View File

@ -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"
}

View File

@ -2825,6 +2825,7 @@
<w>steelseries</w>
<w>stgdict</w>
<w>stickman</w>
<w>stname</w>
<w>storable</w>
<w>storagename</w>
<w>storagenames</w>

View File

@ -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

View File

@ -1680,6 +1680,7 @@
<w>stepnum</w>
<w>stepsize</w>
<w>stgdict</w>
<w>stname</w>
<w>storagenames</w>
<w>storecmd</w>
<w>stot</w>

View File

@ -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",

View File

@ -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 \

View File

@ -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',

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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'

View File

@ -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)

View File

@ -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))
)

View File

@ -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) {

View File

@ -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_{};

View File

@ -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_) {

View File

@ -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) {

View File

@ -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<int> max_chars) {
Log(LogLevel::kError, "FIXME: DoInvokeStringEditor() unimplemented");
}
} // namespace ballistica::base

View File

@ -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<int> 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_;
};

View File

@ -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

View File

@ -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<std::string>;

View File

@ -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<char**>(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<PyMethodDef> {
@ -1589,6 +1662,9 @@ auto PythonMethodsApp::GetMethods() -> std::vector<PyMethodDef> {
PyShutdownSuppressBeginDef,
PyShutdownSuppressEndDef,
PyShutdownSuppressCountDef,
PyGetDevConsoleInputTextDef,
PySetDevConsoleInputTextDef,
PyDevConsoleInputAdapterFinishDef,
};
}

View File

@ -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 <typename F>
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<Runnable> 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, "<console>");
@ -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<float>(g_core->GetAppTimeMillisecs() - transition_start_)
/ static_cast<float>(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<TextGroup>();
}
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<TextGroup>();
}
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;

View File

@ -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<std::string> input_history_;
@ -61,6 +76,9 @@ class DevConsole {
std::string last_line_;
Object::Ref<TextGroup> last_line_mesh_group_;
bool last_line_mesh_dirty_{true};
bool python_console_pressed_{};
PythonRef string_edit_adapter_;
std::list<Button_> buttons_;
};
} // namespace ballistica::base

View File

@ -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

View File

@ -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<InputDevice> ui_input_device_;
millisecs_t last_input_device_use_time_{};
millisecs_t last_widget_input_reject_err_sound_time_{};

View File

@ -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) {

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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).

View File

@ -162,6 +162,15 @@ auto PythonRef::ValueAsString() const -> std::string {
return Python::GetPyString(obj_);
}
auto PythonRef::ValueAsOptionalInt() const -> std::optional<int64_t> {
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);

View File

@ -171,6 +171,7 @@ class PythonRef {
-> std::optional<std::list<std::string>>;
auto ValueAsInt() const -> int64_t;
auto ValueAsOptionalInt() const -> std::optional<int64_t>;
/// Returns whether the underlying PyObject is callable.
/// Throws an exception if unset.

View File

@ -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<char**>(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<TextWidget*>(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<TextWidget*>(
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<TextWidget*>(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);
}
}

View File

@ -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<base::PythonContextCall>(
objs().Get(ObjID::kOnScreenKeyboardClass))
->Schedule(args);
}
void UIV1Python::LaunchStringEditOld(TextWidget* w) {
assert(g_base->InLogicThread());
BA_PRECONDITION(w);

View File

@ -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.
};

View File

@ -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> 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<millisecs_t>(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<base::TextGroup>();
@ -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<millisecs_t>(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<millisecs_t>(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<int>(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<uint32_t> unichars = Utils::UnicodeFromUTF8(text_raw_, "jcjwf8f");
int len = static_cast<int>(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_);

View File

@ -5,6 +5,7 @@
#include <string>
#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<TextWidget> 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<base::PythonContextCall> on_return_press_call_;
Object::Ref<base::PythonContextCall> on_activate_call_;
PythonRef string_edit_adapter_;
};
} // namespace ballistica::ui_v1

View File

@ -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
]

View File

@ -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
]

View File

@ -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: