瀏覽代碼

Add support for embedding game process in the Android Editor

- Implement Android editor specific `EmbeddedGodotGame` to support embedding the game window in the Android editor
Fredia Huya-Kouadio 8 月之前
父節點
當前提交
7495a8a02e
共有 71 個文件被更改,包括 2494 次插入298 次删除
  1. 0 8
      doc/classes/EditorSettings.xml
  2. 7 0
      editor/editor_main_screen.cpp
  3. 2 0
      editor/editor_main_screen.h
  4. 10 7
      editor/editor_settings.cpp
  5. 36 18
      editor/plugins/game_view_plugin.cpp
  6. 12 1
      editor/plugins/game_view_plugin.h
  7. 1 0
      platform/android/SCsub
  8. 127 0
      platform/android/game_menu_utils_jni.cpp
  9. 49 0
      platform/android/game_menu_utils_jni.h
  10. 15 0
      platform/android/java/editor/src/main/AndroidManifest.xml
  11. 369 104
      platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotEditor.kt
  12. 104 0
      platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotGame.kt
  13. 108 9
      platform/android/java/editor/src/main/java/org/godotengine/editor/EditorMessageDispatcher.kt
  14. 4 11
      platform/android/java/editor/src/main/java/org/godotengine/editor/EditorWindowInfo.kt
  15. 137 88
      platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt
  16. 3 1
      platform/android/java/editor/src/main/java/org/godotengine/editor/GodotXRGame.kt
  17. 147 0
      platform/android/java/editor/src/main/java/org/godotengine/editor/embed/EmbeddedGodotGame.kt
  18. 480 0
      platform/android/java/editor/src/main/java/org/godotengine/editor/embed/GameMenuFragment.kt
  19. 9 0
      platform/android/java/editor/src/main/res/color/game_menu_icons_color_state.xml
  20. 5 0
      platform/android/java/editor/src/main/res/drawable/baseline_close_24.xml
  21. 12 0
      platform/android/java/editor/src/main/res/drawable/baseline_expand_less_24.xml
  22. 2 2
      platform/android/java/editor/src/main/res/drawable/baseline_expand_more_48.xml
  23. 5 0
      platform/android/java/editor/src/main/res/drawable/baseline_fullscreen_24.xml
  24. 5 0
      platform/android/java/editor/src/main/res/drawable/baseline_fullscreen_exit_24.xml
  25. 6 0
      platform/android/java/editor/src/main/res/drawable/baseline_fullscreen_selector.xml
  26. 5 0
      platform/android/java/editor/src/main/res/drawable/baseline_minimize_24.xml
  27. 5 0
      platform/android/java/editor/src/main/res/drawable/baseline_picture_in_picture_alt_24.xml
  28. 5 0
      platform/android/java/editor/src/main/res/drawable/baseline_push_pin_24.xml
  29. 9 0
      platform/android/java/editor/src/main/res/drawable/camera.xml
  30. 1 6
      platform/android/java/editor/src/main/res/drawable/expand_more_bg.xml
  31. 7 0
      platform/android/java/editor/src/main/res/drawable/game_menu_button_bg.xml
  32. 6 0
      platform/android/java/editor/src/main/res/drawable/game_menu_message_bg.xml
  33. 10 0
      platform/android/java/editor/src/main/res/drawable/game_menu_selected_bg.xml
  34. 6 0
      platform/android/java/editor/src/main/res/drawable/game_menu_selected_button_bg.xml
  35. 9 0
      platform/android/java/editor/src/main/res/drawable/gui_tab_menu.xml
  36. 9 0
      platform/android/java/editor/src/main/res/drawable/gui_visibility_hidden.xml
  37. 5 0
      platform/android/java/editor/src/main/res/drawable/gui_visibility_selector.xml
  38. 9 0
      platform/android/java/editor/src/main/res/drawable/gui_visibility_visible.xml
  39. 24 0
      platform/android/java/editor/src/main/res/drawable/input_event_joypad_motion.xml
  40. 9 0
      platform/android/java/editor/src/main/res/drawable/list_select.xml
  41. 9 0
      platform/android/java/editor/src/main/res/drawable/next_frame.xml
  42. 11 0
      platform/android/java/editor/src/main/res/drawable/node_3d.xml
  43. 16 0
      platform/android/java/editor/src/main/res/drawable/nodes_2d.xml
  44. 9 0
      platform/android/java/editor/src/main/res/drawable/pause.xml
  45. 5 0
      platform/android/java/editor/src/main/res/drawable/pause_play_selector.xml
  46. 0 9
      platform/android/java/editor/src/main/res/drawable/pip_button_bg_drawable.xml
  47. 0 10
      platform/android/java/editor/src/main/res/drawable/pip_button_default_bg_drawable.xml
  48. 9 0
      platform/android/java/editor/src/main/res/drawable/play.xml
  49. 9 0
      platform/android/java/editor/src/main/res/drawable/play_48dp.xml
  50. 9 0
      platform/android/java/editor/src/main/res/drawable/tool_select.xml
  51. 190 0
      platform/android/java/editor/src/main/res/layout/game_menu_fragment_layout.xml
  52. 44 3
      platform/android/java/editor/src/main/res/layout/godot_editor_layout.xml
  53. 23 16
      platform/android/java/editor/src/main/res/layout/godot_game_layout.xml
  54. 11 0
      platform/android/java/editor/src/main/res/layout/godot_xr_game_layout.xml
  55. 56 0
      platform/android/java/editor/src/main/res/menu/options_menu.xml
  56. 8 0
      platform/android/java/editor/src/main/res/values/colors.xml
  57. 4 0
      platform/android/java/editor/src/main/res/values/dimens.xml
  58. 17 1
      platform/android/java/editor/src/main/res/values/strings.xml
  59. 3 0
      platform/android/java/editor/src/main/res/values/themes.xml
  60. 16 1
      platform/android/java/lib/src/org/godotengine/godot/Godot.kt
  61. 7 0
      platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java
  62. 5 0
      platform/android/java/lib/src/org/godotengine/godot/GodotHost.java
  63. 28 0
      platform/android/java/lib/src/org/godotengine/godot/GodotLib.java
  64. 3 3
      platform/android/java/lib/src/org/godotengine/godot/utils/DialogUtils.kt
  65. 117 0
      platform/android/java/lib/src/org/godotengine/godot/utils/GameMenuUtils.kt
  66. 58 0
      platform/android/java_godot_lib_jni.cpp
  67. 5 0
      platform/android/java_godot_lib_jni.h
  68. 11 0
      platform/android/java_godot_wrapper.cpp
  69. 3 0
      platform/android/java_godot_wrapper.h
  70. 30 0
      platform/android/os_android.cpp
  71. 4 0
      platform/android/os_android.h

+ 0 - 8
doc/classes/EditorSettings.xml

@@ -1154,19 +1154,11 @@
 			- [b]Auto (based on screen size)[/b] (default) will automatically choose how to launch the Play window based on the device and screen metrics. Defaults to [b]Same as Editor[/b] on phones and [b]Side-by-side with Editor[/b] on tablets.
 			- [b]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]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]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.
 			[b]Note:[/b] Only available in the Android editor.
 		</member>
 		</member>
 		<member name="run/window_placement/game_embed_mode" type="int" setter="" getter="">
 		<member name="run/window_placement/game_embed_mode" type="int" setter="" getter="">
 			Overrides game embedding setting for all newly opened projects. If enabled, game embedding settings are not saved.
 			Overrides game embedding setting for all newly opened projects. If enabled, game embedding settings are not saved.
 		</member>
 		</member>
-		<member name="run/window_placement/play_window_pip_mode" type="int" setter="" getter="">
-			Specifies the picture-in-picture (PiP) mode for the Play window.
-			- [b]Disabled:[/b] PiP is disabled for the Play window.
-			- [b]Enabled:[/b] If the device supports it, PiP is always enabled for the Play window. The Play window will contain a button to enter PiP mode.
-			- [b]Enabled when Play window is same as Editor[/b] (default for Android editor): If the device supports it, PiP is enabled when the Play window is the same as the Editor. The Play window will contain a button to enter PiP mode.
-			[b]Note:[/b] Only available in the Android editor.
-		</member>
 		<member name="run/window_placement/rect" type="int" setter="" getter="">
 		<member name="run/window_placement/rect" type="int" setter="" getter="">
 			The window mode to use to display the project when starting the project from the editor.
 			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."
 			[b]Note:[/b] Game embedding is not available for "Force Maximized" or "Force Fullscreen."

+ 7 - 0
editor/editor_main_screen.cpp

