1
0
mirror of https://github.com/godotengine/godot.git synced 2025-11-22 15:06:45 +00:00

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

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