diff --git a/doc/classes/EditorSettings.xml b/doc/classes/EditorSettings.xml
index fd478216733f..4a9b3b8b510b 100644
--- a/doc/classes/EditorSettings.xml
+++ b/doc/classes/EditorSettings.xml
@@ -1134,19 +1134,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.
Overrides game embedding setting for all newly opened projects. If enabled, game embedding settings are not saved.
-
- 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.
-
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."
diff --git a/editor/editor_main_screen.cpp b/editor/editor_main_screen.cpp
index dfc1152d17ec..fdd4615c33f6 100644
--- a/editor/editor_main_screen.cpp
+++ b/editor/editor_main_screen.cpp
@@ -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() {
diff --git a/editor/editor_main_screen.h b/editor/editor_main_screen.h
index ca78ceaa8850..06bf2f7aaf2e 100644
--- a/editor/editor_main_screen.h
+++ b/editor/editor_main_screen.h
@@ -58,6 +58,7 @@ class EditorMainScreen : public PanelContainer {
HBoxContainer *button_hb = nullptr;
Vector buttons;
Vector editor_table;
+ HashMap main_editor_plugins;
int _get_current_main_editor() const;
@@ -80,6 +81,7 @@ class EditorMainScreen : public PanelContainer {
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;
diff --git a/editor/editor_settings.cpp b/editor/editor_settings.cpp
index 9a5a51a18a25..1555c43847b1 100644
--- a/editor/editor_settings.cpp
+++ b/editor/editor_settings.cpp
@@ -936,17 +936,24 @@ void EditorSettings::_load_defaults(Ref 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");
+ String game_embed_mode_hints = "Disabled:-1,Use Per-Project Configuration:0,Embed Game:1,Make Game Workspace Floating:2";
+#ifdef ANDROID_ENABLED
+ 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
+ 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);
- int default_play_window_pip_mode = 0;
#ifdef ANDROID_ENABLED
- default_play_window_pip_mode = 2;
+ EDITOR_SETTING_BASIC(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/enable_game_menu_bar", 0, "Enabled For All Games:0,Enabled For Embedded Games Only: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")
// Auto save
_initial_set("run/auto_save/save_before_running", true, true);
diff --git a/editor/plugins/game_view_plugin.cpp b/editor/plugins/game_view_plugin.cpp
index d96ee851374e..a231fa653782 100644
--- a/editor/plugins/game_view_plugin.cpp
+++ b/editor/plugins/game_view_plugin.cpp
@@ -612,6 +612,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;
@@ -620,10 +624,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);
@@ -1019,6 +1019,18 @@ GameView::GameView(Ref 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();
@@ -1027,13 +1039,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 p_layout) {
game_view->set_window_layout(p_layout);
}
@@ -1050,6 +1055,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: {
@@ -1074,13 +1084,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) {
@@ -1090,7 +1098,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 {
@@ -1099,13 +1107,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);
@@ -1115,6 +1132,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));
}
diff --git a/editor/plugins/game_view_plugin.h b/editor/plugins/game_view_plugin.h
index dd384280b5fb..6be2d961ba08 100644
--- a/editor/plugins/game_view_plugin.h
+++ b/editor/plugins/game_view_plugin.h
@@ -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 @@ class GameView : public VBoxContainer {
class GameViewPlugin : public EditorPlugin {
GDCLASS(GameViewPlugin, EditorPlugin);
+#ifndef ANDROID_ENABLED
GameView *game_view = nullptr;
WindowWrapper *window_wrapper = nullptr;
+#endif
Ref 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 @@ class GameViewPlugin : public EditorPlugin {
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 get_debugger() const { return debugger; }
+
+#ifndef ANDROID_ENABLED
+ virtual void make_visible(bool p_visible) override;
+
virtual void set_window_layout(Ref p_layout) override;
virtual void get_window_layout(Ref p_layout) override;
virtual void set_state(const Dictionary &p_state) override;
virtual Dictionary get_state() const override;
+#endif
GameViewPlugin();
~GameViewPlugin();
diff --git a/platform/android/SCsub b/platform/android/SCsub
index d0928a937b32..2260a84a760f 100644
--- a/platform/android/SCsub
+++ b/platform/android/SCsub
@@ -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()
diff --git a/platform/android/game_menu_utils_jni.cpp b/platform/android/game_menu_utils_jni.cpp
new file mode 100644
index 000000000000..6fdad363baf1
--- /dev/null
+++ b/platform/android/game_menu_utils_jni.cpp
@@ -0,0 +1,120 @@
+/**************************************************************************/
+/* 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_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(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(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(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(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(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(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(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(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(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(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
+}
+}
diff --git a/platform/android/game_menu_utils_jni.h b/platform/android/game_menu_utils_jni.h
new file mode 100644
index 000000000000..9ccc0aaa0f19
--- /dev/null
+++ b/platform/android/game_menu_utils_jni.h
@@ -0,0 +1,48 @@
+/**************************************************************************/
+/* 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
+
+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);
+}
+
+#endif // GAME_MENU_UTILS_JNI_H
diff --git a/platform/android/java/editor/src/main/AndroidManifest.xml b/platform/android/java/editor/src/main/AndroidManifest.xml
index 78ccc9ca96f6..a0166a9c7258 100644
--- a/platform/android/java/editor/src/main/AndroidManifest.xml
+++ b/platform/android/java/editor/src/main/AndroidManifest.xml
@@ -71,6 +71,17 @@
android:defaultWidth="@dimen/editor_default_window_width"
android:defaultHeight="@dimen/editor_default_window_height" />
+
(R.id.embedded_game_view_container)?.apply {
+ setOnClickListener {
+ it.isVisible = false
+ }
+ } }
+ private val embeddedGameStateLabel: TextView? by lazy { findViewById(R.id.embedded_game_state_label)?.apply {
+ setOnClickListener {
+ if (text.isNotBlank()) {
+ Toast.makeText(applicationContext, text, Toast.LENGTH_LONG).show()
+ }
+ }
+ } }
+ 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 +219,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() {
@@ -212,7 +268,7 @@ abstract class BaseGodotEditor : GodotActivity() {
}
@CallSuper
- protected override fun updateCommandLineParams(args: Array) {
+ override fun updateCommandLineParams(args: Array) {
val args = if (BuildConfig.BUILD_TYPE == "dev") {
args + "--benchmark"
} else {
@@ -221,7 +277,7 @@ abstract class BaseGodotEditor : GodotActivity() {
super.updateCommandLineParams(args);
}
- protected open fun retrieveEditorWindowInfo(args: Array): EditorWindowInfo {
+ protected fun retrieveEditorWindowInfo(args: Array, gameEmbedMode: GameEmbedMode): EditorWindowInfo {
var hasEditor = false
var xrMode = XR_MODE_DEFAULT
@@ -238,12 +294,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 +319,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): 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 +345,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): Int {
- val editorWindowInfo = retrieveEditorWindowInfo(args)
+ final override fun onNewGodotInstanceRequested(args: Array): 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 +377,13 @@ 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)
+ putExtra(EXTRA_GAME_MENU_BAR_MODE, GameMenuUtils.fetchGameMenuBarMode())
+ }
+
if (editorWindowInfo.windowClassName == javaClass.name) {
Log.d(TAG, "Restarting ${editorWindowInfo.windowClassName} with parameters ${args.contentToString()}")
triggerRebirth(activityOptions?.toBundle(), newInstance)
@@ -344,7 +406,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 +464,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 +524,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 +611,168 @@ abstract class BaseGodotEditor : GodotActivity() {
return false
}
+
+ override fun onEditorWorkspaceSelected(workspace: String) {
+ if (workspace == GAME_WORKSPACE) {
+ if (editorMessageDispatcher.bringEditorWindowToFront(EMBEDDED_RUN_GAME_INFO)) {
+ return
+ }
+
+ val nonEmbeddedGameRunning = editorMessageDispatcher.bringEditorWindowToFront(RUN_GAME_INFO)
+ if (nonEmbeddedGameRunning && GameMenuUtils.fetchGameMenuBarMode() == GameMenuUtils.GameMenuBarMode.ENABLED_FOR_ALL_GAMES) {
+ return
+ }
+
+ val xrGameRunning = editorMessageDispatcher.hasRecipient(XR_RUN_GAME_INFO)
+ val gameEmbedMode = resolveGameEmbedModeIfNeeded(fetchGameEmbedMode())
+ runOnUiThread {
+ if (nonEmbeddedGameRunning || xrGameRunning) {
+ embeddedGameStateLabel?.setText(R.string.running_game_not_embedded_message)
+ } else {
+ embeddedGameStateLabel?.setText(R.string.embedded_game_not_running_message)
+ }
+
+ gameMenuState.putBoolean(EXTRA_IS_GAME_EMBEDDED, gameEmbedMode != GameEmbedMode.DISABLED)
+ gameMenuState.putBoolean(EXTRA_IS_GAME_RUNNING, nonEmbeddedGameRunning || xrGameRunning)
+ gameMenuFragment?.refreshGameMenu(gameMenuState)
+ embeddedGameViewContainer?.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)
+ }
+ GAME_MENU_ACTION_ALWAYS_ON_TOP -> {
+ onAlwaysOnTopUpdated(actionData.getBoolean(KEY_GAME_MENU_ACTION_PARAM1))
+ }
+ }
+ }
+
+ 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 onAlwaysOnTopUpdated(enabled: Boolean) {
+ gameMenuState.putBoolean(GAME_MENU_ACTION_ALWAYS_ON_TOP, enabled)
+ }
+
+ override fun isGameEmbeddingSupported() = !isNativeXRDevice(applicationContext)
}
diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotGame.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotGame.kt
new file mode 100644
index 000000000000..8e2971e80d3c
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotGame.kt
@@ -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 {
+ val permissionsToEnable = mutableListOf()
+
+ // 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
+}
diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorMessageDispatcher.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorMessageDispatcher.kt
index f5a6ed7dabfe..7e36bc411dcb 100644
--- a/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorMessageDispatcher.kt
+++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorMessageDispatcher.kt
@@ -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()
+ private data class RecipientInfo(
+ val messenger: Messenger,
+ var pendingForceQuit: Boolean = false,
+ val scheduledTasksPendingForceQuit: HashSet = HashSet()
+ )
+ private val recipientsInfos = ConcurrentHashMap()
@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 hasRecipient(editorWindow: EditorWindowInfo) = recipientsInfos.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 = recipientsInfos[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)
+ cleanRecipientInfo(editorId)
+ false
+ }
+ }
+
+ internal fun isPendingForceQuit(editorWindow: EditorWindowInfo): Boolean {
+ return recipientsInfos[editorWindow.windowId]?.pendingForceQuit == true
+ }
+
+ internal fun runTaskAfterForceQuit(editorWindow: EditorWindowInfo, task: Runnable) {
+ val recipientInfo = recipientsInfos[editorWindow.windowId]
+ if (recipientInfo == null || !recipientInfo.pendingForceQuit) {
+ task.run()
+ } else {
+ recipientInfo.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 = recipientsInfos[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)
+ cleanRecipientInfo(editorId)
false
}
}
+ /**
+ * Dispatch a game menu action to another editor instance.
+ */
+ fun dispatchGameMenuAction(editorWindow: EditorWindowInfo, actionData: Bundle) {
+ val editorId = editorWindow.windowId
+ val info = recipientsInfos[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)
+ cleanRecipientInfo(editorId)
+ }
+ }
+
/**
* Utility method to register a receiver messenger.
*/
@@ -121,14 +211,22 @@ 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)
+ cleanRecipientInfo(editorId)
messengerDeathCallback?.run()
}, 0)
- recipientsMessengers[editorId] = messenger
+ recipientsInfos[editorId] = RecipientInfo(messenger)
}
} catch (e: RemoteException) {
Log.e(TAG, "Unable to register messenger from $editorId", e)
- recipientsMessengers.remove(editorId)
+ cleanRecipientInfo(editorId)
+ }
+ }
+
+ private fun cleanRecipientInfo(editorId: Int) {
+ val recipientInfo = recipientsInfos.remove(editorId) ?: return
+ Log.v(TAG, "Cleaning info for recipient $editorId")
+ for (task in recipientInfo.scheduledTasksPendingForceQuit) {
+ task.run()
}
}
diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorWindowInfo.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorWindowInfo.kt
index 2e1de9a60704..c92b22e7df48 100644
--- a/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorWindowInfo.kt
+++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/EditorWindowInfo.kt
@@ -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)
}
diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt
index e52d5663474d..3b0259e556ac 100644
--- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt
+++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt
@@ -30,77 +30,54 @@
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 var pipAvailable = false
+ private var gameMenuBarMode = GameMenuUtils.GameMenuBarMode.ENABLED_FOR_ALL_GAMES
- @SuppressLint("ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) {
+ gameMenuBarMode = intent.getSerializableExtra(EXTRA_GAME_MENU_BAR_MODE) as? GameMenuUtils.GameMenuBarMode ?: gameMenuBarMode
+ intent.getBundleExtra(EXTRA_GAME_MENU_STATE)?.let {
+ gameMenuState.clear()
+ gameMenuState.putAll(it)
+ }
+ gameMenuState.putBoolean(EXTRA_IS_GAME_EMBEDDED, isGameEmbedded())
+ gameMenuState.putBoolean(EXTRA_IS_GAME_RUNNING, true)
+
super.onCreate(savedInstanceState)
+ gameMenuContainer?.isVisible = shouldShowGameMenuBar()
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val gameView = findViewById(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 +91,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 && gameMenuBarMode == GameMenuUtils.GameMenuBarMode.ENABLED_FOR_ALL_GAMES
+ }
+
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 +128,111 @@ 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
- }
+ 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)
+ }
- // Request project runtime permissions if necessary
- val permissionsToEnable = getProjectPermissionsToEnable()
- if (permissionsToEnable.isNotEmpty()) {
- PermissionsUtil.requestPermissions(this, permissionsToEnable)
+ override fun dispatchNextFrame() {
+ val actionBundle = Bundle().apply {
+ putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_NEXT_FRAME)
}
+ editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
}
- /**
- * Check for project permissions to enable
- */
- @CallSuper
- protected open fun getProjectPermissionsToEnable(): MutableList {
- val permissionsToEnable = mutableListOf()
-
- // 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 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)
+ }
- return permissionsToEnable
+ 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 onAlwaysOnTopUpdated(alwaysOnTopEnabled: Boolean) {
+ val actionBundle = Bundle().apply {
+ putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_ALWAYS_ON_TOP)
+ putBoolean(KEY_GAME_MENU_ACTION_PARAM1, alwaysOnTopEnabled)
+ }
+ editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
+ }
+
+ override fun minimizeGameWindow() {
+ moveTaskToBack(false)
+ }
+
+ override fun closeGameWindow() {
+ ProcessPhoenix.forceQuit(this)
+ }
+
}
diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotXRGame.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotXRGame.kt
index 1b3018460765..172dc166b617 100644
--- a/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotXRGame.kt
+++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/GodotXRGame.kt
@@ -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 {
val permissionsToEnable = super.getProjectPermissionsToEnable()
diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/embed/EmbeddedGodotGame.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/embed/EmbeddedGodotGame.kt
new file mode 100644
index 000000000000..681593ad964d
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/embed/EmbeddedGodotGame.kt
@@ -0,0 +1,145 @@
+/**************************************************************************/
+/* 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 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
+}
diff --git a/platform/android/java/editor/src/main/java/org/godotengine/editor/embed/GameMenuFragment.kt b/platform/android/java/editor/src/main/java/org/godotengine/editor/embed/GameMenuFragment.kt
new file mode 100644
index 000000000000..1d9c354a58be
--- /dev/null
+++ b/platform/android/java/editor/src/main/java/org/godotengine/editor/embed/GameMenuFragment.kt
@@ -0,0 +1,423 @@
+/**************************************************************************/
+/* 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.view.LayoutInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import android.widget.PopupMenu
+import android.widget.RadioButton
+import android.widget.Toast
+import androidx.core.view.isVisible
+import androidx.fragment.app.Fragment
+import org.godotengine.editor.BaseGodotEditor
+import org.godotengine.editor.R
+
+/**
+ * Implements the game menu interface for the Android editor.
+ */
+class GameMenuFragment : Fragment(), PopupMenu.OnMenuItemClickListener {
+
+ companion object {
+ val TAG = GameMenuFragment::class.java.simpleName
+ }
+
+ /**
+ * 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 isAlwaysOnTopSupported() = false
+ fun onAlwaysOnTopUpdated(alwaysOnTopEnabled: Boolean)
+
+ fun onFullScreenUpdated(enabled: Boolean) {}
+ }
+
+ 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
+
+ // Show the divider if any of the window controls is visible
+ view.findViewById(R.id.game_menu_window_controls_divider)?.isVisible =
+ isMinimizeButtonEnabled ||
+ isFullScreenButtonEnabled ||
+ isCloseButtonEnabled ||
+ isPiPButtonEnabled
+
+ 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) {
+ alwaysOnTopChecked = gameMenuState.getBoolean(BaseGodotEditor.GAME_MENU_ACTION_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 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) {
+ Toast.makeText(
+ context,
+ if (item.isChecked) "Restart game to embed" else "Restart Game to disable embedding",
+ Toast.LENGTH_LONG
+ ).show()
+ }
+ }
+
+ R.id.menu_embed_game_keep_on_top -> {
+ item.isChecked = !item.isChecked
+ alwaysOnTopChecked = item.isChecked
+ menuListener?.onAlwaysOnTopUpdated(alwaysOnTopChecked)
+ }
+
+ 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
+ }
+}
diff --git a/platform/android/java/editor/src/main/res/color/game_menu_icons_color_state.xml b/platform/android/java/editor/src/main/res/color/game_menu_icons_color_state.xml
new file mode 100644
index 000000000000..33dccf57d131
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/color/game_menu_icons_color_state.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/platform/android/java/editor/src/main/res/drawable/baseline_close_24.xml b/platform/android/java/editor/src/main/res/drawable/baseline_close_24.xml
new file mode 100644
index 000000000000..f862f95fc8e4
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/drawable/baseline_close_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/platform/android/java/editor/src/main/res/drawable/baseline_fullscreen_24.xml b/platform/android/java/editor/src/main/res/drawable/baseline_fullscreen_24.xml
new file mode 100644
index 000000000000..e65659cc6ce7
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/drawable/baseline_fullscreen_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/platform/android/java/editor/src/main/res/drawable/baseline_fullscreen_exit_24.xml b/platform/android/java/editor/src/main/res/drawable/baseline_fullscreen_exit_24.xml
new file mode 100644
index 000000000000..6ae08087139c
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/drawable/baseline_fullscreen_exit_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/platform/android/java/editor/src/main/res/drawable/baseline_fullscreen_selector.xml b/platform/android/java/editor/src/main/res/drawable/baseline_fullscreen_selector.xml
new file mode 100644
index 000000000000..ec228e7a6473
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/drawable/baseline_fullscreen_selector.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/platform/android/java/editor/src/main/res/drawable/baseline_minimize_24.xml b/platform/android/java/editor/src/main/res/drawable/baseline_minimize_24.xml
new file mode 100644
index 000000000000..dedb5c2f9a2d
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/drawable/baseline_minimize_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/platform/android/java/editor/src/main/res/drawable/baseline_picture_in_picture_alt_24.xml b/platform/android/java/editor/src/main/res/drawable/baseline_picture_in_picture_alt_24.xml
new file mode 100644
index 000000000000..66a9e66d473b
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/drawable/baseline_picture_in_picture_alt_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/platform/android/java/editor/src/main/res/drawable/baseline_push_pin_24.xml b/platform/android/java/editor/src/main/res/drawable/baseline_push_pin_24.xml
new file mode 100644
index 000000000000..0c021bee9f1a
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/drawable/baseline_push_pin_24.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/platform/android/java/editor/src/main/res/drawable/camera.xml b/platform/android/java/editor/src/main/res/drawable/camera.xml
new file mode 100644
index 000000000000..b713532aebdb
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/drawable/camera.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/platform/android/java/editor/src/main/res/drawable/game_menu_button_bg.xml b/platform/android/java/editor/src/main/res/drawable/game_menu_button_bg.xml
new file mode 100644
index 000000000000..330a78f12764
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/drawable/game_menu_button_bg.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/platform/android/java/editor/src/main/res/drawable/game_menu_message_bg.xml b/platform/android/java/editor/src/main/res/drawable/game_menu_message_bg.xml
new file mode 100644
index 000000000000..ecbd62944bfd
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/drawable/game_menu_message_bg.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/platform/android/java/editor/src/main/res/drawable/game_menu_selected_bg.xml b/platform/android/java/editor/src/main/res/drawable/game_menu_selected_bg.xml
new file mode 100644
index 000000000000..99ee55893f49
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/drawable/game_menu_selected_bg.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/platform/android/java/editor/src/main/res/drawable/game_menu_selected_button_bg.xml b/platform/android/java/editor/src/main/res/drawable/game_menu_selected_button_bg.xml
new file mode 100644
index 000000000000..ee3f97ac36f1
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/drawable/game_menu_selected_button_bg.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/platform/android/java/editor/src/main/res/drawable/gui_tab_menu.xml b/platform/android/java/editor/src/main/res/drawable/gui_tab_menu.xml
new file mode 100644
index 000000000000..8733ea8af653
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/drawable/gui_tab_menu.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/platform/android/java/editor/src/main/res/drawable/gui_visibility_hidden.xml b/platform/android/java/editor/src/main/res/drawable/gui_visibility_hidden.xml
new file mode 100644
index 000000000000..c0c45407589f
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/drawable/gui_visibility_hidden.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/platform/android/java/editor/src/main/res/drawable/gui_visibility_selector.xml b/platform/android/java/editor/src/main/res/drawable/gui_visibility_selector.xml
new file mode 100644
index 000000000000..e32f7c19e36a
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/drawable/gui_visibility_selector.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/platform/android/java/editor/src/main/res/drawable/gui_visibility_visible.xml b/platform/android/java/editor/src/main/res/drawable/gui_visibility_visible.xml
new file mode 100644
index 000000000000..9207e45685b1
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/drawable/gui_visibility_visible.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/platform/android/java/editor/src/main/res/drawable/input_event_joypad_motion.xml b/platform/android/java/editor/src/main/res/drawable/input_event_joypad_motion.xml
new file mode 100644
index 000000000000..d65203e26a88
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/drawable/input_event_joypad_motion.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/platform/android/java/editor/src/main/res/drawable/list_select.xml b/platform/android/java/editor/src/main/res/drawable/list_select.xml
new file mode 100644
index 000000000000..2534903896ff
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/drawable/list_select.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/platform/android/java/editor/src/main/res/drawable/next_frame.xml b/platform/android/java/editor/src/main/res/drawable/next_frame.xml
new file mode 100644
index 000000000000..1f4c510754e0
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/drawable/next_frame.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/platform/android/java/editor/src/main/res/drawable/node_3d.xml b/platform/android/java/editor/src/main/res/drawable/node_3d.xml
new file mode 100644
index 000000000000..b3967c2a6256
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/drawable/node_3d.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/platform/android/java/editor/src/main/res/drawable/nodes_2d.xml b/platform/android/java/editor/src/main/res/drawable/nodes_2d.xml
new file mode 100644
index 000000000000..2e7fe0077bbb
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/drawable/nodes_2d.xml
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/platform/android/java/editor/src/main/res/drawable/outline_fullscreen_exit_48.xml b/platform/android/java/editor/src/main/res/drawable/outline_fullscreen_exit_48.xml
deleted file mode 100644
index c8b5a15d195c..000000000000
--- a/platform/android/java/editor/src/main/res/drawable/outline_fullscreen_exit_48.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
diff --git a/platform/android/java/editor/src/main/res/drawable/pause.xml b/platform/android/java/editor/src/main/res/drawable/pause.xml
new file mode 100644
index 000000000000..a49a1461de2d
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/drawable/pause.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/platform/android/java/editor/src/main/res/drawable/pause_play_selector.xml b/platform/android/java/editor/src/main/res/drawable/pause_play_selector.xml
new file mode 100644
index 000000000000..aa2508700825
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/drawable/pause_play_selector.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/platform/android/java/editor/src/main/res/drawable/pip_button_activated_bg_drawable.xml b/platform/android/java/editor/src/main/res/drawable/pip_button_activated_bg_drawable.xml
deleted file mode 100644
index aeaa96ce5476..000000000000
--- a/platform/android/java/editor/src/main/res/drawable/pip_button_activated_bg_drawable.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
diff --git a/platform/android/java/editor/src/main/res/drawable/pip_button_bg_drawable.xml b/platform/android/java/editor/src/main/res/drawable/pip_button_bg_drawable.xml
deleted file mode 100644
index e9b2959275fd..000000000000
--- a/platform/android/java/editor/src/main/res/drawable/pip_button_bg_drawable.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/platform/android/java/editor/src/main/res/drawable/pip_button_default_bg_drawable.xml b/platform/android/java/editor/src/main/res/drawable/pip_button_default_bg_drawable.xml
deleted file mode 100644
index a8919689febb..000000000000
--- a/platform/android/java/editor/src/main/res/drawable/pip_button_default_bg_drawable.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
-
-
diff --git a/platform/android/java/editor/src/main/res/drawable/play.xml b/platform/android/java/editor/src/main/res/drawable/play.xml
new file mode 100644
index 000000000000..9d6945ec9c58
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/drawable/play.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/platform/android/java/editor/src/main/res/drawable/tool_select.xml b/platform/android/java/editor/src/main/res/drawable/tool_select.xml
new file mode 100644
index 000000000000..310aa195afe1
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/drawable/tool_select.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/platform/android/java/editor/src/main/res/layout/game_menu_fragment_layout.xml b/platform/android/java/editor/src/main/res/layout/game_menu_fragment_layout.xml
new file mode 100644
index 000000000000..2014f8d11f88
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/layout/game_menu_fragment_layout.xml
@@ -0,0 +1,181 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/platform/android/java/editor/src/main/res/layout/godot_editor_layout.xml b/platform/android/java/editor/src/main/res/layout/godot_editor_layout.xml
index 431a468f2904..a7a9c6661feb 100644
--- a/platform/android/java/editor/src/main/res/layout/godot_editor_layout.xml
+++ b/platform/android/java/editor/src/main/res/layout/godot_editor_layout.xml
@@ -1,14 +1,52 @@
-
+ android:layout_height="match_parent"/>
+
+
+
+
+
+
+
+
+
+
-
+ android:layout_height="match_parent"
+ android:orientation="vertical">
-
-
+ android:minHeight="48dp" />
+
+
-
+
diff --git a/platform/android/java/editor/src/main/res/layout/godot_xr_game_layout.xml b/platform/android/java/editor/src/main/res/layout/godot_xr_game_layout.xml
new file mode 100644
index 000000000000..2d456ea26fe4
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/layout/godot_xr_game_layout.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/platform/android/java/editor/src/main/res/menu/options_menu.xml b/platform/android/java/editor/src/main/res/menu/options_menu.xml
new file mode 100644
index 000000000000..70eea2ef9bad
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/menu/options_menu.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/platform/android/java/editor/src/main/res/values/colors.xml b/platform/android/java/editor/src/main/res/values/colors.xml
new file mode 100644
index 000000000000..6deaa96d0242
--- /dev/null
+++ b/platform/android/java/editor/src/main/res/values/colors.xml
@@ -0,0 +1,8 @@
+
+
+ #e0e0e0
+ @android:color/darker_gray
+ @android:color/holo_blue_light
+ @android:color/transparent
+ @android:color/darker_gray
+
diff --git a/platform/android/java/editor/src/main/res/values/dimens.xml b/platform/android/java/editor/src/main/res/values/dimens.xml
index 7e999dcce3da..958092a089a1 100644
--- a/platform/android/java/editor/src/main/res/values/dimens.xml
+++ b/platform/android/java/editor/src/main/res/values/dimens.xml
@@ -2,4 +2,8 @@
720dp
1024dp
+ 8dp
+ 1dp
+ 640dp
+ 360dp
diff --git a/platform/android/java/editor/src/main/res/values/strings.xml b/platform/android/java/editor/src/main/res/values/strings.xml
index a25b6c0a2d9e..2e111987498e 100644
--- a/platform/android/java/editor/src/main/res/values/strings.xml
+++ b/platform/android/java/editor/src/main/res/values/strings.xml
@@ -3,5 +3,17 @@
Godot Play window
Missing storage access permission!
Missing install packages permission!
- Button used to toggle picture-in-picture mode for the Play window
+ Input
+ 2D
+ 3D
+ Reset 2D Camera
+ Reset 3D Camera
+ Manipulate In-Game
+ Manipulate From Editors
+ Embed Game On Play
+ Camera Options
+ Override Camera
+ Press play to start the game.
+ Game running not embedded.
+ Keep on Top using PiP
diff --git a/platform/android/java/editor/src/main/res/values/themes.xml b/platform/android/java/editor/src/main/res/values/themes.xml
index 8de2c6e28871..ec0ad9808e18 100644
--- a/platform/android/java/editor/src/main/res/values/themes.xml
+++ b/platform/android/java/editor/src/main/res/values/themes.xml
@@ -9,4 +9,7 @@
screen. This is required. -->
- @style/GodotEditorTheme
+
+
diff --git a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt
index 4c0192253dd5..32e5ad3fa765 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/Godot.kt
+++ b/platform/android/java/lib/src/org/godotengine/godot/Godot.kt
@@ -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)
+ }
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java b/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java
index 7cfe3ef3e813..e797c5115e40 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java
@@ -509,4 +509,11 @@ public boolean supportsFeature(String featureTag) {
}
return false;
}
+
+ @Override
+ public void onEditorWorkspaceSelected(String workspace) {
+ if (parentHost != null) {
+ parentHost.onEditorWorkspaceSelected(workspace);
+ }
+ }
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java b/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java
index 60d1f01b2109..9b44fe60769e 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/GodotHost.java
@@ -145,4 +145,9 @@ default Error verifyApk(@NonNull String apkPath) {
default boolean supportsFeature(String featureTag) {
return false;
}
+
+ /**
+ * Invoked on the render thread when an editor workspace has been selected.
+ */
+ default void onEditorWorkspaceSelected(String workspace) {}
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java
index 67aa3283be5c..9bccf76241b9 100644
--- a/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java
+++ b/platform/android/java/lib/src/org/godotengine/godot/GodotLib.java
@@ -196,6 +196,30 @@ public static native boolean initialize(Activity activity,
*/
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 static void calldeferred(long p_id, String p_method, Object[] p_params) {
* @return the project resource directory
*/
public static native String getProjectResourceDir();
+
+ static native boolean isEditorHint();
+
+ static native boolean isProjectManagerHint();
}
diff --git a/platform/android/java/lib/src/org/godotengine/godot/utils/GameMenuUtils.kt b/platform/android/java/lib/src/org/godotengine/godot/utils/GameMenuUtils.kt
new file mode 100644
index 000000000000..bef710805605
--- /dev/null
+++ b/platform/android/java/lib/src/org/godotengine/godot/utils/GameMenuUtils.kt
@@ -0,0 +1,151 @@
+/**************************************************************************/
+/* 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
+ }
+ }
+ }
+
+ /**
+ * Enum representing the "run/window_placement/enable_game_menu_bar" editor setting
+ */
+ enum class GameMenuBarMode(internal val nativeValue: Int) {
+ ENABLED_FOR_ALL_GAMES(0), ENABLED_FOR_EMBEDDED_GAMES_ONLY(1);
+
+ companion object {
+ internal const val SETTING_KEY = "run/window_placement/enable_game_menu_bar"
+
+ @JvmStatic
+ internal fun fromNativeValue(nativeValue: Int): GameMenuBarMode? {
+ for (mode in GameMenuBarMode.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()
+
+ /**
+ * Returns [GameMenuBarMode] stored in the editor settings.
+ *
+ * Must be called on the render thread.
+ */
+ fun fetchGameMenuBarMode(): GameMenuBarMode {
+ try {
+ val gameMenuBarModeValue = Integer.parseInt(GodotLib.getEditorSetting(GameMenuBarMode.SETTING_KEY))
+ val gameMenuBarMode = GameMenuBarMode.fromNativeValue(gameMenuBarModeValue) ?: GameMenuBarMode.ENABLED_FOR_ALL_GAMES
+ return gameMenuBarMode
+ } catch (e: Exception) {
+ Log.w(TAG, "Unable to retrieve game menu bar mode", e)
+ return GameMenuBarMode.ENABLED_FOR_ALL_GAMES
+ }
+ }
+
+ /**
+ * 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)
+ }
+}
diff --git a/platform/android/java_godot_lib_jni.cpp b/platform/android/java_godot_lib_jni.cpp
index b291473e29d3..b56a9dbd2a3c 100644
--- a/platform/android/java_godot_lib_jni.cpp
+++ b/platform/android/java_godot_lib_jni.cpp
@@ -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;
+}
}
diff --git a/platform/android/java_godot_lib_jni.h b/platform/android/java_godot_lib_jni.h
index 6feaec47c599..c5d68b1ac201 100644
--- a/platform/android/java_godot_lib_jni.h
+++ b/platform/android/java_godot_lib_jni.h
@@ -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
diff --git a/platform/android/java_godot_wrapper.cpp b/platform/android/java_godot_wrapper.cpp
index 8c75e666a0a9..1260786b6c56 100644
--- a/platform/android/java_godot_wrapper.cpp
+++ b/platform/android/java_godot_wrapper.cpp
@@ -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);
+ }
+}
diff --git a/platform/android/java_godot_wrapper.h b/platform/android/java_godot_wrapper.h
index 1c74072e1a86..c21d443aba22 100644
--- a/platform/android/java_godot_wrapper.h
+++ b/platform/android/java_godot_wrapper.h
@@ -84,6 +84,7 @@ class GodotJavaWrapper {
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 @@ class GodotJavaWrapper {
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
diff --git a/platform/android/os_android.cpp b/platform/android/os_android.cpp
index c3162290134a..53f41acf2d7d 100644
--- a/platform/android/os_android.cpp
+++ b/platform/android/os_android.cpp
@@ -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(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(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(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()) {
diff --git a/platform/android/os_android.h b/platform/android/os_android.h
index 108a58a2b02a..a59002b73b53 100644
--- a/platform/android/os_android.h
+++ b/platform/android/os_android.h
@@ -187,6 +187,10 @@ class OS_Android : public OS_Unix {
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