@@ -228,6 +228,11 @@ EditorPlugin *EditorMainScreen::get_selected_plugin() const {
 	return selected_plugin;
 	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 {
 VBoxContainer *EditorMainScreen::get_control() const {
 	return main_screen_vbox;
 	return main_screen_vbox;
 }
 }
@@ -254,6 +259,7 @@ void EditorMainScreen::add_main_plugin(EditorPlugin *p_editor) {
 	buttons.push_back(tb);
 	buttons.push_back(tb);
 	button_hb->add_child(tb);
 	button_hb->add_child(tb);
 	editor_table.push_back(p_editor);
 	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) {
 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);
 	editor_table.erase(p_editor);
+	main_editor_plugins.erase(p_editor->get_plugin_name());
 }
 }
 
 
 EditorMainScreen::EditorMainScreen() {
 EditorMainScreen::EditorMainScreen() {

+ 2 - 0
editor/editor_main_screen.h

@@ -58,6 +58,7 @@ private:
 	HBoxContainer *button_hb = nullptr;
 	HBoxContainer *button_hb = nullptr;
 	Vector<Button *> buttons;
 	Vector<Button *> buttons;
 	Vector<EditorPlugin *> editor_table;
 	Vector<EditorPlugin *> editor_table;
+	HashMap<String, EditorPlugin *> main_editor_plugins;
 
 
 	int _get_current_main_editor() const;
 	int _get_current_main_editor() const;
 
 
@@ -80,6 +81,7 @@ public:
 	int get_selected_index() const;
 	int get_selected_index() const;
 	int get_plugin_index(EditorPlugin *p_editor) const;
 	int get_plugin_index(EditorPlugin *p_editor) const;
 	EditorPlugin *get_selected_plugin() const;
 	EditorPlugin *get_selected_plugin() const;
+	EditorPlugin *get_plugin_by_name(const String &p_plugin_name) const;
 
 
 	VBoxContainer *get_control() const;
 	VBoxContainer *get_control() const;
 
 

+ 10 - 7
editor/editor_settings.cpp

@@ -939,17 +939,20 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) {
 	_initial_set("run/window_placement/rect_custom_position", Vector2());
 	_initial_set("run/window_placement/rect_custom_position", Vector2());
 	EDITOR_SETTING_BASIC(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/screen", -5, screen_hints)
 	EDITOR_SETTING_BASIC(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/screen", -5, screen_hints)
 #endif
 #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/android_window", 0, android_window_hints)
 
 
-	EDITOR_SETTING_BASIC(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/game_embed_mode", 0, "Use Per-Project Configuration:0,Embed Game:1,Make Game Workspace Floating:2,Disabled:3");
-
-	int default_play_window_pip_mode = 0;
+	String game_embed_mode_hints = "Disabled:-1,Use Per-Project Configuration:0,Embed Game:1,Make Game Workspace Floating:2";
 #ifdef ANDROID_ENABLED
 #ifdef ANDROID_ENABLED
-	default_play_window_pip_mode = 2;
+	if (OS::get_singleton()->has_feature("xr_editor")) {
+		game_embed_mode_hints = "Disabled:-1";
+	} else {
+		game_embed_mode_hints = "Disabled:-1,Auto (based on screen size):0,Enabled:1";
+	}
 #endif
 #endif
-	EDITOR_SETTING_BASIC(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/play_window_pip_mode", default_play_window_pip_mode, "Disabled:0,Enabled:1,Enabled when Play window is same as Editor:2")
+	int default_game_embed_mode = OS::get_singleton()->has_feature("xr_editor") ? -1 : 0;
+	EDITOR_SETTING_BASIC(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/game_embed_mode", default_game_embed_mode, game_embed_mode_hints);
 
 
 	// Auto save
 	// Auto save
 	_initial_set("run/auto_save/save_before_running", true, true);
 	_initial_set("run/auto_save/save_before_running", true, true);

+ 36 - 18
editor/plugins/game_view_plugin.cpp

@@ -614,6 +614,10 @@ void GameView::_notification(int p_what) {
 				// Embedding available.
 				// Embedding available.
 				int game_mode = EDITOR_GET("run/window_placement/game_embed_mode");
 				int game_mode = EDITOR_GET("run/window_placement/game_embed_mode");
 				switch (game_mode) {
 				switch (game_mode) {
+					case -1: { // Disabled.
+						embed_on_play = false;
+						make_floating_on_play = false;
+					} break;
 					case 1: { // Embed.
 					case 1: { // Embed.
 						embed_on_play = true;
 						embed_on_play = true;
 						make_floating_on_play = false;
 						make_floating_on_play = false;
@@ -622,10 +626,6 @@ void GameView::_notification(int p_what) {
 						embed_on_play = true;
 						embed_on_play = true;
 						make_floating_on_play = true;
 						make_floating_on_play = true;
 					} break;
 					} break;
-					case 3: { // Disabled.
-						embed_on_play = false;
-						make_floating_on_play = false;
-					} break;
 					default: {
 					default: {
 						embed_on_play = EditorSettings::get_singleton()->get_project_metadata("game_view", "embed_on_play", true);
 						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);
 						make_floating_on_play = EditorSettings::get_singleton()->get_project_metadata("game_view", "make_floating_on_play", true);
@@ -1027,6 +1027,18 @@ GameView::GameView(Ref<GameViewDebugger> p_debugger, WindowWrapper *p_wrapper) {
 
 
 ///////
 ///////
 
 
+void GameViewPlugin::selected_notify() {
+	if (_is_window_wrapper_enabled()) {
+#ifdef ANDROID_ENABLED
+		notify_main_screen_changed(get_plugin_name());
+#else
+		window_wrapper->grab_window_focus();
+#endif
+		_focus_another_editor();
+	}
+}
+
+#ifndef ANDROID_ENABLED
 void GameViewPlugin::make_visible(bool p_visible) {
 void GameViewPlugin::make_visible(bool p_visible) {
 	if (p_visible) {
 	if (p_visible) {
 		window_wrapper->show();
 		window_wrapper->show();
@@ -1035,13 +1047,6 @@ void GameViewPlugin::make_visible(bool p_visible) {
 	}
 	}
 }
 }
 
 
-void GameViewPlugin::selected_notify() {
-	if (window_wrapper->get_window_enabled()) {
-		window_wrapper->grab_window_focus();
-		_focus_another_editor();
-	}
-}
-
 void GameViewPlugin::set_window_layout(Ref<ConfigFile> p_layout) {
 void GameViewPlugin::set_window_layout(Ref<ConfigFile> p_layout) {
 	game_view->set_window_layout(p_layout);
 	game_view->set_window_layout(p_layout);
 }
 }
@@ -1058,6 +1063,11 @@ Dictionary GameViewPlugin::get_state() const {
 	return game_view->get_state();
 	return game_view->get_state();
 }
 }
 
 
+void GameViewPlugin::_window_visibility_changed(bool p_visible) {
+	_focus_another_editor();
+}
+#endif
+
 void GameViewPlugin::_notification(int p_what) {
 void GameViewPlugin::_notification(int p_what) {
 	switch (p_what) {
 	switch (p_what) {
 		case NOTIFICATION_ENTER_TREE: {
 		case NOTIFICATION_ENTER_TREE: {
@@ -1082,13 +1092,11 @@ void GameViewPlugin::_feature_profile_changed() {
 		debugger->set_is_feature_enabled(is_feature_enabled);
 		debugger->set_is_feature_enabled(is_feature_enabled);
 	}
 	}
 
 
+#ifndef ANDROID_ENABLED
 	if (game_view) {
 	if (game_view) {
 		game_view->set_is_feature_enabled(is_feature_enabled);
 		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) {
 void GameViewPlugin::_save_last_editor(const String &p_editor) {
@@ -1098,7 +1106,7 @@ void GameViewPlugin::_save_last_editor(const String &p_editor) {
 }
 }
 
 
 void GameViewPlugin::_focus_another_editor() {
 void GameViewPlugin::_focus_another_editor() {
-	if (window_wrapper->get_window_enabled()) {
+	if (_is_window_wrapper_enabled()) {
 		if (last_editor.is_empty()) {
 		if (last_editor.is_empty()) {
 			EditorNode::get_singleton()->get_editor_main_screen()->select(EditorMainScreen::EDITOR_2D);
 			EditorNode::get_singleton()->get_editor_main_screen()->select(EditorMainScreen::EDITOR_2D);
 		} else {
 		} else {
@@ -1107,13 +1115,22 @@ void GameViewPlugin::_focus_another_editor() {
 	}
 	}
 }
 }
 
 
+bool GameViewPlugin::_is_window_wrapper_enabled() const {
+#ifdef ANDROID_ENABLED
+	return true;
+#else
+	return window_wrapper->get_window_enabled();
+#endif
+}
+
 GameViewPlugin::GameViewPlugin() {
 GameViewPlugin::GameViewPlugin() {
+	debugger.instantiate();
+
+#ifndef ANDROID_ENABLED
 	window_wrapper = memnew(WindowWrapper);
 	window_wrapper = memnew(WindowWrapper);
 	window_wrapper->set_window_title(vformat(TTR("%s - Godot Engine"), TTR("Game Workspace")));
 	window_wrapper->set_window_title(vformat(TTR("%s - Godot Engine"), TTR("Game Workspace")));
 	window_wrapper->set_margins_enabled(true);
 	window_wrapper->set_margins_enabled(true);
 
 
-	debugger.instantiate();
-
 	game_view = memnew(GameView(debugger, window_wrapper));
 	game_view = memnew(GameView(debugger, window_wrapper));
 	game_view->set_v_size_flags(Control::SIZE_EXPAND_FILL);
 	game_view->set_v_size_flags(Control::SIZE_EXPAND_FILL);
 
 
@@ -1123,6 +1140,7 @@ GameViewPlugin::GameViewPlugin() {
 	window_wrapper->set_v_size_flags(Control::SIZE_EXPAND_FILL);
 	window_wrapper->set_v_size_flags(Control::SIZE_EXPAND_FILL);
 	window_wrapper->hide();
 	window_wrapper->hide();
 	window_wrapper->connect("window_visibility_changed", callable_mp(this, &GameViewPlugin::_window_visibility_changed));
 	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));
 	EditorFeatureProfileManager::get_singleton()->connect("current_feature_profile_changed", callable_mp(this, &GameViewPlugin::_feature_profile_changed));
 }
 }

+ 12 - 1
editor/plugins/game_view_plugin.h

@@ -32,6 +32,7 @@
 #define GAME_VIEW_PLUGIN_H
 #define GAME_VIEW_PLUGIN_H
 
 
 #include "editor/debugger/editor_debugger_node.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_debugger_plugin.h"
 #include "editor/plugins/editor_plugin.h"
 #include "editor/plugins/editor_plugin.h"
 #include "scene/debugger/scene_debugger.h"
 #include "scene/debugger/scene_debugger.h"
@@ -208,17 +209,22 @@ public:
 class GameViewPlugin : public EditorPlugin {
 class GameViewPlugin : public EditorPlugin {
 	GDCLASS(GameViewPlugin, EditorPlugin);
 	GDCLASS(GameViewPlugin, EditorPlugin);
 
 
+#ifndef ANDROID_ENABLED
 	GameView *game_view = nullptr;
 	GameView *game_view = nullptr;
 	WindowWrapper *window_wrapper = nullptr;
 	WindowWrapper *window_wrapper = nullptr;
+#endif
 
 
 	Ref<GameViewDebugger> debugger;
 	Ref<GameViewDebugger> debugger;
 
 
 	String last_editor;
 	String last_editor;
 
 
 	void _feature_profile_changed();
 	void _feature_profile_changed();
+#ifndef ANDROID_ENABLED
 	void _window_visibility_changed(bool p_visible);
 	void _window_visibility_changed(bool p_visible);
+#endif
 	void _save_last_editor(const String &p_editor);
 	void _save_last_editor(const String &p_editor);
 	void _focus_another_editor();
 	void _focus_another_editor();
+	bool _is_window_wrapper_enabled() const;
 
 
 protected:
 protected:
 	void _notification(int p_what);
 	void _notification(int p_what);
@@ -228,14 +234,19 @@ public:
 	bool has_main_screen() const override { return true; }
 	bool has_main_screen() const override { return true; }
 	virtual void edit(Object *p_object) override {}
 	virtual void edit(Object *p_object) override {}
 	virtual bool handles(Object *p_object) const override { return false; }
 	virtual bool handles(Object *p_object) const override { return false; }
-	virtual void make_visible(bool p_visible) override;
 	virtual void selected_notify() override;
 	virtual void selected_notify() override;
 
 
+	Ref<GameViewDebugger> get_debugger() const { return debugger; }
+
+#ifndef ANDROID_ENABLED
+	virtual void make_visible(bool p_visible) override;
+
 	virtual void set_window_layout(Ref<ConfigFile> p_layout) override;
 	virtual void set_window_layout(Ref<ConfigFile> p_layout) override;
 	virtual void get_window_layout(Ref<ConfigFile> p_layout) override;
 	virtual void get_window_layout(Ref<ConfigFile> p_layout) override;
 
 
 	virtual void set_state(const Dictionary &p_state) override;
 	virtual void set_state(const Dictionary &p_state) override;
 	virtual Dictionary get_state() const override;
 	virtual Dictionary get_state() const override;
+#endif
 
 
 	GameViewPlugin();
 	GameViewPlugin();
 	~GameViewPlugin();
 	~GameViewPlugin();

+ 1 - 0
platform/android/SCsub

@@ -30,6 +30,7 @@ android_files = [
     "rendering_context_driver_vulkan_android.cpp",
     "rendering_context_driver_vulkan_android.cpp",
     "variant/callable_jni.cpp",
     "variant/callable_jni.cpp",
     "dialog_utils_jni.cpp",
     "dialog_utils_jni.cpp",
+    "game_menu_utils_jni.cpp",
 ]
 ]
 
 
 env_android = env.Clone()
 env_android = env.Clone()

+ 127 - 0
platform/android/game_menu_utils_jni.cpp

@@ -0,0 +1,127 @@
+/**************************************************************************/
+/*  game_menu_utils_jni.cpp                                               */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#include "game_menu_utils_jni.h"
+
+#ifdef TOOLS_ENABLED
+#include "editor/editor_interface.h"
+#include "editor/editor_node.h"
+#include "editor/plugins/game_view_plugin.h"
+#endif
+
+extern "C" {
+
+JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSuspend(JNIEnv *env, jclass clazz, jboolean enabled) {
+#ifdef TOOLS_ENABLED
+	GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
+	if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
+		game_view_plugin->get_debugger()->set_suspend(enabled);
+	}
+#endif
+}
+
+JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_nextFrame(JNIEnv *env, jclass clazz) {
+#ifdef TOOLS_ENABLED
+	GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
+	if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
+		game_view_plugin->get_debugger()->next_frame();
+	}
+#endif
+}
+
+JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setNodeType(JNIEnv *env, jclass clazz, jint type) {
+#ifdef TOOLS_ENABLED
+	GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
+	if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
+		game_view_plugin->get_debugger()->set_node_type(type);
+	}
+#endif
+}
+
+JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSelectMode(JNIEnv *env, jclass clazz, jint mode) {
+#ifdef TOOLS_ENABLED
+	GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
+	if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
+		game_view_plugin->get_debugger()->set_select_mode(mode);
+	}
+#endif
+}
+
+JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSelectionVisible(JNIEnv *env, jclass clazz, jboolean visible) {
+#ifdef TOOLS_ENABLED
+	GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
+	if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
+		game_view_plugin->get_debugger()->set_selection_visible(visible);
+	}
+#endif
+}
+
+JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setCameraOverride(JNIEnv *env, jclass clazz, jboolean enabled) {
+#ifdef TOOLS_ENABLED
+	GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
+	if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
+		game_view_plugin->get_debugger()->set_camera_override(enabled);
+	}
+#endif
+}
+
+JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setCameraManipulateMode(JNIEnv *env, jclass clazz, jint mode) {
+#ifdef TOOLS_ENABLED
+	GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
+	if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
+		game_view_plugin->get_debugger()->set_camera_manipulate_mode(static_cast<EditorDebuggerNode::CameraOverride>(mode));
+	}
+#endif
+}
+
+JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_resetCamera2DPosition(JNIEnv *env, jclass clazz) {
+#ifdef TOOLS_ENABLED
+	GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
+	if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
+		game_view_plugin->get_debugger()->reset_camera_2d_position();
+	}
+#endif
+}
+
+JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_resetCamera3DPosition(JNIEnv *env, jclass clazz) {
+#ifdef TOOLS_ENABLED
+	GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
+	if (game_view_plugin != nullptr && game_view_plugin->get_debugger().is_valid()) {
+		game_view_plugin->get_debugger()->reset_camera_3d_position();
+	}
+#endif
+}
+
+JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_playMainScene(JNIEnv *env, jclass clazz) {
+#ifdef TOOLS_ENABLED
+	EditorInterface::get_singleton()->play_main_scene();
+#endif
+}
+}

+ 49 - 0
platform/android/game_menu_utils_jni.h

@@ -0,0 +1,49 @@
+/**************************************************************************/
+/*  game_menu_utils_jni.h                                                 */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+#ifndef GAME_MENU_UTILS_JNI_H
+#define GAME_MENU_UTILS_JNI_H
+
+#include <jni.h>
+
+extern "C" {
+JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSuspend(JNIEnv *env, jclass clazz, jboolean enabled);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_nextFrame(JNIEnv *env, jclass clazz);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setNodeType(JNIEnv *env, jclass clazz, jint type);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSelectMode(JNIEnv *env, jclass clazz, jint mode);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setSelectionVisible(JNIEnv *env, jclass clazz, jboolean visible);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setCameraOverride(JNIEnv *env, jclass clazz, jboolean enabled);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_setCameraManipulateMode(JNIEnv *env, jclass clazz, jint mode);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_resetCamera2DPosition(JNIEnv *env, jclass clazz);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_resetCamera3DPosition(JNIEnv *env, jclass clazz);
+JNIEXPORT void JNICALL Java_org_godotengine_godot_utils_GameMenuUtils_playMainScene(JNIEnv *env, jclass clazz);
+}
+
+#endif // GAME_MENU_UTILS_JNI_H

+ 15 - 0
platform/android/java/editor/src/main/AndroidManifest.xml

@@ -65,12 +65,26 @@
             android:label="@string/godot_game_activity_name"
             android:label="@string/godot_game_activity_name"
             android:launchMode="singleTask"
             android:launchMode="singleTask"
             android:process=":GodotGame"
             android:process=":GodotGame"
+            android:autoRemoveFromRecents="true"
             android:supportsPictureInPicture="true"
             android:supportsPictureInPicture="true"
             android:screenOrientation="userLandscape">
             android:screenOrientation="userLandscape">
             <layout
             <layout
                 android:defaultWidth="@dimen/editor_default_window_width"
                 android:defaultWidth="@dimen/editor_default_window_width"
                 android:defaultHeight="@dimen/editor_default_window_height" />
                 android:defaultHeight="@dimen/editor_default_window_height" />
         </activity>
         </activity>
+        <activity
+            android:name=".embed.EmbeddedGodotGame"
+            android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
+            android:exported="false"
+            android:icon="@mipmap/ic_play_window"
+            android:label="@string/godot_game_activity_name"
+            android:theme="@style/GodotEmbeddedGameTheme"
+            android:taskAffinity=":embed"
+            android:excludeFromRecents="true"
+            android:launchMode="singleTask"
+            android:process=":EmbeddedGodotGame"
+            android:supportsPictureInPicture="true"
+            android:screenOrientation="userLandscape" />
         <activity
         <activity
             android:name=".GodotXRGame"
             android:name=".GodotXRGame"
             android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
             android:configChanges="orientation|keyboardHidden|screenSize|smallestScreenSize|density|keyboard|navigation|screenLayout|uiMode"
@@ -79,6 +93,7 @@
             android:icon="@mipmap/ic_play_window"
             android:icon="@mipmap/ic_play_window"
             android:label="@string/godot_game_activity_name"
             android:label="@string/godot_game_activity_name"
             android:exported="false"
             android:exported="false"
+            android:autoRemoveFromRecents="true"
             android:screenOrientation="landscape"
             android:screenOrientation="landscape"
             android:resizeableActivity="false"
             android:resizeableActivity="false"
             android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen">
             android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen">

+ 369 - 104
platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotEditor.kt

@@ -38,23 +38,32 @@ import android.content.Context
 import android.content.Intent
 import android.content.Intent
 import android.content.pm.PackageManager
 import android.content.pm.PackageManager
 import android.os.*
 import android.os.*
+import android.preference.PreferenceManager
 import android.util.Log
 import android.util.Log
 import android.view.View
 import android.view.View
 import android.view.WindowManager
 import android.view.WindowManager
+import android.widget.TextView
 import android.widget.Toast
 import android.widget.Toast
 import androidx.annotation.CallSuper
 import androidx.annotation.CallSuper
+import androidx.core.content.edit
 import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
 import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
+import androidx.core.view.isVisible
 import androidx.window.layout.WindowMetricsCalculator
 import androidx.window.layout.WindowMetricsCalculator
+import org.godotengine.editor.embed.EmbeddedGodotGame
+import org.godotengine.editor.embed.GameMenuFragment
 import org.godotengine.editor.utils.signApk
 import org.godotengine.editor.utils.signApk
 import org.godotengine.editor.utils.verifyApk
 import org.godotengine.editor.utils.verifyApk
 import org.godotengine.godot.BuildConfig
 import org.godotengine.godot.BuildConfig
 import org.godotengine.godot.GodotActivity
 import org.godotengine.godot.GodotActivity
 import org.godotengine.godot.GodotLib
 import org.godotengine.godot.GodotLib
 import org.godotengine.godot.error.Error
 import org.godotengine.godot.error.Error
+import org.godotengine.godot.utils.DialogUtils
+import org.godotengine.godot.utils.GameMenuUtils
+import org.godotengine.godot.utils.GameMenuUtils.GameEmbedMode
+import org.godotengine.godot.utils.GameMenuUtils.fetchGameEmbedMode
 import org.godotengine.godot.utils.PermissionsUtil
 import org.godotengine.godot.utils.PermissionsUtil
 import org.godotengine.godot.utils.ProcessPhoenix
 import org.godotengine.godot.utils.ProcessPhoenix
 import org.godotengine.godot.utils.isNativeXRDevice
 import org.godotengine.godot.utils.isNativeXRDevice
-import java.util.*
 import kotlin.math.min
 import kotlin.math.min
 
 
 /**
 /**
@@ -64,32 +73,32 @@ import kotlin.math.min
  * Each derived activity runs in its own process, which enable up to have several instances of
  * Each derived activity runs in its own process, which enable up to have several instances of
  * the Godot engine up and running at the same time.
  * the Godot engine up and running at the same time.
  */
  */
-abstract class BaseGodotEditor : GodotActivity() {
+abstract class BaseGodotEditor : GodotActivity(), GameMenuFragment.GameMenuListener {
 
 
 	companion object {
 	companion object {
 		private val TAG = BaseGodotEditor::class.java.simpleName
 		private val TAG = BaseGodotEditor::class.java.simpleName
 
 
 		private const val WAIT_FOR_DEBUGGER = false
 		private const val WAIT_FOR_DEBUGGER = false
 
 
-		@JvmStatic
-		protected val EXTRA_PIP_AVAILABLE = "pip_available"
-		@JvmStatic
-		protected val EXTRA_LAUNCH_IN_PIP = "launch_in_pip_requested"
+		internal const val EXTRA_EDITOR_HINT = "editor_hint"
+		internal const val EXTRA_PROJECT_MANAGER_HINT = "project_manager_hint"
+		internal const val EXTRA_GAME_MENU_STATE = "game_menu_state"
+		internal const val EXTRA_IS_GAME_EMBEDDED = "is_game_embedded"
+		internal const val EXTRA_IS_GAME_RUNNING = "is_game_running"
 
 
-		// Command line arguments
+		// Command line arguments.
 		private const val FULLSCREEN_ARG = "--fullscreen"
 		private const val FULLSCREEN_ARG = "--fullscreen"
 		private const val FULLSCREEN_ARG_SHORT = "-f"
 		private const val FULLSCREEN_ARG_SHORT = "-f"
-		internal const val EDITOR_ARG = "--editor"
-		internal const val EDITOR_ARG_SHORT = "-e"
-		internal const val EDITOR_PROJECT_MANAGER_ARG = "--project-manager"
-		internal const val EDITOR_PROJECT_MANAGER_ARG_SHORT = "-p"
-		internal const val BREAKPOINTS_ARG = "--breakpoints"
-		internal const val BREAKPOINTS_ARG_SHORT = "-b"
+		private const val EDITOR_ARG = "--editor"
+		private const val EDITOR_ARG_SHORT = "-e"
+		private const val EDITOR_PROJECT_MANAGER_ARG = "--project-manager"
+		private const val EDITOR_PROJECT_MANAGER_ARG_SHORT = "-p"
 		internal const val XR_MODE_ARG = "--xr-mode"
 		internal const val XR_MODE_ARG = "--xr-mode"
 
 
-		// Info for the various classes used by the editor
+		// Info for the various classes used by the editor.
 		internal val EDITOR_MAIN_INFO = EditorWindowInfo(GodotEditor::class.java, 777, "")
 		internal val EDITOR_MAIN_INFO = EditorWindowInfo(GodotEditor::class.java, 777, "")
-		internal val RUN_GAME_INFO = EditorWindowInfo(GodotGame::class.java, 667, ":GodotGame", LaunchPolicy.AUTO, true)
+		internal val RUN_GAME_INFO = EditorWindowInfo(GodotGame::class.java, 667, ":GodotGame", LaunchPolicy.AUTO)
+		internal val EMBEDDED_RUN_GAME_INFO = EditorWindowInfo(EmbeddedGodotGame::class.java, 2667, ":EmbeddedGodotGame")
 		internal val XR_RUN_GAME_INFO = EditorWindowInfo(GodotXRGame::class.java, 1667, ":GodotXRGame")
 		internal val XR_RUN_GAME_INFO = EditorWindowInfo(GodotXRGame::class.java, 1667, ":GodotXRGame")
 
 
 		/** Default behavior, means we check project settings **/
 		/** Default behavior, means we check project settings **/
@@ -114,22 +123,54 @@ abstract class BaseGodotEditor : GodotActivity() {
 		private const val ANDROID_WINDOW_AUTO = 0
 		private const val ANDROID_WINDOW_AUTO = 0
 		private const val ANDROID_WINDOW_SAME_AS_EDITOR = 1
 		private const val ANDROID_WINDOW_SAME_AS_EDITOR = 1
 		private const val ANDROID_WINDOW_SIDE_BY_SIDE_WITH_EDITOR = 2
 		private const val ANDROID_WINDOW_SIDE_BY_SIDE_WITH_EDITOR = 2
-		private const val ANDROID_WINDOW_SAME_AS_EDITOR_AND_LAUNCH_IN_PIP_MODE = 3
 
 
-		/**
-		 * Sets of constants to specify the Play window PiP mode.
-		 *
-		 * Should match the values in `editor/editor_settings.cpp'` for the
-		 * 'run/window_placement/play_window_pip_mode' setting.
-		 */
-		private const val PLAY_WINDOW_PIP_DISABLED = 0
-		private const val PLAY_WINDOW_PIP_ENABLED = 1
-		private const val PLAY_WINDOW_PIP_ENABLED_FOR_SAME_AS_EDITOR = 2
+		// Game menu constants.
+		internal const val KEY_GAME_MENU_ACTION = "key_game_menu_action"
+		internal const val KEY_GAME_MENU_ACTION_PARAM1 = "key_game_menu_action_param1"
+
+		internal const val GAME_MENU_ACTION_SET_SUSPEND = "setSuspend"
+		internal const val GAME_MENU_ACTION_NEXT_FRAME = "nextFrame"
+		internal const val GAME_MENU_ACTION_SET_NODE_TYPE = "setNodeType"
+		internal const val GAME_MENU_ACTION_SET_SELECT_MODE = "setSelectMode"
+		internal const val GAME_MENU_ACTION_SET_SELECTION_VISIBLE = "setSelectionVisible"
+		internal const val GAME_MENU_ACTION_SET_CAMERA_OVERRIDE = "setCameraOverride"
+		internal const val GAME_MENU_ACTION_SET_CAMERA_MANIPULATE_MODE = "setCameraManipulateMode"
+		internal const val GAME_MENU_ACTION_RESET_CAMERA_2D_POSITION = "resetCamera2DPosition"
+		internal const val GAME_MENU_ACTION_RESET_CAMERA_3D_POSITION = "resetCamera3DPosition"
+		internal const val GAME_MENU_ACTION_EMBED_GAME_ON_PLAY = "embedGameOnPlay"
+
+		private const val GAME_WORKSPACE = "Game"
+
+		internal const val SNACKBAR_SHOW_DURATION_MS = 5000L
+
+		private const val PREF_KEY_DONT_SHOW_GAME_RESUME_HINT = "pref_key_dont_show_game_resume_hint"
 	}
 	}
 
 
-	private val editorMessageDispatcher = EditorMessageDispatcher(this)
+	internal val editorMessageDispatcher = EditorMessageDispatcher(this)
 	private val editorLoadingIndicator: View? by lazy { findViewById(R.id.editor_loading_indicator) }
 	private val editorLoadingIndicator: View? by lazy { findViewById(R.id.editor_loading_indicator) }
 
 
+	private val embeddedGameViewContainerWindow: View? by lazy { findViewById<View?>(R.id.embedded_game_view_container_window)?.apply {
+		setOnClickListener {
+			// Hide the game menu screen overlay.
+			it.isVisible = false
+		}
+
+		// Prevent the game menu screen overlay from hiding when clicking inside of the panel bounds.
+		findViewById<View?>(R.id.embedded_game_view_container)?.isClickable = true
+	} }
+	private val embeddedGameStateLabel: TextView? by lazy { findViewById<TextView?>(R.id.embedded_game_state_label)?.apply {
+		setOnClickListener {
+			godot?.runOnRenderThread {
+				GameMenuUtils.playMainScene()
+			}
+		}
+	} }
+	protected val gameMenuContainer: View? by lazy {
+		findViewById(R.id.game_menu_fragment_container)
+	}
+	protected var gameMenuFragment: GameMenuFragment? = null
+	protected val gameMenuState = Bundle()
+
 	override fun getGodotAppLayout() = R.layout.godot_editor_layout
 	override fun getGodotAppLayout() = R.layout.godot_editor_layout
 
 
 	internal open fun getEditorWindowInfo() = EDITOR_MAIN_INFO
 	internal open fun getEditorWindowInfo() = EDITOR_MAIN_INFO
@@ -187,6 +228,30 @@ abstract class BaseGodotEditor : GodotActivity() {
 		}
 		}
 
 
 		super.onCreate(savedInstanceState)
 		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() {
 	override fun onGodotSetupCompleted() {
@@ -211,8 +276,32 @@ abstract class BaseGodotEditor : GodotActivity() {
 		}
 		}
 	}
 	}
 
 
+	override fun onResume() {
+		super.onResume()
+		if (getEditorWindowInfo() == EDITOR_MAIN_INFO &&
+			godot?.isEditorHint() == true &&
+			(editorMessageDispatcher.hasEditorConnection(EMBEDDED_RUN_GAME_INFO) ||
+				editorMessageDispatcher.hasEditorConnection(RUN_GAME_INFO))) {
+			// If this is the editor window, and this is not the project manager, and we have a running game, then show
+			// a hint for how to resume the playing game.
+			val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(applicationContext)
+			if (!sharedPrefs.getBoolean(PREF_KEY_DONT_SHOW_GAME_RESUME_HINT, false)) {
+				DialogUtils.showSnackbar(
+					this,
+					getString(R.string.show_game_resume_hint),
+					SNACKBAR_SHOW_DURATION_MS,
+					getString(R.string.dont_show_again_message)
+				) {
+					sharedPrefs.edit {
+						putBoolean(PREF_KEY_DONT_SHOW_GAME_RESUME_HINT, true)
+					}
+				}
+			}
+		}
+	}
+
 	@CallSuper
 	@CallSuper
-	protected override fun updateCommandLineParams(args: Array<String>) {
+	override fun updateCommandLineParams(args: Array<String>) {
 		val args = if (BuildConfig.BUILD_TYPE == "dev") {
 		val args = if (BuildConfig.BUILD_TYPE == "dev") {
 			args + "--benchmark"
 			args + "--benchmark"
 		} else {
 		} else {
@@ -221,7 +310,7 @@ abstract class BaseGodotEditor : GodotActivity() {
 		super.updateCommandLineParams(args);
 		super.updateCommandLineParams(args);
 	}
 	}
 
 
-	protected open fun retrieveEditorWindowInfo(args: Array<String>): EditorWindowInfo {
+	protected fun retrieveEditorWindowInfo(args: Array<String>, gameEmbedMode: GameEmbedMode): EditorWindowInfo {
 		var hasEditor = false
 		var hasEditor = false
 		var xrMode = XR_MODE_DEFAULT
 		var xrMode = XR_MODE_DEFAULT
 
 
@@ -238,12 +327,22 @@ abstract class BaseGodotEditor : GodotActivity() {
 		return if (hasEditor) {
 		return if (hasEditor) {
 			EDITOR_MAIN_INFO
 			EDITOR_MAIN_INFO
 		} else {
 		} else {
+			// Launching a game.
 			val openxrEnabled = xrMode == XR_MODE_ON ||
 			val openxrEnabled = xrMode == XR_MODE_ON ||
 				(xrMode == XR_MODE_DEFAULT && GodotLib.getGlobal("xr/openxr/enabled").toBoolean())
 				(xrMode == XR_MODE_DEFAULT && GodotLib.getGlobal("xr/openxr/enabled").toBoolean())
 			if (openxrEnabled && isNativeXRDevice(applicationContext)) {
 			if (openxrEnabled && isNativeXRDevice(applicationContext)) {
 				XR_RUN_GAME_INFO
 				XR_RUN_GAME_INFO
 			} else {
 			} else {
-				RUN_GAME_INFO
+				if (godot?.isProjectManagerHint() == true || isNativeXRDevice(applicationContext)) {
+					RUN_GAME_INFO
+				} else {
+					val resolvedEmbedMode = resolveGameEmbedModeIfNeeded(gameEmbedMode)
+					if (resolvedEmbedMode == GameEmbedMode.DISABLED) {
+						RUN_GAME_INFO
+					} else {
+						EMBEDDED_RUN_GAME_INFO
+					}
+				}
 			}
 			}
 		}
 		}
 	}
 	}
@@ -253,20 +352,21 @@ abstract class BaseGodotEditor : GodotActivity() {
 			RUN_GAME_INFO.windowId -> RUN_GAME_INFO
 			RUN_GAME_INFO.windowId -> RUN_GAME_INFO
 			EDITOR_MAIN_INFO.windowId -> EDITOR_MAIN_INFO
 			EDITOR_MAIN_INFO.windowId -> EDITOR_MAIN_INFO
 			XR_RUN_GAME_INFO.windowId -> XR_RUN_GAME_INFO
 			XR_RUN_GAME_INFO.windowId -> XR_RUN_GAME_INFO
+			EMBEDDED_RUN_GAME_INFO.windowId -> EMBEDDED_RUN_GAME_INFO
 			else -> null
 			else -> null
 		}
 		}
 	}
 	}
 
 
 	protected fun getNewGodotInstanceIntent(editorWindowInfo: EditorWindowInfo, args: Array<String>): Intent {
 	protected fun getNewGodotInstanceIntent(editorWindowInfo: EditorWindowInfo, args: Array<String>): Intent {
+		// If we're launching an editor window (project manager or editor) and we're in
+		// fullscreen mode, we want to remain in fullscreen mode.
+		// This doesn't apply to the play / game window since for that window fullscreen is
+		// controlled by the game logic.
 		val updatedArgs = if (editorWindowInfo == EDITOR_MAIN_INFO &&
 		val updatedArgs = if (editorWindowInfo == EDITOR_MAIN_INFO &&
 			godot?.isInImmersiveMode() == true &&
 			godot?.isInImmersiveMode() == true &&
 			!args.contains(FULLSCREEN_ARG) &&
 			!args.contains(FULLSCREEN_ARG) &&
 			!args.contains(FULLSCREEN_ARG_SHORT)
 			!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
 			args + FULLSCREEN_ARG
 		} else {
 		} else {
 			args
 			args
@@ -278,40 +378,28 @@ abstract class BaseGodotEditor : GodotActivity() {
 			.putExtra(EXTRA_COMMAND_LINE_PARAMS, updatedArgs)
 			.putExtra(EXTRA_COMMAND_LINE_PARAMS, updatedArgs)
 
 
 		val launchPolicy = resolveLaunchPolicyIfNeeded(editorWindowInfo.launchPolicy)
 		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 (launchPolicy == LaunchPolicy.ADJACENT) {
 			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
 			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
 				Log.v(TAG, "Adding flag for adjacent launch")
 				Log.v(TAG, "Adding flag for adjacent launch")
 				newInstance.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT)
 				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
 		return newInstance
 	}
 	}
 
 
-	override fun onNewGodotInstanceRequested(args: Array<String>): Int {
-		val editorWindowInfo = retrieveEditorWindowInfo(args)
+	final override fun onNewGodotInstanceRequested(args: Array<String>): Int {
+		val editorWindowInfo = retrieveEditorWindowInfo(args, fetchGameEmbedMode())
+
+		// Check if this editor window is being terminated. If it's, delay the creation of a new instance until the
+		// termination is complete.
+		if (editorMessageDispatcher.isPendingForceQuit(editorWindowInfo)) {
+			Log.v(TAG, "Scheduling new launch after termination of ${editorWindowInfo.windowId}")
+			editorMessageDispatcher.runTaskAfterForceQuit(editorWindowInfo) {
+				onNewGodotInstanceRequested(args)
+			}
+			return editorWindowInfo.windowId
+		}
 
 
-		// Launch a new activity
 		val sourceView = godotFragment?.view
 		val sourceView = godotFragment?.view
 		val activityOptions = if (sourceView == null) {
 		val activityOptions = if (sourceView == null) {
 			null
 			null
@@ -322,6 +410,12 @@ abstract class BaseGodotEditor : GodotActivity() {
 		}
 		}
 
 
 		val newInstance = getNewGodotInstanceIntent(editorWindowInfo, args)
 		val newInstance = getNewGodotInstanceIntent(editorWindowInfo, args)
+		newInstance.apply {
+			putExtra(EXTRA_EDITOR_HINT, godot?.isEditorHint() == true)
+			putExtra(EXTRA_PROJECT_MANAGER_HINT, godot?.isProjectManagerHint() == true)
+			putExtra(EXTRA_GAME_MENU_STATE, gameMenuState)
+		}
+
 		if (editorWindowInfo.windowClassName == javaClass.name) {
 		if (editorWindowInfo.windowClassName == javaClass.name) {
 			Log.d(TAG, "Restarting ${editorWindowInfo.windowClassName} with parameters ${args.contentToString()}")
 			Log.d(TAG, "Restarting ${editorWindowInfo.windowClassName} with parameters ${args.contentToString()}")
 			triggerRebirth(activityOptions?.toBundle(), newInstance)
 			triggerRebirth(activityOptions?.toBundle(), newInstance)
@@ -344,7 +438,7 @@ abstract class BaseGodotEditor : GodotActivity() {
 		}
 		}
 
 
 		// Send an inter-process message to request the target editor window to force quit.
 		// 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
 			return true
 		}
 		}
 
 
@@ -402,58 +496,57 @@ abstract class BaseGodotEditor : GodotActivity() {
 	protected open fun enablePanAndScaleGestures() =
 	protected open fun enablePanAndScaleGestures() =
 		java.lang.Boolean.parseBoolean(GodotLib.getEditorSetting("interface/touchscreen/enable_pan_and_scale_gestures"))
 		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
 	 * If the launch policy is [LaunchPolicy.AUTO], resolve it into a specific policy based on the
 	 * editor setting or device and screen metrics.
 	 * 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 {
 	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) {
 		return when (policy) {
 			LaunchPolicy.AUTO -> {
 			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
 					LaunchPolicy.ADJACENT
 				} else {
 				} else {
-					try {
-						when (Integer.parseInt(GodotLib.getEditorSetting("run/window_placement/android_window"))) {
-							ANDROID_WINDOW_SAME_AS_EDITOR -> LaunchPolicy.SAME
-							ANDROID_WINDOW_SIDE_BY_SIDE_WITH_EDITOR -> LaunchPolicy.ADJACENT
-							ANDROID_WINDOW_SAME_AS_EDITOR_AND_LAUNCH_IN_PIP_MODE -> LaunchPolicy.SAME_AND_LAUNCH_IN_PIP_MODE
-							else -> {
-								// ANDROID_WINDOW_AUTO
-								defaultLaunchPolicy
-							}
+					LaunchPolicy.SAME
+				}
+
+				try {
+					when (Integer.parseInt(GodotLib.getEditorSetting("run/window_placement/android_window"))) {
+						ANDROID_WINDOW_SAME_AS_EDITOR -> LaunchPolicy.SAME
+						ANDROID_WINDOW_SIDE_BY_SIDE_WITH_EDITOR -> LaunchPolicy.ADJACENT
+
+						else -> {
+							// ANDROID_WINDOW_AUTO
+							defaultLaunchPolicy
 						}
 						}
-					} catch (e: NumberFormatException) {
-						Log.w(TAG, "Error parsing the Android window placement editor setting", e)
-						// Fall-back to the default launch policy
-						defaultLaunchPolicy
 					}
 					}
+				} catch (e: NumberFormatException) {
+					Log.w(TAG, "Error parsing the Android window placement editor setting", e)
+					// Fall-back to the default launch policy.
+					defaultLaunchPolicy
 				}
 				}
 			}
 			}
 
 
@@ -463,14 +556,6 @@ abstract class BaseGodotEditor : GodotActivity() {
 		}
 		}
 	}
 	}
 
 
-	/**
-	 * Returns true the if the device supports picture-in-picture (PiP)
-	 */
-	protected open fun hasPiPSystemFeature(): Boolean {
-		return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
-			packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
-	}
-
 	override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
 	override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
 		super.onActivityResult(requestCode, resultCode, data)
 		super.onActivityResult(requestCode, resultCode, data)
 		// Check if we got the MANAGE_EXTERNAL_STORAGE permission
 		// Check if we got the MANAGE_EXTERNAL_STORAGE permission
@@ -558,4 +643,184 @@ abstract class BaseGodotEditor : GodotActivity() {
 
 
         return false
         return false
     }
     }
+
+	internal fun onEditorConnected(connectedEditorId: Int) {
+		when (connectedEditorId) {
+			EMBEDDED_RUN_GAME_INFO.windowId, RUN_GAME_INFO.windowId -> {
+				runOnUiThread {
+					embeddedGameViewContainerWindow?.isVisible = false
+				}
+			}
+
+			XR_RUN_GAME_INFO.windowId -> {
+				runOnUiThread {
+					updateEmbeddedGameView(true, false)
+				}
+			}
+		}
+	}
+
+	private fun updateEmbeddedGameView(gameRunning: Boolean, gameEmbedded: Boolean) {
+		if (gameRunning) {
+			embeddedGameStateLabel?.apply {
+				setText(R.string.running_game_not_embedded_message)
+				setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
+				isClickable = false
+			}
+		} else {
+			embeddedGameStateLabel?.apply{
+				setText(R.string.embedded_game_not_running_message)
+				setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, R.drawable.play_48dp)
+				isClickable = true
+			}
+		}
+
+		gameMenuState.putBoolean(EXTRA_IS_GAME_EMBEDDED, gameEmbedded)
+		gameMenuState.putBoolean(EXTRA_IS_GAME_RUNNING, gameRunning)
+		gameMenuFragment?.refreshGameMenu(gameMenuState)
+	}
+
+	override fun onEditorWorkspaceSelected(workspace: String) {
+		if (workspace == GAME_WORKSPACE && shouldShowGameMenuBar()) {
+			if (editorMessageDispatcher.bringEditorWindowToFront(EMBEDDED_RUN_GAME_INFO) || editorMessageDispatcher.bringEditorWindowToFront(RUN_GAME_INFO)) {
+				return
+			}
+
+			val xrGameRunning = editorMessageDispatcher.hasEditorConnection(XR_RUN_GAME_INFO)
+			val gameEmbedMode = resolveGameEmbedModeIfNeeded(fetchGameEmbedMode())
+			runOnUiThread {
+				updateEmbeddedGameView(xrGameRunning, gameEmbedMode != GameEmbedMode.DISABLED)
+				embeddedGameViewContainerWindow?.isVisible = true
+			}
+		}
+	}
+
+	internal open fun bringSelfToFront() {
+		runOnUiThread {
+			Log.v(TAG, "Bringing self to front")
+			val relaunchIntent = Intent(intent)
+			// Don't restart.
+			relaunchIntent.putExtra(EXTRA_NEW_LAUNCH, false)
+			startActivity(relaunchIntent)
+		}
+	}
+
+	internal fun parseGameMenuAction(actionData: Bundle) {
+		val action = actionData.getString(KEY_GAME_MENU_ACTION) ?: return
+		when (action) {
+			GAME_MENU_ACTION_SET_SUSPEND -> {
+				val suspended = actionData.getBoolean(KEY_GAME_MENU_ACTION_PARAM1)
+				suspendGame(suspended)
+			}
+			GAME_MENU_ACTION_NEXT_FRAME -> {
+				dispatchNextFrame()
+			}
+			GAME_MENU_ACTION_SET_NODE_TYPE -> {
+				val nodeType = actionData.getSerializable(KEY_GAME_MENU_ACTION_PARAM1) as GameMenuFragment.GameMenuListener.NodeType?
+				if (nodeType != null) {
+					selectRuntimeNode(nodeType)
+				}
+			}
+			GAME_MENU_ACTION_SET_SELECTION_VISIBLE -> {
+				val enabled = actionData.getBoolean(KEY_GAME_MENU_ACTION_PARAM1)
+				toggleSelectionVisibility(enabled)
+			}
+			GAME_MENU_ACTION_SET_CAMERA_OVERRIDE -> {
+				val enabled = actionData.getBoolean(KEY_GAME_MENU_ACTION_PARAM1)
+				overrideCamera(enabled)
+			}
+			GAME_MENU_ACTION_SET_SELECT_MODE -> {
+				val selectMode = actionData.getSerializable(KEY_GAME_MENU_ACTION_PARAM1) as GameMenuFragment.GameMenuListener.SelectMode?
+				if (selectMode != null) {
+					selectRuntimeNodeSelectMode(selectMode)
+				}
+			}
+			GAME_MENU_ACTION_RESET_CAMERA_2D_POSITION -> {
+				reset2DCamera()
+			}
+			GAME_MENU_ACTION_RESET_CAMERA_3D_POSITION -> {
+				reset3DCamera()
+			}
+			GAME_MENU_ACTION_SET_CAMERA_MANIPULATE_MODE -> {
+				val mode = actionData.getSerializable(KEY_GAME_MENU_ACTION_PARAM1) as? GameMenuFragment.GameMenuListener.CameraMode?
+				if (mode != null) {
+					manipulateCamera(mode)
+				}
+			}
+			GAME_MENU_ACTION_EMBED_GAME_ON_PLAY -> {
+				val embedded = actionData.getBoolean(KEY_GAME_MENU_ACTION_PARAM1)
+				embedGameOnPlay(embedded)
+			}
+		}
+	}
+
+	override fun suspendGame(suspended: Boolean) {
+		gameMenuState.putBoolean(GAME_MENU_ACTION_SET_SUSPEND, suspended)
+		godot?.runOnRenderThread {
+			GameMenuUtils.setSuspend(suspended)
+		}
+	}
+
+	override fun dispatchNextFrame() {
+		godot?.runOnRenderThread {
+			GameMenuUtils.nextFrame()
+		}
+	}
+
+	override fun toggleSelectionVisibility(enabled: Boolean) {
+		gameMenuState.putBoolean(GAME_MENU_ACTION_SET_SELECTION_VISIBLE, enabled)
+		godot?.runOnRenderThread {
+			GameMenuUtils.setSelectionVisible(enabled)
+		}
+	}
+
+	override fun overrideCamera(enabled: Boolean) {
+		gameMenuState.putBoolean(GAME_MENU_ACTION_SET_CAMERA_OVERRIDE, enabled)
+		godot?.runOnRenderThread {
+			GameMenuUtils.setCameraOverride(enabled)
+		}
+	}
+
+	override fun selectRuntimeNode(nodeType: GameMenuFragment.GameMenuListener.NodeType) {
+		gameMenuState.putSerializable(GAME_MENU_ACTION_SET_NODE_TYPE, nodeType)
+		godot?.runOnRenderThread {
+			GameMenuUtils.setNodeType(nodeType.ordinal)
+		}
+	}
+
+	override fun selectRuntimeNodeSelectMode(selectMode: GameMenuFragment.GameMenuListener.SelectMode) {
+		gameMenuState.putSerializable(GAME_MENU_ACTION_SET_SELECT_MODE, selectMode)
+		godot?.runOnRenderThread {
+			GameMenuUtils.setSelectMode(selectMode.ordinal)
+		}
+	}
+
+	override fun reset2DCamera() {
+		godot?.runOnRenderThread {
+			GameMenuUtils.resetCamera2DPosition()
+		}
+	}
+
+	override fun reset3DCamera() {
+		godot?.runOnRenderThread {
+			GameMenuUtils.resetCamera3DPosition()
+		}
+	}
+
+	override fun manipulateCamera(mode: GameMenuFragment.GameMenuListener.CameraMode) {
+		gameMenuState.putSerializable(GAME_MENU_ACTION_SET_CAMERA_MANIPULATE_MODE, mode)
+		godot?.runOnRenderThread {
+			GameMenuUtils.setCameraManipulateMode(mode.ordinal)
+		}
+	}
+
+	override fun embedGameOnPlay(embedded: Boolean) {
+		gameMenuState.putBoolean(GAME_MENU_ACTION_EMBED_GAME_ON_PLAY, embedded)
+		godot?.runOnRenderThread {
+			val gameEmbedMode = if (embedded) GameEmbedMode.ENABLED else GameEmbedMode.DISABLED
+			GameMenuUtils.saveGameEmbedMode(gameEmbedMode)
+		}
+	}
+
+	override fun isGameEmbeddingSupported() = !isNativeXRDevice(applicationContext)
 }
 }

+ 104 - 0
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<String> {
+		val permissionsToEnable = mutableListOf<String>()
+
+		// Check for RECORD_AUDIO permission.
+		val audioInputEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("audio/driver/enable_input"))
+		if (audioInputEnabled) {
+			permissionsToEnable.add(Manifest.permission.RECORD_AUDIO)
+		}
+
+		return permissionsToEnable
+	}
+
+	protected open fun getEditorGameEmbedMode() = GameMenuUtils.GameEmbedMode.AUTO
+}

+ 108 - 9
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.
 		 * Requests the recipient to store the passed [android.os.Messenger] instance.
 		 */
 		 */
 		private const val MSG_REGISTER_MESSENGER = 1
 		private const val MSG_REGISTER_MESSENGER = 1
+
+		/**
+		 * Requests the recipient to dispatch the given game menu action.
+		 */
+		private const val MSG_DISPATCH_GAME_MENU_ACTION = 2
+
+		/**
+		 * Requests the recipient resumes itself / brings itself to front.
+		 */
+		private const val MSG_BRING_SELF_TO_FRONT = 3
 	}
 	}
 
 
-	private val recipientsMessengers = ConcurrentHashMap<Int, Messenger>()
+	private data class EditorConnectionInfo(
+		val messenger: Messenger,
+		var pendingForceQuit: Boolean = false,
+		val scheduledTasksPendingForceQuit: HashSet<Runnable> = HashSet()
+	)
+	private val editorConnectionsInfos = ConcurrentHashMap<Int, EditorConnectionInfo>()
 
 
 	@SuppressLint("HandlerLeak")
 	@SuppressLint("HandlerLeak")
 	private val dispatcherHandler = object : Handler() {
 	private val dispatcherHandler = object : Handler() {
 		override fun handleMessage(msg: Message) {
 		override fun handleMessage(msg: Message) {
 			when (msg.what) {
 			when (msg.what) {
-				MSG_FORCE_QUIT -> editor.finish()
+				MSG_FORCE_QUIT -> {
+					Log.v(TAG, "Force quitting ${editor.getEditorWindowInfo().windowId}")
+					editor.finishAndRemoveTask()
+				}
 
 
 				MSG_REGISTER_MESSENGER -> {
 				MSG_REGISTER_MESSENGER -> {
 					val editorId = msg.arg1
 					val editorId = msg.arg1
@@ -89,28 +107,100 @@ internal class EditorMessageDispatcher(private val editor: BaseGodotEditor) {
 					registerMessenger(editorId, messenger)
 					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)
 				else -> super.handleMessage(msg)
 			}
 			}
 		}
 		}
 	}
 	}
 
 
+	fun hasEditorConnection(editorWindow: EditorWindowInfo) = editorConnectionsInfos.containsKey(editorWindow.windowId)
+
 	/**
 	/**
-	 * Request the window with the given [editorId] to force quit.
+	 * Request the window with the given [editorWindow] to force quit.
 	 */
 	 */
-	fun requestForceQuit(editorId: Int): Boolean {
-		val messenger = recipientsMessengers[editorId] ?: return false
+	fun requestForceQuit(editorWindow: EditorWindowInfo): Boolean {
+		val editorId = editorWindow.windowId
+		val info = editorConnectionsInfos[editorId] ?: return false
+		if (info.pendingForceQuit) {
+			return true
+		}
+
+		val messenger = info.messenger
 		return try {
 		return try {
 			Log.v(TAG, "Requesting 'forceQuit' for $editorId")
 			Log.v(TAG, "Requesting 'forceQuit' for $editorId")
 			val msg = Message.obtain(null, MSG_FORCE_QUIT)
 			val msg = Message.obtain(null, MSG_FORCE_QUIT)
 			messenger.send(msg)
 			messenger.send(msg)
+			info.pendingForceQuit = true
+
 			true
 			true
 		} catch (e: RemoteException) {
 		} catch (e: RemoteException) {
 			Log.e(TAG, "Error requesting 'forceQuit' to $editorId", e)
 			Log.e(TAG, "Error requesting 'forceQuit' to $editorId", e)
-			recipientsMessengers.remove(editorId)
+			cleanEditorConnection(editorId)
+			false
+		}
+	}
+
+	internal fun isPendingForceQuit(editorWindow: EditorWindowInfo): Boolean {
+		return editorConnectionsInfos[editorWindow.windowId]?.pendingForceQuit == true
+	}
+
+	internal fun runTaskAfterForceQuit(editorWindow: EditorWindowInfo, task: Runnable) {
+		val connectionInfo = editorConnectionsInfos[editorWindow.windowId]
+		if (connectionInfo == null || !connectionInfo.pendingForceQuit) {
+			task.run()
+		} else {
+			connectionInfo.scheduledTasksPendingForceQuit.add(task)
+		}
+	}
+
+	/**
+	 * Request the given [editorWindow] to bring itself to front / resume itself.
+	 *
+	 * Returns true if the request was successfully dispatched, false otherwise.
+	 */
+	fun bringEditorWindowToFront(editorWindow: EditorWindowInfo): Boolean {
+		val editorId = editorWindow.windowId
+		val info = editorConnectionsInfos[editorId] ?: return false
+		val messenger = info.messenger
+		return try {
+			Log.v(TAG, "Requesting 'bringSelfToFront' for $editorId")
+			val msg = Message.obtain(null, MSG_BRING_SELF_TO_FRONT)
+			messenger.send(msg)
+			true
+		} catch (e: RemoteException) {
+			Log.e(TAG, "Error requesting 'bringSelfToFront' to $editorId", e)
+			cleanEditorConnection(editorId)
 			false
 			false
 		}
 		}
 	}
 	}
 
 
+	/**
+	 * Dispatch a game menu action to another editor instance.
+	 */
+	fun dispatchGameMenuAction(editorWindow: EditorWindowInfo, actionData: Bundle) {
+		val editorId = editorWindow.windowId
+		val info = editorConnectionsInfos[editorId] ?: return
+		val messenger = info.messenger
+		try {
+			Log.d(TAG, "Dispatch game menu action to $editorId")
+			val msg = Message.obtain(null, MSG_DISPATCH_GAME_MENU_ACTION).apply {
+				data = actionData
+			}
+			messenger.send(msg)
+		} catch (e: RemoteException) {
+			Log.e(TAG, "Error dispatching game menu action to $editorId", e)
+			cleanEditorConnection(editorId)
+		}
+	}
+
 	/**
 	/**
 	 * Utility method to register a receiver messenger.
 	 * Utility method to register a receiver messenger.
 	 */
 	 */
@@ -121,14 +211,23 @@ internal class EditorMessageDispatcher(private val editor: BaseGodotEditor) {
 			} else if (messenger.binder.isBinderAlive) {
 			} else if (messenger.binder.isBinderAlive) {
 				messenger.binder.linkToDeath({
 				messenger.binder.linkToDeath({
 					Log.v(TAG, "Removing messenger for $editorId")
 					Log.v(TAG, "Removing messenger for $editorId")
-					recipientsMessengers.remove(editorId)
+					cleanEditorConnection(editorId)
 					messengerDeathCallback?.run()
 					messengerDeathCallback?.run()
 				}, 0)
 				}, 0)
-				recipientsMessengers[editorId] = messenger
+				editorConnectionsInfos[editorId] = EditorConnectionInfo(messenger)
+				editor.onEditorConnected(editorId)
 			}
 			}
 		} catch (e: RemoteException) {
 		} catch (e: RemoteException) {
 			Log.e(TAG, "Unable to register messenger from $editorId", e)
 			Log.e(TAG, "Unable to register messenger from $editorId", e)
-			recipientsMessengers.remove(editorId)
+			cleanEditorConnection(editorId)
+		}
+	}
+
+	private fun cleanEditorConnection(editorId: Int) {
+		val connectionInfo = editorConnectionsInfos.remove(editorId) ?: return
+		Log.v(TAG, "Cleaning info for recipient $editorId")
+		for (task in connectionInfo.scheduledTasksPendingForceQuit) {
+			task.run()
 		}
 		}
 	}
 	}
 
 

+ 4 - 11
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 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 windowClassName: String,
 	val windowId: Int,
 	val windowId: Int,
 	val processNameSuffix: String,
 	val processNameSuffix: String,
-	val launchPolicy: LaunchPolicy = LaunchPolicy.SAME,
-	val supportsPiPMode: Boolean = false
+	val launchPolicy: LaunchPolicy = LaunchPolicy.SAME
 ) {
 ) {
 	constructor(
 	constructor(
 		windowClass: Class<*>,
 		windowClass: Class<*>,
 		windowId: Int,
 		windowId: Int,
 		processNameSuffix: String,
 		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)
 }
 }

+ 137 - 88
platform/android/java/editor/src/main/java/org/godotengine/editor/GodotGame.kt

@@ -30,77 +30,59 @@
 
 
 package org.godotengine.editor
 package org.godotengine.editor
 
 
-import android.Manifest
-import android.annotation.SuppressLint
 import android.app.PictureInPictureParams
 import android.app.PictureInPictureParams
-import android.content.Intent
+import android.content.pm.PackageManager
 import android.graphics.Rect
 import android.graphics.Rect
 import android.os.Build
 import android.os.Build
 import android.os.Bundle
 import android.os.Bundle
 import android.util.Log
 import android.util.Log
 import android.view.View
 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.ProcessPhoenix
+import org.godotengine.godot.utils.isNativeXRDevice
 
 
 /**
 /**
  * Drives the 'run project' window of the Godot Editor.
  * Drives the 'run project' window of the Godot Editor.
  */
  */
-open class GodotGame : GodotEditor() {
+open class GodotGame : BaseGodotGame() {
 
 
 	companion object {
 	companion object {
 		private val TAG = GodotGame::class.java.simpleName
 		private val TAG = GodotGame::class.java.simpleName
 	}
 	}
 
 
 	private val gameViewSourceRectHint = Rect()
 	private val gameViewSourceRectHint = Rect()
-	private val pipButton: View? by lazy {
-		findViewById(R.id.godot_pip_button)
-	}
-
-	private var pipAvailable = false
+	private val expandGameMenuButton: View? by lazy { findViewById(R.id.game_menu_expand_button) }
 
 
-	@SuppressLint("ClickableViewAccessibility")
 	override fun onCreate(savedInstanceState: Bundle?) {
 	override fun onCreate(savedInstanceState: Bundle?) {
+		gameMenuState.clear()
+		intent.getBundleExtra(EXTRA_GAME_MENU_STATE)?.let {
+			gameMenuState.putAll(it)
+		}
+		gameMenuState.putBoolean(EXTRA_IS_GAME_EMBEDDED, isGameEmbedded())
+		gameMenuState.putBoolean(EXTRA_IS_GAME_RUNNING, true)
+
 		super.onCreate(savedInstanceState)
 		super.onCreate(savedInstanceState)
 
 
+		gameMenuContainer?.isVisible = shouldShowGameMenuBar()
+		expandGameMenuButton?.apply{
+			isVisible = shouldShowGameMenuBar() && isMenuBarCollapsable()
+			setOnClickListener {
+				gameMenuFragment?.expandGameMenu()
+			}
+		}
+
 		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
 		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
 			val gameView = findViewById<View>(R.id.godot_fragment_container)
 			val gameView = findViewById<View>(R.id.godot_fragment_container)
 			gameView?.addOnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
 			gameView?.addOnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
 				gameView.getGlobalVisibleRect(gameViewSourceRectHint)
 				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) {
 			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
 				val builder = PictureInPictureParams.Builder().setSourceRectHint(gameViewSourceRectHint)
 				val builder = PictureInPictureParams.Builder().setSourceRectHint(gameViewSourceRectHint)
 				if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
 				if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@@ -114,10 +96,27 @@ open class GodotGame : GodotEditor() {
 		}
 		}
 	}
 	}
 
 
+	/**
+	 * Returns true the if the device supports picture-in-picture (PiP).
+	 */
+	protected fun hasPiPSystemFeature(): Boolean {
+		return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
+			packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
+	}
+
+	override fun shouldShowGameMenuBar(): Boolean {
+		return intent.getBooleanExtra(
+			EXTRA_EDITOR_HINT,
+			false
+		) && gameMenuContainer != null
+	}
+
 	override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
 	override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
 		super.onPictureInPictureModeChanged(isInPictureInPictureMode)
 		super.onPictureInPictureModeChanged(isInPictureInPictureMode)
 		Log.v(TAG, "onPictureInPictureModeChanged: $isInPictureInPictureMode")
 		Log.v(TAG, "onPictureInPictureModeChanged: $isInPictureInPictureMode")
-		updatePiPButtonVisibility()
+
+		// Hide the game menu fragment when in PiP.
+		gameMenuContainer?.isVisible = !isInPictureInPictureMode
 	}
 	}
 
 
 	override fun onStop() {
 	override fun onStop() {
@@ -134,59 +133,109 @@ open class GodotGame : GodotEditor() {
 
 
 	override fun getEditorWindowInfo() = RUN_GAME_INFO
 	override fun getEditorWindowInfo() = RUN_GAME_INFO
 
 
+	override fun getEditorGameEmbedMode() = GameMenuUtils.GameEmbedMode.DISABLED
+
 	override fun overrideOrientationRequest() = false
 	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<String> {
-		val permissionsToEnable = mutableListOf<String>()
-
-		// Check for RECORD_AUDIO permission
-		val audioInputEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("audio/driver/enable_input"))
-		if (audioInputEnabled) {
-			permissionsToEnable.add(Manifest.permission.RECORD_AUDIO)
+	override fun toggleSelectionVisibility(enabled: Boolean) {
+		val actionBundle = Bundle().apply {
+			putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_SET_SELECTION_VISIBLE)
+			putBoolean(KEY_GAME_MENU_ACTION_PARAM1, enabled)
+		}
+		editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
+	}
+
+	override fun overrideCamera(enabled: Boolean) {
+		val actionBundle = Bundle().apply {
+			putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_SET_CAMERA_OVERRIDE)
+			putBoolean(KEY_GAME_MENU_ACTION_PARAM1, enabled)
+		}
+		editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
+	}
+
+	override fun selectRuntimeNode(nodeType: GameMenuFragment.GameMenuListener.NodeType) {
+		val actionBundle = Bundle().apply {
+			putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_SET_NODE_TYPE)
+			putSerializable(KEY_GAME_MENU_ACTION_PARAM1, nodeType)
+		}
+		editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
+	}
+
+	override fun selectRuntimeNodeSelectMode(selectMode: GameMenuFragment.GameMenuListener.SelectMode) {
+		val actionBundle = Bundle().apply {
+			putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_SET_SELECT_MODE)
+			putSerializable(KEY_GAME_MENU_ACTION_PARAM1, selectMode)
+		}
+		editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
+	}
+
+	override fun reset2DCamera() {
+		val actionBundle = Bundle().apply {
+			putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_RESET_CAMERA_2D_POSITION)
+		}
+		editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
+	}
+
+	override fun reset3DCamera() {
+		val actionBundle = Bundle().apply {
+			putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_RESET_CAMERA_3D_POSITION)
+		}
+		editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
+	}
+
+	override fun manipulateCamera(mode: GameMenuFragment.GameMenuListener.CameraMode) {
+		val actionBundle = Bundle().apply {
+			putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_SET_CAMERA_MANIPULATE_MODE)
+			putSerializable(KEY_GAME_MENU_ACTION_PARAM1, mode)
+		}
+		editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
+	}
+
+	override fun embedGameOnPlay(embedded: Boolean) {
+		val actionBundle = Bundle().apply {
+			putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_EMBED_GAME_ON_PLAY)
+			putBoolean(KEY_GAME_MENU_ACTION_PARAM1, embedded)
 		}
 		}
+		editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
+	}
+
+	protected open fun isGameEmbedded() = false
+
+	override fun isGameEmbeddingSupported() = !isNativeXRDevice(applicationContext)
+
+	override fun isMinimizedButtonEnabled() = isTaskRoot && !isNativeXRDevice(applicationContext)
 
 
-		return permissionsToEnable
+	override fun isCloseButtonEnabled() = !isNativeXRDevice(applicationContext)
+
+	override fun isPiPButtonEnabled() = hasPiPSystemFeature()
+
+	override fun isMenuBarCollapsable() = true
+
+	override fun minimizeGameWindow() {
+		moveTaskToBack(false)
+	}
+
+	override fun closeGameWindow() {
+		ProcessPhoenix.forceQuit(this)
+	}
+
+	override fun onGameMenuCollapsed(collapsed: Boolean) {
+		expandGameMenuButton?.isVisible = shouldShowGameMenuBar() && isMenuBarCollapsable() && collapsed
 	}
 	}
