Add support for embedding game process in the Android Editor

- Implement Android editor specific `EmbeddedGodotGame` to support embedding the game window in the Android editor
This commit is contained in:
Fredia Huya-Kouadio 2025-01-07 21:31:53 -08:00
parent 296de7da83
commit 7495a8a02e
71 changed files with 2497 additions and 301 deletions

View File

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

View File

@ -228,6 +228,11 @@ EditorPlugin *EditorMainScreen::get_selected_plugin() const {
return selected_plugin;
}
EditorPlugin *EditorMainScreen::get_plugin_by_name(const String &p_plugin_name) const {
ERR_FAIL_COND_V(!main_editor_plugins.has(p_plugin_name), nullptr);
return main_editor_plugins[p_plugin_name];
}
VBoxContainer *EditorMainScreen::get_control() const {
return main_screen_vbox;
}
@ -254,6 +259,7 @@ void EditorMainScreen::add_main_plugin(EditorPlugin *p_editor) {
buttons.push_back(tb);
button_hb->add_child(tb);
editor_table.push_back(p_editor);
main_editor_plugins.insert(p_editor->get_plugin_name(), p_editor);
}
void EditorMainScreen::remove_main_plugin(EditorPlugin *p_editor) {
@ -280,6 +286,7 @@ void EditorMainScreen::remove_main_plugin(EditorPlugin *p_editor) {
}
editor_table.erase(p_editor);
main_editor_plugins.erase(p_editor->get_plugin_name());
}
EditorMainScreen::EditorMainScreen() {

View File

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

View File

@ -939,17 +939,20 @@ void EditorSettings::_load_defaults(Ref<ConfigFile> p_extra_config) {
_initial_set("run/window_placement/rect_custom_position", Vector2());
EDITOR_SETTING_BASIC(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/screen", -5, screen_hints)
#endif
// Should match the ANDROID_WINDOW_* constants in 'platform/android/java/editor/src/main/java/org/godotengine/editor/GodotEditor.kt'
String android_window_hints = "Auto (based on screen size):0,Same as Editor:1,Side-by-side with Editor:2,Launch in PiP mode:3";
// Should match the ANDROID_WINDOW_* constants in 'platform/android/java/editor/src/main/java/org/godotengine/editor/BaseGodotEditor.kt'.
String android_window_hints = "Auto (based on screen size):0,Same as Editor:1,Side-by-side with Editor:2";
EDITOR_SETTING_BASIC(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/android_window", 0, android_window_hints)
EDITOR_SETTING_BASIC(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/game_embed_mode", 0, "Use Per-Project Configuration:0,Embed Game:1,Make Game Workspace Floating:2,Disabled:3");
int default_play_window_pip_mode = 0;
String game_embed_mode_hints = "Disabled:-1,Use Per-Project Configuration:0,Embed Game:1,Make Game Workspace Floating:2";
#ifdef ANDROID_ENABLED
default_play_window_pip_mode = 2;
if (OS::get_singleton()->has_feature("xr_editor")) {
game_embed_mode_hints = "Disabled:-1";
} else {
game_embed_mode_hints = "Disabled:-1,Auto (based on screen size):0,Enabled:1";
}
#endif
EDITOR_SETTING_BASIC(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/play_window_pip_mode", default_play_window_pip_mode, "Disabled:0,Enabled:1,Enabled when Play window is same as Editor:2")
int default_game_embed_mode = OS::get_singleton()->has_feature("xr_editor") ? -1 : 0;
EDITOR_SETTING_BASIC(Variant::INT, PROPERTY_HINT_ENUM, "run/window_placement/game_embed_mode", default_game_embed_mode, game_embed_mode_hints);
// Auto save
_initial_set("run/auto_save/save_before_running", true, true);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -48,12 +48,7 @@ enum class LaunchPolicy {
/**
* Adjacent launches are enabled.
*/
ADJACENT,
/**
* Launches happen in the same window but start in PiP mode.
*/
SAME_AND_LAUNCH_IN_PIP_MODE
ADJACENT
}
/**
@ -63,14 +58,12 @@ data class EditorWindowInfo(
val windowClassName: String,
val windowId: Int,
val processNameSuffix: String,
val launchPolicy: LaunchPolicy = LaunchPolicy.SAME,
val supportsPiPMode: Boolean = false
val launchPolicy: LaunchPolicy = LaunchPolicy.SAME
) {
constructor(
windowClass: Class<*>,
windowId: Int,
processNameSuffix: String,
launchPolicy: LaunchPolicy = LaunchPolicy.SAME,
supportsPiPMode: Boolean = false
) : this(windowClass.name, windowId, processNameSuffix, launchPolicy, supportsPiPMode)
launchPolicy: LaunchPolicy = LaunchPolicy.SAME
) : this(windowClass.name, windowId, processNameSuffix, launchPolicy)
}

View File

@ -30,77 +30,59 @@
package org.godotengine.editor
import android.Manifest
import android.annotation.SuppressLint
import android.app.PictureInPictureParams
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Rect
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.annotation.CallSuper
import org.godotengine.godot.GodotLib
import org.godotengine.godot.utils.PermissionsUtil
import androidx.core.view.isVisible
import org.godotengine.editor.embed.GameMenuFragment
import org.godotengine.godot.utils.GameMenuUtils
import org.godotengine.godot.utils.ProcessPhoenix
import org.godotengine.godot.utils.isNativeXRDevice
/**
* Drives the 'run project' window of the Godot Editor.
*/
open class GodotGame : GodotEditor() {
open class GodotGame : BaseGodotGame() {
companion object {
private val TAG = GodotGame::class.java.simpleName
}
private val gameViewSourceRectHint = Rect()
private val pipButton: View? by lazy {
findViewById(R.id.godot_pip_button)
}
private val expandGameMenuButton: View? by lazy { findViewById(R.id.game_menu_expand_button) }
private var pipAvailable = false
@SuppressLint("ClickableViewAccessibility")
override fun onCreate(savedInstanceState: Bundle?) {
gameMenuState.clear()
intent.getBundleExtra(EXTRA_GAME_MENU_STATE)?.let {
gameMenuState.putAll(it)
}
gameMenuState.putBoolean(EXTRA_IS_GAME_EMBEDDED, isGameEmbedded())
gameMenuState.putBoolean(EXTRA_IS_GAME_RUNNING, true)
super.onCreate(savedInstanceState)
gameMenuContainer?.isVisible = shouldShowGameMenuBar()
expandGameMenuButton?.apply{
isVisible = shouldShowGameMenuBar() && isMenuBarCollapsable()
setOnClickListener {
gameMenuFragment?.expandGameMenu()
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val gameView = findViewById<View>(R.id.godot_fragment_container)
gameView?.addOnLayoutChangeListener { v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom ->
gameView.getGlobalVisibleRect(gameViewSourceRectHint)
}
}
pipButton?.setOnClickListener { enterPiPMode() }
handleStartIntent(intent)
}
override fun onNewIntent(newIntent: Intent) {
super.onNewIntent(newIntent)
handleStartIntent(newIntent)
}
private fun handleStartIntent(intent: Intent) {
pipAvailable = intent.getBooleanExtra(EXTRA_PIP_AVAILABLE, pipAvailable)
updatePiPButtonVisibility()
val pipLaunchRequested = intent.getBooleanExtra(EXTRA_LAUNCH_IN_PIP, false)
if (pipLaunchRequested) {
enterPiPMode()
}
}
private fun updatePiPButtonVisibility() {
pipButton?.visibility = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && pipAvailable && !isInPictureInPictureMode) {
View.VISIBLE
} else {
View.GONE
}
}
private fun enterPiPMode() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && pipAvailable) {
override fun enterPiPMode() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && hasPiPSystemFeature()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val builder = PictureInPictureParams.Builder().setSourceRectHint(gameViewSourceRectHint)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
@ -114,10 +96,27 @@ open class GodotGame : GodotEditor() {
}
}
/**
* Returns true the if the device supports picture-in-picture (PiP).
*/
protected fun hasPiPSystemFeature(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N &&
packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
}
override fun shouldShowGameMenuBar(): Boolean {
return intent.getBooleanExtra(
EXTRA_EDITOR_HINT,
false
) && gameMenuContainer != null
}
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode)
Log.v(TAG, "onPictureInPictureModeChanged: $isInPictureInPictureMode")
updatePiPButtonVisibility()
// Hide the game menu fragment when in PiP.
gameMenuContainer?.isVisible = !isInPictureInPictureMode
}
override fun onStop() {
@ -134,59 +133,109 @@ open class GodotGame : GodotEditor() {
override fun getEditorWindowInfo() = RUN_GAME_INFO
override fun getEditorGameEmbedMode() = GameMenuUtils.GameEmbedMode.DISABLED
override fun overrideOrientationRequest() = false
override fun enableLongPressGestures() = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_long_press_as_right_click"))
override fun enablePanAndScaleGestures() = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("input_devices/pointing/android/enable_pan_and_scale_gestures"))
override fun onGodotSetupCompleted() {
super.onGodotSetupCompleted()
Log.v(TAG, "OnGodotSetupCompleted")
// Check if we should be running in XR instead (if available) as it's possible we were
// launched from the project manager which doesn't have that information.
val launchingArgs = intent.getStringArrayExtra(EXTRA_COMMAND_LINE_PARAMS)
if (launchingArgs != null) {
val editorWindowInfo = retrieveEditorWindowInfo(launchingArgs)
if (editorWindowInfo != getEditorWindowInfo()) {
val relaunchIntent = getNewGodotInstanceIntent(editorWindowInfo, launchingArgs)
relaunchIntent.putExtra(EXTRA_NEW_LAUNCH, true)
.putExtra(EditorMessageDispatcher.EXTRA_MSG_DISPATCHER_PAYLOAD, intent.getBundleExtra(EditorMessageDispatcher.EXTRA_MSG_DISPATCHER_PAYLOAD))
Log.d(TAG, "Relaunching XR project using ${editorWindowInfo.windowClassName} with parameters ${launchingArgs.contentToString()}")
val godot = godot
if (godot != null) {
godot.destroyAndKillProcess {
ProcessPhoenix.triggerRebirth(this, relaunchIntent)
}
} else {
ProcessPhoenix.triggerRebirth(this, relaunchIntent)
}
return
}
}
// Request project runtime permissions if necessary
val permissionsToEnable = getProjectPermissionsToEnable()
if (permissionsToEnable.isNotEmpty()) {
PermissionsUtil.requestPermissions(this, permissionsToEnable)
override fun suspendGame(suspended: Boolean) {
val actionBundle = Bundle().apply {
putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_SET_SUSPEND)
putBoolean(KEY_GAME_MENU_ACTION_PARAM1, suspended)
}
editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
}
/**
* Check for project permissions to enable
*/
@CallSuper
protected open fun getProjectPermissionsToEnable(): MutableList<String> {
val permissionsToEnable = mutableListOf<String>()
// Check for RECORD_AUDIO permission
val audioInputEnabled = java.lang.Boolean.parseBoolean(GodotLib.getGlobal("audio/driver/enable_input"))
if (audioInputEnabled) {
permissionsToEnable.add(Manifest.permission.RECORD_AUDIO)
override fun dispatchNextFrame() {
val actionBundle = Bundle().apply {
putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_NEXT_FRAME)
}
return permissionsToEnable
editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
}
override fun toggleSelectionVisibility(enabled: Boolean) {
val actionBundle = Bundle().apply {
putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_SET_SELECTION_VISIBLE)
putBoolean(KEY_GAME_MENU_ACTION_PARAM1, enabled)
}
editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
}
override fun overrideCamera(enabled: Boolean) {
val actionBundle = Bundle().apply {
putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_SET_CAMERA_OVERRIDE)
putBoolean(KEY_GAME_MENU_ACTION_PARAM1, enabled)
}
editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
}
override fun selectRuntimeNode(nodeType: GameMenuFragment.GameMenuListener.NodeType) {
val actionBundle = Bundle().apply {
putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_SET_NODE_TYPE)
putSerializable(KEY_GAME_MENU_ACTION_PARAM1, nodeType)
}
editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
}
override fun selectRuntimeNodeSelectMode(selectMode: GameMenuFragment.GameMenuListener.SelectMode) {
val actionBundle = Bundle().apply {
putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_SET_SELECT_MODE)
putSerializable(KEY_GAME_MENU_ACTION_PARAM1, selectMode)
}
editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
}
override fun reset2DCamera() {
val actionBundle = Bundle().apply {
putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_RESET_CAMERA_2D_POSITION)
}
editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
}
override fun reset3DCamera() {
val actionBundle = Bundle().apply {
putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_RESET_CAMERA_3D_POSITION)
}
editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
}
override fun manipulateCamera(mode: GameMenuFragment.GameMenuListener.CameraMode) {
val actionBundle = Bundle().apply {
putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_SET_CAMERA_MANIPULATE_MODE)
putSerializable(KEY_GAME_MENU_ACTION_PARAM1, mode)
}
editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
}
override fun embedGameOnPlay(embedded: Boolean) {
val actionBundle = Bundle().apply {
putString(KEY_GAME_MENU_ACTION, GAME_MENU_ACTION_EMBED_GAME_ON_PLAY)
putBoolean(KEY_GAME_MENU_ACTION_PARAM1, embedded)
}
editorMessageDispatcher.dispatchGameMenuAction(EDITOR_MAIN_INFO, actionBundle)
}
protected open fun isGameEmbedded() = false
override fun isGameEmbeddingSupported() = !isNativeXRDevice(applicationContext)
override fun isMinimizedButtonEnabled() = isTaskRoot && !isNativeXRDevice(applicationContext)
override fun isCloseButtonEnabled() = !isNativeXRDevice(applicationContext)
override fun isPiPButtonEnabled() = hasPiPSystemFeature()
override fun isMenuBarCollapsable() = true
override fun minimizeGameWindow() {
moveTaskToBack(false)
}
override fun closeGameWindow() {
ProcessPhoenix.forceQuit(this)
}
override fun onGameMenuCollapsed(collapsed: Boolean) {
expandGameMenuButton?.isVisible = shouldShowGameMenuBar() && isMenuBarCollapsable() && collapsed
}
}

