support relative setPosition, add hardware keyboard listener + control, log FPS
This commit is contained in:
@@ -38,7 +38,7 @@ namespace polyvox
|
|||||||
void transformToUnitCube(EntityId e);
|
void transformToUnitCube(EntityId e);
|
||||||
inline void updateTransform(EntityId e);
|
inline void updateTransform(EntityId e);
|
||||||
void setScale(EntityId e, float scale);
|
void setScale(EntityId e, float scale);
|
||||||
void setPosition(EntityId e, float x, float y, float z);
|
void setPosition(EntityId e, float x, float y, float z, bool relative);
|
||||||
void setRotation(EntityId e, float rads, float x, float y, float z);
|
void setRotation(EntityId e, float rads, float x, float y, float z);
|
||||||
const utils::Entity *getCameraEntities(EntityId e);
|
const utils::Entity *getCameraEntities(EntityId e);
|
||||||
size_t getCameraEntityCount(EntityId e);
|
size_t getCameraEntityCount(EntityId e);
|
||||||
|
|||||||
@@ -200,11 +200,15 @@ namespace polyvox
|
|||||||
void loadTextureFromPath(string path);
|
void loadTextureFromPath(string path);
|
||||||
void savePng(void* data, size_t size, int frameNumber);
|
void savePng(void* data, size_t size, int frameNumber);
|
||||||
|
|
||||||
time_point_t _startTime = std::chrono::high_resolution_clock::now();
|
time_point_t _recordingStartTime = std::chrono::high_resolution_clock::now();
|
||||||
|
time_point_t _fpsCounterStartTime = std::chrono::high_resolution_clock::now();
|
||||||
|
|
||||||
bool _recording = false;
|
bool _recording = false;
|
||||||
std::string _recordingOutputDirectory = std::string("/tmp");
|
std::string _recordingOutputDirectory = std::string("/tmp");
|
||||||
std::mutex _recordingMutex;
|
std::mutex _recordingMutex;
|
||||||
|
double _cumulativeAnimationUpdateTime = 0;
|
||||||
|
int _frameCount = 0;
|
||||||
|
int _skippedFrames = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct FrameCallbackData {
|
struct FrameCallbackData {
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ extern "C"
|
|||||||
FLUTTER_PLUGIN_EXPORT void clear_assets(const void *const viewer);
|
FLUTTER_PLUGIN_EXPORT void clear_assets(const void *const viewer);
|
||||||
FLUTTER_PLUGIN_EXPORT bool set_material_color(void *assetManager, EntityId asset, const char *meshName, int materialIndex, const float r, const float g, const float b, const float a);
|
FLUTTER_PLUGIN_EXPORT bool set_material_color(void *assetManager, EntityId asset, const char *meshName, int materialIndex, const float r, const float g, const float b, const float a);
|
||||||
FLUTTER_PLUGIN_EXPORT void transform_to_unit_cube(void *assetManager, EntityId asset);
|
FLUTTER_PLUGIN_EXPORT void transform_to_unit_cube(void *assetManager, EntityId asset);
|
||||||
FLUTTER_PLUGIN_EXPORT void set_position(void *assetManager, EntityId asset, float x, float y, float z);
|
FLUTTER_PLUGIN_EXPORT void set_position(void *assetManager, EntityId asset, float x, float y, float z, bool relative);
|
||||||
FLUTTER_PLUGIN_EXPORT void set_rotation(void *assetManager, EntityId asset, float rads, float x, float y, float z);
|
FLUTTER_PLUGIN_EXPORT void set_rotation(void *assetManager, EntityId asset, float rads, float x, float y, float z);
|
||||||
FLUTTER_PLUGIN_EXPORT void set_scale(void *assetManager, EntityId asset, float scale);
|
FLUTTER_PLUGIN_EXPORT void set_scale(void *assetManager, EntityId asset, float scale);
|
||||||
|
|
||||||
@@ -183,6 +183,7 @@ extern "C"
|
|||||||
FLUTTER_PLUGIN_EXPORT void set_recording_output_directory(void *const viewer, const char* outputDirectory);
|
FLUTTER_PLUGIN_EXPORT void set_recording_output_directory(void *const viewer, const char* outputDirectory);
|
||||||
FLUTTER_PLUGIN_EXPORT void ios_dummy();
|
FLUTTER_PLUGIN_EXPORT void ios_dummy();
|
||||||
FLUTTER_PLUGIN_EXPORT void flutter_filament_free(void *ptr);
|
FLUTTER_PLUGIN_EXPORT void flutter_filament_free(void *ptr);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -1184,7 +1184,7 @@ namespace polyvox
|
|||||||
updateTransform(asset);
|
updateTransform(asset);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AssetManager::setPosition(EntityId entity, float x, float y, float z)
|
void AssetManager::setPosition(EntityId entity, float x, float y, float z, bool relative)
|
||||||
{
|
{
|
||||||
const auto &pos = _entityIdLookup.find(entity);
|
const auto &pos = _entityIdLookup.find(entity);
|
||||||
if (pos == _entityIdLookup.end())
|
if (pos == _entityIdLookup.end())
|
||||||
@@ -1193,7 +1193,14 @@ namespace polyvox
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
auto &asset = _assets[pos->second];
|
auto &asset = _assets[pos->second];
|
||||||
asset.position = math::mat4f::translation(math::float3(x, y, z));
|
if(relative) {
|
||||||
|
asset.position[3][0] += x;
|
||||||
|
asset.position[3][1] += y;
|
||||||
|
asset.position[3][2] += z;
|
||||||
|
} else {
|
||||||
|
asset.position = math::mat4f::translation(math::float3(x, y, z));
|
||||||
|
}
|
||||||
|
|
||||||
updateTransform(asset);
|
updateTransform(asset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1005,9 +1005,6 @@ namespace polyvox
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
double _elapsed = 0;
|
|
||||||
int _frameCount = 0;
|
|
||||||
int _skippedFrames = 0;
|
|
||||||
|
|
||||||
void FilamentViewer::render(
|
void FilamentViewer::render(
|
||||||
uint64_t frameTimeInNanos,
|
uint64_t frameTimeInNanos,
|
||||||
@@ -1022,21 +1019,30 @@ namespace polyvox
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_frameCount == 60)
|
// if (_frameCount == 60)
|
||||||
{
|
// {
|
||||||
// Log("1 sec average for asset animation update %f", _elapsed / 60);
|
// Log("Skipped frames : %d", _skippedFrames);
|
||||||
Log("Skipped frames : %d", _skippedFrames);
|
// _elapsed = 0;
|
||||||
_elapsed = 0;
|
// _frameCount = 0;
|
||||||
|
// _skippedFrames = 0;
|
||||||
|
// }
|
||||||
|
auto now = std::chrono::high_resolution_clock::now();
|
||||||
|
auto secsSinceLastFpsCheck = float(std::chrono::duration_cast<std::chrono::seconds>(now - _fpsCounterStartTime).count());
|
||||||
|
|
||||||
|
if(secsSinceLastFpsCheck >= 1) {
|
||||||
|
auto fps = _frameCount / secsSinceLastFpsCheck;
|
||||||
|
Log("%ffps (_frameCount %d, secs since last check %f)", fps, _frameCount, secsSinceLastFpsCheck);
|
||||||
|
// Log("1 sec average for asset animation update %f", _elapsed / _frameCount);
|
||||||
_frameCount = 0;
|
_frameCount = 0;
|
||||||
_skippedFrames = 0;
|
_skippedFrames = 0;
|
||||||
|
_fpsCounterStartTime = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
Timer tmr;
|
Timer tmr;
|
||||||
|
|
||||||
_assetManager->updateAnimations();
|
_assetManager->updateAnimations();
|
||||||
|
|
||||||
_elapsed += tmr.elapsed();
|
_cumulativeAnimationUpdateTime += tmr.elapsed();
|
||||||
_frameCount++;
|
|
||||||
|
|
||||||
// if a manipulator is active, update the active camera orientation
|
// if a manipulator is active, update the active camera orientation
|
||||||
if(_manipulator) {
|
if(_manipulator) {
|
||||||
@@ -1068,6 +1074,8 @@ namespace polyvox
|
|||||||
if (beginFrame)
|
if (beginFrame)
|
||||||
{
|
{
|
||||||
_renderer->render(_view);
|
_renderer->render(_view);
|
||||||
|
_frameCount++;
|
||||||
|
|
||||||
|
|
||||||
if(_recording) {
|
if(_recording) {
|
||||||
Viewport const &vp = _view->getViewport();
|
Viewport const &vp = _view->getViewport();
|
||||||
@@ -1081,7 +1089,7 @@ namespace polyvox
|
|||||||
};
|
};
|
||||||
|
|
||||||
auto now = std::chrono::high_resolution_clock::now();
|
auto now = std::chrono::high_resolution_clock::now();
|
||||||
auto elapsed = float(std::chrono::duration_cast<std::chrono::milliseconds>(now - _startTime).count());
|
auto elapsed = float(std::chrono::duration_cast<std::chrono::milliseconds>(now - _recordingStartTime).count());
|
||||||
|
|
||||||
auto frameNumber = uint32_t(floor(elapsed / _frameInterval));
|
auto frameNumber = uint32_t(floor(elapsed / _frameInterval));
|
||||||
|
|
||||||
@@ -1161,7 +1169,7 @@ namespace polyvox
|
|||||||
this->_recording = recording;
|
this->_recording = recording;
|
||||||
if(recording) {
|
if(recording) {
|
||||||
_tp = new flutter_filament::ThreadPool(8);
|
_tp = new flutter_filament::ThreadPool(8);
|
||||||
_startTime = std::chrono::high_resolution_clock::now();
|
_recordingStartTime = std::chrono::high_resolution_clock::now();
|
||||||
} else {
|
} else {
|
||||||
delete _tp;
|
delete _tp;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -470,9 +470,9 @@ extern "C"
|
|||||||
((AssetManager *)assetManager)->transformToUnitCube(asset);
|
((AssetManager *)assetManager)->transformToUnitCube(asset);
|
||||||
}
|
}
|
||||||
|
|
||||||
FLUTTER_PLUGIN_EXPORT void set_position(void *assetManager, EntityId asset, float x, float y, float z)
|
FLUTTER_PLUGIN_EXPORT void set_position(void *assetManager, EntityId asset, float x, float y, float z, bool relative)
|
||||||
{
|
{
|
||||||
((AssetManager *)assetManager)->setPosition(asset, x, y, z);
|
((AssetManager *)assetManager)->setPosition(asset, x, y, z, relative);
|
||||||
}
|
}
|
||||||
|
|
||||||
FLUTTER_PLUGIN_EXPORT void set_rotation(void *assetManager, EntityId asset, float rads, float x, float y, float z)
|
FLUTTER_PLUGIN_EXPORT void set_rotation(void *assetManager, EntityId asset, float rads, float x, float y, float z)
|
||||||
|
|||||||
116
lib/entities/entity_transform_controller.dart
Normal file
116
lib/entities/entity_transform_controller.dart
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter_filament/filament_controller.dart';
|
||||||
|
import 'package:vector_math/vector_math_64.dart' as v;
|
||||||
|
|
||||||
|
class EntityTransformController {
|
||||||
|
final FilamentController controller;
|
||||||
|
final FilamentEntity _entity;
|
||||||
|
|
||||||
|
late Timer _ticker;
|
||||||
|
|
||||||
|
double translationSpeed;
|
||||||
|
double rotationRadsPerSecond;
|
||||||
|
|
||||||
|
bool _forward = false;
|
||||||
|
bool _strafeLeft = false;
|
||||||
|
bool _strafeRight = false;
|
||||||
|
bool _back = false;
|
||||||
|
bool _rotateLeft = false;
|
||||||
|
bool _rotateRight = false;
|
||||||
|
|
||||||
|
EntityTransformController(this.controller, this._entity,
|
||||||
|
{this.translationSpeed = 1, this.rotationRadsPerSecond = pi / 2}) {
|
||||||
|
var translationSpeedPerTick = translationSpeed / (1000 / 16.667);
|
||||||
|
_ticker = Timer.periodic(const Duration(milliseconds: 16), (timer) {
|
||||||
|
_update(translationSpeedPerTick);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _update(double translationSpeedPerTick) async {
|
||||||
|
var _position = v.Vector3.zero();
|
||||||
|
var _rotation = v.Quaternion.identity();
|
||||||
|
bool requiresUpdate = false;
|
||||||
|
if (_forward) {
|
||||||
|
_position.add(v.Vector3(0, 0, -translationSpeedPerTick));
|
||||||
|
requiresUpdate = true;
|
||||||
|
}
|
||||||
|
if (_back) {
|
||||||
|
_position.add(v.Vector3(0, 0, translationSpeedPerTick));
|
||||||
|
requiresUpdate = true;
|
||||||
|
}
|
||||||
|
if (_strafeLeft) {
|
||||||
|
_position.add(v.Vector3(-translationSpeedPerTick, 0, 0));
|
||||||
|
requiresUpdate = true;
|
||||||
|
}
|
||||||
|
if (_strafeRight) {
|
||||||
|
_position.add(v.Vector3(translationSpeedPerTick, 0, 0));
|
||||||
|
requiresUpdate = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo - better to use pitch/yaw/roll
|
||||||
|
if (_rotateLeft) {}
|
||||||
|
if (_rotateRight) {}
|
||||||
|
|
||||||
|
if (requiresUpdate) {
|
||||||
|
await controller.setPosition(
|
||||||
|
_entity, _position.x, _position.y, _position.z,
|
||||||
|
relative: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
_ticker.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
void forwardPressed() {
|
||||||
|
print("forward");
|
||||||
|
_forward = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer? _forwardTimer;
|
||||||
|
Timer? _backwardsTimer;
|
||||||
|
Timer? _strafeLeftTimer;
|
||||||
|
Timer? _strafeRightTimer;
|
||||||
|
|
||||||
|
void forwardReleased() async {
|
||||||
|
_forwardTimer?.cancel();
|
||||||
|
_forwardTimer = Timer(Duration(milliseconds: 50), () {
|
||||||
|
_forward = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void backPressed() {
|
||||||
|
_back = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void backReleased() async {
|
||||||
|
_backwardsTimer?.cancel();
|
||||||
|
_backwardsTimer = Timer(Duration(milliseconds: 50), () {
|
||||||
|
_back = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void strafeLeftPressed() {
|
||||||
|
_strafeLeft = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void strafeLeftReleased() async {
|
||||||
|
_strafeLeftTimer?.cancel();
|
||||||
|
_strafeLeftTimer = Timer(Duration(milliseconds: 50), () {
|
||||||
|
_strafeLeft = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void strafeRightPressed() {
|
||||||
|
_strafeRight = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void strafeRightReleased() async {
|
||||||
|
_strafeRightTimer?.cancel();
|
||||||
|
_strafeRightTimer = Timer(Duration(milliseconds: 50), () {
|
||||||
|
_strafeRight = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -450,7 +450,8 @@ abstract class FilamentController {
|
|||||||
///
|
///
|
||||||
/// Sets the world space position for [entity] to the given coordinates.
|
/// Sets the world space position for [entity] to the given coordinates.
|
||||||
///
|
///
|
||||||
Future setPosition(FilamentEntity entity, double x, double y, double z);
|
Future setPosition(FilamentEntity entity, double x, double y, double z,
|
||||||
|
{bool relative = false});
|
||||||
|
|
||||||
///
|
///
|
||||||
/// Enable/disable postprocessing.
|
/// Enable/disable postprocessing.
|
||||||
@@ -523,4 +524,9 @@ abstract class FilamentController {
|
|||||||
/// Sets the output directory where recorded PNGs will be placed.
|
/// Sets the output directory where recorded PNGs will be placed.
|
||||||
///
|
///
|
||||||
Future setRecordingOutputDirectory(String outputDirectory);
|
Future setRecordingOutputDirectory(String outputDirectory);
|
||||||
|
|
||||||
|
// Stream get keyboardFocusRequested;
|
||||||
|
// void requestKeyboardFocus();
|
||||||
|
|
||||||
|
void control(FilamentEntity entity, {double? translationSpeed});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ import 'dart:developer' as dev;
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:ffi/ffi.dart';
|
import 'package:ffi/ffi.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_filament/entities/entity_transform_controller.dart';
|
||||||
|
|
||||||
import 'package:flutter_filament/filament_controller.dart';
|
import 'package:flutter_filament/filament_controller.dart';
|
||||||
|
|
||||||
import 'package:flutter_filament/animations/animation_data.dart';
|
import 'package:flutter_filament/animations/animation_data.dart';
|
||||||
import 'package:flutter_filament/generated_bindings.dart';
|
import 'package:flutter_filament/generated_bindings.dart';
|
||||||
|
import 'package:flutter_filament/hardware/hardware_keyboard_listener.dart';
|
||||||
import 'package:flutter_filament/rendering_surface.dart';
|
import 'package:flutter_filament/rendering_surface.dart';
|
||||||
import 'package:vector_math/vector_math_64.dart';
|
import 'package:vector_math/vector_math_64.dart';
|
||||||
|
|
||||||
@@ -1049,12 +1051,12 @@ class FilamentControllerFFI extends FilamentController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future setPosition(
|
Future setPosition(FilamentEntity entity, double x, double y, double z,
|
||||||
FilamentEntity entity, double x, double y, double z) async {
|
{bool relative = false}) async {
|
||||||
if (_viewer == null) {
|
if (_viewer == null) {
|
||||||
throw Exception("No viewer available, ignoring");
|
throw Exception("No viewer available, ignoring");
|
||||||
}
|
}
|
||||||
set_position(_assetManager!, entity, x, y, z);
|
set_position(_assetManager!, entity, x, y, z, relative);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -1321,4 +1323,12 @@ class FilamentControllerFFI extends FilamentController {
|
|||||||
set_recording_output_directory(_viewer!, pathPtr.cast<Char>());
|
set_recording_output_directory(_viewer!, pathPtr.cast<Char>());
|
||||||
allocator.free(pathPtr);
|
allocator.free(pathPtr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
HardwareKeyboardListener? _keyboardListener;
|
||||||
|
void control(FilamentEntity entity, {double? translationSpeed}) {
|
||||||
|
_keyboardListener?.dispose();
|
||||||
|
_keyboardListener = HardwareKeyboardListener(EntityTransformController(
|
||||||
|
this, entity,
|
||||||
|
translationSpeed: translationSpeed ?? 1.0));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -541,14 +541,20 @@ external void transform_to_unit_cube(
|
|||||||
);
|
);
|
||||||
|
|
||||||
@ffi.Native<
|
@ffi.Native<
|
||||||
ffi.Void Function(ffi.Pointer<ffi.Void>, EntityId, ffi.Float, ffi.Float,
|
ffi.Void Function(
|
||||||
ffi.Float)>(symbol: 'set_position', assetId: 'flutter_filament_plugin')
|
ffi.Pointer<ffi.Void>,
|
||||||
|
EntityId,
|
||||||
|
ffi.Float,
|
||||||
|
ffi.Float,
|
||||||
|
ffi.Float,
|
||||||
|
ffi.Bool)>(symbol: 'set_position', assetId: 'flutter_filament_plugin')
|
||||||
external void set_position(
|
external void set_position(
|
||||||
ffi.Pointer<ffi.Void> assetManager,
|
ffi.Pointer<ffi.Void> assetManager,
|
||||||
int asset,
|
int asset,
|
||||||
double x,
|
double x,
|
||||||
double y,
|
double y,
|
||||||
double z,
|
double z,
|
||||||
|
bool relative,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ffi.Native<
|
@ffi.Native<
|
||||||
|
|||||||
54
lib/hardware/hardware_keyboard_listener.dart
Normal file
54
lib/hardware/hardware_keyboard_listener.dart
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_filament/entities/entity_transform_controller.dart';
|
||||||
|
import 'package:flutter_filament/filament_controller.dart';
|
||||||
|
|
||||||
|
class HardwareKeyboardListener {
|
||||||
|
final EntityTransformController _controller;
|
||||||
|
HardwareKeyboardListener(this._controller) {
|
||||||
|
// Get the global handler.
|
||||||
|
final KeyMessageHandler? existing =
|
||||||
|
ServicesBinding.instance.keyEventManager.keyMessageHandler;
|
||||||
|
// The handler is guaranteed non-null since
|
||||||
|
// `FallbackKeyEventRegistrar.instance` is only called during
|
||||||
|
// `Focus.onFocusChange`, at which time `ServicesBinding.instance` must
|
||||||
|
// have been called somewhere.
|
||||||
|
assert(existing != null);
|
||||||
|
// Assign the global handler with a patched handler.
|
||||||
|
ServicesBinding.instance.keyEventManager.keyMessageHandler = (keyMessage) {
|
||||||
|
if (keyMessage.rawEvent == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var event = keyMessage.rawEvent!;
|
||||||
|
switch (event.logicalKey) {
|
||||||
|
case LogicalKeyboardKey.keyW:
|
||||||
|
(event is RawKeyDownEvent)
|
||||||
|
? _controller.forwardPressed()
|
||||||
|
: _controller.forwardReleased();
|
||||||
|
break;
|
||||||
|
case LogicalKeyboardKey.keyA:
|
||||||
|
event is RawKeyDownEvent
|
||||||
|
? _controller.strafeLeftPressed()
|
||||||
|
: _controller.strafeLeftReleased();
|
||||||
|
break;
|
||||||
|
case LogicalKeyboardKey.keyS:
|
||||||
|
event is RawKeyDownEvent
|
||||||
|
? _controller.backPressed()
|
||||||
|
: _controller.backReleased();
|
||||||
|
break;
|
||||||
|
case LogicalKeyboardKey.keyD:
|
||||||
|
event is RawKeyDownEvent
|
||||||
|
? _controller.strafeRightPressed()
|
||||||
|
: _controller.strafeRightReleased();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
ServicesBinding.instance.keyEventManager.keyMessageHandler = null;
|
||||||
|
_controller.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user