+
 }
 }

+ 3 - 1
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.
  * Provide support for running XR apps / games from the editor window.
  */
  */
-open class GodotXRGame: GodotGame() {
+open class GodotXRGame: BaseGodotGame() {
 
 
 	override fun overrideOrientationRequest() = true
 	override fun overrideOrientationRequest() = true
 
 
@@ -56,6 +56,8 @@ open class GodotXRGame: GodotGame() {
 
 
 	override fun getEditorWindowInfo() = XR_RUN_GAME_INFO
 	override fun getEditorWindowInfo() = XR_RUN_GAME_INFO
 
 
+	override fun getGodotAppLayout() = R.layout.godot_xr_game_layout
+
 	override fun getProjectPermissionsToEnable(): MutableList<String> {
 	override fun getProjectPermissionsToEnable(): MutableList<String> {
 		val permissionsToEnable = super.getProjectPermissionsToEnable()
 		val permissionsToEnable = super.getProjectPermissionsToEnable()
 
 

+ 147 - 0
platform/android/java/editor/src/main/java/org/godotengine/editor/embed/EmbeddedGodotGame.kt

@@ -0,0 +1,147 @@
+/**************************************************************************/
+/*  EmbeddedGodotGame.kt                                                  */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+package org.godotengine.editor.embed
+
+import android.os.Bundle
+import android.view.Gravity
+import android.view.MotionEvent
+import android.view.WindowManager
+import android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND
+import android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+import android.view.WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
+import org.godotengine.editor.GodotGame
+import org.godotengine.editor.R
+import org.godotengine.godot.utils.GameMenuUtils
+
+/**
+ * Host the Godot game from the editor when the embedded mode is enabled.
+ */
+class EmbeddedGodotGame : GodotGame() {
+
+	companion object {
+		private val TAG = EmbeddedGodotGame::class.java.simpleName
+
+		private const val FULL_SCREEN_WIDTH = WindowManager.LayoutParams.MATCH_PARENT
+		private const val FULL_SCREEN_HEIGHT = WindowManager.LayoutParams.MATCH_PARENT
+	}
+
+	private val defaultWidthInPx : Int by lazy {
+		resources.getDimensionPixelSize(R.dimen.embed_game_window_default_width)
+	}
+	private val defaultHeightInPx : Int by lazy {
+		resources.getDimensionPixelSize(R.dimen.embed_game_window_default_height)
+	}
+
+	private var layoutWidthInPx = 0
+	private var layoutHeightInPx = 0
+
+	override fun onCreate(savedInstanceState: Bundle?) {
+		super.onCreate(savedInstanceState)
+
+		setFinishOnTouchOutside(false)
+
+		val layoutParams = window.attributes
+		layoutParams.flags = layoutParams.flags or FLAG_NOT_TOUCH_MODAL or FLAG_WATCH_OUTSIDE_TOUCH
+		layoutParams.flags = layoutParams.flags and FLAG_DIM_BEHIND.inv()
+		layoutParams.gravity = Gravity.END or Gravity.BOTTOM
+
+		layoutWidthInPx = defaultWidthInPx
+		layoutHeightInPx = defaultHeightInPx
+
+		layoutParams.width = layoutWidthInPx
+		layoutParams.height = layoutHeightInPx
+		window.attributes = layoutParams
+	}
+
+	override fun dispatchTouchEvent(event: MotionEvent): Boolean {
+		when (event.actionMasked) {
+			MotionEvent.ACTION_OUTSIDE -> {
+				if (gameMenuFragment?.isAlwaysOnTop() == true) {
+					enterPiPMode()
+				} else {
+					minimizeGameWindow()
+				}
+			}
+
+			MotionEvent.ACTION_MOVE -> {
+//				val layoutParams = window.attributes
+				// TODO: Add logic to move the embedded window.
+//				window.attributes = layoutParams
+			}
+		}
+		return super.dispatchTouchEvent(event)
+	}
+
+	override fun getEditorWindowInfo() = EMBEDDED_RUN_GAME_INFO
+
+	override fun getEditorGameEmbedMode() = GameMenuUtils.GameEmbedMode.ENABLED
+
+	override fun isGameEmbedded() = true
+
+	private fun updateWindowDimensions(widthInPx: Int, heightInPx: Int) {
+		val layoutParams = window.attributes
+		layoutParams.width = widthInPx
+		layoutParams.height = heightInPx
+		window.attributes = layoutParams
+	}
+
+	override fun isMinimizedButtonEnabled() = true
+
+	override fun isCloseButtonEnabled() = true
+
+	override fun isFullScreenButtonEnabled() = true
+
+	override fun isPiPButtonEnabled() = false
+
+	override fun isMenuBarCollapsable() = false
+
+	override fun isAlwaysOnTopSupported() = hasPiPSystemFeature()
+
+	override fun onFullScreenUpdated(enabled: Boolean) {
+		godot?.enableImmersiveMode(enabled)
+		if (enabled) {
+			layoutWidthInPx = FULL_SCREEN_WIDTH
+			layoutHeightInPx = FULL_SCREEN_HEIGHT
+		} else {
+			layoutWidthInPx = defaultWidthInPx
+			layoutHeightInPx = defaultHeightInPx
+		}
+		updateWindowDimensions(layoutWidthInPx, layoutHeightInPx)
+	}
+
+	override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
+		super.onPictureInPictureModeChanged(isInPictureInPictureMode)
+		// Maximize the dimensions when entering PiP so the window fills the full PiP bounds.
+		onFullScreenUpdated(isInPictureInPictureMode)
+	}
+
+	override fun shouldShowGameMenuBar() = gameMenuContainer != null
+}

+ 480 - 0
platform/android/java/editor/src/main/java/org/godotengine/editor/embed/GameMenuFragment.kt

@@ -0,0 +1,480 @@
+/**************************************************************************/
+/*  GameMenuFragment.kt                                                   */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+package org.godotengine.editor.embed
+
+import android.content.Context
+import android.os.Build
+import android.os.Bundle
+import android.preference.PreferenceManager
+import android.view.LayoutInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import android.widget.PopupMenu
+import android.widget.RadioButton
+import androidx.core.content.edit
+import androidx.core.view.isVisible
+import androidx.fragment.app.Fragment
+import org.godotengine.editor.BaseGodotEditor
+import org.godotengine.editor.BaseGodotEditor.Companion.SNACKBAR_SHOW_DURATION_MS
+import org.godotengine.editor.R
+import org.godotengine.godot.utils.DialogUtils
+
+/**
+ * Implements the game menu interface for the Android editor.
+ */
+class GameMenuFragment : Fragment(), PopupMenu.OnMenuItemClickListener {
+
+	companion object {
+		val TAG = GameMenuFragment::class.java.simpleName
+
+		private const val PREF_KEY_ALWAYS_ON_TOP = "pref_key_always_on_top"
+		private const val PREF_KEY_DONT_SHOW_RESTART_GAME_HINT = "pref_key_dont_show_restart_game_hint"
+		private const val PREF_KEY_GAME_MENU_BAR_COLLAPSED = "pref_key_game_menu_bar_collapsed"
+	}
+
+	/**
+	 * Used to be notified of events fired when interacting with the game menu.
+	 */
+	interface GameMenuListener {
+
+		/**
+		 * Kotlin representation of the RuntimeNodeSelect::SelectMode enum in 'scene/debugger/scene_debugger.h'.
+		 */
+		enum class SelectMode {
+			SINGLE,
+			LIST
+		}
+
+		/**
+		 * Kotlin representation of the RuntimeNodeSelect::NodeType enum in 'scene/debugger/scene_debugger.h'.
+		 */
+		enum class NodeType {
+			NONE,
+			TYPE_2D,
+			TYPE_3D
+		}
+
+		/**
+		 * Kotlin representation of the EditorDebuggerNode::CameraOverride in 'editor/debugger/editor_debugger_node.h'.
+		 */
+		enum class CameraMode {
+			NONE,
+			IN_GAME,
+			EDITORS
+		}
+
+		fun suspendGame(suspended: Boolean)
+		fun dispatchNextFrame()
+		fun toggleSelectionVisibility(enabled: Boolean)
+		fun overrideCamera(enabled: Boolean)
+		fun selectRuntimeNode(nodeType: NodeType)
+		fun selectRuntimeNodeSelectMode(selectMode: SelectMode)
+		fun reset2DCamera()
+		fun reset3DCamera()
+		fun manipulateCamera(mode: CameraMode)
+
+		fun isGameEmbeddingSupported(): Boolean
+		fun embedGameOnPlay(embedded: Boolean)
+
+		fun enterPiPMode() {}
+		fun minimizeGameWindow() {}
+		fun closeGameWindow() {}
+
+		fun isMinimizedButtonEnabled() = false
+		fun isFullScreenButtonEnabled() = false
+		fun isCloseButtonEnabled() = false
+		fun isPiPButtonEnabled() = false
+		fun isMenuBarCollapsable() = false
+
+		fun isAlwaysOnTopSupported() = false
+
+		fun onFullScreenUpdated(enabled: Boolean) {}
+		fun onGameMenuCollapsed(collapsed: Boolean) {}
+	}
+
+	private val collapseMenuButton: View? by lazy {
+		view?.findViewById(R.id.game_menu_collapse_button)
+	}
+	private val pauseButton: View? by lazy {
+		view?.findViewById(R.id.game_menu_pause_button)
+	}
+	private val nextFrameButton: View? by lazy {
+		view?.findViewById(R.id.game_menu_next_frame_button)
+	}
+	private val unselectNodesButton: RadioButton? by lazy {
+		view?.findViewById(R.id.game_menu_unselect_nodes_button)
+	}
+	private val select2DNodesButton: RadioButton? by lazy {
+		view?.findViewById(R.id.game_menu_select_2d_nodes_button)
+	}
+	private val select3DNodesButton: RadioButton? by lazy {
+		view?.findViewById(R.id.game_menu_select_3d_nodes_button)
+	}
+	private val guiVisibilityButton: View? by lazy {
+		view?.findViewById(R.id.game_menu_gui_visibility_button)
+	}
+	private val toolSelectButton: RadioButton? by lazy {
+		view?.findViewById(R.id.game_menu_tool_select_button)
+	}
+	private val listSelectButton: RadioButton? by lazy {
+		view?.findViewById(R.id.game_menu_list_select_button)
+	}
+	private val optionsButton: View? by lazy {
+		view?.findViewById(R.id.game_menu_options_button)
+	}
+	private val minimizeButton: View? by lazy {
+		view?.findViewById(R.id.game_menu_minimize_button)
+	}
+	private val pipButton: View? by lazy {
+		view?.findViewById(R.id.game_menu_pip_button)
+	}
+	private val fullscreenButton: View? by lazy {
+		view?.findViewById(R.id.game_menu_fullscreen_button)
+	}
+	private val closeButton: View? by lazy {
+		view?.findViewById(R.id.game_menu_close_button)
+	}
+
+	private val popupMenu: PopupMenu by lazy {
+		PopupMenu(context, optionsButton).apply {
+			setOnMenuItemClickListener(this@GameMenuFragment)
+			inflate(R.menu.options_menu)
+
+			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+				menu.setGroupDividerEnabled(true)
+			}
+		}
+	}
+
+	private val menuItemActionView: View by lazy {
+		View(context)
+	}
+	private val menuItemActionExpandListener = object: MenuItem.OnActionExpandListener {
+		override fun onMenuItemActionExpand(item: MenuItem): Boolean {
+			return false
+		}
+
+		override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
+			return false
+		}
+	}
+
+	private var menuListener: GameMenuListener? = null
+	private var alwaysOnTopChecked = false
+	private var isGameEmbedded = false
+	private var isGameRunning = false
+
+	override fun onAttach(context: Context) {
+		super.onAttach(context)
+		val parentActivity = activity
+		if (parentActivity is GameMenuListener) {
+			menuListener = parentActivity
+		} else {
+			val parentFragment = parentFragment
+			if (parentFragment is GameMenuListener) {
+				menuListener = parentFragment
+			}
+		}
+	}
+
+	override fun onDetach() {
+		super.onDetach()
+		menuListener = null
+	}
+
+	override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, bundle: Bundle?): View? {
+		return inflater.inflate(R.layout.game_menu_fragment_layout, container, false)
+	}
+
+	override fun onViewCreated(view: View, bundle: Bundle?) {
+		super.onViewCreated(view, bundle)
+
+		val isMinimizeButtonEnabled = menuListener?.isMinimizedButtonEnabled() == true
+		val isFullScreenButtonEnabled = menuListener?.isFullScreenButtonEnabled() == true
+		val isCloseButtonEnabled = menuListener?.isCloseButtonEnabled() == true
+		val isPiPButtonEnabled = menuListener?.isPiPButtonEnabled() == true
+		val isMenuBarCollapsable = menuListener?.isMenuBarCollapsable() == true
+
+		// Show the divider if any of the window controls is visible
+		view.findViewById<View>(R.id.game_menu_window_controls_divider)?.isVisible =
+			isMinimizeButtonEnabled ||
+				isFullScreenButtonEnabled ||
+				isCloseButtonEnabled ||
+				isPiPButtonEnabled ||
+				isMenuBarCollapsable
+
+		collapseMenuButton?.apply {
+			isVisible = isMenuBarCollapsable
+			setOnClickListener {
+				collapseGameMenu()
+			}
+		}
+		fullscreenButton?.apply{
+			isVisible = isFullScreenButtonEnabled
+			setOnClickListener {
+				it.isActivated = !it.isActivated
+				menuListener?.onFullScreenUpdated(it.isActivated)
+			}
+		}
+		pipButton?.apply {
+			isVisible = isPiPButtonEnabled
+			setOnClickListener {
+				menuListener?.enterPiPMode()
+			}
+		}
+		minimizeButton?.apply {
+			isVisible = isMinimizeButtonEnabled
+			setOnClickListener {
+				menuListener?.minimizeGameWindow()
+			}
+		}
+		closeButton?.apply{
+			isVisible = isCloseButtonEnabled
+			setOnClickListener {
+				menuListener?.closeGameWindow()
+			}
+		}
+		pauseButton?.apply {
+			setOnClickListener {
+				val isActivated = !it.isActivated
+				menuListener?.suspendGame(isActivated)
+				it.isActivated = isActivated
+			}
+		}
+		nextFrameButton?.apply {
+			setOnClickListener {
+				menuListener?.dispatchNextFrame()
+			}
+		}
+
+		unselectNodesButton?.apply{
+			setOnCheckedChangeListener { buttonView, isChecked ->
+				if (isChecked) {
+					menuListener?.selectRuntimeNode(GameMenuListener.NodeType.NONE)
+				}
+			}
+		}
+		select2DNodesButton?.apply{
+			setOnCheckedChangeListener { buttonView, isChecked ->
+				if (isChecked) {
+					menuListener?.selectRuntimeNode(GameMenuListener.NodeType.TYPE_2D)
+				}
+			}
+		}
+		select3DNodesButton?.apply{
+			setOnCheckedChangeListener { buttonView, isChecked ->
+				if (isChecked) {
+					menuListener?.selectRuntimeNode(GameMenuListener.NodeType.TYPE_3D)
+				}
+			}
+		}
+		guiVisibilityButton?.apply{
+			setOnClickListener {
+				val isActivated = !it.isActivated
+				menuListener?.toggleSelectionVisibility(!isActivated)
+				it.isActivated = isActivated
+			}
+		}
+
+		toolSelectButton?.apply{
+			setOnCheckedChangeListener { buttonView, isChecked ->
+				if (isChecked) {
+					menuListener?.selectRuntimeNodeSelectMode(GameMenuListener.SelectMode.SINGLE)
+				}
+			}
+		}
+		listSelectButton?.apply{
+			setOnCheckedChangeListener { buttonView, isChecked ->
+				if (isChecked) {
+					menuListener?.selectRuntimeNodeSelectMode(GameMenuListener.SelectMode.LIST)
+				}
+			}
+		}
+		optionsButton?.setOnClickListener {
+			popupMenu.show()
+		}
+
+		refreshGameMenu(arguments?.getBundle(BaseGodotEditor.EXTRA_GAME_MENU_STATE) ?: Bundle())
+	}
+
+	internal fun refreshGameMenu(gameMenuState: Bundle) {
+		val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context)
+		if (menuListener?.isMenuBarCollapsable() == true) {
+			val collapsed = sharedPrefs.getBoolean(PREF_KEY_GAME_MENU_BAR_COLLAPSED, false)
+			view?.isVisible = !collapsed
+			menuListener?.onGameMenuCollapsed(collapsed)
+		}
+		alwaysOnTopChecked = sharedPrefs.getBoolean(PREF_KEY_ALWAYS_ON_TOP, false)
+		isGameEmbedded = gameMenuState.getBoolean(BaseGodotEditor.EXTRA_IS_GAME_EMBEDDED, false)
+		isGameRunning = gameMenuState.getBoolean(BaseGodotEditor.EXTRA_IS_GAME_RUNNING, false)
+
+		pauseButton?.isEnabled = isGameRunning
+		nextFrameButton?.isEnabled = isGameRunning
+
+		val nodeType = gameMenuState.getSerializable(BaseGodotEditor.GAME_MENU_ACTION_SET_NODE_TYPE) as GameMenuListener.NodeType? ?: GameMenuListener.NodeType.NONE
+		unselectNodesButton?.isChecked = nodeType == GameMenuListener.NodeType.NONE
+		select2DNodesButton?.isChecked = nodeType == GameMenuListener.NodeType.TYPE_2D
+		select3DNodesButton?.isChecked = nodeType == GameMenuListener.NodeType.TYPE_3D
+
+		guiVisibilityButton?.isActivated = !gameMenuState.getBoolean(BaseGodotEditor.GAME_MENU_ACTION_SET_SELECTION_VISIBLE, true)
+
+		val selectMode = gameMenuState.getSerializable(BaseGodotEditor.GAME_MENU_ACTION_SET_SELECT_MODE) as GameMenuListener.SelectMode? ?: GameMenuListener.SelectMode.SINGLE
+		toolSelectButton?.isChecked = selectMode == GameMenuListener.SelectMode.SINGLE
+		listSelectButton?.isChecked = selectMode == GameMenuListener.SelectMode.LIST
+
+		popupMenu.menu.apply {
+			if (menuListener?.isGameEmbeddingSupported() == false) {
+				setGroupEnabled(R.id.group_menu_embed_options, false)
+				setGroupVisible(R.id.group_menu_embed_options, false)
+			} else {
+				findItem(R.id.menu_embed_game_on_play)?.isChecked = isGameEmbedded
+
+				val keepOnTopMenuItem = findItem(R.id.menu_embed_game_keep_on_top)
+				if (menuListener?.isAlwaysOnTopSupported() == false) {
+					keepOnTopMenuItem?.isVisible = false
+				} else {
+					keepOnTopMenuItem?.isEnabled = isGameEmbedded
+				}
+			}
+
+			setGroupEnabled(R.id.group_menu_camera_options, isGameRunning)
+			setGroupVisible(R.id.group_menu_camera_options, isGameRunning)
+			findItem(R.id.menu_camera_options)?.isEnabled = false
+
+			findItem(R.id.menu_embed_game_keep_on_top)?.isChecked = alwaysOnTopChecked
+
+			val cameraMode = gameMenuState.getSerializable(BaseGodotEditor.GAME_MENU_ACTION_SET_CAMERA_MANIPULATE_MODE) as GameMenuListener.CameraMode? ?: GameMenuListener.CameraMode.NONE
+			if (cameraMode == GameMenuListener.CameraMode.IN_GAME || cameraMode == GameMenuListener.CameraMode.NONE) {
+				findItem(R.id.menu_manipulate_camera_in_game)?.isChecked = true
+			} else {
+				findItem(R.id.menu_manipulate_camera_from_editors)?.isChecked = true
+			}
+		}
+	}
+
+	internal fun isAlwaysOnTop() = isGameEmbedded && alwaysOnTopChecked
+
+	private fun collapseGameMenu() {
+		view?.isVisible = false
+		PreferenceManager.getDefaultSharedPreferences(context).edit {
+			putBoolean(PREF_KEY_GAME_MENU_BAR_COLLAPSED, true)
+		}
+		menuListener?.onGameMenuCollapsed(true)
+	}
+
+	internal fun expandGameMenu() {
+		view?.isVisible = true
+		PreferenceManager.getDefaultSharedPreferences(context).edit {
+			putBoolean(PREF_KEY_GAME_MENU_BAR_COLLAPSED, false)
+		}
+		menuListener?.onGameMenuCollapsed(false)
+	}
+
+	private fun updateAlwaysOnTop(enabled: Boolean) {
+		alwaysOnTopChecked = enabled
+		PreferenceManager.getDefaultSharedPreferences(context).edit {
+			putBoolean(PREF_KEY_ALWAYS_ON_TOP, enabled)
+		}
+	}
+
+	private fun preventMenuItemCollapse(item: MenuItem) {
+		item.setShowAsAction(MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW)
+		item.setActionView(menuItemActionView)
+		item.setOnActionExpandListener(menuItemActionExpandListener)
+	}
+
+	override fun onMenuItemClick(item: MenuItem): Boolean {
+		if (!item.hasSubMenu()) {
+			preventMenuItemCollapse(item)
+		}
+
+		when(item.itemId) {
+			R.id.menu_embed_game_on_play -> {
+				item.isChecked = !item.isChecked
+				menuListener?.embedGameOnPlay(item.isChecked)
+
+				if (item.isChecked != isGameEmbedded && isGameRunning) {
+					activity?.let {
+						val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context)
+						if (!sharedPrefs.getBoolean(PREF_KEY_DONT_SHOW_RESTART_GAME_HINT, false)) {
+							DialogUtils.showSnackbar(
+								it,
+								if (item.isChecked) getString(R.string.restart_embed_game_hint) else getString(R.string.restart_non_embedded_game_hint),
+								SNACKBAR_SHOW_DURATION_MS,
+								getString(R.string.dont_show_again_message)
+							) {
+								sharedPrefs.edit {
+									putBoolean(PREF_KEY_DONT_SHOW_RESTART_GAME_HINT, true)
+								}
+							}
+						}
+					}
+				}
+			}
+
+			R.id.menu_embed_game_keep_on_top -> {
+				item.isChecked = !item.isChecked
+				updateAlwaysOnTop(item.isChecked)
+			}
+
+			R.id.menu_camera_override -> {
+				item.isChecked = !item.isChecked
+				menuListener?.overrideCamera(item.isChecked)
+
+				popupMenu.menu.findItem(R.id.menu_camera_options)?.isEnabled = item.isChecked
+			}
+
+			R.id.menu_reset_2d_camera -> {
+				menuListener?.reset2DCamera()
+			}
+
+			R.id.menu_reset_3d_camera -> {
+				menuListener?.reset3DCamera()
+			}
+
+			R.id.menu_manipulate_camera_in_game -> {
+				if (!item.isChecked) {
+					item.isChecked = true
+					menuListener?.manipulateCamera(GameMenuListener.CameraMode.IN_GAME)
+				}
+			}
+
+			R.id.menu_manipulate_camera_from_editors -> {
+				if (!item.isChecked) {
+					item.isChecked = true
+					menuListener?.manipulateCamera(GameMenuListener.CameraMode.EDITORS)
+				}
+			}
+		}
+		return false
+	}
+}

