Add support for embedding game process in the Android Editor
- Implement Android editor specific `EmbeddedGodotGame` to support embedding the game window in the Android editor
This commit is contained in:
parent
296de7da83
commit
7495a8a02e
@ -1154,19 +1154,11 @@
|
||||
- [b]Auto (based on screen size)[/b] (default) will automatically choose how to launch the Play window based on the device and screen metrics. Defaults to [b]Same as Editor[/b] on phones and [b]Side-by-side with Editor[/b] on tablets.
|
||||
- [b]Same as Editor[/b] will launch the Play window in the same window as the Editor.
|
||||
- [b]Side-by-side with Editor[/b] will launch the Play window side-by-side with the Editor window.
|
||||
- [b]Launch in PiP mode[/b] will launch the Play window directly in picture-in-picture (PiP) mode if PiP mode is supported and enabled. When maximized, the Play window will occupy the same window as the Editor.
|
||||
[b]Note:[/b] Only available in the Android editor.
|
||||
</member>
|
||||
<member name="run/window_placement/game_embed_mode" type="int" setter="" getter="">
|
||||
Overrides game embedding setting for all newly opened projects. If enabled, game embedding settings are not saved.
|
||||
</member>
|
||||
<member name="run/window_placement/play_window_pip_mode" type="int" setter="" getter="">
|
||||
Specifies the picture-in-picture (PiP) mode for the Play window.
|
||||
- [b]Disabled:[/b] PiP is disabled for the Play window.
|
||||
- [b]Enabled:[/b] If the device supports it, PiP is always enabled for the Play window. The Play window will contain a button to enter PiP mode.
|
||||
- [b]Enabled when Play window is same as Editor[/b] (default for Android editor): If the device supports it, PiP is enabled when the Play window is the same as the Editor. The Play window will contain a button to enter PiP mode.
|
||||
[b]Note:[/b] Only available in the Android editor.
|
||||
</member>
|
||||
<member name="run/window_placement/rect" type="int" setter="" getter="">
|
||||
The window mode to use to display the project when starting the project from the editor.
|
||||
[b]Note:[/b] Game embedding is not available for "Force Maximized" or "Force Fullscreen."
|
||||
|
@ -228,6 +228,11 @@ EditorPlugin *EditorMainScreen::get_selected_plugin() const {
|
||||
return selected_plugin;
|
||||
}
|
||||
|
||||
EditorPlugin *EditorMainScreen::get_plugin_by_name(const String &p_plugin_name) const {
|
||||
ERR_FAIL_COND_V(!main_editor_plugins.has(p_plugin_name), nullptr);
|
||||
return main_editor_plugins[p_plugin_name];
|
||||
}
|
||||
|
||||
VBoxContainer *EditorMainScreen::get_control() const {
|
||||
return main_screen_vbox;
|
||||
}
|
||||
@ -254,6 +259,7 @@ void EditorMainScreen::add_main_plugin(EditorPlugin *p_editor) {
|
||||
buttons.push_back(tb);
|
||||
button_hb->add_child(tb);
|
||||
editor_table.push_back(p_editor);
|
||||
main_editor_plugins.insert(p_editor->get_plugin_name(), p_editor);
|
||||
}
|
||||
|
||||
void EditorMainScreen::remove_main_plugin(EditorPlugin *p_editor) {
|
||||
@ -280,6 +286,7 @@ void EditorMainScreen::remove_main_plugin(EditorPlugin *p_editor) {
|
||||
}
|
||||
|
||||
editor_table.erase(p_editor);
|
||||
main_editor_plugins.erase(p_editor->get_plugin_name());
|
||||
}
|
||||
|
||||
EditorMainScreen::EditorMainScreen() {
|
||||
|
@ -58,6 +58,7 @@ private:
|
||||
HBoxContainer *button_hb = nullptr;
|
||||
Vector<Button *> buttons;
|
||||
Vector<EditorPlugin *> editor_table;
|
||||
HashMap<String, EditorPlugin *> main_editor_plugins;
|
||||
|
||||
int _get_current_main_editor() const;
|
||||
|
||||
@ -80,6 +81,7 @@ public:
|
||||
int get_selected_index() const;
|
||||
int get_plugin_index(EditorPlugin *p_editor) const;
|
||||
EditorPlugin *get_selected_plugin() const;
|
||||
EditorPlugin *get_plugin_by_name(const String &p_plugin_name) const;
|
||||
|
||||
VBoxContainer *get_control() const;
|
||||
|
||||
|
@ -939,17 +939,20 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) {
|
||||
_initial_set("run/window_placement/rect_custom_position", Vector2());
|
||||
EDITOR_SETTING_BASIC(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/screen", -5, screen_hints)
|
||||
#endif
|
||||
// Should match the ANDROID_WINDOW_* constants in 'platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt'
|
||||
String android_window_hints = "Auto (based on screen size):0,Same as Editor:1,Side-by-side with Editor:2,Launch in PiP mode:3";
|
||||
// Should match the ANDROID_WINDOW_* constants in 'platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotEditor.kt'.
|
||||
String android_window_hints = "Auto (based on screen size):0,Same as Editor:1,Side-by-side with Editor:2";
|
||||
EDITOR_SETTING_BASIC(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/android_window", 0, android_window_hints)
|
||||
|
||||
EDITOR_SETTING_BASIC(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/game_embed_mode", 0, "Use Per-Project Configuration:0,Embed Game:1,Make Game Workspace Floating:2,Disabled:3");
|
||||
|
||||
int default_play_window_pip_mode = 0;
|
||||
String game_embed_mode_hints = "Disabled:-1,Use Per-Project Configuration:0,Embed Game:1,Make Game Workspace Floating:2";
|
||||
#ifdef ANDROID_ENABLED
|
||||
default_play_window_pip_mode = 2;
|
||||
if (OS::get_singleton()->has_feature("xr_editor")) {
|
||||
game_embed_mode_hints = "Disabled:-1";
|
||||
} else {
|
||||
game_embed_mode_hints = "Disabled:-1,Auto (based on screen size):0,Enabled:1";
|
||||
}
|
||||
#endif
|
||||
EDITOR_SETTING_BASIC(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/play_window_pip_mode", default_play_window_pip_mode, "Disabled:0,Enabled:1,Enabled when Play window is same as Editor:2")
|
||||
int default_game_embed_mode = OS::get_singleton()->has_feature("xr_editor") ? -1 : 0;
|
||||
EDITOR_SETTING_BASIC(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/game_embed_mode", default_game_embed_mode, game_embed_mode_hints);
|
||||
|
||||
// Auto save
|
||||
_initial_set("run/auto_save/save_before_running", true, true);
|
||||
|
@ -614,6 +614,10 @@ void GameView::_notification(int p_what) {
|
||||
// Embedding available.
|
||||
int game_mode = EDITOR_GET("run/window_placement/game_embed_mode");
|
||||
switch (game_mode) {
|
||||
case -1: { // Disabled.
|
||||
embed_on_play = false;
|
||||
make_floating_on_play = false;
|
||||
} break;
|
||||
case 1: { // Embed.
|
||||
embed_on_play = true;
|
||||
make_floating_on_play = false;
|
||||
@ -622,10 +626,6 @@ void GameView::_notification(int p_what) {
|
||||
embed_on_play = true;
|
||||
make_floating_on_play = true;
|
||||
} break;
|
||||
case 3: { // Disabled.
|
||||
embed_on_play = false;
|
||||
make_floating_on_play = false;
|
||||
} break;
|
||||
default: {
|
||||
embed_on_play = EditorSettings::get_singleton()->get_project_metadata("game_view", "embed_on_play", true);
|
||||
make_floating_on_play = EditorSettings::get_singleton()->get_project_metadata("game_view", "make_floating_on_play", true);
|
||||
@ -1027,6 +1027,18 @@ GameView::GameView(Ref<GameViewDebugger> p_debugger, WindowWrapper *p_wrapper) {
|
||||
|
||||
///////
|
||||
|
||||
void GameViewPlugin::selected_notify() {
|
||||
if (_is_window_wrapper_enabled()) {
|
||||
#ifdef ANDROID_ENABLED
|
||||
notify_main_screen_changed(get_plugin_name());
|
||||
#else
|
||||
window_wrapper->grab_window_focus();
|
||||
#endif
|
||||
_focus_another_editor();
|
||||
}
|
||||
}
|
||||
|
||||
#ifndef ANDROID_ENABLED
|
||||
void GameViewPlugin::make_visible(bool p_visible) {
|
||||
if (p_visible) {
|
||||
window_wrapper->show();
|
||||
@ -1035,13 +1047,6 @@ void GameViewPlugin::make_visible(bool p_visible) {
|
||||
}
|
||||
}
|
||||
|
||||
void GameViewPlugin::selected_notify() {
|
||||
if (window_wrapper->get_window_enabled()) {
|
||||
window_wrapper->grab_window_focus();
|
||||
_focus_another_editor();
|
||||
}
|
||||
}
|
||||
|
||||
void GameViewPlugin::set_window_layout(Ref<ConfigFile> p_layout) {
|
||||
game_view->set_window_layout(p_layout);
|
||||
}
|
||||
@ -1058,6 +1063,11 @@ Dictionary GameViewPlugin::get_state() const {
|
||||
return game_view->get_state();
|
||||
}
|
||||
|
||||
void GameViewPlugin::_window_visibility_changed(bool p_visible) {
|
||||
_focus_another_editor();
|
||||
}
|
||||
#endif
|
||||
|
||||
void GameViewPlugin::_notification(int p_what) {
|
||||
switch (p_what) {
|
||||
case NOTIFICATION_ENTER_TREE: {
|
||||
@ -1082,13 +1092,11 @@ void GameViewPlugin::_feature_profile_changed() {
|
||||
debugger->set_is_feature_enabled(is_feature_enabled);
|
||||
}
|
||||
|
||||
#ifndef ANDROID_ENABLED
|
||||
if (game_view) {
|
||||
game_view->set_is_feature_enabled(is_feature_enabled);
|
||||
}
|
||||
}
|
||||
|
||||
void GameViewPlugin::_window_visibility_changed(bool p_visible) {
|
||||
_focus_another_editor();
|
||||
#endif
|
||||
}
|
||||
|
||||
void GameViewPlugin::_save_last_editor(const String &p_editor) {
|
||||
@ -1098,7 +1106,7 @@ void GameViewPlugin::_save_last_editor(const String &p_editor) {
|
||||
}
|
||||
|
||||
void GameViewPlugin::_focus_another_editor() {
|
||||
if (window_wrapper->get_window_enabled()) {
|
||||
if (_is_window_wrapper_enabled()) {
|
||||
if (last_editor.is_empty()) {
|
||||
EditorNode::get_singleton()->get_editor_main_screen()->select(EditorMainScreen::EDITOR_2D);
|
||||
} else {
|
||||
@ -1107,13 +1115,22 @@ void GameViewPlugin::_focus_another_editor() {
|
||||
}
|
||||
}
|
||||
|
||||
bool GameViewPlugin::_is_window_wrapper_enabled() const {
|
||||
#ifdef ANDROID_ENABLED
|
||||
return true;
|
||||
#else
|
||||
return window_wrapper->get_window_enabled();
|
||||
#endif
|
||||
}
|
||||
|
||||
GameViewPlugin::GameViewPlugin() {
|
||||
debugger.instantiate();
|
||||
|
||||
#ifndef ANDROID_ENABLED
|
||||
window_wrapper = memnew(WindowWrapper);
|
||||
window_wrapper->set_window_title(vformat(TTR("%s - Godot Engine"), TTR("Game Workspace")));
|
||||
window_wrapper->set_margins_enabled(true);
|
||||
|
||||
debugger.instantiate();
|
||||
|
||||
game_view = memnew(GameView(debugger, window_wrapper));
|
||||
game_view->set_v_size_flags(Control::SIZE_EXPAND_FILL);
|
||||
|
||||
@ -1123,6 +1140,7 @@ GameViewPlugin::GameViewPlugin() {
|
||||
window_wrapper->set_v_size_flags(Control::SIZE_EXPAND_FILL);
|
||||
window_wrapper->hide();
|
||||
window_wrapper->connect("window_visibility_changed", callable_mp(this, &GameViewPlugin::_window_visibility_changed));
|
||||
#endif
|
||||
|
||||
EditorFeatureProfileManager::get_singleton()->connect("current_feature_profile_changed", callable_mp(this, &GameViewPlugin::_feature_profile_changed));
|
||||
}
|
||||
|
@ -32,6 +32,7 @@
|
||||
#define GAME_VIEW_PLUGIN_H
|
||||
|
||||
#include "editor/debugger/editor_debugger_node.h"
|
||||
#include "editor/editor_main_screen.h"
|
||||
#include "editor/plugins/editor_debugger_plugin.h"
|
||||
#include "editor/plugins/editor_plugin.h"
|
||||
#include "scene/debugger/scene_debugger.h"
|
||||
@ -208,17 +209,22 @@ public:
|
||||
class GameViewPlugin : public EditorPlugin {
|
||||
GDCLASS(GameViewPlugin, EditorPlugin);
|
||||
|
||||
#ifndef ANDROID_ENABLED
|
||||
GameView *game_view = nullptr;
|
||||
WindowWrapper *window_wrapper = nullptr;
|
||||
#endif
|
||||
|
||||
Ref<GameViewDebugger> debugger;
|
||||
|
||||
String last_editor;
|
||||
|
||||
void _feature_profile_changed();
|
||||
#ifndef ANDROID_ENABLED
|
||||
void _window_visibility_changed(bool p_visible);
|
||||
#endif
|
||||
void _save_last_editor(const String &p_editor);
|
||||
void _focus_another_editor();
|
||||
bool _is_window_wrapper_enabled() const;
|
||||
|
||||
protected:
|
||||
void _notification(int p_what);
|
||||
@ -228,14 +234,19 @@ public:
|
||||
bool has_main_screen() const override { return true; }
|
||||
virtual void edit(Object *p_object) override {}
|
||||
virtual bool handles(Object *p_object) const override { return false; }
|
||||
virtual void make_visible(bool p_visible) override;
|
||||
virtual void selected_notify() override;
|
||||
|
||||
Ref<GameViewDebugger> get_debugger() const { return debugger; }
|
||||
|
||||
#ifndef ANDROID_ENABLED
|
||||
virtual void make_visible(bool p_visible) override;
|
||||
|
||||
virtual void set_window_layout(Ref<ConfigFile> p_layout) override;
|
||||
virtual void get_window_layout(Ref<ConfigFile> p_layout) override;
|
||||
|
||||
virtual void set_state(const Dictionary &p_state) override;
|
||||
virtual Dictionary get_state() const override;
|
||||
#endif
|
||||
|
||||
GameViewPlugin();
|
||||
~GameViewPlugin();
|
||||
|
@ -30,6 +30,7 @@ android_files = [
|
||||
"rendering_context_driver_vulkan_android.cpp",
|
||||
"variant/callable_jni.cpp",
|
||||
"dialog_utils_jni.cpp",
|
||||
"game_menu_utils_jni.cpp",
|
||||
]
|
||||
|
||||
env_android = env.Clone()
|
||||
|
127
platform/android/game_menu_utils_jni.cpp
Normal file
127
platform/android/game_menu_utils_jni.cpp
Normal file
@ -0,0 +1,127 @@
|
||||
/**************************************************************************/
|
||||
/* game_menu_utils_jni.cpp */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
#include "game_menu_utils_jni.h"
|
||||
|
||||
#ifdef TOOLS_ENABLED
|
||||
#include "editor/editor_interface.h"
|
||||
#include "editor/editor_node.h"
|
||||
#include "editor/plugins/game_view_plugin.h"
|
||||
#endif
|
||||
|
||||
extern "C" {
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSuspend(JNIEnv *env, jclass clazz, jboolean enabled) {
|
||||
#ifdef TOOLS_ENABLED
|
||||
GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
|
||||
if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
|
||||
game_view_plugin->get_debugger()->set_suspend(enabled);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_nextFrame(JNIEnv *env, jclass clazz) {
|
||||
#ifdef TOOLS_ENABLED
|
||||
GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
|
||||
if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
|
||||
game_view_plugin->get_debugger()->next_frame();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setNodeType(JNIEnv *env, jclass clazz, jint type) {
|
||||
#ifdef TOOLS_ENABLED
|
||||
GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
|
||||
if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
|
||||
game_view_plugin->get_debugger()->set_node_type(type);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSelectMode(JNIEnv *env, jclass clazz, jint mode) {
|
||||
#ifdef TOOLS_ENABLED
|
||||
GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
|
||||
if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
|
||||
game_view_plugin->get_debugger()->set_select_mode(mode);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSelectionVisible(JNIEnv *env, jclass clazz, jboolean visible) {
|
||||
#ifdef TOOLS_ENABLED
|
||||
GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
|
||||
if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
|
||||
game_view_plugin->get_debugger()->set_selection_visible(visible);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setCameraOverride(JNIEnv *env, jclass clazz, jboolean enabled) {
|
||||
#ifdef TOOLS_ENABLED
|
||||
GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
|
||||
if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
|
||||
game_view_plugin->get_debugger()->set_camera_override(enabled);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setCameraManipulateMode(JNIEnv *env, jclass clazz, jint mode) {
|
||||
#ifdef TOOLS_ENABLED
|
||||
GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
|
||||
if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
|
||||
game_view_plugin->get_debugger()->set_camera_manipulate_mode(static_cast<EditorDebuggerNode::CameraOverride>(mode));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_resetCamera2DPosition(JNIEnv *env, jclass clazz) {
|
||||
#ifdef TOOLS_ENABLED
|
||||
GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
|
||||
if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
|
||||
game_view_plugin->get_debugger()->reset_camera_2d_position();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_resetCamera3DPosition(JNIEnv *env, jclass clazz) {
|
||||
#ifdef TOOLS_ENABLED
|
||||
GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
|
||||
if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
|
||||
game_view_plugin->get_debugger()->reset_camera_3d_position();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_playMainScene(JNIEnv *env, jclass clazz) {
|
||||
#ifdef TOOLS_ENABLED
|
||||
EditorInterface::get_singleton()->play_main_scene();
|
||||
#endif
|
||||
}
|
||||
}
|
49
platform/android/game_menu_utils_jni.h
Normal file
49
platform/android/game_menu_utils_jni.h
Normal file
@ -0,0 +1,49 @@
|
||||
/**************************************************************************/
|
||||
/* game_menu_utils_jni.h */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
#ifndef GAME_MENU_UTILS_JNI_H
|
||||
#define GAME_MENU_UTILS_JNI_H
|
||||
|
||||
#include <jni.h>
|
||||
|
||||
extern "C" {
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSuspend(JNIEnv *env, jclass clazz, jboolean enabled);
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_nextFrame(JNIEnv *env, jclass clazz);
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setNodeType(JNIEnv *env, jclass clazz, jint type);
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSelectMode(JNIEnv *env, jclass clazz, jint mode);
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSelectionVisible(JNIEnv *env, jclass clazz, jboolean visible);
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setCameraOverride(JNIEnv *env, jclass clazz, jboolean enabled);
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setCameraManipulateMode(JNIEnv *env, jclass clazz, jint mode);
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_resetCamera2DPosition(JNIEnv *env, jclass clazz);
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_resetCamera3DPosition(JNIEnv *env, jclass clazz);
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_playMainScene(JNIEnv *env, jclass clazz);
|
||||
}
|
||||
|
||||
#endif // GAME_MENU_UTILS_JNI_H
|
@ -65,12 +65,26 @@
|
||||
android:label="@string/godot_game_activity_name"
|
||||
android:launchMode="singleTask"
|
||||
android:process=":GodotGame"
|
||||
android:autoRemoveFromRecents="true"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:screenOrientation="userLandscape">
|
||||
<layout
|
||||
android:defaultWidth="@dimen/editor_default_window_width"
|
||||
android:defaultHeight="@dimen/editor_default_window_height" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".embed.EmbeddedGodotGame"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
|
||||
android:exported="false"
|
||||
android:icon="@mipmap/ic_play_window"
|
||||
android:label="@string/godot_game_activity_name"
|
||||
android:theme="@style/GodotEmbeddedGameTheme"
|
||||
android:taskAffinity=":embed"
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleTask"
|
||||
android:process=":EmbeddedGodotGame"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:screenOrientation="userLandscape" />
|
||||
<activity
|
||||
android:name=".GodotXRGame"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
|
||||
@ -79,6 +93,7 @@
|
||||
android:icon="@mipmap/ic_play_window"
|
||||
android:label="@string/godot_game_activity_name"
|
||||
android:exported="false"
|
||||
android:autoRemoveFromRecents="true"
|
||||
android:screenOrientation="landscape"
|
||||
android:resizeableActivity="false"
|
||||
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen">
|
||||
|
@ -38,23 +38,32 @@ import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.*
|
||||
import android.preference.PreferenceManager
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.window.layout.WindowMetricsCalculator
|
||||
import org.godotengine.editor.embed.EmbeddedGodotGame
|
||||
import org.godotengine.editor.embed.GameMenuFragment
|
||||
import org.godotengine.editor.utils.signApk
|
||||
import org.godotengine.editor.utils.verifyApk
|
||||
import org.godotengine.godot.BuildConfig
|
||||
import org.godotengine.godot.GodotActivity
|
||||
import org.godotengine.godot.GodotLib
|
||||
import org.godotengine.godot.error.Error
|
||||
import org.godotengine.godot.utils.DialogUtils
|
||||
import org.godotengine.godot.utils.GameMenuUtils
|
||||
import org.godotengine.godot.utils.GameMenuUtils.GameEmbedMode
|
||||
import org.godotengine.godot.utils.GameMenuUtils.fetchGameEmbedMode
|
||||
import org.godotengine.godot.utils.PermissionsUtil
|
||||
import org.godotengine.godot.utils.ProcessPhoenix
|
||||
import org.godotengine.godot.utils.isNativeXRDevice
|
||||
import java.util.*
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
@ -64,32 +73,32 @@ import kotlin.math.min
|
||||
* Each derived activity runs in its own process, which enable up to have several instances of
|
||||
* the Godot engine up and running at the same time.
|
||||
*/
|
||||
abstract class BaseGodotEditor : GodotActivity() {
|
||||
abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListener {
|
||||
|
||||
companion object {
|
||||
private val TAG = BaseGodotEditor::class.java.simpleName
|
||||
|
||||
private const val WAIT_FOR_DEBUGGER = false
|
||||
|
||||
@JvmStatic
|
||||
protected val EXTRA_PIP_AVAILABLE = "pip_available"
|
||||
@JvmStatic
|
||||
protected val EXTRA_LAUNCH_IN_PIP = "launch_in_pip_requested"
|
||||
internal const val EXTRA_EDITOR_HINT = "editor_hint"
|
||||
internal const val EXTRA_PROJECT_MANAGER_HINT = "project_manager_hint"
|
||||
internal const val EXTRA_GAME_MENU_STATE = "game_menu_state"
|
||||
internal const val EXTRA_IS_GAME_EMBEDDED = "is_game_embedded"
|
||||
internal const val EXTRA_IS_GAME_RUNNING = "is_game_running"
|
||||
|
||||
// Command line arguments
|
||||
// Command line arguments.
|
||||
private const val FULLSCREEN_ARG = "--fullscreen"
|
||||
private const val FULLSCREEN_ARG_SHORT = "-f"
|
||||
internal const val EDITOR_ARG = "--editor"
|
||||
internal const val EDITOR_ARG_SHORT = "-e"
|
||||
internal const val EDITOR_PROJECT_MANAGER_ARG = "--project-manager"
|
||||
internal const val EDITOR_PROJECT_MANAGER_ARG_SHORT = "-p"
|
||||
internal const val BREAKPOINTS_ARG = "--breakpoints"
|
||||
internal const val BREAKPOINTS_ARG_SHORT = "-b"
|
||||
private const val EDITOR_ARG = "--editor"
|
||||
private const val EDITOR_ARG_SHORT = "-e"
|
||||
private const val EDITOR_PROJECT_MANAGER_ARG = "--project-manager"
|
||||
private const val EDITOR_PROJECT_MANAGER_ARG_SHORT = "-p"
|
||||
internal const val XR_MODE_ARG = "--xr-mode"
|
||||
|
||||
// Info for the various classes used by the editor
|
||||
// Info for the various classes used by the editor.
|
||||
internal val EDITOR_MAIN_INFO = EditorWindowInfo(GodotEditor::class.java, 777, "")
|
||||
internal val RUN_GAME_INFO = EditorWindowInfo(GodotGame::class.java, 667, ":GodotGame", LaunchPolicy.AUTO, true)
|
||||
internal val RUN_GAME_INFO = EditorWindowInfo(GodotGame::class.java, 667, ":GodotGame", LaunchPolicy.AUTO)
|
||||
internal val EMBEDDED_RUN_GAME_INFO = EditorWindowInfo(EmbeddedGodotGame::class.java, 2667, ":EmbeddedGodotGame")
|
||||
internal val XR_RUN_GAME_INFO = EditorWindowInfo(GodotXRGame::class.java, 1667, ":GodotXRGame")
|
||||
|
||||
/** Default behavior, means we check project settings **/
|
||||
@ -114,22 +123,54 @@ abstract class BaseGodotEditor : GodotActivity() {
|
||||
private const val ANDROID_WINDOW_AUTO = 0
|
||||
private const val ANDROID_WINDOW_SAME_AS_EDITOR = 1
|
||||
private const val ANDROID_WINDOW_SIDE_BY_SIDE_WITH_EDITOR = 2
|
||||
private const val ANDROID_WINDOW_SAME_AS_EDITOR_AND_LAUNCH_IN_PIP_MODE = 3
|
||||
|
||||
/**
|
||||
* Sets of constants to specify the Play window PiP mode.
|
||||
*
|
||||
* Should match the values in `editor/editor_settings.cpp'` for the
|
||||
* 'run/window_placement/play_window_pip_mode' setting.
|
||||
*/
|
||||
private const val PLAY_WINDOW_PIP_DISABLED = 0
|
||||
private const val PLAY_WINDOW_PIP_ENABLED = 1
|
||||
private const val PLAY_WINDOW_PIP_ENABLED_FOR_SAME_AS_EDITOR = 2
|
||||
// Game menu constants.
|
||||
internal const val KEY_GAME_MENU_ACTION = "key_game_menu_action"
|
||||
internal const val KEY_GAME_MENU_ACTION_PARAM1 = "key_game_menu_action_param1"
|
||||
|
||||
internal const val GAME_MENU_ACTION_SET_SUSPEND = "setSuspend"
|
||||
internal const val GAME_MENU_ACTION_NEXT_FRAME = "nextFrame"
|
||||
internal const val GAME_MENU_ACTION_SET_NODE_TYPE = "setNodeType"
|
||||
internal const val GAME_MENU_ACTION_SET_SELECT_MODE = "setSelectMode"
|
||||
internal const val GAME_MENU_ACTION_SET_SELECTION_VISIBLE = "setSelectionVisible"
|
||||
internal const val GAME_MENU_ACTION_SET_CAMERA_OVERRIDE = "setCameraOverride"
|
||||
internal const val GAME_MENU_ACTION_SET_CAMERA_MANIPULATE_MODE = "setCameraManipulateMode"
|
||||
internal const val GAME_MENU_ACTION_RESET_CAMERA_2D_POSITION = "resetCamera2DPosition"
|
||||
internal const val GAME_MENU_ACTION_RESET_CAMERA_3D_POSITION = "resetCamera3DPosition"
|
||||
internal const val GAME_MENU_ACTION_EMBED_GAME_ON_PLAY = "embedGameOnPlay"
|
||||
|
||||
private const val GAME_WORKSPACE = "Game"
|
||||
|
||||
internal const val SNACKBAR_SHOW_DURATION_MS = 5000L
|
||||
|
||||
private const val PREF_KEY_DONT_SHOW_GAME_RESUME_HINT = "pref_key_dont_show_game_resume_hint"
|
||||
}
|
||||
|
||||
private val editorMessageDispatcher = EditorMessageDispatcher(this)
|
||||
internal val editorMessageDispatcher = EditorMessageDispatcher(this)
|
||||
private val editorLoadingIndicator: View? by lazy { findViewById(R.id.editor_loading_indicator) }
|
||||
|
||||
private val embeddedGameViewContainerWindow: View? by lazy { findViewById<View?>(R.id.embedded_game_view_container_window)?.apply {
|
||||
setOnClickListener {
|
||||
// Hide the game menu screen overlay.
|
||||
it.isVisible = false
|
||||
}
|
||||
|
||||
// Prevent the game menu screen overlay from hiding when clicking inside of the panel bounds.
|
||||
findViewById<View?>(R.id.embedded_game_view_container)?.isClickable = true
|
||||
} }
|
||||
private val embeddedGameStateLabel: TextView? by lazy { findViewById<TextView?>(R.id.embedded_game_state_label)?.apply {
|
||||
setOnClickListener {
|
||||
godot?.runOnRenderThread {
|
||||
GameMenuUtils.playMainScene()
|
||||
}
|
||||
}
|
||||
} }
|
||||
protected val gameMenuContainer: View? by lazy {
|
||||
findViewById(R.id.game_menu_fragment_container)
|
||||
}
|
||||
protected var gameMenuFragment: GameMenuFragment? = null
|
||||
protected val gameMenuState = Bundle()
|
||||
|
||||
override fun getGodotAppLayout() = R.layout.godot_editor_layout
|
||||
|
||||
internal open fun getEditorWindowInfo() = EDITOR_MAIN_INFO
|
||||
@ -187,6 +228,30 @@ abstract class BaseGodotEditor : GodotActivity() {
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Add the game menu bar.
|
||||
setupGameMenuBar()
|
||||
}
|
||||
|
||||
protected open fun shouldShowGameMenuBar() = gameMenuContainer != null
|
||||
|
||||
private fun setupGameMenuBar() {
|
||||
if (shouldShowGameMenuBar()) {
|
||||
var currentFragment = supportFragmentManager.findFragmentById(R.id.game_menu_fragment_container)
|
||||
if (currentFragment !is GameMenuFragment) {
|
||||
Log.v(TAG, "Creating game menu fragment instance")
|
||||
currentFragment = GameMenuFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putBundle(EXTRA_GAME_MENU_STATE, gameMenuState)
|
||||
}
|
||||
}
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.game_menu_fragment_container, currentFragment, GameMenuFragment.TAG)
|
||||
.commitNowAllowingStateLoss()
|
||||
}
|
||||
|
||||
gameMenuFragment = currentFragment
|
||||
}
|
||||
}
|
||||
|
||||
override fun onGodotSetupCompleted() {
|
||||
@ -211,8 +276,32 @@ abstract class BaseGodotEditor : GodotActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (getEditorWindowInfo() == EDITOR_MAIN_INFO &&
|
||||
godot?.isEditorHint() == true &&
|
||||
(editorMessageDispatcher.hasEditorConnection(EMBEDDED_RUN_GAME_INFO) ||
|
||||
editorMessageDispatcher.hasEditorConnection(RUN_GAME_INFO))) {
|
||||
// If this is the editor window, and this is not the project manager, and we have a running game, then show
|
||||
// a hint for how to resume the playing game.
|
||||
val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
||||
if (!sharedPrefs.getBoolean(PREF_KEY_DONT_SHOW_GAME_RESUME_HINT, false)) {
|
||||
DialogUtils.showSnackbar(
|
||||
this,
|
||||
getString(R.string.show_game_resume_hint),
|
||||
SNACKBAR_SHOW_DURATION_MS,
|
||||
getString(R.string.dont_show_again_message)
|
||||
) {
|
||||
sharedPrefs.edit {
|
||||
putBoolean(PREF_KEY_DONT_SHOW_GAME_RESUME_HINT, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
protected override fun updateCommandLineParams(args: Array<String>) {
|
||||
override fun updateCommandLineParams(args: Array<String>) {
|
||||
val args = if (BuildConfig.BUILD_TYPE == "dev") {
|
||||
args + "--benchmark"
|
||||
} else {
|
||||
@ -221,7 +310,7 @@ abstract class BaseGodotEditor : GodotActivity() {
|
||||
super.updateCommandLineParams(args);
|
||||
}
|
||||
|
||||
protected open fun retrieveEditorWindowInfo(args: Array<String>): EditorWindowInfo {
|
||||
protected fun retrieveEditorWindowInfo(args: Array<String>, gameEmbedMode: GameEmbedMode): EditorWindowInfo {
|
||||
var hasEditor = false
|
||||
var xrMode = XR_MODE_DEFAULT
|
||||
|
||||
@ -238,12 +327,22 @@ abstract class BaseGodotEditor : GodotActivity() {
|
||||
return if (hasEditor) {
|
||||
EDITOR_MAIN_INFO
|
||||
} else {
|
||||
// Launching a game.
|
||||
val openxrEnabled = xrMode == XR_MODE_ON ||
|
||||
(xrMode == XR_MODE_DEFAULT && GodotLib.getGlobal("xr/openxr/enabled").toBoolean())
|
||||
if (openxrEnabled && isNativeXRDevice(applicationContext)) {
|
||||
XR_RUN_GAME_INFO
|
||||
} else {
|
||||
RUN_GAME_INFO
|
||||
if (godot?.isProjectManagerHint() == true || isNativeXRDevice(applicationContext)) {
|
||||
RUN_GAME_INFO
|
||||
} else {
|
||||
val resolvedEmbedMode = resolveGameEmbedModeIfNeeded(gameEmbedMode)
|
||||
if (resolvedEmbedMode == GameEmbedMode.DISABLED) {
|
||||
RUN_GAME_INFO
|
||||
} else {
|
||||
EMBEDDED_RUN_GAME_INFO
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -253,20 +352,21 @@ abstract class BaseGodotEditor : GodotActivity() {
|
||||
RUN_GAME_INFO.windowId -> RUN_GAME_INFO
|
||||
EDITOR_MAIN_INFO.windowId -> EDITOR_MAIN_INFO
|
||||
XR_RUN_GAME_INFO.windowId -> XR_RUN_GAME_INFO
|
||||
EMBEDDED_RUN_GAME_INFO.windowId -> EMBEDDED_RUN_GAME_INFO
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
protected fun getNewGodotInstanceIntent(editorWindowInfo: EditorWindowInfo, args: Array<String>): Intent {
|
||||
// If we're launching an editor window (project manager or editor) and we're in
|
||||
// fullscreen mode, we want to remain in fullscreen mode.
|
||||
// This doesn't apply to the play / game window since for that window fullscreen is
|
||||
// controlled by the game logic.
|
||||
val updatedArgs = if (editorWindowInfo == EDITOR_MAIN_INFO &&
|
||||
godot?.isInImmersiveMode() == true &&
|
||||
!args.contains(FULLSCREEN_ARG) &&
|
||||
!args.contains(FULLSCREEN_ARG_SHORT)
|
||||
) {
|
||||
// If we're launching an editor window (project manager or editor) and we're in
|
||||
// fullscreen mode, we want to remain in fullscreen mode.
|
||||
// This doesn't apply to the play / game window since for that window fullscreen is
|
||||
// controlled by the game logic.
|
||||
args + FULLSCREEN_ARG
|
||||
} else {
|
||||
args
|
||||
@ -278,40 +378,28 @@ abstract class BaseGodotEditor : GodotActivity() {
|
||||
.putExtra(EXTRA_COMMAND_LINE_PARAMS, updatedArgs)
|
||||
|
||||
val launchPolicy = resolveLaunchPolicyIfNeeded(editorWindowInfo.launchPolicy)
|
||||
val isPiPAvailable = if (editorWindowInfo.supportsPiPMode && hasPiPSystemFeature()) {
|
||||
val pipMode = getPlayWindowPiPMode()
|
||||
pipMode == PLAY_WINDOW_PIP_ENABLED ||
|
||||
(pipMode == PLAY_WINDOW_PIP_ENABLED_FOR_SAME_AS_EDITOR &&
|
||||
(launchPolicy == LaunchPolicy.SAME || launchPolicy == LaunchPolicy.SAME_AND_LAUNCH_IN_PIP_MODE))
|
||||
} else {
|
||||
false
|
||||
}
|
||||
newInstance.putExtra(EXTRA_PIP_AVAILABLE, isPiPAvailable)
|
||||
|
||||
var launchInPiP = false
|
||||
if (launchPolicy == LaunchPolicy.ADJACENT) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
Log.v(TAG, "Adding flag for adjacent launch")
|
||||
newInstance.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT)
|
||||
}
|
||||
} else if (launchPolicy == LaunchPolicy.SAME) {
|
||||
launchInPiP = isPiPAvailable &&
|
||||
(updatedArgs.contains(BREAKPOINTS_ARG) || updatedArgs.contains(BREAKPOINTS_ARG_SHORT))
|
||||
} else if (launchPolicy == LaunchPolicy.SAME_AND_LAUNCH_IN_PIP_MODE) {
|
||||
launchInPiP = isPiPAvailable
|
||||
}
|
||||
|
||||
if (launchInPiP) {
|
||||
Log.v(TAG, "Launching in PiP mode")
|
||||
newInstance.putExtra(EXTRA_LAUNCH_IN_PIP, launchInPiP)
|
||||
}
|
||||
return newInstance
|
||||
}
|
||||
|
||||
override fun onNewGodotInstanceRequested(args: Array<String>): Int {
|
||||
val editorWindowInfo = retrieveEditorWindowInfo(args)
|
||||
final override fun onNewGodotInstanceRequested(args: Array<String>): Int {
|
||||
val editorWindowInfo = retrieveEditorWindowInfo(args, fetchGameEmbedMode())
|
||||
|
||||
// Check if this editor window is being terminated. If it's, delay the creation of a new instance until the
|
||||
// termination is complete.
|
||||
if (editorMessageDispatcher.isPendingForceQuit(editorWindowInfo)) {
|
||||
Log.v(TAG, "Scheduling new launch after termination of ${editorWindowInfo.windowId}")
|
||||
editorMessageDispatcher.runTaskAfterForceQuit(editorWindowInfo) {
|
||||
onNewGodotInstanceRequested(args)
|
||||
}
|
||||
return editorWindowInfo.windowId
|
||||
}
|
||||
|
||||
// Launch a new activity
|
||||
val sourceView = godotFragment?.view
|
||||
val activityOptions = if (sourceView == null) {
|
||||
null
|
||||
@ -322,6 +410,12 @@ abstract class BaseGodotEditor : GodotActivity() {
|
||||
}
|
||||
|
||||
val newInstance = getNewGodotInstanceIntent(editorWindowInfo, args)
|
||||
newInstance.apply {
|
||||
putExtra(EXTRA_EDITOR_HINT, godot?.isEditorHint() == true)
|
||||
putExtra(EXTRA_PROJECT_MANAGER_HINT, godot?.isProjectManagerHint() == true)
|
||||
putExtra(EXTRA_GAME_MENU_STATE, gameMenuState)
|
||||
}
|
||||
|
||||
if (editorWindowInfo.windowClassName == javaClass.name) {
|
||||
Log.d(TAG, "Restarting ${editorWindowInfo.windowClassName} with parameters ${args.contentToString()}")
|
||||
triggerRebirth(activityOptions?.toBundle(), newInstance)
|
||||
@ -344,7 +438,7 @@ abstract class BaseGodotEditor : GodotActivity() {
|
||||
}
|
||||
|
||||
// Send an inter-process message to request the target editor window to force quit.
|
||||
if (editorMessageDispatcher.requestForceQuit(editorWindowInfo.windowId)) {
|
||||
if (editorMessageDispatcher.requestForceQuit(editorWindowInfo)) {
|
||||
return true
|
||||
}
|
||||
|
||||
@ -402,58 +496,57 @@ abstract class BaseGodotEditor : GodotActivity() {
|
||||
protected open fun enablePanAndScaleGestures() =
|
||||
java.lang.Boolean.parseBoolean(GodotLib.getEditorSetting("interface/touchscreen/enable_pan_and_scale_gestures"))
|
||||
|
||||
/**
|
||||
* Retrieves the play window pip mode editor setting.
|
||||
*/
|
||||
private fun getPlayWindowPiPMode(): Int {
|
||||
return try {
|
||||
Integer.parseInt(GodotLib.getEditorSetting("run/window_placement/play_window_pip_mode"))
|
||||
} catch (e: NumberFormatException) {
|
||||
PLAY_WINDOW_PIP_ENABLED_FOR_SAME_AS_EDITOR
|
||||
private fun resolveGameEmbedModeIfNeeded(embedMode: GameEmbedMode): GameEmbedMode {
|
||||
return when (embedMode) {
|
||||
GameEmbedMode.AUTO -> {
|
||||
val inMultiWindowMode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
isInMultiWindowMode
|
||||
} else {
|
||||
false
|
||||
}
|
||||
if (inMultiWindowMode || isLargeScreen || isNativeXRDevice(applicationContext)) {
|
||||
GameEmbedMode.DISABLED
|
||||
} else {
|
||||
GameEmbedMode.ENABLED
|
||||
}
|
||||
}
|
||||
|
||||
else -> embedMode
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If the launch policy is [LaunchPolicy.AUTO], resolve it into a specific policy based on the
|
||||
* editor setting or device and screen metrics.
|
||||
*
|
||||
* If the launch policy is [LaunchPolicy.SAME_AND_LAUNCH_IN_PIP_MODE] but PIP is not supported, fallback to the default
|
||||
* launch policy.
|
||||
*/
|
||||
private fun resolveLaunchPolicyIfNeeded(policy: LaunchPolicy): LaunchPolicy {
|
||||
val inMultiWindowMode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
isInMultiWindowMode
|
||||
} else {
|
||||
false
|
||||
}
|
||||
val defaultLaunchPolicy = if (inMultiWindowMode || isLargeScreen) {
|
||||
LaunchPolicy.ADJACENT
|
||||
} else {
|
||||
LaunchPolicy.SAME
|
||||
}
|
||||
|
||||
return when (policy) {
|
||||
LaunchPolicy.AUTO -> {
|
||||
if (isNativeXRDevice(applicationContext)) {
|
||||
// Native XR devices are more desktop-like and have support for launching adjacent
|
||||
// windows. So we always want to launch in adjacent mode when auto is selected.
|
||||
val inMultiWindowMode = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
isInMultiWindowMode
|
||||
} else {
|
||||
false
|
||||
}
|
||||
val defaultLaunchPolicy = if (inMultiWindowMode || isLargeScreen || isNativeXRDevice(applicationContext)) {
|
||||
LaunchPolicy.ADJACENT
|
||||
} else {
|
||||
try {
|
||||
when (Integer.parseInt(GodotLib.getEditorSetting("run/window_placement/android_window"))) {
|
||||
ANDROID_WINDOW_SAME_AS_EDITOR -> LaunchPolicy.SAME
|
||||
ANDROID_WINDOW_SIDE_BY_SIDE_WITH_EDITOR -> LaunchPolicy.ADJACENT
|
||||
ANDROID_WINDOW_SAME_AS_EDITOR_AND_LAUNCH_IN_PIP_MODE -> LaunchPolicy.SAME_AND_LAUNCH_IN_PIP_MODE
|
||||
else -> {
|
||||
// ANDROID_WINDOW_AUTO
|
||||
defaultLaunchPolicy
|
||||
}
|
||||
LaunchPolicy.SAME
|
||||
}
|
||||
|
||||
try {
|
||||
when (Integer.parseInt(GodotLib.getEditorSetting("run/window_placement/android_window"))) {
|
||||
ANDROID_WINDOW_SAME_AS_EDITOR -> LaunchPolicy.SAME
|
||||
ANDROID_WINDOW_SIDE_BY_SIDE_WITH_EDITOR -> LaunchPolicy.ADJACENT
|
||||
|
||||
else -> {
|
||||
// ANDROID_WINDOW_AUTO
|
||||
defaultLaunchPolicy
|
||||
}
|
||||
} catch (e: NumberFormatException) {
|
||||
Log.w(TAG, "Error parsing the Android window placement editor setting", e)
|
||||
// Fall-back to the default launch policy
|
||||
defaultLaunchPolicy
|
||||
}
|
||||
} catch (e: NumberFormatException) {
|
||||
Log.w(TAG, "Error parsing the Android window placement editor setting", e)
|
||||
// Fall-back to the default launch policy.
|
||||
defaultLaunchPolicy
|
||||
}
|
||||
}
|
||||
|
||||
@ -463,14 +556,6 @@ abstract class BaseGodotEditor : GodotActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true the if the device supports picture-in-picture (PiP)
|
||||
*/
|
||||
protected open fun hasPiPSystemFeature(): Boolean {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
|
||||
packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
// Check if we got the MANAGE_EXTERNAL_STORAGE permission
|
||||
@ -558,4 +643,184 @@ abstract class BaseGodotEditor : GodotActivity() {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
internal fun onEditorConnected(connectedEditorId: Int) {
|
||||
when (connectedEditorId) {
|
||||
EMBEDDED_RUN_GAME_INFO.windowId, RUN_GAME_INFO.windowId -> {
|
||||
runOnUiThread {
|
||||
embeddedGameViewContainerWindow?.isVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
XR_RUN_GAME_INFO.windowId -> {
|
||||
runOnUiThread {
|
||||
updateEmbeddedGameView(true, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateEmbeddedGameView(gameRunning: Boolean, gameEmbedded: Boolean) {
|
||||
if (gameRunning) {
|
||||
embeddedGameStateLabel?.apply {
|
||||
setText(R.string.running_game_not_embedded_message)
|
||||
setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
|
||||
isClickable = false
|
||||
}
|
||||
} else {
|
||||
embeddedGameStateLabel?.apply{
|
||||
setText(R.string.embedded_game_not_running_message)
|
||||
setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, R.drawable.play_48dp)
|
||||
isClickable = true
|
||||
}
|
||||
}
|
||||
|
||||
gameMenuState.putBoolean(EXTRA_IS_GAME_EMBEDDED, gameEmbedded)
|
||||
gameMenuState.putBoolean(EXTRA_IS_GAME_RUNNING, gameRunning)
|
||||
gameMenuFragment?.refreshGameMenu(gameMenuState)
|
||||
}
|
||||
|
||||
override fun onEditorWorkspaceSelected(workspace: String) {
|
||||
if (workspace == GAME_WORKSPACE && shouldShowGameMenuBar()) {
|
||||
if (editorMessageDispatcher.bringEditorWindowToFront(EMBEDDED_RUN_GAME_INFO) || editorMessageDispatcher.bringEditorWindowToFront(RUN_GAME_INFO)) {
|
||||
return
|
||||
}
|
||||
|
||||
val xrGameRunning = editorMessageDispatcher.hasEditorConnection(XR_RUN_GAME_INFO)
|
||||
val gameEmbedMode = resolveGameEmbedModeIfNeeded(fetchGameEmbedMode())
|
||||
runOnUiThread {
|
||||
updateEmbeddedGameView(xrGameRunning, gameEmbedMode != GameEmbedMode.DISABLED)
|
||||
embeddedGameViewContainerWindow?.isVisible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal open fun bringSelfToFront() {
|
||||
runOnUiThread {
|
||||
Log.v(TAG, "Bringing self to front")
|
||||
val relaunchIntent = Intent(intent)
|
||||
// Don't restart.
|
||||
relaunchIntent.putExtra(EXTRA_NEW_LAUNCH, false)
|
||||
startActivity(relaunchIntent)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun parseGameMenuAction(actionData: Bundle) {
|
||||
val action = actionData.getString(KEY_GAME_MENU_ACTION) ?: return
|
||||
when (action) {
|
||||
GAME_MENU_ACTION_SET_SUSPEND -> {
|
||||
val suspended = actionData.getBoolean(KEY_GAME_MENU_ACTION_PARAM1)
|
||||
suspendGame(suspended)
|
||||
}
|
||||
GAME_MENU_ACTION_NEXT_FRAME -> {
|
||||
dispatchNextFrame()
|
||||
}
|
||||
GAME_MENU_ACTION_SET_NODE_TYPE -> {
|
||||
val nodeType = actionData.getSerializable(KEY_GAME_MENU_ACTION_PARAM1) as GameMenuFragment.GameMenuListener.NodeType?
|
||||
if (nodeType != null) {
|
||||
selectRuntimeNode(nodeType)
|
||||
}
|
||||
}
|
||||
GAME_MENU_ACTION_SET_SELECTION_VISIBLE -> {
|
||||
val enabled = actionData.getBoolean(KEY_GAME_MENU_ACTION_PARAM1)
|
||||
toggleSelectionVisibility(enabled)
|
||||
}
|
||||
GAME_MENU_ACTION_SET_CAMERA_OVERRIDE -> {
|
||||
val enabled = actionData.getBoolean(KEY_GAME_MENU_ACTION_PARAM1)
|
||||
overrideCamera(enabled)
|
||||
}
|
||||
GAME_MENU_ACTION_SET_SELECT_MODE -> {
|
||||
val selectMode = actionData.getSerializable(KEY_GAME_MENU_ACTION_PARAM1) as GameMenuFragment.GameMenuListener.SelectMode?
|
||||
if (selectMode != null) {
|
||||
selectRuntimeNodeSelectMode(selectMode)
|
||||
}
|
||||
}
|
||||
GAME_MENU_ACTION_RESET_CAMERA_2D_POSITION -> {
|
||||
reset2DCamera()
|
||||
}
|
||||
GAME_MENU_ACTION_RESET_CAMERA_3D_POSITION -> {
|
||||
reset3DCamera()
|
||||
}
|
||||
GAME_MENU_ACTION_SET_CAMERA_MANIPULATE_MODE -> {
|
||||
val mode = actionData.getSerializable(KEY_GAME_MENU_ACTION_PARAM1) as? GameMenuFragment.GameMenuListener.CameraMode?
|
||||
if (mode != null) {
|
||||
manipulateCamera(mode)
|
||||
}
|
||||
}
|
||||
GAME_MENU_ACTION_EMBED_GAME_ON_PLAY -> {
|
||||
val embedded = actionData.getBoolean(KEY_GAME_MENU_ACTION_PARAM1)
|
||||
embedGameOnPlay(embedded)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun suspendGame(suspended: Boolean) {
|
||||
gameMenuState.putBoolean(GAME_MENU_ACTION_SET_SUSPEND, suspended)
|
||||
godot?.runOnRenderThread {
|
||||
GameMenuUtils.setSuspend(suspended)
|
||||
}
|
||||
}
|
||||
|
||||
override fun dispatchNextFrame() {
|
||||
godot?.runOnRenderThread {
|
||||
GameMenuUtils.nextFrame()
|
||||
}
|
||||
}
|
||||
|
||||
override fun toggleSelectionVisibility(enabled: Boolean) {
|
||||
gameMenuState.putBoolean(GAME_MENU_ACTION_SET_SELECTION_VISIBLE, enabled)
|
||||
godot?.runOnRenderThread {
|
||||
GameMenuUtils.setSelectionVisible(enabled)
|
||||
}
|
||||
}
|
||||
|
||||
override fun overrideCamera(enabled: Boolean) {
|
||||
gameMenuState.putBoolean(GAME_MENU_ACTION_SET_CAMERA_OVERRIDE, enabled)
|
||||
godot?.runOnRenderThread {
|
||||
GameMenuUtils.setCameraOverride(enabled)
|
||||
}
|
||||
}
|
||||
|
||||
override fun selectRuntimeNode(nodeType: GameMenuFragment.GameMenuListener.NodeType) {
|
||||
gameMenuState.putSerializable(GAME_MENU_ACTION_SET_NODE_TYPE, nodeType)
|
||||
godot?.runOnRenderThread {
|
||||
GameMenuUtils.setNodeType(nodeType.ordinal)
|
||||
}
|
||||
}
|
||||
|
||||
override fun selectRuntimeNodeSelectMode(selectMode: GameMenuFragment.GameMenuListener.SelectMode) {
|
||||
gameMenuState.putSerializable(GAME_MENU_ACTION_SET_SELECT_MODE, selectMode)
|
||||
godot?.runOnRenderThread {
|
||||
GameMenuUtils.setSelectMode(selectMode.ordinal)
|
||||
}
|
||||
}
|
||||
|
||||
override fun reset2DCamera() {
|
||||
godot?.runOnRenderThread {
|
||||
GameMenuUtils.resetCamera2DPosition()
|
||||
}
|
||||
}
|
||||
|
||||
override fun reset3DCamera() {
|
||||
godot?.runOnRenderThread {
|
||||
GameMenuUtils.resetCamera3DPosition()
|
||||
}
|
||||
}
|
||||
|
||||
override fun manipulateCamera(mode: GameMenuFragment.GameMenuListener.CameraMode) {
|
||||
gameMenuState.putSerializable(GAME_MENU_ACTION_SET_CAMERA_MANIPULATE_MODE, mode)
|
||||
godot?.runOnRenderThread {
|
||||
GameMenuUtils.setCameraManipulateMode(mode.ordinal)
|
||||
}
|
||||
}
|
||||
|
||||
override fun embedGameOnPlay(embedded: Boolean) {
|
||||
gameMenuState.putBoolean(GAME_MENU_ACTION_EMBED_GAME_ON_PLAY, embedded)
|
||||
godot?.runOnRenderThread {
|
||||
val gameEmbedMode = if (embedded) GameEmbedMode.ENABLED else GameEmbedMode.DISABLED
|
||||
GameMenuUtils.saveGameEmbedMode(gameEmbedMode)
|
||||
}
|
||||
}
|
||||
|
||||
override fun isGameEmbeddingSupported() = !isNativeXRDevice(applicationContext)
|
||||
}
|
||||
|
@ -0,0 +1,104 @@
|
||||
/**************************************************************************/
|
||||
/* BaseGodotGame.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.editor
|
||||
|
||||
import android.Manifest
|
||||
import android.util.Log
|
||||
import androidx.annotation.CallSuper
|
||||
import org.godotengine.godot.GodotLib
|
||||
import org.godotengine.godot.utils.GameMenuUtils
|
||||
import org.godotengine.godot.utils.PermissionsUtil
|
||||
import org.godotengine.godot.utils.ProcessPhoenix
|
||||
|
||||
/**
|
||||
* Base class for the Godot play windows.
|
||||
*/
|
||||
abstract class BaseGodotGame: GodotEditor() {
|
||||
companion object {
|
||||
private val TAG = BaseGodotGame::class.java.simpleName
|
||||
}
|
||||
|
||||
override fun enableLongPressGestures() = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_long_press_as_right_click"))
|
||||
|
||||
override fun enablePanAndScaleGestures() = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_pan_and_scale_gestures"))
|
||||
|
||||
override fun onGodotSetupCompleted() {
|
||||
super.onGodotSetupCompleted()
|
||||
Log.v(TAG, "OnGodotSetupCompleted")
|
||||
|
||||
// Check if we should be running in XR instead (if available) as it's possible we were
|
||||
// launched from the project manager which doesn't have that information.
|
||||
val launchingArgs = intent.getStringArrayExtra(EXTRA_COMMAND_LINE_PARAMS)
|
||||
if (launchingArgs != null) {
|
||||
val editorWindowInfo = retrieveEditorWindowInfo(launchingArgs, getEditorGameEmbedMode())
|
||||
if (editorWindowInfo != getEditorWindowInfo()) {
|
||||
val relaunchIntent = getNewGodotInstanceIntent(editorWindowInfo, launchingArgs)
|
||||
relaunchIntent.putExtra(EXTRA_NEW_LAUNCH, true)
|
||||
.putExtra(EditorMessageDispatcher.EXTRA_MSG_DISPATCHER_PAYLOAD, intent.getBundleExtra(EditorMessageDispatcher.EXTRA_MSG_DISPATCHER_PAYLOAD))
|
||||
|
||||
Log.d(TAG, "Relaunching XR project using ${editorWindowInfo.windowClassName} with parameters ${launchingArgs.contentToString()}")
|
||||
val godot = godot
|
||||
if (godot != null) {
|
||||
godot.destroyAndKillProcess {
|
||||
ProcessPhoenix.triggerRebirth(this, relaunchIntent)
|
||||
}
|
||||
} else {
|
||||
ProcessPhoenix.triggerRebirth(this, relaunchIntent)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Request project runtime permissions if necessary.
|
||||
val permissionsToEnable = getProjectPermissionsToEnable()
|
||||
if (permissionsToEnable.isNotEmpty()) {
|
||||
PermissionsUtil.requestPermissions(this, permissionsToEnable)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for project permissions to enable.
|
||||
*/
|
||||
@CallSuper
|
||||
protected open fun getProjectPermissionsToEnable(): MutableList<String> {
|
||||
val permissionsToEnable = mutableListOf<String>()
|
||||
|
||||
// Check for RECORD_AUDIO permission.
|
||||
val audioInputEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("audio/driver/enable_input"))
|
||||
if (audioInputEnabled) {
|
||||
permissionsToEnable.add(Manifest.permission.RECORD_AUDIO)
|
||||
}
|
||||
|
||||
return permissionsToEnable
|
||||
}
|
||||
|
||||
protected open fun getEditorGameEmbedMode() = GameMenuUtils.GameEmbedMode.AUTO
|
||||
}
|
@ -73,15 +73,33 @@ internal class EditorMessageDispatcher(private val editor: BaseGodotEditor) {
|
||||
* Requests the recipient to store the passed [android.os.Messenger] instance.
|
||||
*/
|
||||
private const val MSG_REGISTER_MESSENGER = 1
|
||||
|
||||
/**
|
||||
* Requests the recipient to dispatch the given game menu action.
|
||||
*/
|
||||
private const val MSG_DISPATCH_GAME_MENU_ACTION = 2
|
||||
|
||||
/**
|
||||
* Requests the recipient resumes itself / brings itself to front.
|
||||
*/
|
||||
private const val MSG_BRING_SELF_TO_FRONT = 3
|
||||
}
|
||||
|
||||
private val recipientsMessengers = ConcurrentHashMap<Int, Messenger>()
|
||||
private data class EditorConnectionInfo(
|
||||
val messenger: Messenger,
|
||||
var pendingForceQuit: Boolean = false,
|
||||
val scheduledTasksPendingForceQuit: HashSet<Runnable> = HashSet()
|
||||
)
|
||||
private val editorConnectionsInfos = ConcurrentHashMap<Int, EditorConnectionInfo>()
|
||||
|
||||
@SuppressLint("HandlerLeak")
|
||||
private val dispatcherHandler = object : Handler() {
|
||||
override fun handleMessage(msg: Message) {
|
||||
when (msg.what) {
|
||||
MSG_FORCE_QUIT -> editor.finish()
|
||||
MSG_FORCE_QUIT -> {
|
||||
Log.v(TAG, "Force quitting ${editor.getEditorWindowInfo().windowId}")
|
||||
editor.finishAndRemoveTask()
|
||||
}
|
||||
|
||||
MSG_REGISTER_MESSENGER -> {
|
||||
val editorId = msg.arg1
|
||||
@ -89,28 +107,100 @@ internal class EditorMessageDispatcher(private val editor: BaseGodotEditor) {
|
||||
registerMessenger(editorId, messenger)
|
||||
}
|
||||
|
||||
MSG_DISPATCH_GAME_MENU_ACTION -> {
|
||||
val actionData = msg.data
|
||||
if (actionData != null) {
|
||||
editor.parseGameMenuAction(actionData)
|
||||
}
|
||||
}
|
||||
|
||||
MSG_BRING_SELF_TO_FRONT -> editor.bringSelfToFront()
|
||||
|
||||
else -> super.handleMessage(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun hasEditorConnection(editorWindow: EditorWindowInfo) = editorConnectionsInfos.containsKey(editorWindow.windowId)
|
||||
|
||||
/**
|
||||
* Request the window with the given [editorId] to force quit.
|
||||
* Request the window with the given [editorWindow] to force quit.
|
||||
*/
|
||||
fun requestForceQuit(editorId: Int): Boolean {
|
||||
val messenger = recipientsMessengers[editorId] ?: return false
|
||||
fun requestForceQuit(editorWindow: EditorWindowInfo): Boolean {
|
||||
val editorId = editorWindow.windowId
|
||||
val info = editorConnectionsInfos[editorId] ?: return false
|
||||
if (info.pendingForceQuit) {
|
||||
return true
|
||||
}
|
||||
|
||||
val messenger = info.messenger
|
||||
return try {
|
||||
Log.v(TAG, "Requesting 'forceQuit' for $editorId")
|
||||
val msg = Message.obtain(null, MSG_FORCE_QUIT)
|
||||
messenger.send(msg)
|
||||
info.pendingForceQuit = true
|
||||
|
||||
true
|
||||
} catch (e: RemoteException) {
|
||||
Log.e(TAG, "Error requesting 'forceQuit' to $editorId", e)
|
||||
recipientsMessengers.remove(editorId)
|
||||
cleanEditorConnection(editorId)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
internal fun isPendingForceQuit(editorWindow: EditorWindowInfo): Boolean {
|
||||
return editorConnectionsInfos[editorWindow.windowId]?.pendingForceQuit == true
|
||||
}
|
||||
|
||||
internal fun runTaskAfterForceQuit(editorWindow: EditorWindowInfo, task: Runnable) {
|
||||
val connectionInfo = editorConnectionsInfos[editorWindow.windowId]
|
||||
if (connectionInfo == null || !connectionInfo.pendingForceQuit) {
|
||||
task.run()
|
||||
} else {
|
||||
connectionInfo.scheduledTasksPendingForceQuit.add(task)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the given [editorWindow] to bring itself to front / resume itself.
|
||||
*
|
||||
* Returns true if the request was successfully dispatched, false otherwise.
|
||||
*/
|
||||
fun bringEditorWindowToFront(editorWindow: EditorWindowInfo): Boolean {
|
||||
val editorId = editorWindow.windowId
|
||||
val info = editorConnectionsInfos[editorId] ?: return false
|
||||
val messenger = info.messenger
|
||||
return try {
|
||||
Log.v(TAG, "Requesting 'bringSelfToFront' for $editorId")
|
||||
val msg = Message.obtain(null, MSG_BRING_SELF_TO_FRONT)
|
||||
messenger.send(msg)
|
||||
true
|
||||
} catch (e: RemoteException) {
|
||||
Log.e(TAG, "Error requesting 'bringSelfToFront' to $editorId", e)
|
||||
cleanEditorConnection(editorId)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch a game menu action to another editor instance.
|
||||
*/
|
||||
fun dispatchGameMenuAction(editorWindow: EditorWindowInfo, actionData: Bundle) {
|
||||
val editorId = editorWindow.windowId
|
||||
val info = editorConnectionsInfos[editorId] ?: return
|
||||
val messenger = info.messenger
|
||||
try {
|
||||
Log.d(TAG, "Dispatch game menu action to $editorId")
|
||||
val msg = Message.obtain(null, MSG_DISPATCH_GAME_MENU_ACTION).apply {
|
||||
data = actionData
|
||||
}
|
||||
messenger.send(msg)
|
||||
} catch (e: RemoteException) {
|
||||
Log.e(TAG, "Error dispatching game menu action to $editorId", e)
|
||||
cleanEditorConnection(editorId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility method to register a receiver messenger.
|
||||
*/
|
||||
@ -121,14 +211,23 @@ internal class EditorMessageDispatcher(private val editor: BaseGodotEditor) {
|
||||
} else if (messenger.binder.isBinderAlive) {
|
||||
messenger.binder.linkToDeath({
|
||||
Log.v(TAG, "Removing messenger for $editorId")
|
||||
recipientsMessengers.remove(editorId)
|
||||
cleanEditorConnection(editorId)
|
||||
messengerDeathCallback?.run()
|
||||
}, 0)
|
||||
recipientsMessengers[editorId] = messenger
|
||||
editorConnectionsInfos[editorId] = EditorConnectionInfo(messenger)
|
||||
editor.onEditorConnected(editorId)
|
||||
}
|
||||
} catch (e: RemoteException) {
|
||||
Log.e(TAG, "Unable to register messenger from $editorId", e)
|
||||
recipientsMessengers.remove(editorId)
|
||||
cleanEditorConnection(editorId)
|
||||
}
|
||||
}
|
||||
|
||||
private fun cleanEditorConnection(editorId: Int) {
|
||||
val connectionInfo = editorConnectionsInfos.remove(editorId) ?: return
|
||||
Log.v(TAG, "Cleaning info for recipient $editorId")
|
||||
for (task in connectionInfo.scheduledTasksPendingForceQuit) {
|
||||
task.run()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -48,12 +48,7 @@ enum class LaunchPolicy {
|
||||
/**
|
||||
* Adjacent launches are enabled.
|
||||
*/
|
||||
ADJACENT,
|
||||
|
||||
/**
|
||||
* Launches happen in the same window but start in PiP mode.
|
||||
*/
|
||||
SAME_AND_LAUNCH_IN_PIP_MODE
|
||||
ADJACENT
|
||||
}
|
||||
|
||||
/**
|
||||
@ -63,14 +58,12 @@ data class EditorWindowInfo(
|
||||
val windowClassName: String,
|
||||
val windowId: Int,
|
||||
val processNameSuffix: String,
|
||||
val launchPolicy: LaunchPolicy = LaunchPolicy.SAME,
|
||||
val supportsPiPMode: Boolean = false
|
||||
val launchPolicy: LaunchPolicy = LaunchPolicy.SAME
|
||||
) {
|
||||
constructor(
|
||||
windowClass: Class<*>,
|
||||
windowId: Int,
|
||||
processNameSuffix: String,
|
||||
launchPolicy: LaunchPolicy = LaunchPolicy.SAME,
|
||||
supportsPiPMode: Boolean = false
|
||||
) : this(windowClass.name, windowId, processNameSuffix, launchPolicy, supportsPiPMode)
|
||||
launchPolicy: LaunchPolicy = LaunchPolicy.SAME
|
||||
) : this(windowClass.name, windowId, processNameSuffix, launchPolicy)
|
||||
}
|
||||
|
@ -30,77 +30,59 @@
|
||||
|
||||
package org.godotengine.editor
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PictureInPictureParams
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Rect
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.annotation.CallSuper
|
||||
import org.godotengine.godot.GodotLib
|
||||
import org.godotengine.godot.utils.PermissionsUtil
|
||||
import androidx.core.view.isVisible
|
||||
import org.godotengine.editor.embed.GameMenuFragment
|
||||
import org.godotengine.godot.utils.GameMenuUtils
|
||||
import org.godotengine.godot.utils.ProcessPhoenix
|
||||
import org.godotengine.godot.utils.isNativeXRDevice
|
||||
|
||||
/**
|
||||
* Drives the 'run project' window of the Godot Editor.
|
||||
*/
|
||||
open class GodotGame : GodotEditor() {
|
||||
open class GodotGame : BaseGodotGame() {
|
||||
|
||||
companion object {
|
||||
private val TAG = GodotGame::class.java.simpleName
|
||||
}
|
||||
|
||||
private val gameViewSourceRectHint = Rect()
|
||||
private val pipButton: View? by lazy {
|
||||
findViewById(R.id.godot_pip_button)
|
||||
}
|
||||
private val expandGameMenuButton: View? by lazy { findViewById(R.id.game_menu_expand_button) }
|
||||
|
||||
private var pipAvailable = false
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
gameMenuState.clear()
|
||||
intent.getBundleExtra(EXTRA_GAME_MENU_STATE)?.let {
|
||||
gameMenuState.putAll(it)
|
||||
}
|
||||
gameMenuState.putBoolean(EXTRA_IS_GAME_EMBEDDED, isGameEmbedded())
|
||||
gameMenuState.putBoolean(EXTRA_IS_GAME_RUNNING, true)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
gameMenuContainer?.isVisible = shouldShowGameMenuBar()
|
||||
expandGameMenuButton?.apply{
|
||||
isVisible = shouldShowGameMenuBar() && isMenuBarCollapsable()
|
||||
setOnClickListener {
|
||||
gameMenuFragment?.expandGameMenu()
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val gameView = findViewById<View>(R.id.godot_fragment_container)
|
||||
gameView?.addOnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
|
||||
gameView.getGlobalVisibleRect(gameViewSourceRectHint)
|
||||
}
|
||||
}
|
||||
|
||||
pipButton?.setOnClickListener { enterPiPMode() }
|
||||
|
||||
handleStartIntent(intent)
|
||||
}
|
||||
|
||||
override fun onNewIntent(newIntent: Intent) {
|
||||
super.onNewIntent(newIntent)
|
||||
handleStartIntent(newIntent)
|
||||
}
|
||||
|
||||
private fun handleStartIntent(intent: Intent) {
|
||||
pipAvailable = intent.getBooleanExtra(EXTRA_PIP_AVAILABLE, pipAvailable)
|
||||
updatePiPButtonVisibility()
|
||||
|
||||
val pipLaunchRequested = intent.getBooleanExtra(EXTRA_LAUNCH_IN_PIP, false)
|
||||
if (pipLaunchRequested) {
|
||||
enterPiPMode()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updatePiPButtonVisibility() {
|
||||
pipButton?.visibility = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && pipAvailable && !isInPictureInPictureMode) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun enterPiPMode() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && pipAvailable) {
|
||||
override fun enterPiPMode() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && hasPiPSystemFeature()) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val builder = PictureInPictureParams.Builder().setSourceRectHint(gameViewSourceRectHint)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
@ -114,10 +96,27 @@ open class GodotGame : GodotEditor() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true the if the device supports picture-in-picture (PiP).
|
||||
*/
|
||||
protected fun hasPiPSystemFeature(): Boolean {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
|
||||
packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
|
||||
}
|
||||
|
||||
override fun shouldShowGameMenuBar(): Boolean {
|
||||
return intent.getBooleanExtra(
|
||||
EXTRA_EDITOR_HINT,
|
||||
false
|
||||
) && gameMenuContainer != null
|
||||
}
|
||||
|
||||
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
|
||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode)
|
||||
Log.v(TAG, "onPictureInPictureModeChanged: $isInPictureInPictureMode")
|
||||
updatePiPButtonVisibility()
|
||||
|
||||
// Hide the game menu fragment when in PiP.
|
||||
gameMenuContainer?.isVisible = !isInPictureInPictureMode
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
@ -134,59 +133,109 @@ open class GodotGame : GodotEditor() {
|
||||
|
||||
override fun getEditorWindowInfo() = RUN_GAME_INFO
|
||||
|
||||
override fun getEditorGameEmbedMode() = GameMenuUtils.GameEmbedMode.DISABLED
|
||||
|
||||
override fun overrideOrientationRequest() = false
|
||||
|
||||
override fun enableLongPressGestures() = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_long_press_as_right_click"))
|
||||
|
||||
override fun enablePanAndScaleGestures() = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_pan_and_scale_gestures"))
|
||||
|
||||
override fun onGodotSetupCompleted() {
|
||||
super.onGodotSetupCompleted()
|
||||
Log.v(TAG, "OnGodotSetupCompleted")
|
||||
|
||||
// Check if we should be running in XR instead (if available) as it's possible we were
|
||||
// launched from the project manager which doesn't have that information.
|
||||
val launchingArgs = intent.getStringArrayExtra(EXTRA_COMMAND_LINE_PARAMS)
|
||||
if (launchingArgs != null) {
|
||||
val editorWindowInfo = retrieveEditorWindowInfo(launchingArgs)
|
||||
if (editorWindowInfo != getEditorWindowInfo()) {
|
||||
val relaunchIntent = getNewGodotInstanceIntent(editorWindowInfo, launchingArgs)
|
||||
relaunchIntent.putExtra(EXTRA_NEW_LAUNCH, true)
|
||||
.putExtra(EditorMessageDispatcher.EXTRA_MSG_DISPATCHER_PAYLOAD, intent.getBundleExtra(EditorMessageDispatcher.EXTRA_MSG_DISPATCHER_PAYLOAD))
|
||||
|
||||
Log.d(TAG, "Relaunching XR project using ${editorWindowInfo.windowClassName} with parameters ${launchingArgs.contentToString()}")
|
||||
val godot = godot
|
||||
if (godot != null) {
|
||||
godot.destroyAndKillProcess {
|
||||
ProcessPhoenix.triggerRebirth(this, relaunchIntent)
|
||||
}
|
||||
} else {
|
||||
ProcessPhoenix.triggerRebirth(this, relaunchIntent)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Request project runtime permissions if necessary
|
||||
val permissionsToEnable = getProjectPermissionsToEnable()
|
||||
if (permissionsToEnable.isNotEmpty()) {
|
||||
PermissionsUtil.requestPermissions(this, permissionsToEnable)
|
||||
override fun suspendGame(suspended: Boolean) {
|
||||
val actionBundle = Bundle().apply {
|
||||
putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_SET_SUSPEND)
|
||||
putBoolean(KEY_GAME_MENU_ACTION_PARAM1, suspended)
|
||||
}
|
||||
editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for project permissions to enable
|
||||
*/
|
||||
@CallSuper
|
||||
protected open fun getProjectPermissionsToEnable(): MutableList<String> {
|
||||
val permissionsToEnable = mutableListOf<String>()
|
||||
|
||||
// Check for RECORD_AUDIO permission
|
||||
val audioInputEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("audio/driver/enable_input"))
|
||||
if (audioInputEnabled) {
|
||||
permissionsToEnable.add(Manifest.permission.RECORD_AUDIO)
|
||||
override fun dispatchNextFrame() {
|
||||
val actionBundle = Bundle().apply {
|
||||
putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_NEXT_FRAME)
|
||||
}
|
||||
|
||||
return permissionsToEnable
|
||||
editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
|
||||
}
|
||||
|
||||
override fun toggleSelectionVisibility(enabled: Boolean) {
|
||||
val actionBundle = Bundle().apply {
|
||||
putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_SET_SELECTION_VISIBLE)
|
||||
putBoolean(KEY_GAME_MENU_ACTION_PARAM1, enabled)
|
||||
}
|
||||
editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
|
||||
}
|
||||
|
||||
override fun overrideCamera(enabled: Boolean) {
|
||||
val actionBundle = Bundle().apply {
|
||||
putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_SET_CAMERA_OVERRIDE)
|
||||
putBoolean(KEY_GAME_MENU_ACTION_PARAM1, enabled)
|
||||
}
|
||||
editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
|
||||
}
|
||||
|
||||
override fun selectRuntimeNode(nodeType: GameMenuFragment.GameMenuListener.NodeType) {
|
||||
val actionBundle = Bundle().apply {
|
||||
putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_SET_NODE_TYPE)
|
||||
putSerializable(KEY_GAME_MENU_ACTION_PARAM1, nodeType)
|
||||
}
|
||||
editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
|
||||
}
|
||||
|
||||
override fun selectRuntimeNodeSelectMode(selectMode: GameMenuFragment.GameMenuListener.SelectMode) {
|
||||
val actionBundle = Bundle().apply {
|
||||
putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_SET_SELECT_MODE)
|
||||
putSerializable(KEY_GAME_MENU_ACTION_PARAM1, selectMode)
|
||||
}
|
||||
editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
|
||||
}
|
||||
|
||||
override fun reset2DCamera() {
|
||||
val actionBundle = Bundle().apply {
|
||||
putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_RESET_CAMERA_2D_POSITION)
|
||||
}
|
||||
editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
|
||||
}
|
||||
|
||||
override fun reset3DCamera() {
|
||||
val actionBundle = Bundle().apply {
|
||||
putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_RESET_CAMERA_3D_POSITION)
|
||||
}
|
||||
editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
|
||||
}
|
||||
|
||||
override fun manipulateCamera(mode: GameMenuFragment.GameMenuListener.CameraMode) {
|
||||
val actionBundle = Bundle().apply {
|
||||
putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_SET_CAMERA_MANIPULATE_MODE)
|
||||
putSerializable(KEY_GAME_MENU_ACTION_PARAM1, mode)
|
||||
}
|
||||
editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
|
||||
}
|
||||
|
||||
override fun embedGameOnPlay(embedded: Boolean) {
|
||||
val actionBundle = Bundle().apply {
|
||||
putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_EMBED_GAME_ON_PLAY)
|
||||
putBoolean(KEY_GAME_MENU_ACTION_PARAM1, embedded)
|
||||
}
|
||||
editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
|
||||
}
|
||||
|
||||
protected open fun isGameEmbedded() = false
|
||||
|
||||
override fun isGameEmbeddingSupported() = !isNativeXRDevice(applicationContext)
|
||||
|
||||
override fun isMinimizedButtonEnabled() = isTaskRoot && !isNativeXRDevice(applicationContext)
|
||||
|
||||
override fun isCloseButtonEnabled() = !isNativeXRDevice(applicationContext)
|
||||
|
||||
override fun isPiPButtonEnabled() = hasPiPSystemFeature()
|
||||
|
||||
override fun isMenuBarCollapsable() = true
|
||||
|
||||
override fun minimizeGameWindow() {
|
||||
moveTaskToBack(false)
|
||||
}
|
||||
|
||||
override fun closeGameWindow() {
|
||||
ProcessPhoenix.forceQuit(this)
|
||||
}
|
||||
|
||||
override fun onGameMenuCollapsed(collapsed: Boolean) {
|
||||
expandGameMenuButton?.isVisible = shouldShowGameMenuBar() && isMenuBarCollapsable() && collapsed
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ import org.godotengine.godot.xr.XRMode
|
||||
/**
|
||||
* Provide support for running XR apps / games from the editor window.
|
||||
*/
|
||||
open class GodotXRGame: GodotGame() {
|
||||
open class GodotXRGame: BaseGodotGame() {
|
||||
|
||||
override fun overrideOrientationRequest() = true
|
||||
|
||||
@ -56,6 +56,8 @@ open class GodotXRGame: GodotGame() {
|
||||
|
||||
override fun getEditorWindowInfo() = XR_RUN_GAME_INFO
|
||||
|
||||
override fun getGodotAppLayout() = R.layout.godot_xr_game_layout
|
||||
|
||||
override fun getProjectPermissionsToEnable(): MutableList<String> {
|
||||
val permissionsToEnable = super.getProjectPermissionsToEnable()
|
||||
|
||||
|
@ -0,0 +1,147 @@
|
||||
/**************************************************************************/
|
||||
/* EmbeddedGodotGame.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.editor.embed
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.MotionEvent
|
||||
import android.view.WindowManager
|
||||
import android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND
|
||||
import android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
|
||||
import android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
|
||||
import org.godotengine.editor.GodotGame
|
||||
import org.godotengine.editor.R
|
||||
import org.godotengine.godot.utils.GameMenuUtils
|
||||
|
||||
/**
|
||||
* Host the Godot game from the editor when the embedded mode is enabled.
|
||||
*/
|
||||
class EmbeddedGodotGame : GodotGame() {
|
||||
|
||||
companion object {
|
||||
private val TAG = EmbeddedGodotGame::class.java.simpleName
|
||||
|
||||
private const val FULL_SCREEN_WIDTH = WindowManager.LayoutParams.MATCH_PARENT
|
||||
private const val FULL_SCREEN_HEIGHT = WindowManager.LayoutParams.MATCH_PARENT
|
||||
}
|
||||
|
||||
private val defaultWidthInPx : Int by lazy {
|
||||
resources.getDimensionPixelSize(R.dimen.embed_game_window_default_width)
|
||||
}
|
||||
private val defaultHeightInPx : Int by lazy {
|
||||
resources.getDimensionPixelSize(R.dimen.embed_game_window_default_height)
|
||||
}
|
||||
|
||||
private var layoutWidthInPx = 0
|
||||
private var layoutHeightInPx = 0
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setFinishOnTouchOutside(false)
|
||||
|
||||
val layoutParams = window.attributes
|
||||
layoutParams.flags = layoutParams.flags or FLAG_NOT_TOUCH_MODAL or FLAG_WATCH_OUTSIDE_TOUCH
|
||||
layoutParams.flags = layoutParams.flags and FLAG_DIM_BEHIND.inv()
|
||||
layoutParams.gravity = Gravity.END or Gravity.BOTTOM
|
||||
|
||||
layoutWidthInPx = defaultWidthInPx
|
||||
layoutHeightInPx = defaultHeightInPx
|
||||
|
||||
layoutParams.width = layoutWidthInPx
|
||||
layoutParams.height = layoutHeightInPx
|
||||
window.attributes = layoutParams
|
||||
}
|
||||
|
||||
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_OUTSIDE -> {
|
||||
if (gameMenuFragment?.isAlwaysOnTop() == true) {
|
||||
enterPiPMode()
|
||||
} else {
|
||||
minimizeGameWindow()
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
// val layoutParams = window.attributes
|
||||
// TODO: Add logic to move the embedded window.
|
||||
// window.attributes = layoutParams
|
||||
}
|
||||
}
|
||||
return super.dispatchTouchEvent(event)
|
||||
}
|
||||
|
||||
override fun getEditorWindowInfo() = EMBEDDED_RUN_GAME_INFO
|
||||
|
||||
override fun getEditorGameEmbedMode() = GameMenuUtils.GameEmbedMode.ENABLED
|
||||
|
||||
override fun isGameEmbedded() = true
|
||||
|
||||
private fun updateWindowDimensions(widthInPx: Int, heightInPx: Int) {
|
||||
val layoutParams = window.attributes
|
||||
layoutParams.width = widthInPx
|
||||
layoutParams.height = heightInPx
|
||||
window.attributes = layoutParams
|
||||
}
|
||||
|
||||
override fun isMinimizedButtonEnabled() = true
|
||||
|
||||
override fun isCloseButtonEnabled() = true
|
||||
|
||||
override fun isFullScreenButtonEnabled() = true
|
||||
|
||||
override fun isPiPButtonEnabled() = false
|
||||
|
||||
override fun isMenuBarCollapsable() = false
|
||||
|
||||
override fun isAlwaysOnTopSupported() = hasPiPSystemFeature()
|
||||
|
||||
override fun onFullScreenUpdated(enabled: Boolean) {
|
||||
godot?.enableImmersiveMode(enabled)
|
||||
if (enabled) {
|
||||
layoutWidthInPx = FULL_SCREEN_WIDTH
|
||||
layoutHeightInPx = FULL_SCREEN_HEIGHT
|
||||
} else {
|
||||
layoutWidthInPx = defaultWidthInPx
|
||||
layoutHeightInPx = defaultHeightInPx
|
||||
}
|
||||
updateWindowDimensions(layoutWidthInPx, layoutHeightInPx)
|
||||
}
|
||||
|
||||
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
|
||||
super.onPictureInPictureModeChanged(isInPictureInPictureMode)
|
||||
// Maximize the dimensions when entering PiP so the window fills the full PiP bounds.
|
||||
onFullScreenUpdated(isInPictureInPictureMode)
|
||||
}
|
||||
|
||||
override fun shouldShowGameMenuBar() = gameMenuContainer != null
|
||||
}
|
@ -0,0 +1,480 @@
|
||||
/**************************************************************************/
|
||||
/* GameMenuFragment.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.editor.embed
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.PopupMenu
|
||||
import android.widget.RadioButton
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.Fragment
|
||||
import org.godotengine.editor.BaseGodotEditor
|
||||
import org.godotengine.editor.BaseGodotEditor.Companion.SNACKBAR_SHOW_DURATION_MS
|
||||
import org.godotengine.editor.R
|
||||
import org.godotengine.godot.utils.DialogUtils
|
||||
|
||||
/**
|
||||
* Implements the game menu interface for the Android editor.
|
||||
*/
|
||||
class GameMenuFragment : Fragment(), PopupMenu.OnMenuItemClickListener {
|
||||
|
||||
companion object {
|
||||
val TAG = GameMenuFragment::class.java.simpleName
|
||||
|
||||
private const val PREF_KEY_ALWAYS_ON_TOP = "pref_key_always_on_top"
|
||||
private const val PREF_KEY_DONT_SHOW_RESTART_GAME_HINT = "pref_key_dont_show_restart_game_hint"
|
||||
private const val PREF_KEY_GAME_MENU_BAR_COLLAPSED = "pref_key_game_menu_bar_collapsed"
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to be notified of events fired when interacting with the game menu.
|
||||
*/
|
||||
interface GameMenuListener {
|
||||
|
||||
/**
|
||||
* Kotlin representation of the RuntimeNodeSelect::SelectMode enum in 'scene/debugger/scene_debugger.h'.
|
||||
*/
|
||||
enum class SelectMode {
|
||||
SINGLE,
|
||||
LIST
|
||||
}
|
||||
|
||||
/**
|
||||
* Kotlin representation of the RuntimeNodeSelect::NodeType enum in 'scene/debugger/scene_debugger.h'.
|
||||
*/
|
||||
enum class NodeType {
|
||||
NONE,
|
||||
TYPE_2D,
|
||||
TYPE_3D
|
||||
}
|
||||
|
||||
/**
|
||||
* Kotlin representation of the EditorDebuggerNode::CameraOverride in 'editor/debugger/editor_debugger_node.h'.
|
||||
*/
|
||||
enum class CameraMode {
|
||||
NONE,
|
||||
IN_GAME,
|
||||
EDITORS
|
||||
}
|
||||
|
||||
fun suspendGame(suspended: Boolean)
|
||||
fun dispatchNextFrame()
|
||||
fun toggleSelectionVisibility(enabled: Boolean)
|
||||
fun overrideCamera(enabled: Boolean)
|
||||
fun selectRuntimeNode(nodeType: NodeType)
|
||||
fun selectRuntimeNodeSelectMode(selectMode: SelectMode)
|
||||
fun reset2DCamera()
|
||||
fun reset3DCamera()
|
||||
fun manipulateCamera(mode: CameraMode)
|
||||
|
||||
fun isGameEmbeddingSupported(): Boolean
|
||||
fun embedGameOnPlay(embedded: Boolean)
|
||||
|
||||
fun enterPiPMode() {}
|
||||
fun minimizeGameWindow() {}
|
||||
fun closeGameWindow() {}
|
||||
|
||||
fun isMinimizedButtonEnabled() = false
|
||||
fun isFullScreenButtonEnabled() = false
|
||||
fun isCloseButtonEnabled() = false
|
||||
fun isPiPButtonEnabled() = false
|
||||
fun isMenuBarCollapsable() = false
|
||||
|
||||
fun isAlwaysOnTopSupported() = false
|
||||
|
||||
fun onFullScreenUpdated(enabled: Boolean) {}
|
||||
fun onGameMenuCollapsed(collapsed: Boolean) {}
|
||||
}
|
||||
|
||||
private val collapseMenuButton: View? by lazy {
|
||||
view?.findViewById(R.id.game_menu_collapse_button)
|
||||
}
|
||||
private val pauseButton: View? by lazy {
|
||||
view?.findViewById(R.id.game_menu_pause_button)
|
||||
}
|
||||
private val nextFrameButton: View? by lazy {
|
||||
view?.findViewById(R.id.game_menu_next_frame_button)
|
||||
}
|
||||
private val unselectNodesButton: RadioButton? by lazy {
|
||||
view?.findViewById(R.id.game_menu_unselect_nodes_button)
|
||||
}
|
||||
private val select2DNodesButton: RadioButton? by lazy {
|
||||
view?.findViewById(R.id.game_menu_select_2d_nodes_button)
|
||||
}
|
||||
private val select3DNodesButton: RadioButton? by lazy {
|
||||
view?.findViewById(R.id.game_menu_select_3d_nodes_button)
|
||||
}
|
||||
private val guiVisibilityButton: View? by lazy {
|
||||
view?.findViewById(R.id.game_menu_gui_visibility_button)
|
||||
}
|
||||
private val toolSelectButton: RadioButton? by lazy {
|
||||
view?.findViewById(R.id.game_menu_tool_select_button)
|
||||
}
|
||||
private val listSelectButton: RadioButton? by lazy {
|
||||
view?.findViewById(R.id.game_menu_list_select_button)
|
||||
}
|
||||
private val optionsButton: View? by lazy {
|
||||
view?.findViewById(R.id.game_menu_options_button)
|
||||
}
|
||||
private val minimizeButton: View? by lazy {
|
||||
view?.findViewById(R.id.game_menu_minimize_button)
|
||||
}
|
||||
private val pipButton: View? by lazy {
|
||||
view?.findViewById(R.id.game_menu_pip_button)
|
||||
}
|
||||
private val fullscreenButton: View? by lazy {
|
||||
view?.findViewById(R.id.game_menu_fullscreen_button)
|
||||
}
|
||||
private val closeButton: View? by lazy {
|
||||
view?.findViewById(R.id.game_menu_close_button)
|
||||
}
|
||||
|
||||
private val popupMenu: PopupMenu by lazy {
|
||||
PopupMenu(context, optionsButton).apply {
|
||||
setOnMenuItemClickListener(this@GameMenuFragment)
|
||||
inflate(R.menu.options_menu)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
menu.setGroupDividerEnabled(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val menuItemActionView: View by lazy {
|
||||
View(context)
|
||||
}
|
||||
private val menuItemActionExpandListener = object: MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private var menuListener: GameMenuListener? = null
|
||||
private var alwaysOnTopChecked = false
|
||||
private var isGameEmbedded = false
|
||||
private var isGameRunning = false
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
val parentActivity = activity
|
||||
if (parentActivity is GameMenuListener) {
|
||||
menuListener = parentActivity
|
||||
} else {
|
||||
val parentFragment = parentFragment
|
||||
if (parentFragment is GameMenuListener) {
|
||||
menuListener = parentFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDetach() {
|
||||
super.onDetach()
|
||||
menuListener = null
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, bundle: Bundle?): View? {
|
||||
return inflater.inflate(R.layout.game_menu_fragment_layout, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, bundle: Bundle?) {
|
||||
super.onViewCreated(view, bundle)
|
||||
|
||||
val isMinimizeButtonEnabled = menuListener?.isMinimizedButtonEnabled() == true
|
||||
val isFullScreenButtonEnabled = menuListener?.isFullScreenButtonEnabled() == true
|
||||
val isCloseButtonEnabled = menuListener?.isCloseButtonEnabled() == true
|
||||
val isPiPButtonEnabled = menuListener?.isPiPButtonEnabled() == true
|
||||
val isMenuBarCollapsable = menuListener?.isMenuBarCollapsable() == true
|
||||
|
||||
// Show the divider if any of the window controls is visible
|
||||
view.findViewById<View>(R.id.game_menu_window_controls_divider)?.isVisible =
|
||||
isMinimizeButtonEnabled ||
|
||||
isFullScreenButtonEnabled ||
|
||||
isCloseButtonEnabled ||
|
||||
isPiPButtonEnabled ||
|
||||
isMenuBarCollapsable
|
||||
|
||||
collapseMenuButton?.apply {
|
||||
isVisible = isMenuBarCollapsable
|
||||
setOnClickListener {
|
||||
collapseGameMenu()
|
||||
}
|
||||
}
|
||||
fullscreenButton?.apply{
|
||||
isVisible = isFullScreenButtonEnabled
|
||||
setOnClickListener {
|
||||
it.isActivated = !it.isActivated
|
||||
menuListener?.onFullScreenUpdated(it.isActivated)
|
||||
}
|
||||
}
|
||||
pipButton?.apply {
|
||||
isVisible = isPiPButtonEnabled
|
||||
setOnClickListener {
|
||||
menuListener?.enterPiPMode()
|
||||
}
|
||||
}
|
||||
minimizeButton?.apply {
|
||||
isVisible = isMinimizeButtonEnabled
|
||||
setOnClickListener {
|
||||
menuListener?.minimizeGameWindow()
|
||||
}
|
||||
}
|
||||
closeButton?.apply{
|
||||
isVisible = isCloseButtonEnabled
|
||||
setOnClickListener {
|
||||
menuListener?.closeGameWindow()
|
||||
}
|
||||
}
|
||||
pauseButton?.apply {
|
||||
setOnClickListener {
|
||||
val isActivated = !it.isActivated
|
||||
menuListener?.suspendGame(isActivated)
|
||||
it.isActivated = isActivated
|
||||
}
|
||||
}
|
||||
nextFrameButton?.apply {
|
||||
setOnClickListener {
|
||||
menuListener?.dispatchNextFrame()
|
||||
}
|
||||
}
|
||||
|
||||
unselectNodesButton?.apply{
|
||||
setOnCheckedChangeListener { buttonView, isChecked ->
|
||||
if (isChecked) {
|
||||
menuListener?.selectRuntimeNode(GameMenuListener.NodeType.NONE)
|
||||
}
|
||||
}
|
||||
}
|
||||
select2DNodesButton?.apply{
|
||||
setOnCheckedChangeListener { buttonView, isChecked ->
|
||||
if (isChecked) {
|
||||
menuListener?.selectRuntimeNode(GameMenuListener.NodeType.TYPE_2D)
|
||||
}
|
||||
}
|
||||
}
|
||||
select3DNodesButton?.apply{
|
||||
setOnCheckedChangeListener { buttonView, isChecked ->
|
||||
if (isChecked) {
|
||||
menuListener?.selectRuntimeNode(GameMenuListener.NodeType.TYPE_3D)
|
||||
}
|
||||
}
|
||||
}
|
||||
guiVisibilityButton?.apply{
|
||||
setOnClickListener {
|
||||
val isActivated = !it.isActivated
|
||||
menuListener?.toggleSelectionVisibility(!isActivated)
|
||||
it.isActivated = isActivated
|
||||
}
|
||||
}
|
||||
|
||||
toolSelectButton?.apply{
|
||||
setOnCheckedChangeListener { buttonView, isChecked ->
|
||||
if (isChecked) {
|
||||
menuListener?.selectRuntimeNodeSelectMode(GameMenuListener.SelectMode.SINGLE)
|
||||
}
|
||||
}
|
||||
}
|
||||
listSelectButton?.apply{
|
||||
setOnCheckedChangeListener { buttonView, isChecked ->
|
||||
if (isChecked) {
|
||||
menuListener?.selectRuntimeNodeSelectMode(GameMenuListener.SelectMode.LIST)
|
||||
}
|
||||
}
|
||||
}
|
||||
optionsButton?.setOnClickListener {
|
||||
popupMenu.show()
|
||||
}
|
||||
|
||||
refreshGameMenu(arguments?.getBundle(BaseGodotEditor.EXTRA_GAME_MENU_STATE) ?: Bundle())
|
||||
}
|
||||
|
||||
internal fun refreshGameMenu(gameMenuState: Bundle) {
|
||||
val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
if (menuListener?.isMenuBarCollapsable() == true) {
|
||||
val collapsed = sharedPrefs.getBoolean(PREF_KEY_GAME_MENU_BAR_COLLAPSED, false)
|
||||
view?.isVisible = !collapsed
|
||||
menuListener?.onGameMenuCollapsed(collapsed)
|
||||
}
|
||||
alwaysOnTopChecked = sharedPrefs.getBoolean(PREF_KEY_ALWAYS_ON_TOP, false)
|
||||
isGameEmbedded = gameMenuState.getBoolean(BaseGodotEditor.EXTRA_IS_GAME_EMBEDDED, false)
|
||||
isGameRunning = gameMenuState.getBoolean(BaseGodotEditor.EXTRA_IS_GAME_RUNNING, false)
|
||||
|
||||
pauseButton?.isEnabled = isGameRunning
|
||||
nextFrameButton?.isEnabled = isGameRunning
|
||||
|
||||
val nodeType = gameMenuState.getSerializable(BaseGodotEditor.GAME_MENU_ACTION_SET_NODE_TYPE) as GameMenuListener.NodeType? ?: GameMenuListener.NodeType.NONE
|
||||
unselectNodesButton?.isChecked = nodeType == GameMenuListener.NodeType.NONE
|
||||
select2DNodesButton?.isChecked = nodeType == GameMenuListener.NodeType.TYPE_2D
|
||||
select3DNodesButton?.isChecked = nodeType == GameMenuListener.NodeType.TYPE_3D
|
||||
|
||||
guiVisibilityButton?.isActivated = !gameMenuState.getBoolean(BaseGodotEditor.GAME_MENU_ACTION_SET_SELECTION_VISIBLE, true)
|
||||
|
||||
val selectMode = gameMenuState.getSerializable(BaseGodotEditor.GAME_MENU_ACTION_SET_SELECT_MODE) as GameMenuListener.SelectMode? ?: GameMenuListener.SelectMode.SINGLE
|
||||
toolSelectButton?.isChecked = selectMode == GameMenuListener.SelectMode.SINGLE
|
||||
listSelectButton?.isChecked = selectMode == GameMenuListener.SelectMode.LIST
|
||||
|
||||
popupMenu.menu.apply {
|
||||
if (menuListener?.isGameEmbeddingSupported() == false) {
|
||||
setGroupEnabled(R.id.group_menu_embed_options, false)
|
||||
setGroupVisible(R.id.group_menu_embed_options, false)
|
||||
} else {
|
||||
findItem(R.id.menu_embed_game_on_play)?.isChecked = isGameEmbedded
|
||||
|
||||
val keepOnTopMenuItem = findItem(R.id.menu_embed_game_keep_on_top)
|
||||
if (menuListener?.isAlwaysOnTopSupported() == false) {
|
||||
keepOnTopMenuItem?.isVisible = false
|
||||
} else {
|
||||
keepOnTopMenuItem?.isEnabled = isGameEmbedded
|
||||
}
|
||||
}
|
||||
|
||||
setGroupEnabled(R.id.group_menu_camera_options, isGameRunning)
|
||||
setGroupVisible(R.id.group_menu_camera_options, isGameRunning)
|
||||
findItem(R.id.menu_camera_options)?.isEnabled = false
|
||||
|
||||
findItem(R.id.menu_embed_game_keep_on_top)?.isChecked = alwaysOnTopChecked
|
||||
|
||||
val cameraMode = gameMenuState.getSerializable(BaseGodotEditor.GAME_MENU_ACTION_SET_CAMERA_MANIPULATE_MODE) as GameMenuListener.CameraMode? ?: GameMenuListener.CameraMode.NONE
|
||||
if (cameraMode == GameMenuListener.CameraMode.IN_GAME || cameraMode == GameMenuListener.CameraMode.NONE) {
|
||||
findItem(R.id.menu_manipulate_camera_in_game)?.isChecked = true
|
||||
} else {
|
||||
findItem(R.id.menu_manipulate_camera_from_editors)?.isChecked = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun isAlwaysOnTop() = isGameEmbedded && alwaysOnTopChecked
|
||||
|
||||
private fun collapseGameMenu() {
|
||||
view?.isVisible = false
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit {
|
||||
putBoolean(PREF_KEY_GAME_MENU_BAR_COLLAPSED, true)
|
||||
}
|
||||
menuListener?.onGameMenuCollapsed(true)
|
||||
}
|
||||
|
||||
internal fun expandGameMenu() {
|
||||
view?.isVisible = true
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit {
|
||||
putBoolean(PREF_KEY_GAME_MENU_BAR_COLLAPSED, false)
|
||||
}
|
||||
menuListener?.onGameMenuCollapsed(false)
|
||||
}
|
||||
|
||||
private fun updateAlwaysOnTop(enabled: Boolean) {
|
||||
alwaysOnTopChecked = enabled
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit {
|
||||
putBoolean(PREF_KEY_ALWAYS_ON_TOP, enabled)
|
||||
}
|
||||
}
|
||||
|
||||
private fun preventMenuItemCollapse(item: MenuItem) {
|
||||
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW)
|
||||
item.setActionView(menuItemActionView)
|
||||
item.setOnActionExpandListener(menuItemActionExpandListener)
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
if (!item.hasSubMenu()) {
|
||||
preventMenuItemCollapse(item)
|
||||
}
|
||||
|
||||
when(item.itemId) {
|
||||
R.id.menu_embed_game_on_play -> {
|
||||
item.isChecked = !item.isChecked
|
||||
menuListener?.embedGameOnPlay(item.isChecked)
|
||||
|
||||
if (item.isChecked != isGameEmbedded && isGameRunning) {
|
||||
activity?.let {
|
||||
val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
if (!sharedPrefs.getBoolean(PREF_KEY_DONT_SHOW_RESTART_GAME_HINT, false)) {
|
||||
DialogUtils.showSnackbar(
|
||||
it,
|
||||
if (item.isChecked) getString(R.string.restart_embed_game_hint) else getString(R.string.restart_non_embedded_game_hint),
|
||||
SNACKBAR_SHOW_DURATION_MS,
|
||||
getString(R.string.dont_show_again_message)
|
||||
) {
|
||||
sharedPrefs.edit {
|
||||
putBoolean(PREF_KEY_DONT_SHOW_RESTART_GAME_HINT, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
R.id.menu_embed_game_keep_on_top -> {
|
||||
item.isChecked = !item.isChecked
|
||||
updateAlwaysOnTop(item.isChecked)
|
||||
}
|
||||
|
||||
R.id.menu_camera_override -> {
|
||||
item.isChecked = !item.isChecked
|
||||
menuListener?.overrideCamera(item.isChecked)
|
||||
|
||||
popupMenu.menu.findItem(R.id.menu_camera_options)?.isEnabled = item.isChecked
|
||||
}
|
||||
|
||||
R.id.menu_reset_2d_camera -> {
|
||||
menuListener?.reset2DCamera()
|
||||
}
|
||||
|
||||
R.id.menu_reset_3d_camera -> {
|
||||
menuListener?.reset3DCamera()
|
||||
}
|
||||
|
||||
R.id.menu_manipulate_camera_in_game -> {
|
||||
if (!item.isChecked) {
|
||||
item.isChecked = true
|
||||
menuListener?.manipulateCamera(GameMenuListener.CameraMode.IN_GAME)
|
||||
}
|
||||
}
|
||||
|
||||
R.id.menu_manipulate_camera_from_editors -> {
|
||||
if (!item.isChecked) {
|
||||
item.isChecked = true
|
||||
menuListener?.manipulateCamera(GameMenuListener.CameraMode.EDITORS)
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:color="@color/game_menu_icon_activated_color" android:state_activated="true" />
|
||||
<item android:color="@color/game_menu_icon_activated_color" android:state_selected="true" />
|
||||
<item android:color="@color/game_menu_icon_activated_color" android:state_pressed="true" />
|
||||
<item android:color="@color/game_menu_icon_activated_color" android:state_checked="true" />
|
||||
<item android:color="@color/game_menu_icon_disabled_color" android:state_enabled="false" />
|
||||
<item android:color="@color/game_menu_icon_default_color" />
|
||||
</selector>
|
@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="@color/game_menu_icons_color_state" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
|
||||
|
||||
</vector>
|
@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="@color/game_menu_icons_color_state"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,8l-6,6 1.41,1.41L12,10.83l4.59,4.58L18,14z" />
|
||||
|
||||
</vector>
|
@ -1,12 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:tint="#FFFFFF"
|
||||
android:tint="@color/game_menu_icons_color_state"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M5,16h3v3h2v-5L5,14v2zM8,8L5,8v2h5L10,5L8,5v3zM14,19h2v-3h3v-2h-5v5zM16,8L16,5h-2v5h5L19,8h-3z" />
|
||||
android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z" />
|
||||
|
||||
</vector>
|
@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="@color/game_menu_icons_color_state" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M7,14L5,14v5h5v-2L7,17v-3zM5,10h2L7,7h3L10,5L5,5v5zM17,17h-3v2h5v-5h-2v3zM14,5v2h3v3h2L19,5h-5z"/>
|
||||
|
||||
</vector>
|
@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="@color/game_menu_icons_color_state" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M5,16h3v3h2v-5L5,14v2zM8,8L5,8v2h5L10,5L8,5v3zM14,19h2v-3h3v-2h-5v5zM16,8L16,5h-2v5h5L19,8h-3z"/>
|
||||
|
||||
</vector>
|
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item android:drawable="@drawable/baseline_fullscreen_exit_24" android:state_activated="true" />
|
||||
<item android:drawable="@drawable/baseline_fullscreen_24" />
|
||||
</selector>
|
@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="@color/game_menu_icons_color_state" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M6,19h12v2H6z"/>
|
||||
|
||||
</vector>
|
@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="@color/game_menu_icons_color_state" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M19,11h-8v6h8v-6zM23,19L23,4.98C23,3.88 22.1,3 21,3L3,3c-1.1,0 -2,0.88 -2,1.98L1,19c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2zM21,19.02L3,19.02L3,4.97h18v14.05z"/>
|
||||
|
||||
</vector>
|
@ -0,0 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="@color/game_menu_icons_color_state" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:fillType="evenOdd" android:pathData="M16,9V4l1,0c0.55,0 1,-0.45 1,-1v0c0,-0.55 -0.45,-1 -1,-1H7C6.45,2 6,2.45 6,3v0c0,0.55 0.45,1 1,1l1,0v5c0,1.66 -1.34,3 -3,3h0v2h5.97v7l1,1l1,-1v-7H19v-2h0C17.34,12 16,10.66 16,9z"/>
|
||||
|
||||
</vector>
|
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:pathData="M9,2a3,3 0,0 0,-3 2.777,3 3,0 1,0 -3,5.047V12a1,1 0,0 0,1 1h6a1,1 0,0 0,1 -1v-1l3,2V7l-3,2V7.23A3,3 0,0 0,9 2z"
|
||||
android:fillColor="@color/game_menu_icons_color_state"/>
|
||||
</vector>
|
@ -1,10 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
|
||||
<size
|
||||
android:width="60dp"
|
||||
android:height="60dp" />
|
||||
|
||||
<solid android:color="#44000000" />
|
||||
<solid android:color="#99aaaaaa" />
|
||||
</shape>
|
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/game_menu_selected_bg" android:state_pressed="true" />
|
||||
<item android:drawable="@drawable/game_menu_selected_bg" android:state_activated="true" />
|
||||
<item android:drawable="@drawable/game_menu_selected_bg" android:state_checked="true" />
|
||||
<item android:drawable="@color/game_menu_default_bg" />
|
||||
</selector>
|
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<solid android:color="#232b2b" />
|
||||
</shape>
|
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<solid android:color="#3333b5e5" />
|
||||
<corners android:radius="5dp" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="@android:color/holo_blue_dark" />
|
||||
</shape>
|
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/game_menu_selected_bg" android:state_pressed="true" />
|
||||
<item android:drawable="@drawable/game_menu_selected_bg" android:state_selected="true" />
|
||||
<item android:drawable="@color/game_menu_default_bg" />
|
||||
</selector>
|
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:pathData="M8,0a2,2 0,0 0,0 4,2 2,0 0,0 0,-4zM8,6a2,2 0,0 0,0 4,2 2,0 0,0 0,-4zM8,12a2,2 0,0 0,0 4,2 2,0 0,0 0,-4z"
|
||||
android:fillColor="@color/game_menu_icons_color_state"/>
|
||||
</vector>
|
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:pathData="m2.96,7.727 l-1.921,0.548c0.32,1.12 0.824,2.06 1.432,2.84l-0.834,0.834 1.414,1.414 0.843,-0.843c0.986,0.747 2.077,1.206 3.106,1.386V15h2v-1.094c1.029,-0.18 2.12,-0.639 3.105,-1.386l0.844,0.843 1.414,-1.414 -0.834,-0.834a8.285,8.285 0,0 0,1.432 -2.84l-1.922,-0.548C12.163,10.79 9.499,12 7.999,12s-4.163,-1.209 -5.038,-4.273z"
|
||||
android:fillColor="@color/game_menu_icon_activated_color"/>
|
||||
</vector>
|
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/gui_visibility_hidden" android:state_activated="true" />
|
||||
<item android:drawable="@drawable/gui_visibility_visible" />
|
||||
</selector>
|
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:pathData="M8,2C5.443,2 2.209,3.948 1.045,7.705a1,1 0,0 0,0 0.55C2.163,12.211 5.5,14 8,14s5.836,-1.789 6.961,-5.725a1,1 0,0 0,0 -0.55C13.861,3.935 10.554,2 8,2zM8,4a4,4 0,0 1,0 8,4 4,0 0,1 0,-8zM8,6a2,2 0,0 0,0 4,2 2,0 0,0 0,-4z"
|
||||
android:fillColor="@color/game_menu_icons_color_state"/>
|
||||
</vector>
|
@ -0,0 +1,24 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:pathData="M11.5,16v-1.5a1,1 0,0 0,-1 -1H10l1.6,-4a4,4 0,1 0,-3.5 -1.4l-2.2,5.4h-0.7a1,1 0,0 0,-1 1V16z"
|
||||
android:fillColor="@color/game_menu_icon_default_color"/>
|
||||
<path
|
||||
android:pathData="M5.25,12.2 L2,4a8,8 0,0 1,7 -3,5 5,0 0,0 -2.1,7.2z">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="2"
|
||||
android:startY="0"
|
||||
android:endX="9"
|
||||
android:endY="0"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#0069C4D4"/>
|
||||
<item android:offset="0.6" android:color="#FF69C4D4"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</vector>
|
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:pathData="m1,1v14h8.258l-0.822,-2h-5.436v-2h4.611l-0.822,-2h-3.789v-2h3.887a1.5,1.5 0,0 1,1.098 -0.498v-0.002a1.5,1.5 0,0 1,0.586 0.111l0.945,0.389h0.484v0.199l2,0.822v-7.022h-11zM3,3h7v2h-7zM8,8 L11.291,16 12.238,13.18 14.121,15.063 15.064,14.121 13.18,12.238 16,11.291 8,8z"
|
||||
android:fillColor="@color/game_menu_icons_color_state"/>
|
||||
</vector>
|
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:pathData="m12,3c-0.552,0 -1,0.448 -1,1v8c0,0.552 0.448,1 1,1h1c0.552,0 1,-0.448 1,-1V4C14,3.448 13.552,3 13,3ZM2.975,3.002C2.433,3.016 2.001,3.458 2,4v8c-0,0.839 0.97,1.305 1.625,0.781l5,-4c0.499,-0.4 0.499,-1.16 0,-1.56l-5,-4C3.441,3.074 3.211,2.996 2.975,3.002Z"
|
||||
android:fillColor="@color/game_menu_icons_color_state"/>
|
||||
</vector>
|
@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:pathData="M8,8m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#fc7f7f"/>
|
||||
</vector>
|
@ -0,0 +1,16 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:pathData="M8,13C5.239,13 3,10.761 3,8 3,5.239 5.239,3 8,3"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#8da5f3"/>
|
||||
<path
|
||||
android:pathData="m8,13c2.761,0 5,-2.239 5,-5C13,5.239 10.761,3 8,3"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#8eef97"/>
|
||||
</vector>
|
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:pathData="M4,3a1,1 0,0 0,-1 1v8a1,1 0,0 0,1 1h2a1,1 0,0 0,1 -1L7,4a1,1 0,0 0,-1 -1zM10,3a1,1 0,0 0,-1 1v8a1,1 0,0 0,1 1h2a1,1 0,0 0,1 -1L13,4a1,1 0,0 0,-1 -1z"
|
||||
android:fillColor="@color/game_menu_icons_color_state"/>
|
||||
</vector>
|
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/play" android:state_activated="true" />
|
||||
<item android:drawable="@drawable/pause" />
|
||||
</selector>
|
@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item android:drawable="@drawable/pip_button_activated_bg_drawable" android:state_pressed="true" />
|
||||
<item android:drawable="@drawable/pip_button_activated_bg_drawable" android:state_hovered="true" />
|
||||
|
||||
<item android:drawable="@drawable/pip_button_default_bg_drawable" />
|
||||
|
||||
</selector>
|
@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
|
||||
<size
|
||||
android:width="60dp"
|
||||
android:height="60dp" />
|
||||
|
||||
<solid android:color="#13000000" />
|
||||
</shape>
|
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:pathData="M4,12a1,1 0,0 0,1.555 0.832l6,-4a1,1 0,0 0,0 -1.664l-6,-4A1,1 0,0 0,4 4z"
|
||||
android:fillColor="@color/game_menu_icons_color_state"/>
|
||||
</vector>
|
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="48dp"
|
||||
android:height="48dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:pathData="M4,12a1,1 0,0 0,1.555 0.832l6,-4a1,1 0,0 0,0 -1.664l-6,-4A1,1 0,0 0,4 4z"
|
||||
android:fillColor="@color/game_menu_icons_color_state"/>
|
||||
</vector>
|
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="16dp"
|
||||
android:height="16dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:pathData="M14,6.932 L2,1.995l4.936,12 1.421,-4.23 2.826,2.825 1.412,-1.412L9.77,8.352z"
|
||||
android:fillColor="@color/game_menu_icons_color_state"/>
|
||||
</vector>
|
@ -0,0 +1,190 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:background="@android:color/black">
|
||||
|
||||
<HorizontalScrollView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="48dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/game_menu_pause_button"
|
||||
style="?android:attr/borderlessButtonStyle"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="@drawable/game_menu_button_bg"
|
||||
android:src="@drawable/pause_play_selector" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/game_menu_next_frame_button"
|
||||
style="?android:attr/borderlessButtonStyle"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="@drawable/game_menu_button_bg"
|
||||
android:src="@drawable/next_frame" />
|
||||
|
||||
<View
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginHorizontal="@dimen/game_menu_vseparator_horizontal_margin"
|
||||
android:layout_marginVertical="@dimen/game_menu_vseparator_vertical_margin"
|
||||
android:background="@color/game_menu_divider_color" />
|
||||
|
||||
<RadioGroup
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/game_menu_unselect_nodes_button"
|
||||
style="?android:attr/borderlessButtonStyle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="48dp"
|
||||
android:background="@drawable/game_menu_button_bg"
|
||||
android:button="@null"
|
||||
android:checked="true"
|
||||
android:drawableStart="@drawable/input_event_joypad_motion"
|
||||
android:padding="5dp"
|
||||
android:text="@string/game_menu_input_event_joypad_motion_label" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/game_menu_select_2d_nodes_button"
|
||||
style="?android:attr/borderlessButtonStyle"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="@drawable/game_menu_button_bg"
|
||||
android:button="@null"
|
||||
android:drawableStart="@drawable/nodes_2d"
|
||||
android:padding="5dp"
|
||||
android:text="@string/game_menu_nodes_2d_button_label" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/game_menu_select_3d_nodes_button"
|
||||
style="?android:attr/borderlessButtonStyle"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="@drawable/game_menu_button_bg"
|
||||
android:button="@null"
|
||||
android:drawableStart="@drawable/node_3d"
|
||||
android:padding="5dp"
|
||||
android:text="@string/game_menu_node_3d_button_label" />
|
||||
</RadioGroup>
|
||||
|
||||
<View
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginHorizontal="@dimen/game_menu_vseparator_horizontal_margin"
|
||||
android:layout_marginVertical="@dimen/game_menu_vseparator_vertical_margin"
|
||||
android:background="@color/game_menu_divider_color" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/game_menu_gui_visibility_button"
|
||||
style="?android:attr/borderlessButtonStyle"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="@drawable/game_menu_button_bg"
|
||||
android:src="@drawable/gui_visibility_selector" />
|
||||
|
||||
<View
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginHorizontal="@dimen/game_menu_vseparator_horizontal_margin"
|
||||
android:layout_marginVertical="@dimen/game_menu_vseparator_vertical_margin"
|
||||
android:background="@color/game_menu_divider_color" />
|
||||
|
||||
<RadioGroup
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/game_menu_tool_select_button"
|
||||
style="?android:attr/borderlessButtonStyle"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="@drawable/game_menu_button_bg"
|
||||
android:button="@null"
|
||||
android:checked="true"
|
||||
android:drawableStart="@drawable/tool_select"
|
||||
android:padding="15dp" />
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/game_menu_list_select_button"
|
||||
style="?android:attr/borderlessButtonStyle"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="@drawable/game_menu_button_bg"
|
||||
android:button="@null"
|
||||
android:drawableStart="@drawable/list_select"
|
||||
android:padding="15dp" />
|
||||
</RadioGroup>
|
||||
</LinearLayout>
|
||||
</HorizontalScrollView>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/game_menu_options_button"
|
||||
style="?android:attr/borderlessButtonStyle"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="@drawable/game_menu_button_bg"
|
||||
android:src="@drawable/gui_tab_menu" />
|
||||
|
||||
<View
|
||||
android:id="@+id/game_menu_window_controls_divider"
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginHorizontal="@dimen/game_menu_vseparator_horizontal_margin"
|
||||
android:layout_marginVertical="@dimen/game_menu_vseparator_vertical_margin"
|
||||
android:background="@color/game_menu_divider_color" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/game_menu_collapse_button"
|
||||
style="?android:attr/borderlessButtonStyle"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="@drawable/game_menu_selected_button_bg"
|
||||
android:src="@drawable/baseline_expand_less_24"
|
||||
/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/game_menu_minimize_button"
|
||||
style="?android:attr/borderlessButtonStyle"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="@drawable/game_menu_button_bg"
|
||||
android:src="@drawable/baseline_minimize_24"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/game_menu_pip_button"
|
||||
style="?android:attr/borderlessButtonStyle"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="@drawable/game_menu_selected_button_bg"
|
||||
android:src="@drawable/baseline_picture_in_picture_alt_24"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/game_menu_fullscreen_button"
|
||||
style="?android:attr/borderlessButtonStyle"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="@drawable/game_menu_selected_button_bg"
|
||||
android:src="@drawable/baseline_fullscreen_selector"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/game_menu_close_button"
|
||||
style="?android:attr/borderlessButtonStyle"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="@drawable/game_menu_button_bg"
|
||||
android:src="@drawable/baseline_close_24"/>
|
||||
</LinearLayout>
|
@ -1,14 +1,55 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:background="@android:color/black"
|
||||
tools:background="@android:color/background_light"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/godot_fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/embedded_game_view_container_window"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#22bebebe"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/embedded_game_view_container"
|
||||
android:layout_width="@dimen/embed_game_window_default_width"
|
||||
android:layout_height="@dimen/embed_game_window_default_height"
|
||||
android:background="@drawable/game_menu_message_bg"
|
||||
android:layout_gravity="bottom|end">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/game_menu_fragment_container"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/embedded_game_state_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/embedded_game_not_running_message"
|
||||
android:drawableBottom="@drawable/play_48dp"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintTop_toBottomOf="@+id/game_menu_fragment_container"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<ProgressBar
|
||||
style="@android:style/Widget.Holo.ProgressBar.Large"
|
||||
|
@ -1,25 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/game_menu_fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
/>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/godot_fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
android:layout_height="match_parent"
|
||||
android:layout_below="@+id/game_menu_fragment_container"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/godot_pip_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="36dp"
|
||||
android:contentDescription="@string/pip_button_description"
|
||||
android:background="@drawable/pip_button_bg_drawable"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/outline_fullscreen_exit_48"
|
||||
android:visibility="gone"
|
||||
android:layout_gravity="end|top"
|
||||
tools:visibility="visible" />
|
||||
<ImageButton
|
||||
android:id="@+id/game_menu_expand_button"
|
||||
style="?android:attr/borderlessButtonStyle"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="@drawable/expand_more_bg"
|
||||
android:src="@drawable/baseline_expand_more_48"
|
||||
android:layout_below="@+id/game_menu_fragment_container"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:layout_marginTop="24dp"
|
||||
/>
|
||||
|
||||
</FrameLayout>
|
||||
</RelativeLayout>
|
||||
|
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/godot_fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
</FrameLayout>
|
@ -0,0 +1,56 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<group android:id="@+id/group_menu_embed_options">
|
||||
<item
|
||||
android:id="@+id/menu_embed_game_on_play"
|
||||
android:checkable="true"
|
||||
android:checked="true"
|
||||
android:title="@string/menu_embed_game_on_play_label" />
|
||||
<item android:id="@+id/menu_embed_game_keep_on_top"
|
||||
android:checkable="true"
|
||||
android:checked="false"
|
||||
android:enabled="false"
|
||||
android:title="@string/menu_keep_embed_game_on_top_label"
|
||||
android:icon="@drawable/baseline_push_pin_24" />
|
||||
</group>
|
||||
|
||||
<group android:id="@+id/group_menu_camera_options">
|
||||
<item
|
||||
android:id="@+id/menu_camera_override"
|
||||
android:checkable="true"
|
||||
android:checked="false"
|
||||
android:icon="@drawable/camera"
|
||||
android:title="@string/menu_camera_override_label" />
|
||||
<item
|
||||
android:id="@+id/menu_camera_options"
|
||||
android:icon="@drawable/camera"
|
||||
android:enabled="false"
|
||||
android:title="@string/menu_camera_label">
|
||||
<menu>
|
||||
<group android:id="@+id/group_menu_camera_reset_mode">
|
||||
<item
|
||||
android:id="@+id/menu_reset_2d_camera"
|
||||
android:title="@string/menu_reset_2d_camera_label" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_reset_3d_camera"
|
||||
android:title="@string/menu_reset_3d_camera_label" />
|
||||
</group>
|
||||
|
||||
<group
|
||||
android:id="@+id/group_menu_camera_manipulation"
|
||||
android:checkableBehavior="single">
|
||||
<item
|
||||
android:id="@+id/menu_manipulate_camera_in_game"
|
||||
android:checked="true"
|
||||
android:title="@string/menu_manipulate_in_game_label" />
|
||||
<item
|
||||
android:id="@+id/menu_manipulate_camera_from_editors"
|
||||
android:title="@string/menu_manipulate_from_editors_label" />
|
||||
</group>
|
||||
</menu>
|
||||
</item>
|
||||
</group>
|
||||
|
||||
</menu>
|
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="game_menu_icon_default_color">#e0e0e0</color>
|
||||
<color name="game_menu_icon_disabled_color">@android:color/darker_gray</color>
|
||||
<color name="game_menu_icon_activated_color">@android:color/holo_blue_light</color>
|
||||
<color name="game_menu_default_bg">@android:color/transparent</color>
|
||||
<color name="game_menu_divider_color">@android:color/darker_gray</color>
|
||||
</resources>
|
@ -2,4 +2,8 @@
|
||||
<resources>
|
||||
<dimen name="editor_default_window_height">720dp</dimen>
|
||||
<dimen name="editor_default_window_width">1024dp</dimen>
|
||||
<dimen name="game_menu_vseparator_vertical_margin">8dp</dimen>
|
||||
<dimen name="game_menu_vseparator_horizontal_margin">1dp</dimen>
|
||||
<dimen name="embed_game_window_default_width">640dp</dimen>
|
||||
<dimen name="embed_game_window_default_height">360dp</dimen>
|
||||
</resources>
|
||||
|
@ -3,5 +3,21 @@
|
||||
<string name="godot_game_activity_name">Godot Play window</string>
|
||||
<string name="denied_storage_permission_error_msg">Missing storage access permission!</string>
|
||||
<string name="denied_install_packages_permission_error_msg">Missing install packages permission!</string>
|
||||
<string name="pip_button_description">Button used to toggle picture-in-picture mode for the Play window</string>
|
||||
<string name="game_menu_input_event_joypad_motion_label">Input</string>
|
||||
<string name="game_menu_nodes_2d_button_label">2D</string>
|
||||
<string name="game_menu_node_3d_button_label">3D</string>
|
||||
<string name="menu_reset_2d_camera_label">Reset 2D Camera</string>
|
||||
<string name="menu_reset_3d_camera_label">Reset 3D Camera</string>
|
||||
<string name="menu_manipulate_in_game_label">Manipulate In-Game</string>
|
||||
<string name="menu_manipulate_from_editors_label">Manipulate From Editors</string>
|
||||
<string name="menu_embed_game_on_play_label">Embed Game On Play</string>
|
||||
<string name="menu_camera_label">Camera Options</string>
|
||||
<string name="menu_camera_override_label">Override Camera</string>
|
||||
<string name="embedded_game_not_running_message">Press play to start the game.</string>
|
||||
<string name="running_game_not_embedded_message">Game running not embedded.</string>
|
||||
<string name="menu_keep_embed_game_on_top_label">Keep on Top using PiP</string>
|
||||
<string name="dont_show_again_message">Don\'t show again</string>
|
||||
<string name="show_game_resume_hint">Tap on \'Game\' to resume</string>
|
||||
<string name="restart_embed_game_hint">Restart game to embed</string>
|
||||
<string name="restart_non_embedded_game_hint">Restart Game to disable embedding</string>
|
||||
</resources>
|
||||
|
@ -9,4 +9,7 @@
|
||||
screen. This is required. -->
|
||||
<item name="postSplashScreenTheme">@style/GodotEditorTheme</item>
|
||||
</style>
|
||||
|
||||
<style name="GodotEmbeddedGameTheme" parent="@android:style/Theme.DeviceDefault.Panel">
|
||||
</style>
|
||||
</resources>
|
||||
|
@ -333,7 +333,7 @@ class Godot(private val context: Context) {
|
||||
* Toggle immersive mode.
|
||||
* Must be called from the UI thread.
|
||||
*/
|
||||
private fun enableImmersiveMode(enabled: Boolean, override: Boolean = false) {
|
||||
fun enableImmersiveMode(enabled: Boolean, override: Boolean = false) {
|
||||
val activity = getActivity() ?: return
|
||||
val window = activity.window ?: return
|
||||
|
||||
@ -1068,6 +1068,16 @@ class Godot(private val context: Context) {
|
||||
return PermissionsUtil.getGrantedPermissions(getActivity())
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this is the Godot editor.
|
||||
*/
|
||||
fun isEditorHint() = isEditorBuild() && GodotLib.isEditorHint()
|
||||
|
||||
/**
|
||||
* Returns true if this is the Godot project manager.
|
||||
*/
|
||||
fun isProjectManagerHint() = isEditorBuild() && GodotLib.isProjectManagerHint()
|
||||
|
||||
/**
|
||||
* Return true if the given feature is supported.
|
||||
*/
|
||||
@ -1177,4 +1187,9 @@ class Godot(private val context: Context) {
|
||||
val verifyResult = primaryHost?.verifyApk(apkPath) ?: Error.ERR_UNAVAILABLE
|
||||
return verifyResult.toNativeValue()
|
||||
}
|
||||
|
||||
@Keep
|
||||
private fun nativeOnEditorWorkspaceSelected(workspace: String) {
|
||||
primaryHost?.onEditorWorkspaceSelected(workspace)
|
||||
}
|
||||
}
|
||||
|
@ -509,4 +509,11 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEditorWorkspaceSelected(String workspace) {
|
||||
if (parentHost != null) {
|
||||
parentHost.onEditorWorkspaceSelected(workspace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -145,4 +145,9 @@ public interface GodotHost {
|
||||
default boolean supportsFeature(String featureTag) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked on the render thread when an editor workspace has been selected.
|
||||
*/
|
||||
default void onEditorWorkspaceSelected(String workspace) {}
|
||||
}
|
||||
|
@ -196,6 +196,30 @@ public class GodotLib {
|
||||
*/
|
||||
public static native String getEditorSetting(String settingKey);
|
||||
|
||||
/**
|
||||
* Update the 'key' editor setting with the given data. Must be called on the render thread.
|
||||
* @param key
|
||||
* @param data
|
||||
*/
|
||||
public static native void setEditorSetting(String key, Object data);
|
||||
|
||||
/**
|
||||
* Used to access project metadata from the editor settings. Must be accessed on the render thread.
|
||||
* @param section
|
||||
* @param key
|
||||
* @param defaultValue
|
||||
* @return
|
||||
*/
|
||||
public static native Object getEditorProjectMetadata(String section, String key, Object defaultValue);
|
||||
|
||||
/**
|
||||
* Set the project metadata to the editor settings. Must be accessed on the render thread.
|
||||
* @param section
|
||||
* @param key
|
||||
* @param data
|
||||
*/
|
||||
public static native void setEditorProjectMetadata(String section, String key, Object data);
|
||||
|
||||
/**
|
||||
* Invoke method |p_method| on the Godot object specified by |p_id|
|
||||
* @param p_id Id of the Godot object to invoke
|
||||
@ -267,4 +291,8 @@ public class GodotLib {
|
||||
* @return the project resource directory
|
||||
*/
|
||||
public static native String getProjectResourceDir();
|
||||
|
||||
static native boolean isEditorHint();
|
||||
|
||||
static native boolean isProjectManagerHint();
|
||||
}
|
||||
|
@ -52,7 +52,7 @@ import kotlin.math.abs
|
||||
/**
|
||||
* Utility class for managing dialogs.
|
||||
*/
|
||||
internal class DialogUtils {
|
||||
class DialogUtils {
|
||||
companion object {
|
||||
private val TAG = DialogUtils::class.java.simpleName
|
||||
|
||||
@ -79,7 +79,7 @@ internal class DialogUtils {
|
||||
* @param message The message displayed in the dialog.
|
||||
* @param buttons An array of button labels to display.
|
||||
*/
|
||||
fun showDialog(activity: Activity, title: String, message: String, buttons: Array<String>) {
|
||||
internal fun showDialog(activity: Activity, title: String, message: String, buttons: Array<String>) {
|
||||
var dismissDialog: () -> Unit = {} // Helper to dismiss the Dialog when a button is clicked.
|
||||
activity.runOnUiThread {
|
||||
val builder = AlertDialog.Builder(activity)
|
||||
@ -174,7 +174,7 @@ internal class DialogUtils {
|
||||
* @param message The message displayed in the input dialog.
|
||||
* @param existingText The existing text that will be pre-filled in the input field.
|
||||
*/
|
||||
fun showInputDialog(activity: Activity, title: String, message: String, existingText: String) {
|
||||
internal fun showInputDialog(activity: Activity, title: String, message: String, existingText: String) {
|
||||
val inputField = EditText(activity)
|
||||
val paddingHorizontal = activity.resources.getDimensionPixelSize(R.dimen.dialog_padding_horizontal)
|
||||
val paddingVertical = activity.resources.getDimensionPixelSize(R.dimen.dialog_padding_vertical)
|
||||
|
@ -0,0 +1,117 @@
|
||||
/**************************************************************************/
|
||||
/* GameMenuUtils.kt */
|
||||
/**************************************************************************/
|
||||
/* This file is part of: */
|
||||
/* GODOT ENGINE */
|
||||
/* https://godotengine.org */
|
||||
/**************************************************************************/
|
||||
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
|
||||
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
|
||||
/* */
|
||||
/* Permission is hereby granted, free of charge, to any person obtaining */
|
||||
/* a copy of this software and associated documentation files (the */
|
||||
/* "Software"), to deal in the Software without restriction, including */
|
||||
/* without limitation the rights to use, copy, modify, merge, publish, */
|
||||
/* distribute, sublicense, and/or sell copies of the Software, and to */
|
||||
/* permit persons to whom the Software is furnished to do so, subject to */
|
||||
/* the following conditions: */
|
||||
/* */
|
||||
/* The above copyright notice and this permission notice shall be */
|
||||
/* included in all copies or substantial portions of the Software. */
|
||||
/* */
|
||||
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
|
||||
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
|
||||
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
|
||||
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
|
||||
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
|
||||
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
|
||||
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
|
||||
/**************************************************************************/
|
||||
|
||||
package org.godotengine.godot.utils
|
||||
|
||||
import android.util.Log
|
||||
import org.godotengine.godot.GodotLib
|
||||
|
||||
/**
|
||||
* Utility class for accessing and using game menu APIs.
|
||||
*/
|
||||
object GameMenuUtils {
|
||||
private val TAG = GameMenuUtils::class.java.simpleName
|
||||
|
||||
/**
|
||||
* Enum representing the "run/window_placement/game_embed_mode" editor settings.
|
||||
*/
|
||||
enum class GameEmbedMode(internal val nativeValue: Int) {
|
||||
DISABLED(-1), AUTO(0), ENABLED(1);
|
||||
|
||||
companion object {
|
||||
internal const val SETTING_KEY = "run/window_placement/game_embed_mode"
|
||||
|
||||
@JvmStatic
|
||||
internal fun fromNativeValue(nativeValue: Int): GameEmbedMode? {
|
||||
for (mode in GameEmbedMode.entries) {
|
||||
if (mode.nativeValue == nativeValue) {
|
||||
return mode
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
external fun setSuspend(enabled: Boolean)
|
||||
|
||||
@JvmStatic
|
||||
external fun nextFrame()
|
||||
|
||||
@JvmStatic
|
||||
external fun setNodeType(type: Int)
|
||||
|
||||
@JvmStatic
|
||||
external fun setSelectMode(mode: Int)
|
||||
|
||||
@JvmStatic
|
||||
external fun setSelectionVisible(visible: Boolean)
|
||||
|
||||
@JvmStatic
|
||||
external fun setCameraOverride(enabled: Boolean)
|
||||
|
||||
@JvmStatic
|
||||
external fun setCameraManipulateMode(mode: Int)
|
||||
|
||||
@JvmStatic
|
||||
external fun resetCamera2DPosition()
|
||||
|
||||
@JvmStatic
|
||||
external fun resetCamera3DPosition()
|
||||
|
||||
@JvmStatic
|
||||
external fun playMainScene()
|
||||
|
||||
/**
|
||||
* Returns [GameEmbedMode] stored in the editor settings.
|
||||
*
|
||||
* Must be called on the render thread.
|
||||
*/
|
||||
fun fetchGameEmbedMode(): GameEmbedMode {
|
||||
try {
|
||||
val gameEmbedModeValue = Integer.parseInt(GodotLib.getEditorSetting(GameEmbedMode.SETTING_KEY))
|
||||
val gameEmbedMode = GameEmbedMode.fromNativeValue(gameEmbedModeValue) ?: GameEmbedMode.AUTO
|
||||
return gameEmbedMode
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Unable to retrieve game embed mode", e)
|
||||
return GameEmbedMode.AUTO
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the 'game_embed_mode' editor setting.
|
||||
*
|
||||
* Must be called on the render thread.
|
||||
*/
|
||||
fun saveGameEmbedMode(gameEmbedMode: GameEmbedMode) {
|
||||
GodotLib.setEditorSetting(GameEmbedMode.SETTING_KEY, gameEmbedMode.nativeValue)
|
||||
}
|
||||
}
|
@ -487,6 +487,49 @@ JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getEditorSetting(J
|
||||
return env->NewStringUTF(editor_setting_value.utf8().get_data());
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setEditorSetting(JNIEnv *env, jclass clazz, jstring p_key, jobject p_data) {
|
||||
#ifdef TOOLS_ENABLED
|
||||
if (EditorSettings::get_singleton() != nullptr) {
|
||||
String key = jstring_to_string(p_key, env);
|
||||
Variant data = _jobject_to_variant(env, p_data);
|
||||
EditorSettings::get_singleton()->set(key, data);
|
||||
}
|
||||
#else
|
||||
WARN_PRINT("Access to the Editor Settings in only available on Editor builds");
|
||||
#endif
|
||||
}
|
||||
|
||||
JNIEXPORT jobject JNICALL Java_org_godotengine_godot_GodotLib_getEditorProjectMetadata(JNIEnv *env, jclass clazz, jstring p_section, jstring p_key, jobject p_default_value) {
|
||||
jvalret result;
|
||||
|
||||
#ifdef TOOLS_ENABLED
|
||||
if (EditorSettings::get_singleton() != nullptr) {
|
||||
String section = jstring_to_string(p_section, env);
|
||||
String key = jstring_to_string(p_key, env);
|
||||
Variant default_value = _jobject_to_variant(env, p_default_value);
|
||||
Variant data = EditorSettings::get_singleton()->get_project_metadata(section, key, default_value);
|
||||
result = _variant_to_jvalue(env, data.get_type(), &data, true);
|
||||
}
|
||||
#else
|
||||
WARN_PRINT("Access to the Editor Settings Project Metadata is only available on Editor builds");
|
||||
#endif
|
||||
|
||||
return result.obj;
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setEditorProjectMetadata(JNIEnv *env, jclass clazz, jstring p_section, jstring p_key, jobject p_data) {
|
||||
#ifdef TOOLS_ENABLED
|
||||
if (EditorSettings::get_singleton() != nullptr) {
|
||||
String section = jstring_to_string(p_section, env);
|
||||
String key = jstring_to_string(p_key, env);
|
||||
Variant data = _jobject_to_variant(env, p_data);
|
||||
EditorSettings::get_singleton()->set_project_metadata(section, key, data);
|
||||
}
|
||||
#else
|
||||
WARN_PRINT("Access to the Editor Settings Project Metadata is only available on Editor builds");
|
||||
#endif
|
||||
}
|
||||
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onNightModeChanged(JNIEnv *env, jclass clazz) {
|
||||
DisplayServerAndroid *ds = (DisplayServerAndroid *)DisplayServer::get_singleton();
|
||||
if (ds) {
|
||||
@ -555,4 +598,19 @@ JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getProjectResource
|
||||
const String resource_dir = OS::get_singleton()->get_resource_dir();
|
||||
return env->NewStringUTF(resource_dir.utf8().get_data());
|
||||
}
|
||||
JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_isEditorHint(JNIEnv *env, jclass clazz) {
|
||||
Engine *engine = Engine::get_singleton();
|
||||
if (engine) {
|
||||
return engine->is_editor_hint();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_isProjectManagerHint(JNIEnv *env, jclass clazz) {
|
||||
Engine *engine = Engine::get_singleton();
|
||||
if (engine) {
|
||||
return engine->is_project_manager_hint();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -62,6 +62,9 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusin(JNIEnv *env,
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusout(JNIEnv *env, jclass clazz);
|
||||
JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getGlobal(JNIEnv *env, jclass clazz, jstring path);
|
||||
JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getEditorSetting(JNIEnv *env, jclass clazz, jstring p_setting_key);
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setEditorSetting(JNIEnv *env, jclass clazz, jstring p_key, jobject p_data);
|
||||
JNIEXPORT jobject JNICALL Java_org_godotengine_godot_GodotLib_getEditorProjectMetadata(JNIEnv *env, jclass clazz, jstring p_section, jstring p_key, jobject p_default_value);
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setEditorProjectMetadata(JNIEnv *env, jclass clazz, jstring p_section, jstring p_key, jobject p_data);
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setVirtualKeyboardHeight(JNIEnv *env, jclass clazz, jint p_height);
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_requestPermissionResult(JNIEnv *env, jclass clazz, jstring p_permission, jboolean p_result);
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onNightModeChanged(JNIEnv *env, jclass clazz);
|
||||
@ -70,6 +73,8 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererResumed(JNI
|
||||
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererPaused(JNIEnv *env, jclass clazz);
|
||||
JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_shouldDispatchInputToRenderThread(JNIEnv *env, jclass clazz);
|
||||
JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getProjectResourceDir(JNIEnv *env, jclass clazz);
|
||||
JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_isEditorHint(JNIEnv *env, jclass clazz);
|
||||
JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_isProjectManagerHint(JNIEnv *env, jclass clazz);
|
||||
}
|
||||
|
||||
#endif // JAVA_GODOT_LIB_JNI_H
|
||||
|
@ -93,6 +93,7 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_
|
||||
_verify_apk = p_env->GetMethodID(godot_class, "nativeVerifyApk", "(Ljava/lang/String;)I");
|
||||
_enable_immersive_mode = p_env->GetMethodID(godot_class, "nativeEnableImmersiveMode", "(Z)V");
|
||||
_is_in_immersive_mode = p_env->GetMethodID(godot_class, "isInImmersiveMode", "()Z");
|
||||
_on_editor_workspace_selected = p_env->GetMethodID(godot_class, "nativeOnEditorWorkspaceSelected", "(Ljava/lang/String;)V");
|
||||
}
|
||||
|
||||
GodotJavaWrapper::~GodotJavaWrapper() {
|
||||
@ -588,3 +589,13 @@ bool GodotJavaWrapper::is_in_immersive_mode() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void GodotJavaWrapper::on_editor_workspace_selected(const String &p_workspace) {
|
||||
if (_on_editor_workspace_selected) {
|
||||
JNIEnv *env = get_jni_env();
|
||||
ERR_FAIL_NULL(env);
|
||||
|
||||
jstring j_workspace = env->NewStringUTF(p_workspace.utf8().get_data());
|
||||
env->CallVoidMethod(godot_instance, _on_editor_workspace_selected, j_workspace);
|
||||
}
|
||||
}
|
||||
|
@ -84,6 +84,7 @@ private:
|
||||
jmethodID _verify_apk = nullptr;
|
||||
jmethodID _enable_immersive_mode = nullptr;
|
||||
jmethodID _is_in_immersive_mode = nullptr;
|
||||
jmethodID _on_editor_workspace_selected = nullptr;
|
||||
|
||||
public:
|
||||
GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_godot_instance);
|
||||
@ -137,6 +138,8 @@ public:
|
||||
|
||||
void enable_immersive_mode(bool p_enabled);
|
||||
bool is_in_immersive_mode();
|
||||
|
||||
void on_editor_workspace_selected(const String &p_workspace);
|
||||
};
|
||||
|
||||
#endif // JAVA_GODOT_WRAPPER_H
|
||||
|
@ -43,6 +43,10 @@
|
||||
#include "core/io/xml_parser.h"
|
||||
#include "drivers/unix/dir_access_unix.h"
|
||||
#include "drivers/unix/file_access_unix.h"
|
||||
#ifdef TOOLS_ENABLED
|
||||
#include "editor/editor_node.h"
|
||||
#include "editor/plugins/game_view_plugin.h"
|
||||
#endif
|
||||
#include "main/main.h"
|
||||
#include "scene/main/scene_tree.h"
|
||||
#include "servers/rendering_server.h"
|
||||
@ -331,6 +335,15 @@ void OS_Android::main_loop_begin() {
|
||||
if (main_loop) {
|
||||
main_loop->initialize();
|
||||
}
|
||||
|
||||
#ifdef TOOLS_ENABLED
|
||||
if (Engine::get_singleton()->is_editor_hint()) {
|
||||
GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
|
||||
if (game_view_plugin != nullptr) {
|
||||
game_view_plugin->connect("main_screen_changed", callable_mp_static(&OS_Android::_on_main_screen_changed));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool OS_Android::main_loop_iterate(bool *r_should_swap_buffers) {
|
||||
@ -353,6 +366,15 @@ bool OS_Android::main_loop_iterate(bool *r_should_swap_buffers) {
|
||||
}
|
||||
|
||||
void OS_Android::main_loop_end() {
|
||||
#ifdef TOOLS_ENABLED
|
||||
if (Engine::get_singleton()->is_editor_hint()) {
|
||||
GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
|
||||
if (game_view_plugin != nullptr) {
|
||||
game_view_plugin->disconnect("main_screen_changed", callable_mp_static(&OS_Android::_on_main_screen_changed));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (main_loop) {
|
||||
SceneTree *scene_tree = Object::cast_to<SceneTree>(main_loop);
|
||||
if (scene_tree) {
|
||||
@ -362,6 +384,14 @@ void OS_Android::main_loop_end() {
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef TOOLS_ENABLED
|
||||
void OS_Android::_on_main_screen_changed(const String &p_screen_name) {
|
||||
if (OS_Android::get_singleton() != nullptr && OS_Android::get_singleton()->get_godot_java() != nullptr) {
|
||||
OS_Android::get_singleton()->get_godot_java()->on_editor_workspace_selected(p_screen_name);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void OS_Android::main_loop_focusout() {
|
||||
DisplayServerAndroid::get_singleton()->send_window_event(DisplayServer::WINDOW_EVENT_FOCUS_OUT);
|
||||
if (OS::get_singleton()->get_main_loop()) {
|
||||
|
@ -187,6 +187,10 @@ private:
|
||||
String get_dynamic_libraries_path() const;
|
||||
// Copy a dynamic library to the given location to make it accessible for loading.
|
||||
bool copy_dynamic_library(const String &p_library_path, const String &p_target_dir, String *r_copy_path = nullptr);
|
||||
|
||||
#ifdef TOOLS_ENABLED
|
||||
static void _on_main_screen_changed(const String &p_screen_name);
|
||||
#endif
|
||||
};
|
||||
|
||||
#endif // OS_ANDROID_H
|
||||
|
Loading…
x
Reference in New Issue
Block a user