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