+ 9 - 0
platform/android/java/editor/src/main/res/color/game_menu_icons_color_state.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+	<item android:color="@color/game_menu_icon_activated_color" android:state_activated="true" />
+	<item android:color="@color/game_menu_icon_activated_color" android:state_selected="true" />
+	<item android:color="@color/game_menu_icon_activated_color" android:state_pressed="true" />
+	<item android:color="@color/game_menu_icon_activated_color" android:state_checked="true" />
+	<item android:color="@color/game_menu_icon_disabled_color" android:state_enabled="false" />
+	<item android:color="@color/game_menu_icon_default_color" />
+</selector>

+ 5 - 0
platform/android/java/editor/src/main/res/drawable/baseline_close_24.xml

@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="@color/game_menu_icons_color_state" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+    <path android:fillColor="@android:color/white" android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
+
+</vector>

+ 12 - 0
platform/android/java/editor/src/main/res/drawable/baseline_expand_less_24.xml

@@ -0,0 +1,12 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+	android:width="24dp"
+	android:height="24dp"
+	android:tint="@color/game_menu_icons_color_state"
+	android:viewportWidth="24"
+	android:viewportHeight="24">
+
+	<path
+		android:fillColor="@android:color/white"
+		android:pathData="M12,8l-6,6 1.41,1.41L12,10.83l4.59,4.58L18,14z" />
+
+</vector>