View File

@ -36,7 +36,7 @@ import org.godotengine.godot.xr.XRMode
/**
* Provide support for running XR apps / games from the editor window.
*/
open class GodotXRGame: GodotGame() {
open class GodotXRGame: BaseGodotGame() {
override fun overrideOrientationRequest() = true
@ -56,6 +56,8 @@ open class GodotXRGame: GodotGame() {
override fun getEditorWindowInfo() = XR_RUN_GAME_INFO
override fun getGodotAppLayout() = R.layout.godot_xr_game_layout
override fun getProjectPermissionsToEnable(): MutableList<String> {
val permissionsToEnable = super.getProjectPermissionsToEnable()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -333,7 +333,7 @@ class Godot(private val context: Context) {
* Toggle immersive mode.
* Must be called from the UI thread.
*/
private fun enableImmersiveMode(enabled: Boolean, override: Boolean = false) {
fun enableImmersiveMode(enabled: Boolean, override: Boolean = false) {
val activity = getActivity() ?: return
val window = activity.window ?: return
@ -1068,6 +1068,16 @@ class Godot(private val context: Context) {
return PermissionsUtil.getGrantedPermissions(getActivity())
}
/**
* Returns true if this is the Godot editor.
*/
fun isEditorHint() = isEditorBuild() && GodotLib.isEditorHint()
/**
* Returns true if this is the Godot project manager.
*/
fun isProjectManagerHint() = isEditorBuild() && GodotLib.isProjectManagerHint()
/**
* Return true if the given feature is supported.
*/
@ -1177,4 +1187,9 @@ class Godot(private val context: Context) {
val verifyResult = primaryHost?.verifyApk(apkPath) ?: Error.ERR_UNAVAILABLE
return verifyResult.toNativeValue()
}
@Keep
private fun nativeOnEditorWorkspaceSelected(workspace: String) {
primaryHost?.onEditorWorkspaceSelected(workspace)
}
}

View File

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

View File

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

View File

@ -196,6 +196,30 @@ public class GodotLib {
*/
public static native String getEditorSetting(String settingKey);
/**
* Update the 'key' editor setting with the given data. Must be called on the render thread.
* @param key
* @param data
*/
public static native void setEditorSetting(String key, Object data);
/**
* Used to access project metadata from the editor settings. Must be accessed on the render thread.
* @param section
* @param key
* @param defaultValue
* @return
*/
public static native Object getEditorProjectMetadata(String section, String key, Object defaultValue);
/**
* Set the project metadata to the editor settings. Must be accessed on the render thread.
* @param section
* @param key
* @param data
*/
public static native void setEditorProjectMetadata(String section, String key, Object data);
/**
* Invoke method |p_method| on the Godot object specified by |p_id|
* @param p_id Id of the Godot object to invoke
@ -267,4 +291,8 @@ public class GodotLib {
* @return the project resource directory
*/
public static native String getProjectResourceDir();
static native boolean isEditorHint();
static native boolean isProjectManagerHint();
}

View File

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

View File

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

View File

@ -487,6 +487,49 @@ JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getEditorSetting(J
return env->NewStringUTF(editor_setting_value.utf8().get_data());
}
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setEditorSetting(JNIEnv *env, jclass clazz, jstring p_key, jobject p_data) {
#ifdef TOOLS_ENABLED
if (EditorSettings::get_singleton() != nullptr) {
String key = jstring_to_string(p_key, env);
Variant data = _jobject_to_variant(env, p_data);
EditorSettings::get_singleton()->set(key, data);
}
#else
WARN_PRINT("Access to the Editor Settings in only available on Editor builds");
#endif
}
JNIEXPORT jobject JNICALL Java_org_godotengine_godot_GodotLib_getEditorProjectMetadata(JNIEnv *env, jclass clazz, jstring p_section, jstring p_key, jobject p_default_value) {
jvalret result;
#ifdef TOOLS_ENABLED
if (EditorSettings::get_singleton() != nullptr) {
String section = jstring_to_string(p_section, env);
String key = jstring_to_string(p_key, env);
Variant default_value = _jobject_to_variant(env, p_default_value);
Variant data = EditorSettings::get_singleton()->get_project_metadata(section, key, default_value);
result = _variant_to_jvalue(env, data.get_type(), &data, true);
}
#else
WARN_PRINT("Access to the Editor Settings Project Metadata is only available on Editor builds");
#endif
return result.obj;
}
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setEditorProjectMetadata(JNIEnv *env, jclass clazz, jstring p_section, jstring p_key, jobject p_data) {
#ifdef TOOLS_ENABLED
if (EditorSettings::get_singleton() != nullptr) {
String section = jstring_to_string(p_section, env);
String key = jstring_to_string(p_key, env);
Variant data = _jobject_to_variant(env, p_data);
EditorSettings::get_singleton()->set_project_metadata(section, key, data);
}
#else
WARN_PRINT("Access to the Editor Settings Project Metadata is only available on Editor builds");
#endif
}
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onNightModeChanged(JNIEnv *env, jclass clazz) {
DisplayServerAndroid *ds = (DisplayServerAndroid *)DisplayServer::get_singleton();
if (ds) {
@ -555,4 +598,19 @@ JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getProjectResource
const String resource_dir = OS::get_singleton()->get_resource_dir();
return env->NewStringUTF(resource_dir.utf8().get_data());
}
JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_isEditorHint(JNIEnv *env, jclass clazz) {
Engine *engine = Engine::get_singleton();
if (engine) {
return engine->is_editor_hint();
}
return false;
}
JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_isProjectManagerHint(JNIEnv *env, jclass clazz) {
Engine *engine = Engine::get_singleton();
if (engine) {
return engine->is_project_manager_hint();
}
return false;
}
}

View File

@ -62,6 +62,9 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusin(JNIEnv *env,
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_focusout(JNIEnv *env, jclass clazz);
JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getGlobal(JNIEnv *env, jclass clazz, jstring path);
JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getEditorSetting(JNIEnv *env, jclass clazz, jstring p_setting_key);
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setEditorSetting(JNIEnv *env, jclass clazz, jstring p_key, jobject p_data);
JNIEXPORT jobject JNICALL Java_org_godotengine_godot_GodotLib_getEditorProjectMetadata(JNIEnv *env, jclass clazz, jstring p_section, jstring p_key, jobject p_default_value);
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setEditorProjectMetadata(JNIEnv *env, jclass clazz, jstring p_section, jstring p_key, jobject p_data);
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_setVirtualKeyboardHeight(JNIEnv *env, jclass clazz, jint p_height);
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_requestPermissionResult(JNIEnv *env, jclass clazz, jstring p_permission, jboolean p_result);
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onNightModeChanged(JNIEnv *env, jclass clazz);
@ -70,6 +73,8 @@ JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererResumed(JNI
JNIEXPORT void JNICALL Java_org_godotengine_godot_GodotLib_onRendererPaused(JNIEnv *env, jclass clazz);
JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_shouldDispatchInputToRenderThread(JNIEnv *env, jclass clazz);
JNIEXPORT jstring JNICALL Java_org_godotengine_godot_GodotLib_getProjectResourceDir(JNIEnv *env, jclass clazz);
JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_isEditorHint(JNIEnv *env, jclass clazz);
JNIEXPORT jboolean JNICALL Java_org_godotengine_godot_GodotLib_isProjectManagerHint(JNIEnv *env, jclass clazz);
}
#endif // JAVA_GODOT_LIB_JNI_H

View File

@ -93,6 +93,7 @@ GodotJavaWrapper::GodotJavaWrapper(JNIEnv *p_env, jobject p_activity, jobject p_
_verify_apk = p_env->GetMethodID(godot_class, "nativeVerifyApk", "(Ljava/lang/String;)I");
_enable_immersive_mode = p_env->GetMethodID(godot_class, "nativeEnableImmersiveMode", "(Z)V");
_is_in_immersive_mode = p_env->GetMethodID(godot_class, "isInImmersiveMode", "()Z");
_on_editor_workspace_selected = p_env->GetMethodID(godot_class, "nativeOnEditorWorkspaceSelected", "(Ljava/lang/String;)V");
}
GodotJavaWrapper::~GodotJavaWrapper() {
@ -588,3 +589,13 @@ bool GodotJavaWrapper::is_in_immersive_mode() {
return false;
}
}
void GodotJavaWrapper::on_editor_workspace_selected(const String &p_workspace) {
if (_on_editor_workspace_selected) {
JNIEnv *env = get_jni_env();
ERR_FAIL_NULL(env);
jstring j_workspace = env->NewStringUTF(p_workspace.utf8().get_data());
env->CallVoidMethod(godot_instance, _on_editor_workspace_selected, j_workspace);
}
}

View File

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

View File

@ -43,6 +43,10 @@
#include "core/io/xml_parser.h"
#include "drivers/unix/dir_access_unix.h"
#include "drivers/unix/file_access_unix.h"
#ifdef TOOLS_ENABLED
#include "editor/editor_node.h"
#include "editor/plugins/game_view_plugin.h"
#endif
#include "main/main.h"
#include "scene/main/scene_tree.h"
#include "servers/rendering_server.h"
@ -331,6 +335,15 @@ void OS_Android::main_loop_begin() {
if (main_loop) {
main_loop->initialize();
}
#ifdef TOOLS_ENABLED
if (Engine::get_singleton()->is_editor_hint()) {
GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
if (game_view_plugin != nullptr) {
game_view_plugin->connect("main_screen_changed", callable_mp_static(&OS_Android::_on_main_screen_changed));
}
}
#endif
}
bool OS_Android::main_loop_iterate(bool *r_should_swap_buffers) {
@ -353,6 +366,15 @@ bool OS_Android::main_loop_iterate(bool *r_should_swap_buffers) {
}
void OS_Android::main_loop_end() {
#ifdef TOOLS_ENABLED
if (Engine::get_singleton()->is_editor_hint()) {
GameViewPlugin *game_view_plugin = Object::cast_to<GameViewPlugin>(EditorNode::get_singleton()->get_editor_main_screen()->get_plugin_by_name("Game"));
if (game_view_plugin != nullptr) {
game_view_plugin->disconnect("main_screen_changed", callable_mp_static(&OS_Android::_on_main_screen_changed));
}
}
#endif
if (main_loop) {
SceneTree *scene_tree = Object::cast_to<SceneTree>(main_loop);
if (scene_tree) {
@ -362,6 +384,14 @@ void OS_Android::main_loop_end() {
}
}
#ifdef TOOLS_ENABLED
void OS_Android::_on_main_screen_changed(const String &p_screen_name) {
if (OS_Android::get_singleton() != nullptr && OS_Android::get_singleton()->get_godot_java() != nullptr) {
OS_Android::get_singleton()->get_godot_java()->on_editor_workspace_selected(p_screen_name);
}
}
#endif
void OS_Android::main_loop_focusout() {
DisplayServerAndroid::get_singleton()->send_window_event(DisplayServer::WINDOW_EVENT_FOCUS_OUT);
if (OS::get_singleton()->get_main_loop()) {

View File

@ -187,6 +187,10 @@ private:
String get_dynamic_libraries_path() const;
// Copy a dynamic library to the given location to make it accessible for loading.
bool copy_dynamic_library(const String &p_library_path, const String &p_target_dir, String *r_copy_path = nullptr);
#ifdef TOOLS_ENABLED
static void _on_main_screen_changed(const String &p_screen_name);
#endif
};
#endif // OS_ANDROID_H