+ 2 - 2
platform/android/java/editor/src/main/res/drawable/outline_fullscreen_exit_48.xml → platform/android/java/editor/src/main/res/drawable/baseline_expand_more_48.xml

@@ -1,12 +1,12 @@
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
 <vector xmlns:android="http://schemas.android.com/apk/res/android"
 	android:width="48dp"
 	android:width="48dp"
 	android:height="48dp"
 	android:height="48dp"
-	android:tint="#FFFFFF"
+	android:tint="@color/game_menu_icons_color_state"
 	android:viewportWidth="24"
 	android:viewportWidth="24"
 	android:viewportHeight="24">
 	android:viewportHeight="24">
 
 
 	<path
 	<path
 		android:fillColor="@android:color/white"
 		android:fillColor="@android:color/white"
-		android:pathData="M5,16h3v3h2v-5L5,14v2zM8,8L5,8v2h5L10,5L8,5v3zM14,19h2v-3h3v-2h-5v5zM16,8L16,5h-2v5h5L19,8h-3z" />
+		android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z" />
 
 
 </vector>
 </vector>

+ 5 - 0
platform/android/java/editor/src/main/res/drawable/baseline_fullscreen_24.xml

@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="@color/game_menu_icons_color_state" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+    <path android:fillColor="@android:color/white" android:pathData="M7,14L5,14v5h5v-2L7,17v-3zM5,10h2L7,7h3L10,5L5,5v5zM17,17h-3v2h5v-5h-2v3zM14,5v2h3v3h2L19,5h-5z"/>
+
+</vector>

+ 5 - 0
platform/android/java/editor/src/main/res/drawable/baseline_fullscreen_exit_24.xml

@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="@color/game_menu_icons_color_state" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+    <path android:fillColor="@android:color/white" android:pathData="M5,16h3v3h2v-5L5,14v2zM8,8L5,8v2h5L10,5L8,5v3zM14,19h2v-3h3v-2h-5v5zM16,8L16,5h-2v5h5L19,8h-3z"/>
+
+</vector>

+ 6 - 0
platform/android/java/editor/src/main/res/drawable/baseline_fullscreen_selector.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+
+	<item android:drawable="@drawable/baseline_fullscreen_exit_24" android:state_activated="true" />
+	<item android:drawable="@drawable/baseline_fullscreen_24" />
+</selector>

+ 5 - 0
platform/android/java/editor/src/main/res/drawable/baseline_minimize_24.xml

@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="@color/game_menu_icons_color_state" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+    <path android:fillColor="@android:color/white" android:pathData="M6,19h12v2H6z"/>
+
+</vector>

+ 5 - 0
platform/android/java/editor/src/main/res/drawable/baseline_picture_in_picture_alt_24.xml

@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="@color/game_menu_icons_color_state" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+    <path android:fillColor="@android:color/white" android:pathData="M19,11h-8v6h8v-6zM23,19L23,4.98C23,3.88 22.1,3 21,3L3,3c-1.1,0 -2,0.88 -2,1.98L1,19c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2zM21,19.02L3,19.02L3,4.97h18v14.05z"/>
+
+</vector>

+ 5 - 0
platform/android/java/editor/src/main/res/drawable/baseline_push_pin_24.xml

@@ -0,0 +1,5 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="@color/game_menu_icons_color_state" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
+
+    <path android:fillColor="@android:color/white" android:fillType="evenOdd" android:pathData="M16,9V4l1,0c0.55,0 1,-0.45 1,-1v0c0,-0.55 -0.45,-1 -1,-1H7C6.45,2 6,2.45 6,3v0c0,0.55 0.45,1 1,1l1,0v5c0,1.66 -1.34,3 -3,3h0v2h5.97v7l1,1l1,-1v-7H19v-2h0C17.34,12 16,10.66 16,9z"/>
+
+</vector>

+ 9 - 0
platform/android/java/editor/src/main/res/drawable/camera.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="16dp"
+    android:height="16dp"
+    android:viewportWidth="16"
+    android:viewportHeight="16">
+  <path
+      android:pathData="M9,2a3,3 0,0 0,-3 2.777,3 3,0 1,0 -3,5.047V12a1,1 0,0 0,1 1h6a1,1 0,0 0,1 -1v-1l3,2V7l-3,2V7.23A3,3 0,0 0,9 2z"
+      android:fillColor="@color/game_menu_icons_color_state"/>
+</vector>

+ 1 - 6
platform/android/java/editor/src/main/res/drawable/pip_button_activated_bg_drawable.xml → platform/android/java/editor/src/main/res/drawable/expand_more_bg.xml

@@ -1,10 +1,5 @@
 <?xml version="1.0" encoding="utf-8"?>
 <?xml version="1.0" encoding="utf-8"?>
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
 <shape xmlns:android="http://schemas.android.com/apk/res/android"
 	android:shape="oval">
 	android:shape="oval">
-
-	<size
-		android:width="60dp"
-		android:height="60dp" />
-
-	<solid android:color="#44000000" />
+	<solid android:color="#99aaaaaa" />
 </shape>
 </shape>

+ 7 - 0
platform/android/java/editor/src/main/res/drawable/game_menu_button_bg.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+	<item android:drawable="@drawable/game_menu_selected_bg" android:state_pressed="true" />
+	<item android:drawable="@drawable/game_menu_selected_bg" android:state_activated="true" />
+	<item android:drawable="@drawable/game_menu_selected_bg" android:state_checked="true" />
+	<item android:drawable="@color/game_menu_default_bg" />
+</selector>

+ 6 - 0
platform/android/java/editor/src/main/res/drawable/game_menu_message_bg.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+	android:shape="rectangle">
+
+	<solid android:color="#232b2b" />
+</shape>

+ 10 - 0
platform/android/java/editor/src/main/res/drawable/game_menu_selected_bg.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+	android:shape="rectangle">
+
+	<solid android:color="#3333b5e5" />
+	<corners android:radius="5dp" />
+	<stroke
+		android:width="1dp"
+		android:color="@android:color/holo_blue_dark" />
+</shape>

+ 6 - 0
platform/android/java/editor/src/main/res/drawable/game_menu_selected_button_bg.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+	<item android:drawable="@drawable/game_menu_selected_bg" android:state_pressed="true" />
+	<item android:drawable="@drawable/game_menu_selected_bg" android:state_selected="true" />
+	<item android:drawable="@color/game_menu_default_bg" />
+</selector>

+ 9 - 0
platform/android/java/editor/src/main/res/drawable/gui_tab_menu.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="16dp"
+    android:height="16dp"
+    android:viewportWidth="16"
+    android:viewportHeight="16">
+  <path
+      android:pathData="M8,0a2,2 0,0 0,0 4,2 2,0 0,0 0,-4zM8,6a2,2 0,0 0,0 4,2 2,0 0,0 0,-4zM8,12a2,2 0,0 0,0 4,2 2,0 0,0 0,-4z"
+      android:fillColor="@color/game_menu_icons_color_state"/>
+</vector>

+ 9 - 0
platform/android/java/editor/src/main/res/drawable/gui_visibility_hidden.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="16dp"
+    android:height="16dp"
+    android:viewportWidth="16"
+    android:viewportHeight="16">
+  <path
+      android:pathData="m2.96,7.727 l-1.921,0.548c0.32,1.12 0.824,2.06 1.432,2.84l-0.834,0.834 1.414,1.414 0.843,-0.843c0.986,0.747 2.077,1.206 3.106,1.386V15h2v-1.094c1.029,-0.18 2.12,-0.639 3.105,-1.386l0.844,0.843 1.414,-1.414 -0.834,-0.834a8.285,8.285 0,0 0,1.432 -2.84l-1.922,-0.548C12.163,10.79 9.499,12 7.999,12s-4.163,-1.209 -5.038,-4.273z"
+      android:fillColor="@color/game_menu_icon_activated_color"/>
+</vector>

+ 5 - 0
platform/android/java/editor/src/main/res/drawable/gui_visibility_selector.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+	<item android:drawable="@drawable/gui_visibility_hidden" android:state_activated="true" />
+	<item android:drawable="@drawable/gui_visibility_visible" />
+</selector>

+ 9 - 0
platform/android/java/editor/src/main/res/drawable/gui_visibility_visible.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="16dp"
+    android:height="16dp"
+    android:viewportWidth="16"
+    android:viewportHeight="16">
+  <path
+      android:pathData="M8,2C5.443,2 2.209,3.948 1.045,7.705a1,1 0,0 0,0 0.55C2.163,12.211 5.5,14 8,14s5.836,-1.789 6.961,-5.725a1,1 0,0 0,0 -0.55C13.861,3.935 10.554,2 8,2zM8,4a4,4 0,0 1,0 8,4 4,0 0,1 0,-8zM8,6a2,2 0,0 0,0 4,2 2,0 0,0 0,-4z"
+      android:fillColor="@color/game_menu_icons_color_state"/>
+</vector>

+ 24 - 0
platform/android/java/editor/src/main/res/drawable/input_event_joypad_motion.xml

@@ -0,0 +1,24 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt"
+    android:width="16dp"
+    android:height="16dp"
+    android:viewportWidth="16"
+    android:viewportHeight="16">
+  <path
+      android:pathData="M11.5,16v-1.5a1,1 0,0 0,-1 -1H10l1.6,-4a4,4 0,1 0,-3.5 -1.4l-2.2,5.4h-0.7a1,1 0,0 0,-1 1V16z"
+      android:fillColor="@color/game_menu_icon_default_color"/>
+  <path
+      android:pathData="M5.25,12.2 L2,4a8,8 0,0 1,7 -3,5 5,0 0,0 -2.1,7.2z">
+    <aapt:attr name="android:fillColor">
+      <gradient
+          android:startX="2"
+          android:startY="0"
+          android:endX="9"
+          android:endY="0"
+          android:type="linear">
+        <item android:offset="0" android:color="#0069C4D4"/>
+        <item android:offset="0.6" android:color="#FF69C4D4"/>
+      </gradient>
+    </aapt:attr>
+  </path>
+</vector>

+ 9 - 0
platform/android/java/editor/src/main/res/drawable/list_select.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="16dp"
+    android:height="16dp"
+    android:viewportWidth="16"
+    android:viewportHeight="16">
+  <path
+      android:pathData="m1,1v14h8.258l-0.822,-2h-5.436v-2h4.611l-0.822,-2h-3.789v-2h3.887a1.5,1.5 0,0 1,1.098 -0.498v-0.002a1.5,1.5 0,0 1,0.586 0.111l0.945,0.389h0.484v0.199l2,0.822v-7.022h-11zM3,3h7v2h-7zM8,8 L11.291,16 12.238,13.18 14.121,15.063 15.064,14.121 13.18,12.238 16,11.291 8,8z"
+      android:fillColor="@color/game_menu_icons_color_state"/>
+</vector>

+ 9 - 0
platform/android/java/editor/src/main/res/drawable/next_frame.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="16dp"
+    android:height="16dp"
+    android:viewportWidth="16"
+    android:viewportHeight="16">
+  <path
+      android:pathData="m12,3c-0.552,0 -1,0.448 -1,1v8c0,0.552 0.448,1 1,1h1c0.552,0 1,-0.448 1,-1V4C14,3.448 13.552,3 13,3ZM2.975,3.002C2.433,3.016 2.001,3.458 2,4v8c-0,0.839 0.97,1.305 1.625,0.781l5,-4c0.499,-0.4 0.499,-1.16 0,-1.56l-5,-4C3.441,3.074 3.211,2.996 2.975,3.002Z"
+      android:fillColor="@color/game_menu_icons_color_state"/>
+</vector>

+ 11 - 0
platform/android/java/editor/src/main/res/drawable/node_3d.xml

@@ -0,0 +1,11 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="16dp"
+    android:height="16dp"
+    android:viewportWidth="16"
+    android:viewportHeight="16">
+  <path
+      android:pathData="M8,8m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
+      android:strokeWidth="2"
+      android:fillColor="#00000000"
+      android:strokeColor="#fc7f7f"/>
+</vector>

+ 16 - 0
platform/android/java/editor/src/main/res/drawable/nodes_2d.xml

@@ -0,0 +1,16 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="16dp"
+    android:height="16dp"
+    android:viewportWidth="16"
+    android:viewportHeight="16">
+  <path
+      android:pathData="M8,13C5.239,13 3,10.761 3,8 3,5.239 5.239,3 8,3"
+      android:strokeWidth="2"
+      android:fillColor="#00000000"
+      android:strokeColor="#8da5f3"/>
+  <path
+      android:pathData="m8,13c2.761,0 5,-2.239 5,-5C13,5.239 10.761,3 8,3"
+      android:strokeWidth="2"
+      android:fillColor="#00000000"
+      android:strokeColor="#8eef97"/>
+</vector>

+ 9 - 0
platform/android/java/editor/src/main/res/drawable/pause.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="16dp"
+    android:height="16dp"
+    android:viewportWidth="16"
+    android:viewportHeight="16">
+  <path
+      android:pathData="M4,3a1,1 0,0 0,-1 1v8a1,1 0,0 0,1 1h2a1,1 0,0 0,1 -1L7,4a1,1 0,0 0,-1 -1zM10,3a1,1 0,0 0,-1 1v8a1,1 0,0 0,1 1h2a1,1 0,0 0,1 -1L13,4a1,1 0,0 0,-1 -1z"
+      android:fillColor="@color/game_menu_icons_color_state"/>
+</vector>

+ 5 - 0
platform/android/java/editor/src/main/res/drawable/pause_play_selector.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<selector xmlns:android="http://schemas.android.com/apk/res/android">
+	<item android:drawable="@drawable/play" android:state_activated="true" />
+	<item android:drawable="@drawable/pause" />
+</selector>

+ 0 - 9
platform/android/java/editor/src/main/res/drawable/pip_button_bg_drawable.xml

@@ -1,9 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<selector xmlns:android="http://schemas.android.com/apk/res/android">
-
-	<item android:drawable="@drawable/pip_button_activated_bg_drawable" android:state_pressed="true" />
-	<item android:drawable="@drawable/pip_button_activated_bg_drawable" android:state_hovered="true" />
-
-	<item android:drawable="@drawable/pip_button_default_bg_drawable" />
-
-</selector>

+ 0 - 10
platform/android/java/editor/src/main/res/drawable/pip_button_default_bg_drawable.xml

@@ -1,10 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<shape xmlns:android="http://schemas.android.com/apk/res/android"
-	android:shape="oval">
-
-	<size
-		android:width="60dp"
-		android:height="60dp" />
-
-	<solid android:color="#13000000" />
-</shape>

+ 9 - 0
platform/android/java/editor/src/main/res/drawable/play.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="16dp"
+    android:height="16dp"
+    android:viewportWidth="16"
+    android:viewportHeight="16">
+  <path
+      android:pathData="M4,12a1,1 0,0 0,1.555 0.832l6,-4a1,1 0,0 0,0 -1.664l-6,-4A1,1 0,0 0,4 4z"
+      android:fillColor="@color/game_menu_icons_color_state"/>
+</vector>

+ 9 - 0
platform/android/java/editor/src/main/res/drawable/play_48dp.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="48dp"
+    android:height="48dp"
+    android:viewportWidth="16"
+    android:viewportHeight="16">
+  <path
+      android:pathData="M4,12a1,1 0,0 0,1.555 0.832l6,-4a1,1 0,0 0,0 -1.664l-6,-4A1,1 0,0 0,4 4z"
+      android:fillColor="@color/game_menu_icons_color_state"/>
+</vector>

+ 9 - 0
platform/android/java/editor/src/main/res/drawable/tool_select.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="16dp"
+    android:height="16dp"
+    android:viewportWidth="16"
+    android:viewportHeight="16">
+  <path
+      android:pathData="M14,6.932 L2,1.995l4.936,12 1.421,-4.23 2.826,2.825 1.412,-1.412L9.77,8.352z"
+      android:fillColor="@color/game_menu_icons_color_state"/>
+</vector>

+ 190 - 0
platform/android/java/editor/src/main/res/layout/game_menu_fragment_layout.xml

@@ -0,0 +1,190 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+	android:layout_width="match_parent"
+	android:layout_height="wrap_content"
+	android:orientation="horizontal"
+	android:background="@android:color/black">
+
+	<HorizontalScrollView
+		android:layout_width="0dp"
+		android:layout_height="wrap_content"
+		android:layout_weight="1">
+
+		<LinearLayout
+			android:layout_width="wrap_content"
+			android:layout_height="wrap_content"
+			android:gravity="center_vertical"
+			android:minHeight="48dp"
+			android:orientation="horizontal">
+
+			<ImageButton
+				android:id="@+id/game_menu_pause_button"
+				style="?android:attr/borderlessButtonStyle"
+				android:layout_width="48dp"
+				android:layout_height="48dp"
+				android:background="@drawable/game_menu_button_bg"
+				android:src="@drawable/pause_play_selector" />
+
+			<ImageButton
+				android:id="@+id/game_menu_next_frame_button"
+				style="?android:attr/borderlessButtonStyle"
+				android:layout_width="48dp"
+				android:layout_height="48dp"
+				android:background="@drawable/game_menu_button_bg"
+				android:src="@drawable/next_frame" />
+
+			<View
+				android:layout_width="1dp"
+				android:layout_height="match_parent"
+				android:layout_marginHorizontal="@dimen/game_menu_vseparator_horizontal_margin"
+				android:layout_marginVertical="@dimen/game_menu_vseparator_vertical_margin"
+				android:background="@color/game_menu_divider_color" />
+
+			<RadioGroup
+				android:layout_width="wrap_content"
+				android:layout_height="wrap_content"
+				android:orientation="horizontal">
+
+				<RadioButton
+					android:id="@+id/game_menu_unselect_nodes_button"
+					style="?android:attr/borderlessButtonStyle"
+					android:layout_width="wrap_content"
+					android:layout_height="48dp"
+					android:background="@drawable/game_menu_button_bg"
+					android:button="@null"
+					android:checked="true"
+					android:drawableStart="@drawable/input_event_joypad_motion"
+					android:padding="5dp"
+					android:text="@string/game_menu_input_event_joypad_motion_label" />
+
+				<RadioButton
+					android:id="@+id/game_menu_select_2d_nodes_button"
+					style="?android:attr/borderlessButtonStyle"
+					android:layout_width="48dp"
+					android:layout_height="48dp"
+					android:background="@drawable/game_menu_button_bg"
+					android:button="@null"
+					android:drawableStart="@drawable/nodes_2d"
+					android:padding="5dp"
+					android:text="@string/game_menu_nodes_2d_button_label" />
+
+				<RadioButton
+					android:id="@+id/game_menu_select_3d_nodes_button"
+					style="?android:attr/borderlessButtonStyle"
+					android:layout_width="48dp"
+					android:layout_height="48dp"
+					android:background="@drawable/game_menu_button_bg"
+					android:button="@null"
+					android:drawableStart="@drawable/node_3d"
+					android:padding="5dp"
+					android:text="@string/game_menu_node_3d_button_label" />
+			</RadioGroup>
+
+			<View
+				android:layout_width="1dp"
+				android:layout_height="match_parent"
+				android:layout_marginHorizontal="@dimen/game_menu_vseparator_horizontal_margin"
+				android:layout_marginVertical="@dimen/game_menu_vseparator_vertical_margin"
+				android:background="@color/game_menu_divider_color" />
+
+			<ImageButton
+				android:id="@+id/game_menu_gui_visibility_button"
+				style="?android:attr/borderlessButtonStyle"
+				android:layout_width="48dp"
+				android:layout_height="48dp"
+				android:background="@drawable/game_menu_button_bg"
+				android:src="@drawable/gui_visibility_selector" />
+
+			<View
+				android:layout_width="1dp"
+				android:layout_height="match_parent"
+				android:layout_marginHorizontal="@dimen/game_menu_vseparator_horizontal_margin"
+				android:layout_marginVertical="@dimen/game_menu_vseparator_vertical_margin"
+				android:background="@color/game_menu_divider_color" />
+
+			<RadioGroup
+				android:layout_width="wrap_content"
+				android:layout_height="wrap_content"
+				android:orientation="horizontal">
+
+				<RadioButton
+					android:id="@+id/game_menu_tool_select_button"
+					style="?android:attr/borderlessButtonStyle"
+					android:layout_width="48dp"
+					android:layout_height="48dp"
+					android:background="@drawable/game_menu_button_bg"
+					android:button="@null"
+					android:checked="true"
+					android:drawableStart="@drawable/tool_select"
+					android:padding="15dp" />
+
+				<RadioButton
+					android:id="@+id/game_menu_list_select_button"
+					style="?android:attr/borderlessButtonStyle"
+					android:layout_width="48dp"
+					android:layout_height="48dp"
+					android:background="@drawable/game_menu_button_bg"
+					android:button="@null"
+					android:drawableStart="@drawable/list_select"
+					android:padding="15dp" />
+			</RadioGroup>
+		</LinearLayout>
+	</HorizontalScrollView>
+
+	<ImageButton
+		android:id="@+id/game_menu_options_button"
+		style="?android:attr/borderlessButtonStyle"
+		android:layout_width="48dp"
+		android:layout_height="48dp"
+		android:background="@drawable/game_menu_button_bg"
+		android:src="@drawable/gui_tab_menu" />
+
+	<View
+		android:id="@+id/game_menu_window_controls_divider"
+		android:layout_width="1dp"
+		android:layout_height="match_parent"
+		android:layout_marginHorizontal="@dimen/game_menu_vseparator_horizontal_margin"
+		android:layout_marginVertical="@dimen/game_menu_vseparator_vertical_margin"
+		android:background="@color/game_menu_divider_color" />
+
+	<ImageButton
+		android:id="@+id/game_menu_collapse_button"
+		style="?android:attr/borderlessButtonStyle"
+		android:layout_width="48dp"
+		android:layout_height="48dp"
+		android:background="@drawable/game_menu_selected_button_bg"
+		android:src="@drawable/baseline_expand_less_24"
+		/>
+
+	<ImageButton
+		android:id="@+id/game_menu_minimize_button"
+		style="?android:attr/borderlessButtonStyle"
+		android:layout_width="48dp"
+		android:layout_height="48dp"
+		android:background="@drawable/game_menu_button_bg"
+		android:src="@drawable/baseline_minimize_24"/>
+
+	<ImageButton
+		android:id="@+id/game_menu_pip_button"
+		style="?android:attr/borderlessButtonStyle"
+		android:layout_width="48dp"
+		android:layout_height="48dp"
+		android:background="@drawable/game_menu_selected_button_bg"
+		android:src="@drawable/baseline_picture_in_picture_alt_24"/>
+
+	<ImageButton
+		android:id="@+id/game_menu_fullscreen_button"
+		style="?android:attr/borderlessButtonStyle"
+		android:layout_width="48dp"
+		android:layout_height="48dp"
+		android:background="@drawable/game_menu_selected_button_bg"
+		android:src="@drawable/baseline_fullscreen_selector"/>
+
+	<ImageButton
+		android:id="@+id/game_menu_close_button"
+		style="?android:attr/borderlessButtonStyle"
+		android:layout_width="48dp"
+		android:layout_height="48dp"
+		android:background="@drawable/game_menu_button_bg"
+		android:src="@drawable/baseline_close_24"/>
+</LinearLayout>

+ 44 - 3
platform/android/java/editor/src/main/res/layout/godot_editor_layout.xml

@@ -1,14 +1,55 @@
 <?xml version="1.0" encoding="utf-8"?>
 <?xml version="1.0" encoding="utf-8"?>
-<androidx.constraintlayout.widget.ConstraintLayout
-	xmlns:android="http://schemas.android.com/apk/res/android"
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
 	android:layout_width="match_parent"
 	android:layout_width="match_parent"
 	android:layout_height="match_parent"
 	android:layout_height="match_parent"
+	xmlns:tools="http://schemas.android.com/tools"
+	android:background="@android:color/black"
+	tools:background="@android:color/background_light"
 	xmlns:app="http://schemas.android.com/apk/res-auto">
 	xmlns:app="http://schemas.android.com/apk/res-auto">
 
 
 	<FrameLayout
 	<FrameLayout
 		android:id="@+id/godot_fragment_container"
 		android:id="@+id/godot_fragment_container"
 		android:layout_width="match_parent"
 		android:layout_width="match_parent"
-		android:layout_height="match_parent" />
+		android:layout_height="match_parent"/>
+
+	<FrameLayout
+		android:id="@+id/embedded_game_view_container_window"
+		android:layout_width="match_parent"
+		android:layout_height="match_parent"
+		android:background="#22bebebe"
+		android:visibility="gone"
+		tools:visibility="visible">
+
+		<androidx.constraintlayout.widget.ConstraintLayout
+			android:id="@+id/embedded_game_view_container"
+			android:layout_width="@dimen/embed_game_window_default_width"
+			android:layout_height="@dimen/embed_game_window_default_height"
+			android:background="@drawable/game_menu_message_bg"
+			android:layout_gravity="bottom|end">
+
+			<FrameLayout
+				android:layout_width="match_parent"
+				android:layout_height="wrap_content"
+				android:id="@+id/game_menu_fragment_container"
+				app:layout_constraintTop_toTopOf="parent" />
+
+			<TextView
+				android:id="@+id/embedded_game_state_label"
+				android:layout_width="wrap_content"
+				android:layout_height="wrap_content"
+				android:gravity="center"
+				android:text="@string/embedded_game_not_running_message"
+				android:drawableBottom="@drawable/play_48dp"
+				android:textColor="@android:color/white"
+				android:textSize="18sp"
+				android:textStyle="bold"
+				app:layout_constraintTop_toBottomOf="@+id/game_menu_fragment_container"
+				app:layout_constraintBottom_toBottomOf="parent"
+				app:layout_constraintStart_toStartOf="parent"
+				app:layout_constraintEnd_toEndOf="parent"
+				/>
+		</androidx.constraintlayout.widget.ConstraintLayout>
+	</FrameLayout>
 
 
 	<ProgressBar
 	<ProgressBar
 		style="@android:style/Widget.Holo.ProgressBar.Large"
 		style="@android:style/Widget.Holo.ProgressBar.Large"

+ 23 - 16
platform/android/java/editor/src/main/res/layout/godot_game_layout.xml

@@ -1,25 +1,32 @@
 <?xml version="1.0" encoding="utf-8"?>
 <?xml version="1.0" encoding="utf-8"?>
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
-	xmlns:tools="http://schemas.android.com/tools"
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
 	android:layout_width="match_parent"
 	android:layout_width="match_parent"
 	android:layout_height="match_parent">
 	android:layout_height="match_parent">
 
 
+	<FrameLayout
+		android:id="@+id/game_menu_fragment_container"
+		android:layout_width="match_parent"
+		android:layout_height="wrap_content"
+		android:layout_alignParentTop="true"
+		/>
+
 	<FrameLayout
 	<FrameLayout
 		android:id="@+id/godot_fragment_container"
 		android:id="@+id/godot_fragment_container"
 		android:layout_width="match_parent"
 		android:layout_width="match_parent"
-		android:layout_height="match_parent"/>
+		android:layout_height="match_parent"
+		android:layout_below="@+id/game_menu_fragment_container"/>
 
 
-	<ImageView
-		android:id="@+id/godot_pip_button"
-		android:layout_width="wrap_content"
-		android:layout_height="wrap_content"
-		android:layout_margin="36dp"
-		android:contentDescription="@string/pip_button_description"
-		android:background="@drawable/pip_button_bg_drawable"
-		android:scaleType="center"
-		android:src="@drawable/outline_fullscreen_exit_48"
-		android:visibility="gone"
-		android:layout_gravity="end|top"
-		tools:visibility="visible" />
+	<ImageButton
+		android:id="@+id/game_menu_expand_button"
+		style="?android:attr/borderlessButtonStyle"
+		android:layout_width="48dp"
+		android:layout_height="48dp"
+		android:background="@drawable/expand_more_bg"
+		android:src="@drawable/baseline_expand_more_48"
+		android:layout_below="@+id/game_menu_fragment_container"
+		android:layout_alignParentEnd="true"
+		android:layout_marginEnd="24dp"
+		android:layout_marginTop="24dp"
+		/>
 
 
-</FrameLayout>
+</RelativeLayout>

+ 11 - 0
platform/android/java/editor/src/main/res/layout/godot_xr_game_layout.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+	android:layout_width="match_parent"
+	android:layout_height="match_parent">
+
+	<FrameLayout
+		android:id="@+id/godot_fragment_container"
+		android:layout_width="match_parent"
+		android:layout_height="match_parent"/>
+
+</FrameLayout>

+ 56 - 0
platform/android/java/editor/src/main/res/menu/options_menu.xml

@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="utf-8"?>
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+
+	<group android:id="@+id/group_menu_embed_options">
+		<item
+			android:id="@+id/menu_embed_game_on_play"
+			android:checkable="true"
+			android:checked="true"
+			android:title="@string/menu_embed_game_on_play_label" />
+		<item android:id="@+id/menu_embed_game_keep_on_top"
+			android:checkable="true"
+			android:checked="false"
+			android:enabled="false"
+			android:title="@string/menu_keep_embed_game_on_top_label"
+			android:icon="@drawable/baseline_push_pin_24" />
+	</group>
+
+	<group android:id="@+id/group_menu_camera_options">
+		<item
+			android:id="@+id/menu_camera_override"
+			android:checkable="true"
+			android:checked="false"
+			android:icon="@drawable/camera"
+			android:title="@string/menu_camera_override_label" />
+		<item
+			android:id="@+id/menu_camera_options"
+			android:icon="@drawable/camera"
+			android:enabled="false"
+			android:title="@string/menu_camera_label">
+			<menu>
+				<group android:id="@+id/group_menu_camera_reset_mode">
+					<item
+						android:id="@+id/menu_reset_2d_camera"
+						android:title="@string/menu_reset_2d_camera_label" />
+
+					<item
+						android:id="@+id/menu_reset_3d_camera"
+						android:title="@string/menu_reset_3d_camera_label" />
+				</group>
+
+				<group
+					android:id="@+id/group_menu_camera_manipulation"
+					android:checkableBehavior="single">
+					<item
+						android:id="@+id/menu_manipulate_camera_in_game"
+						android:checked="true"
+						android:title="@string/menu_manipulate_in_game_label" />
+					<item
+						android:id="@+id/menu_manipulate_camera_from_editors"
+						android:title="@string/menu_manipulate_from_editors_label" />
+				</group>
+			</menu>
+		</item>
+	</group>
+
+</menu>

+ 8 - 0
platform/android/java/editor/src/main/res/values/colors.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+	<color name="game_menu_icon_default_color">#e0e0e0</color>
+	<color name="game_menu_icon_disabled_color">@android:color/darker_gray</color>
+	<color name="game_menu_icon_activated_color">@android:color/holo_blue_light</color>
+	<color name="game_menu_default_bg">@android:color/transparent</color>
+	<color name="game_menu_divider_color">@android:color/darker_gray</color>
+</resources>

+ 4 - 0
platform/android/java/editor/src/main/res/values/dimens.xml

@@ -2,4 +2,8 @@
 <resources>
 <resources>
 	<dimen name="editor_default_window_height">720dp</dimen>
 	<dimen name="editor_default_window_height">720dp</dimen>
 	<dimen name="editor_default_window_width">1024dp</dimen>
 	<dimen name="editor_default_window_width">1024dp</dimen>
+	<dimen name="game_menu_vseparator_vertical_margin">8dp</dimen>
+	<dimen name="game_menu_vseparator_horizontal_margin">1dp</dimen>
+	<dimen name="embed_game_window_default_width">640dp</dimen>
+	<dimen name="embed_game_window_default_height">360dp</dimen>
 </resources>
 </resources>

+ 17 - 1
platform/android/java/editor/src/main/res/values/strings.xml

@@ -3,5 +3,21 @@
 	<string name="godot_game_activity_name">Godot Play window</string>
 	<string name="godot_game_activity_name">Godot Play window</string>
 	<string name="denied_storage_permission_error_msg">Missing storage access permission!</string>
 	<string name="denied_storage_permission_error_msg">Missing storage access permission!</string>
 	<string name="denied_install_packages_permission_error_msg">Missing install packages permission!</string>
 	<string name="denied_install_packages_permission_error_msg">Missing install packages permission!</string>
-    <string name="pip_button_description">Button used to toggle picture-in-picture mode for the Play window</string>
+	<string name="game_menu_input_event_joypad_motion_label">Input</string>
+	<string name="game_menu_nodes_2d_button_label">2D</string>
+	<string name="game_menu_node_3d_button_label">3D</string>
+	<string name="menu_reset_2d_camera_label">Reset 2D Camera</string>
+	<string name="menu_reset_3d_camera_label">Reset 3D Camera</string>
+	<string name="menu_manipulate_in_game_label">Manipulate In-Game</string>
+	<string name="menu_manipulate_from_editors_label">Manipulate From Editors</string>
+    <string name="menu_embed_game_on_play_label">Embed Game On Play</string>
+	<string name="menu_camera_label">Camera Options</string>
+	<string name="menu_camera_override_label">Override Camera</string>
+	<string name="embedded_game_not_running_message">Press play to start the game.</string>
+	<string name="running_game_not_embedded_message">Game running not embedded.</string>
+	<string name="menu_keep_embed_game_on_top_label">Keep on Top using PiP</string>
+	<string name="dont_show_again_message">Don\'t show again</string>
+	<string name="show_game_resume_hint">Tap on \'Game\' to resume</string>
+	<string name="restart_embed_game_hint">Restart game to embed</string>
+	<string name="restart_non_embedded_game_hint">Restart Game to disable embedding</string>
 </resources>
 </resources>

+ 3 - 0
platform/android/java/editor/src/main/res/values/themes.xml

@@ -9,4 +9,7 @@
 		screen. This is required. -->
 		screen. This is required. -->
 		<item name="postSplashScreenTheme">@style/GodotEditorTheme</item>
 		<item name="postSplashScreenTheme">@style/GodotEditorTheme</item>
 	</style>
 	</style>
+
+	<style name="GodotEmbeddedGameTheme" parent="@android:style/Theme.DeviceDefault.Panel">
+	</style>
 </resources>
 </resources>

+ 16 - 1
platform/android/java/lib/src/org/godotengine/godot/Godot.kt

@@ -333,7 +333,7 @@ class Godot(private val context: Context) {
 	 * Toggle immersive mode.
 	 * Toggle immersive mode.
 	 * Must be called from the UI thread.
 	 * 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 activity = getActivity() ?: return
 		val window = activity.window ?: return
 		val window = activity.window ?: return
 
 
@@ -1068,6 +1068,16 @@ class Godot(private val context: Context) {
 		return PermissionsUtil.getGrantedPermissions(getActivity())
 		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.
 	 * 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
 		val verifyResult = primaryHost?.verifyApk(apkPath) ?: Error.ERR_UNAVAILABLE
 		return verifyResult.toNativeValue()
 		return verifyResult.toNativeValue()
 	}
 	}
+
+	@Keep
+	private fun nativeOnEditorWorkspaceSelected(workspace: String) {
+		primaryHost?.onEditorWorkspaceSelected(workspace)
+	}
 }
 }

+ 7 - 0
platform/android/java/lib/src/org/godotengine/godot/GodotFragment.java

@@ -509,4 +509,11 @@ public class GodotFragment extends Fragment implements IDownloaderClient, GodotH
 		}
 		}
 		return false;
 		return false;
 	}
 	}
+
+	@Override
+	public void onEditorWorkspaceSelected(String workspace) {
+		if (parentHost != null) {
+			parentHost.onEditorWorkspaceSelected(workspace);
+		}
+	}
 }
 }

+ 5 - 0
platform/android/java/lib/src/org/godotengine/godot/GodotHost.java

@@ -145,4 +145,9 @@ public interface GodotHost {
 	default boolean supportsFeature(String featureTag) {
 	default boolean supportsFeature(String featureTag) {
 		return false;
 		return false;
 	}
 	}
+
+	/**
+	 * Invoked on the render thread when an editor workspace has been selected.
+	 */
+	default void onEditorWorkspaceSelected(String workspace) {}
 }
 }

+ 28 - 0
platform/android/java/lib/src/org/godotengine/godot/GodotLib.java

@@ -196,6 +196,30 @@ public class GodotLib {
 	 */
 	 */
 	public static native String getEditorSetting(String settingKey);
 	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|
 	 * Invoke method |p_method| on the Godot object specified by |p_id|
 	 * @param p_id Id of the Godot object to invoke
 	 * @param p_id Id of the Godot object to invoke
@@ -267,4 +291,8 @@ public class GodotLib {
 	 * @return the project resource directory
 	 * @return the project resource directory
 	 */
 	 */
 	public static native String getProjectResourceDir();
 	public static native String getProjectResourceDir();
+
+	static native boolean isEditorHint();
+
+	static native boolean isProjectManagerHint();
 }
 }

+ 3 - 3
platform/android/java/lib/src/org/godotengine/godot/utils/DialogUtils.kt

@@ -52,7 +52,7 @@ import kotlin.math.abs
 /**
 /**
  * Utility class for managing dialogs.
  * Utility class for managing dialogs.
  */
  */
-internal class DialogUtils {
+class DialogUtils {
 	companion object {
 	companion object {
 		private val TAG = DialogUtils::class.java.simpleName
 		private val TAG = DialogUtils::class.java.simpleName
 
 
@@ -79,7 +79,7 @@ internal class DialogUtils {
 		 * @param message The message displayed in the dialog.
 		 * @param message The message displayed in the dialog.
 		 * @param buttons An array of button labels to display.
 		 * @param buttons An array of button labels to display.
 		 */
 		 */
-		fun showDialog(activity: Activity, title: String, message: String, buttons: Array<String>) {
+		internal fun showDialog(activity: Activity, title: String, message: String, buttons: Array<String>) {
 			var dismissDialog: () -> Unit = {} // Helper to dismiss the Dialog when a button is clicked.
 			var dismissDialog: () -> Unit = {} // Helper to dismiss the Dialog when a button is clicked.
 			activity.runOnUiThread {
 			activity.runOnUiThread {
 				val builder = AlertDialog.Builder(activity)
 				val builder = AlertDialog.Builder(activity)
@@ -174,7 +174,7 @@ internal class DialogUtils {
 		 * @param message The message displayed in the input dialog.
 		 * @param message The message displayed in the input dialog.
 		 * @param existingText The existing text that will be pre-filled in the input field.
 		 * @param existingText The existing text that will be pre-filled in the input field.
 		 */
 		 */
-		fun showInputDialog(activity: Activity, title: String, message: String, existingText: String) {
+		internal fun showInputDialog(activity: Activity, title: String, message: String, existingText: String) {
 			val inputField = EditText(activity)
 			val inputField = EditText(activity)
 			val paddingHorizontal = activity.resources.getDimensionPixelSize(R.dimen.dialog_padding_horizontal)
 			val paddingHorizontal = activity.resources.getDimensionPixelSize(R.dimen.dialog_padding_horizontal)
 			val paddingVertical = activity.resources.getDimensionPixelSize(R.dimen.dialog_padding_vertical)
 			val paddingVertical = activity.resources.getDimensionPixelSize(R.dimen.dialog_padding_vertical)

+ 117 - 0
platform/android/java/lib/src/org/godotengine/godot/utils/GameMenuUtils.kt

@@ -0,0 +1,117 @@
+/**************************************************************************/
+/*  GameMenuUtils.kt                                                      */
+/**************************************************************************/
+/*                         This file is part of:                          */
+/*                             GODOT ENGINE                               */
+/*                        https://godotengine.org                         */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.                  */
+/*                                                                        */
+/* Permission is hereby granted, free of charge, to any person obtaining  */
+/* a copy of this software and associated documentation files (the        */
+/* "Software"), to deal in the Software without restriction, including    */
+/* without limitation the rights to use, copy, modify, merge, publish,    */
+/* distribute, sublicense, and/or sell copies of the Software, and to     */
+/* permit persons to whom the Software is furnished to do so, subject to  */
+/* the following conditions:                                              */
+/*                                                                        */
+/* The above copyright notice and this permission notice shall be         */
+/* included in all copies or substantial portions of the Software.        */
+/*                                                                        */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,        */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF     */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY   */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,   */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE      */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
+/**************************************************************************/
+
+package org.godotengine.godot.utils
+
+import android.util.Log
+import org.godotengine.godot.GodotLib
+
+/**
+ * Utility class for accessing and using game menu APIs.
+ */
+object GameMenuUtils {
+	private val TAG = GameMenuUtils::class.java.simpleName
+
+	/**
+	 * Enum representing the "run/window_placement/game_embed_mode" editor settings.
+ 	 */
+	enum class GameEmbedMode(internal val nativeValue: Int) {
+		DISABLED(-1), AUTO(0), ENABLED(1);
+
+		companion object {
+			internal const val SETTING_KEY = "run/window_placement/game_embed_mode"
+
+			@JvmStatic
+			internal fun fromNativeValue(nativeValue: Int): GameEmbedMode? {
+				for (mode in GameEmbedMode.entries) {
+					if (mode.nativeValue == nativeValue) {
+						return mode
+					}
+				}
+				return null
+			}
+		}
+	}
+
+	@JvmStatic
+	external fun setSuspend(enabled: Boolean)
+
+	@JvmStatic
+	external fun nextFrame()
+
+	@JvmStatic
+	external fun setNodeType(type: Int)
+
+	@JvmStatic
+	external fun setSelectMode(mode: Int)
+
+	@JvmStatic
+	external fun setSelectionVisible(visible: Boolean)
+
+	@JvmStatic
+	external fun setCameraOverride(enabled: Boolean)
+
+	@JvmStatic
+	external fun setCameraManipulateMode(mode: Int)
+
+	@JvmStatic
+	external fun resetCamera2DPosition()
+
+	@JvmStatic
+	external fun resetCamera3DPosition()
+
+	@JvmStatic
+	external fun playMainScene()
+
+	/**
+	 * Returns [GameEmbedMode] stored in the editor settings.
+	 *
+	 * Must be called on the render thread.
+	 */
+	fun fetchGameEmbedMode(): GameEmbedMode {
+		try {
+			val gameEmbedModeValue = Integer.parseInt(GodotLib.getEditorSetting(GameEmbedMode.SETTING_KEY))
+			val gameEmbedMode = GameEmbedMode.fromNativeValue(gameEmbedModeValue) ?: GameEmbedMode.AUTO
+			return gameEmbedMode
+		} catch (e: Exception) {
+			Log.w(TAG, "Unable to retrieve game embed mode", e)
+			return GameEmbedMode.AUTO
+		}
+	}
+
+	/**
+	 * Update the 'game_embed_mode' editor setting.
+	 *
+	 * Must be called on the render thread.
+	 */
+	fun saveGameEmbedMode(gameEmbedMode: GameEmbedMode) {
+		GodotLib.setEditorSetting(GameEmbedMode.SETTING_KEY, gameEmbedMode.nativeValue)
+	}
+}

+ 58 - 0
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());
 	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) {
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onNightModeChanged(JNIEnv *env, jclass clazz) {
 	DisplayServerAndroid *ds = (DisplayServerAndroid *)DisplayServer::get_singleton();
 	DisplayServerAndroid *ds = (DisplayServerAndroid *)DisplayServer::get_singleton();
 	if (ds) {
 	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();
 	const String resource_dir = OS::get_singleton()->get_resource_dir();
 	return env->NewStringUTF(resource_dir.utf8().get_data());
 	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;
+}
 }
 }

+ 5 - 0
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 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_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 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_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_requestPermissionResult(JNIEnv *env, jclass clazz, jstring p_permission, jboolean p_result);
 JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onNightModeChanged(JNIEnv *env, jclass clazz);
 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 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 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 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
 #endif // JAVA_GODOT_LIB_JNI_H

+ 11 - 0
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");
 	_verify_apk = p_env->GetMethodID(godot_class, "nativeVerifyApk", "(Ljava/lang/String;)I");
 	_enable_immersive_mode = p_env->GetMethodID(godot_class, "nativeEnableImmersiveMode", "(Z)V");
 	_enable_immersive_mode = p_env->GetMethodID(godot_class, "nativeEnableImmersiveMode", "(Z)V");
 	_is_in_immersive_mode = p_env->GetMethodID(godot_class, "isInImmersiveMode", "()Z");
 	_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() {
 GodotJavaWrapper::~GodotJavaWrapper() {
@@ -588,3 +589,13 @@ bool GodotJavaWrapper::is_in_immersive_mode() {
 		return false;
 		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);
+	}
+}

+ 3 - 0
platform/android/java_godot_wrapper.h

@@ -84,6 +84,7 @@ private:
 	jmethodID _verify_apk = nullptr;
 	jmethodID _verify_apk = nullptr;
 	jmethodID _enable_immersive_mode = nullptr;
 	jmethodID _enable_immersive_mode = nullptr;
 	jmethodID _is_in_immersive_mode = nullptr;
 	jmethodID _is_in_immersive_mode = nullptr;
+	jmethodID _on_editor_workspace_selected = nullptr;
 
 
 public:
 public:
 	GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_godot_instance);
 	GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_godot_instance);
@@ -137,6 +138,8 @@ public:
 
 
 	void enable_immersive_mode(bool p_enabled);
 	void enable_immersive_mode(bool p_enabled);
 	bool is_in_immersive_mode();
 	bool is_in_immersive_mode();
+
+	void on_editor_workspace_selected(const String &p_workspace);
 };
 };
 
 
 #endif // JAVA_GODOT_WRAPPER_H
 #endif // JAVA_GODOT_WRAPPER_H

+ 30 - 0
platform/android/os_android.cpp

@@ -43,6 +43,10 @@
 #include "core/io/xml_parser.h"
 #include "core/io/xml_parser.h"
 #include "drivers/unix/dir_access_unix.h"
 #include "drivers/unix/dir_access_unix.h"
 #include "drivers/unix/file_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 "main/main.h"
 #include "scene/main/scene_tree.h"
 #include "scene/main/scene_tree.h"
 #include "servers/rendering_server.h"
 #include "servers/rendering_server.h"
@@ -331,6 +335,15 @@ void OS_Android::main_loop_begin() {
 	if (main_loop) {
 	if (main_loop) {
 		main_loop->initialize();
 		main_loop->initialize();
 	}
 	}
+
+#ifdef TOOLS_ENABLED
+	if (Engine::get_singleton()->is_editor_hint()) {
+		GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
+		if (game_view_plugin != nullptr) {
+			game_view_plugin->connect("main_screen_changed", callable_mp_static(&OS_Android::_on_main_screen_changed));
+		}
+	}
+#endif
 }
 }
 
 
 bool OS_Android::main_loop_iterate(bool *r_should_swap_buffers) {
 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() {
 void OS_Android::main_loop_end() {
+#ifdef TOOLS_ENABLED
+	if (Engine::get_singleton()->is_editor_hint()) {
+		GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
+		if (game_view_plugin != nullptr) {
+			game_view_plugin->disconnect("main_screen_changed", callable_mp_static(&OS_Android::_on_main_screen_changed));
+		}
+	}
+#endif
+
 	if (main_loop) {
 	if (main_loop) {
 		SceneTree *scene_tree = Object::cast_to<SceneTree>(main_loop);
 		SceneTree *scene_tree = Object::cast_to<SceneTree>(main_loop);
 		if (scene_tree) {
 		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() {
 void OS_Android::main_loop_focusout() {
 	DisplayServerAndroid::get_singleton()->send_window_event(DisplayServer::WINDOW_EVENT_FOCUS_OUT);
 	DisplayServerAndroid::get_singleton()->send_window_event(DisplayServer::WINDOW_EVENT_FOCUS_OUT);
 	if (OS::get_singleton()->get_main_loop()) {
 	if (OS::get_singleton()->get_main_loop()) {

+ 4 - 0
platform/android/os_android.h

@@ -187,6 +187,10 @@ private:
 	String get_dynamic_libraries_path() const;
 	String get_dynamic_libraries_path() const;
 	// Copy a dynamic library to the given location to make it accessible for loading.
 	// 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);
 	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
 #endif // OS_ANDROID_H