android: Secondary Layout and Menu Improvements (#1385)

* Add a new Secondary Display Layout option on android that makes the secondary display honor swap button

* add quick menu option for secondary layout

* fix icon

* added other secondary layouts

* Add a new Secondary Display Layout option on android that makes the secondary display honor swap button

# Conflicts:
#	src/core/frontend/framebuffer_layout.cpp

* add quick menu option for secondary layout

* fix icon

* added other secondary layouts

* updated secondary menu with functionality to switch external displays

 Conflicts:
	src/android/app/src/main/java/org/citra/citra_emu/display/SecondaryDisplay.kt

* safety checks for crash prevention

* make secondary menu only appear if a secondary display is available

* update default displayid behavior to exclude "Built"

* update odin 2 bugfix to handle other languages

* Apply clang-format

* Rename "Opposite of Primary Display" option for brevity

* Rename menu_secondary_layout_reverse_primary for consistency

* Rename SecondaryDisplayLayout::ReversePrimary for consistency

* first round of code fixes based on review

* Added android-side enable_secondary_display boolean setting, replacing secondary_display_layout = none on the android side. Exposed it in Layout settings, and secondary display layout is now only selectable if it is enabled.  Support for the old option is still in the code, but should no longer be selectable.

Also renamed opposite to reverse_primary in a few other places.

* Update framebuffer layout and renderers to correctly render hybrid mode on both primary and secondary displays

* Apply clang format

* Re-apply terminology changes which were undone by a recent commit

* Removed unused string emulation_secondary_display_default

---------

Co-authored-by: OpenSauce04 <opensauce04@gmail.com>
This commit is contained in:
NovaChild 2026-06-14 11:37:32 -05:00 committed by GitHub
parent 3dc357a8c9
commit 8a6d597dec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 429 additions and 90 deletions

View file

@ -238,6 +238,7 @@ if (ANDROID)
"android_hide_images" "android_hide_images"
"screen_orientation" "screen_orientation"
"performance_overlay_position" "performance_overlay_position"
"enable_secondary_display"
) )
string(REPLACE "_" "_1" KEY_JNI_ESCAPED ${KEY}) string(REPLACE "_" "_1" KEY_JNI_ESCAPED ${KEY})
set(SETTING_KEY_LIST "${SETTING_KEY_LIST}\n\"${KEY}\",") set(SETTING_KEY_LIST "${SETTING_KEY_LIST}\n\"${KEY}\",")

View file

@ -64,7 +64,7 @@ class EmulationActivity : AppCompatActivity() {
private lateinit var binding: ActivityEmulationBinding private lateinit var binding: ActivityEmulationBinding
private lateinit var screenAdjustmentUtil: ScreenAdjustmentUtil private lateinit var screenAdjustmentUtil: ScreenAdjustmentUtil
private lateinit var hotkeyUtility: HotkeyUtility private lateinit var hotkeyUtility: HotkeyUtility
private lateinit var secondaryDisplay: SecondaryDisplay lateinit var secondaryDisplayManager: SecondaryDisplay
private val onShutdown = Runnable { private val onShutdown = Runnable {
if (intent.getBooleanExtra("launched_from_shortcut", false)) { if (intent.getBooleanExtra("launched_from_shortcut", false)) {
@ -102,8 +102,8 @@ class EmulationActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
secondaryDisplay = SecondaryDisplay(this) secondaryDisplayManager = SecondaryDisplay(this)
secondaryDisplay.updateDisplay() secondaryDisplayManager.updateDisplay()
binding = ActivityEmulationBinding.inflate(layoutInflater) binding = ActivityEmulationBinding.inflate(layoutInflater)
hotkeyUtility = HotkeyUtility(screenAdjustmentUtil, this) hotkeyUtility = HotkeyUtility(screenAdjustmentUtil, this)
@ -188,7 +188,7 @@ class EmulationActivity : AppCompatActivity() {
} }
override fun onStop() { override fun onStop() {
secondaryDisplay.releasePresentation() secondaryDisplayManager.releasePresentation()
super.onStop() super.onStop()
} }
@ -199,7 +199,7 @@ class EmulationActivity : AppCompatActivity() {
public override fun onRestart() { public override fun onRestart() {
super.onRestart() super.onRestart()
secondaryDisplay.updateDisplay() secondaryDisplayManager.updateDisplay()
NativeLibrary.reloadCameraDevices() NativeLibrary.reloadCameraDevices()
} }
@ -222,8 +222,8 @@ class EmulationActivity : AppCompatActivity() {
NativeLibrary.playTimeManagerStop() NativeLibrary.playTimeManagerStop()
isEmulationRunning = false isEmulationRunning = false
instance = null instance = null
secondaryDisplay.releasePresentation() secondaryDisplayManager.releasePresentation()
secondaryDisplay.releaseVD() secondaryDisplayManager.releaseVD()
super.onDestroy() super.onDestroy()
} }

View file

@ -71,6 +71,24 @@ class ScreenAdjustmentUtil(
NativeLibrary.updateFramebuffer(NativeLibrary.isPortraitMode) NativeLibrary.updateFramebuffer(NativeLibrary.isPortraitMode)
} }
fun changeSecondaryOrientation(layoutOption: Int) {
IntSetting.SECONDARY_DISPLAY_LAYOUT.int = layoutOption
settings.saveSetting(IntSetting.SECONDARY_DISPLAY_LAYOUT,SettingsFile.FILE_NAME_CONFIG)
NativeLibrary.reloadSettings()
NativeLibrary.updateFramebuffer(NativeLibrary.isPortraitMode)
}
fun enableSecondaryDisplay(layoutOption: Int) {
BooleanSetting.ENABLE_SECONDARY_DISPLAY.boolean = true
settings.saveSetting(BooleanSetting.ENABLE_SECONDARY_DISPLAY, SettingsFile.FILE_NAME_CONFIG)
changeSecondaryOrientation(layoutOption)
}
fun disableSecondaryDisplay() {
BooleanSetting.ENABLE_SECONDARY_DISPLAY.boolean = false
settings.saveSetting(BooleanSetting.ENABLE_SECONDARY_DISPLAY, SettingsFile.FILE_NAME_CONFIG)
}
fun changeActivityOrientation(orientationOption: Int) { fun changeActivityOrientation(orientationOption: Int) {
val activity = context as? Activity ?: return val activity = context as? Activity ?: return
IntSetting.ORIENTATION_OPTION.int = orientationOption IntSetting.ORIENTATION_OPTION.int = orientationOption

View file

@ -53,10 +53,17 @@ enum class PortraitScreenLayout(val int: Int) {
enum class SecondaryDisplayLayout(val int: Int) { enum class SecondaryDisplayLayout(val int: Int) {
// These must match what is defined in src/common/settings.h // These must match what is defined in src/common/settings.h
// NONE is no longer selectable in the interface, having been replaced with
// the boolean ENABLE_SECONDARY_DISPLAY setting, but is left here for backwards compatibility
NONE(0), NONE(0),
TOP_SCREEN(1), TOP_SCREEN(1),
BOTTOM_SCREEN(2), BOTTOM_SCREEN(2),
SIDE_BY_SIDE(3); SIDE_BY_SIDE(3),
REVERSE_PRIMARY(4),
ORIGINAL(5),
HYBRID(6),
LARGE_SCREEN(7)
;
companion object { companion object {
fun from(int: Int): SecondaryDisplayLayout { fun from(int: Int): SecondaryDisplayLayout {

View file

@ -8,6 +8,7 @@ import android.app.Presentation
import android.content.Context import android.content.Context
import android.hardware.display.DisplayManager import android.hardware.display.DisplayManager
import android.hardware.display.VirtualDisplay import android.hardware.display.VirtualDisplay
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.view.Display import android.view.Display
import android.view.MotionEvent import android.view.MotionEvent
@ -16,11 +17,18 @@ import android.view.SurfaceView
import android.view.WindowManager import android.view.WindowManager
import org.citra.citra_emu.features.settings.model.IntSetting import org.citra.citra_emu.features.settings.model.IntSetting
import org.citra.citra_emu.NativeLibrary import org.citra.citra_emu.NativeLibrary
import org.citra.citra_emu.features.settings.model.BooleanSetting
import org.citra.citra_emu.utils.Log
class SecondaryDisplay(val context: Context) : DisplayManager.DisplayListener { class SecondaryDisplay(val context: Context) : DisplayManager.DisplayListener {
private var pres: SecondaryDisplayPresentation? = null private var pres: SecondaryDisplayPresentation? = null
private val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager private val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
private val vd: VirtualDisplay private val vd: VirtualDisplay
var preferredDisplayId = -1
var currentDisplayId = -1
val availableDisplays: List<Display>
get() = getSecondaryDisplays()
init { init {
vd = displayManager.createVirtualDisplay( vd = displayManager.createVirtualDisplay(
@ -35,31 +43,39 @@ class SecondaryDisplay(val context: Context) : DisplayManager.DisplayListener {
} }
fun updateSurface() { fun updateSurface() {
NativeLibrary.secondarySurfaceChanged(pres!!.getSurfaceHolder().surface) val surface = pres?.getSurfaceHolder()?.surface
if (surface != null && surface.isValid) {
NativeLibrary.secondarySurfaceChanged(surface)
} else {
Log.warning("SecondaryDisplay Attempted to update null or invalid surface")
}
} }
fun destroySurface() { fun destroySurface() {
NativeLibrary.secondarySurfaceDestroyed() NativeLibrary.secondarySurfaceDestroyed()
} }
private fun getExternalDisplay(context: Context): Display? { private fun getSecondaryDisplays(): List<Display> {
val dm = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager val dm = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
val currentDisplayId = context.display.displayId val currentDisplayId = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
context.display.displayId
} else {
@Suppress("DEPRECATION")
(context.getSystemService(Context.WINDOW_SERVICE) as WindowManager)
.defaultDisplay.displayId
}
val displays = dm.displays val displays = dm.displays
val presDisplays = dm.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION); val presDisplays = dm.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION);
val extDisplays = displays.filter { return displays.filter {
val isPresentable = presDisplays.any { pd -> pd.displayId == it.displayId } val isPresentable = presDisplays.any { pd -> pd.displayId == it.displayId }
val isNotDefaultOrPresentable = it.displayId != Display.DEFAULT_DISPLAY || isPresentable val isNotDefaultOrPresentable = (it != null && it.displayId != Display.DEFAULT_DISPLAY) || isPresentable
isNotDefaultOrPresentable && isNotDefaultOrPresentable &&
it.displayId != currentDisplayId && it.displayId != currentDisplayId &&
it.name != "HiddenDisplay" && it.name != "HiddenDisplay" &&
it.state != Display.STATE_OFF && it.state != Display.STATE_OFF &&
it.isValid it.isValid
} }
// if there is a display called Built-In Display or Built-In Screen, prioritize the OTHER screen
val selected = extDisplays.firstOrNull { ! it.name.contains("Built",true) }
?: extDisplays.firstOrNull()
return selected
} }
fun updateDisplay() { fun updateDisplay() {
@ -68,21 +84,37 @@ class SecondaryDisplay(val context: Context) : DisplayManager.DisplayListener {
return return
} }
// decide if we are going to the external display or the internal one val displayToUse = if (availableDisplays.isEmpty() ||
var display = getExternalDisplay(context) // Theoretically, the NONE option is no longer selectable, but
if (display == null || // I am leaving this in for backwards compatibility
IntSetting.SECONDARY_DISPLAY_LAYOUT.int == SecondaryDisplayLayout.NONE.int) { IntSetting.SECONDARY_DISPLAY_LAYOUT.int == SecondaryDisplayLayout.NONE.int ||
display = vd.display ! BooleanSetting.ENABLE_SECONDARY_DISPLAY.boolean
) {
currentDisplayId = -1
vd.display
} else if (preferredDisplayId >=0 && availableDisplays.any { it.displayId == preferredDisplayId }) {
currentDisplayId = preferredDisplayId
availableDisplays.first { it.displayId == preferredDisplayId }
} else {
val dm = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
val default = dm.displays.first {it.displayId == Display.DEFAULT_DISPLAY}
// prioritize displays that have a different name from the default display, as
// some devices such as the Odin 2 create a permanent virtual display with the same
// name as the default display that should be skipped in most cases
currentDisplayId = availableDisplays.firstOrNull{
it.name != default.name && !it.name.contains("Built",true)}?.displayId ?:
availableDisplays[0].displayId
availableDisplays.first{ it.displayId == currentDisplayId }
} }
// if our presentation is already on the right display, ignore // if our presentation is already on the right display, ignore
if (pres?.display == display) return if (pres?.display == displayToUse) return
// otherwise, make a new presentation // otherwise, make a new presentation
releasePresentation() releasePresentation()
try { try {
pres = SecondaryDisplayPresentation(context, display!!, this) pres = SecondaryDisplayPresentation(context, displayToUse!!, this)
pres?.show() pres?.show()
} }
// catch BadTokenException and InvalidDisplayException, // catch BadTokenException and InvalidDisplayException,
@ -137,16 +169,18 @@ class SecondaryDisplayPresentation(
surfaceView = SurfaceView(context) surfaceView = SurfaceView(context)
surfaceView.holder.addCallback(object : SurfaceHolder.Callback { surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) { override fun surfaceCreated(holder: SurfaceHolder) {
Log.debug("SecondaryDisplay Surface created")
} }
override fun surfaceChanged( override fun surfaceChanged(
holder: SurfaceHolder, format: Int, width: Int, height: Int holder: SurfaceHolder, format: Int, width: Int, height: Int
) { ) {
Log.debug("SecondaryDisplay Surface changed: ${width}x${height}")
parent.updateSurface() parent.updateSurface()
} }
override fun surfaceDestroyed(holder: SurfaceHolder) { override fun surfaceDestroyed(holder: SurfaceHolder) {
Log.debug("SecondaryDisplay Surface destroyed")
parent.destroySurface() parent.destroySurface()
} }
}) })

View file

@ -141,4 +141,5 @@ object SettingKeys {
external fun android_hide_images(): String external fun android_hide_images(): String
external fun screen_orientation(): String external fun screen_orientation(): String
external fun performance_overlay_position(): String external fun performance_overlay_position(): String
external fun enable_secondary_display(): String
} }

View file

@ -59,6 +59,7 @@ enum class BooleanSetting(
ANDROID_HIDE_IMAGES(SettingKeys.android_hide_images(), Settings.SECTION_MISC, false), ANDROID_HIDE_IMAGES(SettingKeys.android_hide_images(), Settings.SECTION_MISC, false),
APPLY_REGION_FREE_PATCH(SettingKeys.apply_region_free_patch(), Settings.SECTION_SYSTEM, true), APPLY_REGION_FREE_PATCH(SettingKeys.apply_region_free_patch(), Settings.SECTION_SYSTEM, true),
USE_INTEGER_SCALING(SettingKeys.use_integer_scaling(), Settings.SECTION_RENDERER, false), USE_INTEGER_SCALING(SettingKeys.use_integer_scaling(), Settings.SECTION_RENDERER, false),
ENABLE_SECONDARY_DISPLAY(SettingKeys.enable_secondary_display(), Settings.SECTION_LAYOUT, true),
SIMULATE_3DS_GPU_TIMINGS(SettingKeys.simulate_3ds_gpu_timings(), Settings.SECTION_RENDERER, true); SIMULATE_3DS_GPU_TIMINGS(SettingKeys.simulate_3ds_gpu_timings(), Settings.SECTION_RENDERER, true);
override var boolean: Boolean = defaultValue override var boolean: Boolean = defaultValue

View file

@ -37,7 +37,7 @@ enum class IntSetting(
LANDSCAPE_BOTTOM_HEIGHT(SettingKeys.custom_bottom_height(),Settings.SECTION_LAYOUT,480), LANDSCAPE_BOTTOM_HEIGHT(SettingKeys.custom_bottom_height(),Settings.SECTION_LAYOUT,480),
SCREEN_GAP(SettingKeys.screen_gap(),Settings.SECTION_LAYOUT,0), SCREEN_GAP(SettingKeys.screen_gap(),Settings.SECTION_LAYOUT,0),
PORTRAIT_SCREEN_LAYOUT(SettingKeys.portrait_layout_option(),Settings.SECTION_LAYOUT,0), PORTRAIT_SCREEN_LAYOUT(SettingKeys.portrait_layout_option(),Settings.SECTION_LAYOUT,0),
SECONDARY_DISPLAY_LAYOUT(SettingKeys.secondary_display_layout(),Settings.SECTION_LAYOUT,0), SECONDARY_DISPLAY_LAYOUT(SettingKeys.secondary_display_layout(),Settings.SECTION_LAYOUT,4),
PORTRAIT_TOP_X(SettingKeys.custom_portrait_top_x(),Settings.SECTION_LAYOUT,0), PORTRAIT_TOP_X(SettingKeys.custom_portrait_top_x(),Settings.SECTION_LAYOUT,0),
PORTRAIT_TOP_Y(SettingKeys.custom_portrait_top_y(),Settings.SECTION_LAYOUT,0), PORTRAIT_TOP_Y(SettingKeys.custom_portrait_top_y(),Settings.SECTION_LAYOUT,0),
PORTRAIT_TOP_WIDTH(SettingKeys.custom_portrait_top_width(),Settings.SECTION_LAYOUT,800), PORTRAIT_TOP_WIDTH(SettingKeys.custom_portrait_top_width(),Settings.SECTION_LAYOUT,800),

View file

@ -1201,6 +1201,15 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
IntSetting.PORTRAIT_SCREEN_LAYOUT.defaultValue IntSetting.PORTRAIT_SCREEN_LAYOUT.defaultValue
) )
) )
add (
SwitchSetting(
BooleanSetting.ENABLE_SECONDARY_DISPLAY,
R.string.emulation_secondary_display_enable,
R.string.emulation_secondary_display_enable_description,
BooleanSetting.ENABLE_SECONDARY_DISPLAY.key,
BooleanSetting.ENABLE_SECONDARY_DISPLAY.defaultValue
)
)
add( add(
SingleChoiceSetting( SingleChoiceSetting(
IntSetting.SECONDARY_DISPLAY_LAYOUT, IntSetting.SECONDARY_DISPLAY_LAYOUT,
@ -1209,7 +1218,8 @@ class SettingsFragmentPresenter(private val fragmentView: SettingsFragmentView)
R.array.secondaryLayouts, R.array.secondaryLayouts,
R.array.secondaryLayoutValues, R.array.secondaryLayoutValues,
IntSetting.SECONDARY_DISPLAY_LAYOUT.key, IntSetting.SECONDARY_DISPLAY_LAYOUT.key,
IntSetting.SECONDARY_DISPLAY_LAYOUT.defaultValue IntSetting.SECONDARY_DISPLAY_LAYOUT.defaultValue,
BooleanSetting.ENABLE_SECONDARY_DISPLAY.boolean
) )
) )
add( add(

View file

@ -67,9 +67,9 @@ import org.citra.citra_emu.databinding.FragmentEmulationBinding
import org.citra.citra_emu.display.PortraitScreenLayout import org.citra.citra_emu.display.PortraitScreenLayout
import org.citra.citra_emu.display.ScreenAdjustmentUtil import org.citra.citra_emu.display.ScreenAdjustmentUtil
import org.citra.citra_emu.display.ScreenLayout import org.citra.citra_emu.display.ScreenLayout
import org.citra.citra_emu.display.SecondaryDisplayLayout
import org.citra.citra_emu.features.settings.model.BooleanSetting import org.citra.citra_emu.features.settings.model.BooleanSetting
import org.citra.citra_emu.features.settings.model.IntSetting import org.citra.citra_emu.features.settings.model.IntSetting
import org.citra.citra_emu.features.settings.model.Settings
import org.citra.citra_emu.features.settings.model.SettingsViewModel import org.citra.citra_emu.features.settings.model.SettingsViewModel
import org.citra.citra_emu.features.settings.ui.SettingsActivity import org.citra.citra_emu.features.settings.ui.SettingsActivity
import org.citra.citra_emu.features.settings.utils.SettingsFile import org.citra.citra_emu.features.settings.utils.SettingsFile
@ -78,7 +78,6 @@ import org.citra.citra_emu.utils.BuildUtil
import org.citra.citra_emu.utils.DirectoryInitialization import org.citra.citra_emu.utils.DirectoryInitialization
import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState import org.citra.citra_emu.utils.DirectoryInitialization.DirectoryInitializationState
import org.citra.citra_emu.utils.EmulationMenuSettings import org.citra.citra_emu.utils.EmulationMenuSettings
import org.citra.citra_emu.utils.FileUtil
import org.citra.citra_emu.utils.GameHelper import org.citra.citra_emu.utils.GameHelper
import org.citra.citra_emu.utils.GameIconUtils import org.citra.citra_emu.utils.GameIconUtils
import org.citra.citra_emu.utils.EmulationLifecycleUtil import org.citra.citra_emu.utils.EmulationLifecycleUtil
@ -93,7 +92,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
private lateinit var emulationState: EmulationState private lateinit var emulationState: EmulationState
private var perfStatsUpdater: Runnable? = null private var perfStatsUpdater: Runnable? = null
private lateinit var emulationActivity: EmulationActivity private val emulationActivity: EmulationActivity
get() = (requireActivity() as EmulationActivity)
private var _binding: FragmentEmulationBinding? = null private var _binding: FragmentEmulationBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
@ -107,8 +107,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
private val settingsViewModel: SettingsViewModel by viewModels() private val settingsViewModel: SettingsViewModel by viewModels()
private val settings get() = settingsViewModel.settings private val settings get() = settingsViewModel.settings
private val onPause = Runnable{ togglePause() } private val onPause = Runnable { togglePause() }
private val onShutdown = Runnable{ emulationState.stop() } private val onShutdown = Runnable { emulationState.stop() }
// Only used if a game is passed through intent on google play variant // Only used if a game is passed through intent on google play variant
private var gameFd: Int? = null private var gameFd: Int? = null
@ -116,8 +116,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
super.onAttach(context) super.onAttach(context)
if (context is EmulationActivity) { if (context is EmulationActivity) {
emulationActivity = context NativeLibrary.setEmulationActivity(context)
NativeLibrary.setEmulationActivity(context)
} else { } else {
throw IllegalStateException("EmulationFragment must have EmulationActivity parent") throw IllegalStateException("EmulationFragment must have EmulationActivity parent")
} }
@ -183,8 +182,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
// So this fragment doesn't restart on configuration changes; i.e. rotation. // So this fragment doesn't restart on configuration changes; i.e. rotation.
retainInstance = true retainInstance = true
emulationState = EmulationState(game.path) emulationState = EmulationState(game.path)
emulationActivity = requireActivity() as EmulationActivity screenAdjustmentUtil =
screenAdjustmentUtil = ScreenAdjustmentUtil(requireContext(), requireActivity().windowManager, settings) ScreenAdjustmentUtil(requireContext(), requireActivity().windowManager, settings)
EmulationLifecycleUtil.addPauseResumeHook(onPause) EmulationLifecycleUtil.addPauseResumeHook(onPause)
EmulationLifecycleUtil.addShutdownHook(onShutdown) EmulationLifecycleUtil.addShutdownHook(onShutdown)
} }
@ -195,6 +194,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View { ): View {
_binding = FragmentEmulationBinding.inflate(inflater) _binding = FragmentEmulationBinding.inflate(inflater)
binding.inGameMenu.menu.findItem(R.id.menu_secondary_screen_layout).isVisible =
emulationActivity.secondaryDisplayManager.availableDisplays.isNotEmpty()
binding.inGameMenu.menu.findItem(R.id.menu_landscape_screen_layout).isVisible = binding.inGameMenu.menu.findItem(R.id.menu_landscape_screen_layout).isVisible =
CitraApplication.appContext.resources.configuration.orientation != CitraApplication.appContext.resources.configuration.orientation !=
Configuration.ORIENTATION_PORTRAIT Configuration.ORIENTATION_PORTRAIT
@ -336,6 +337,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
true true
} }
R.id.menu_secondary_screen_layout -> {
showSecondaryScreenLayoutMenu()
true
}
R.id.menu_swap_screens -> { R.id.menu_swap_screens -> {
screenAdjustmentUtil.swapScreen() screenAdjustmentUtil.swapScreen()
true true
@ -624,17 +630,21 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
} }
add(text).setEnabled(enableClick).setOnMenuItemClickListener { add(text).setEnabled(enableClick).setOnMenuItemClickListener {
if(isSaving) { if (isSaving) {
NativeLibrary.saveState(slot) NativeLibrary.saveState(slot)
Toast.makeText(context, Toast.makeText(
context,
getString(R.string.saving), getString(R.string.saving),
Toast.LENGTH_SHORT).show() Toast.LENGTH_SHORT
).show()
} else { } else {
NativeLibrary.loadState(slot) NativeLibrary.loadState(slot)
binding.drawerLayout.close() binding.drawerLayout.close()
Toast.makeText(context, Toast.makeText(
context,
getString(R.string.loading), getString(R.string.loading),
Toast.LENGTH_SHORT).show() Toast.LENGTH_SHORT
).show()
} }
true true
} }
@ -643,9 +653,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
savestates?.forEach { savestates?.forEach {
var enableClick = true var enableClick = true
val text = if(it.slot == NativeLibrary.QUICKSAVE_SLOT) { val text = if (it.slot == NativeLibrary.QUICKSAVE_SLOT) {
getString(R.string.emulation_occupied_quicksave_slot, it.time) getString(R.string.emulation_occupied_quicksave_slot, it.time)
} else{ } else {
getString(R.string.emulation_occupied_state_slot, it.slot, it.time) getString(R.string.emulation_occupied_state_slot, it.slot, it.time)
} }
popupMenu.menu.getItem(it.slot).setTitle(text).setEnabled(enableClick) popupMenu.menu.getItem(it.slot).setTitle(text).setEnabled(enableClick)
@ -727,8 +737,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
} }
R.id.menu_performance_overlay_show -> { R.id.menu_performance_overlay_show -> {
BooleanSetting.PERF_OVERLAY_ENABLE.boolean = !BooleanSetting.PERF_OVERLAY_ENABLE.boolean BooleanSetting.PERF_OVERLAY_ENABLE.boolean =
settings.saveSetting(BooleanSetting.PERF_OVERLAY_ENABLE, SettingsFile.FILE_NAME_CONFIG) !BooleanSetting.PERF_OVERLAY_ENABLE.boolean
settings.saveSetting(
BooleanSetting.PERF_OVERLAY_ENABLE,
SettingsFile.FILE_NAME_CONFIG
)
updateShowPerformanceOverlay() updateShowPerformanceOverlay()
true true
} }
@ -999,10 +1013,13 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
val layoutOptionMenuItem = when (IntSetting.PORTRAIT_SCREEN_LAYOUT.int) { val layoutOptionMenuItem = when (IntSetting.PORTRAIT_SCREEN_LAYOUT.int) {
PortraitScreenLayout.TOP_FULL_WIDTH.int -> PortraitScreenLayout.TOP_FULL_WIDTH.int ->
R.id.menu_portrait_layout_top_full R.id.menu_portrait_layout_top_full
PortraitScreenLayout.ORIGINAL.int -> PortraitScreenLayout.ORIGINAL.int ->
R.id.menu_portrait_layout_original R.id.menu_portrait_layout_original
PortraitScreenLayout.CUSTOM_PORTRAIT_LAYOUT.int -> PortraitScreenLayout.CUSTOM_PORTRAIT_LAYOUT.int ->
R.id.menu_portrait_layout_custom R.id.menu_portrait_layout_custom
else -> else ->
R.id.menu_portrait_layout_top_full R.id.menu_portrait_layout_top_full
@ -1039,6 +1056,145 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
popupMenu.show() popupMenu.show()
} }
private fun showSecondaryScreenLayoutMenu() {
val popupMenu = PopupMenu(
requireContext(),
binding.inGameMenu.findViewById(R.id.menu_secondary_screen_layout)
)
popupMenu.menuInflater.inflate(R.menu.menu_secondary_screen_layout, popupMenu.menu)
var selectedLayout = IntSetting.SECONDARY_DISPLAY_LAYOUT.int
val chooserMenu = popupMenu.menu.findItem(R.id.menu_secondary_choose)
val enableSecondaryCheckbox = popupMenu.menu.findItem(R.id.menu_enable_secondary_layout)
chooserMenu?.subMenu?.removeGroup(R.id.menu_secondary_management_display_group)
val displays =
emulationActivity.secondaryDisplayManager.availableDisplays
if (selectedLayout == SecondaryDisplayLayout.NONE.int || !BooleanSetting.ENABLE_SECONDARY_DISPLAY.boolean) {
BooleanSetting.ENABLE_SECONDARY_DISPLAY.boolean = false
enableSecondaryCheckbox.isChecked = false
chooserMenu.isVisible = false
popupMenu.menu.setGroupEnabled(R.id.menu_secondary_layout_group, false)
} else {
popupMenu.menu.setGroupEnabled(R.id.menu_secondary_layout_group, true)
chooserMenu.isVisible = (displays.size > 1)
}
val layoutOptionMenuItem = when (selectedLayout) {
SecondaryDisplayLayout.NONE.int ->
R.id.menu_secondary_layout_opposite
SecondaryDisplayLayout.REVERSE_PRIMARY.int ->
R.id.menu_secondary_layout_opposite
SecondaryDisplayLayout.TOP_SCREEN.int ->
R.id.menu_secondary_layout_top
SecondaryDisplayLayout.BOTTOM_SCREEN.int ->
R.id.menu_secondary_layout_bottom
SecondaryDisplayLayout.HYBRID.int ->
R.id.menu_secondary_layout_hybrid
SecondaryDisplayLayout.LARGE_SCREEN.int ->
R.id.menu_secondary_layout_largescreen
SecondaryDisplayLayout.ORIGINAL.int ->
R.id.menu_secondary_layout_original
else ->
R.id.menu_secondary_layout_side_by_side
}
popupMenu.menu.findItem(layoutOptionMenuItem).isChecked = true
// Add the available secondary displays to the display chooser list
// Use the display ID as the menu ID - since generated menu IDs are all > 1,000,000 this
// *should* result in unique ids
if (displays.size > 1 && selectedLayout != SecondaryDisplayLayout.NONE.int) {
val current = emulationActivity.secondaryDisplayManager.currentDisplayId
chooserMenu.isVisible = true
displays.forEachIndexed { index, display ->
chooserMenu?.subMenu?.add(
R.id.menu_secondary_management_display_group,
display.displayId,
index,
"Display ${display.displayId} - ${display.name}"
)?.apply {
isChecked = (display.displayId == current)
}
}
chooserMenu.subMenu?.setGroupCheckable(
R.id.menu_secondary_management_display_group,
true,
true
)
}
popupMenu.setOnMenuItemClickListener {
when (it.itemId) {
R.id.menu_enable_secondary_layout -> {
if (!it.isChecked) {
screenAdjustmentUtil.enableSecondaryDisplay(selectedLayout)
} else {
screenAdjustmentUtil.disableSecondaryDisplay()
}
emulationActivity.secondaryDisplayManager.updateDisplay()
showSecondaryScreenLayoutMenu() // reopen menu to get new behaviors
true
}
R.id.menu_secondary_layout_opposite -> {
screenAdjustmentUtil.changeSecondaryOrientation(SecondaryDisplayLayout.REVERSE_PRIMARY.int)
true
}
R.id.menu_secondary_layout_top -> {
screenAdjustmentUtil.changeSecondaryOrientation(SecondaryDisplayLayout.TOP_SCREEN.int)
true
}
R.id.menu_secondary_layout_bottom -> {
screenAdjustmentUtil.changeSecondaryOrientation(SecondaryDisplayLayout.BOTTOM_SCREEN.int)
true
}
R.id.menu_secondary_layout_side_by_side -> {
screenAdjustmentUtil.changeSecondaryOrientation(SecondaryDisplayLayout.SIDE_BY_SIDE.int)
true
}
R.id.menu_secondary_layout_hybrid -> {
screenAdjustmentUtil.changeSecondaryOrientation(SecondaryDisplayLayout.HYBRID.int)
true
}
R.id.menu_secondary_layout_original -> {
screenAdjustmentUtil.changeSecondaryOrientation(SecondaryDisplayLayout.ORIGINAL.int)
true
}
R.id.menu_secondary_layout_largescreen -> {
screenAdjustmentUtil.changeSecondaryOrientation(SecondaryDisplayLayout.LARGE_SCREEN.int)
true
}
R.id.menu_secondary_choose -> {
true
}
else -> {
// display ID selection
// If we are clicking on a menu item that isn't one of the options above, it must
// be one of the dynamically generated menu items added to the secondary display
// choice list.
emulationActivity.secondaryDisplayManager.preferredDisplayId = it.itemId
emulationActivity.secondaryDisplayManager.updateDisplay()
true
}
}
}
popupMenu.show()
}
private fun editControlsPlacement() { private fun editControlsPlacement() {
if (binding.surfaceInputOverlay.isInEditMode) { if (binding.surfaceInputOverlay.isInEditMode) {
binding.doneControlConfig.visibility = View.GONE binding.doneControlConfig.visibility = View.GONE
@ -1095,7 +1251,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
slider.valueFrom = 0f slider.valueFrom = 0f
slider.value = preferences.getInt(target, 50).toFloat() slider.value = preferences.getInt(target, 50).toFloat()
textValue.setText((slider.value + 50).toInt().toString()) textValue.setText((slider.value + 50).toInt().toString())
textValue.addTextChangedListener( object : TextWatcher { textValue.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable) { override fun afterTextChanged(s: Editable) {
val value = s.toString().toIntOrNull() val value = s.toString().toIntOrNull()
if (value == null || value < 50 || value > 150) { if (value == null || value < 50 || value > 150) {
@ -1105,6 +1261,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
slider.value = value.toFloat() - 50 slider.value = value.toFloat() - 50
} }
} }
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {} override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {} override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
}) })
@ -1145,7 +1302,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
slider.value = preferences.getInt("controlOpacity", 50).toFloat() slider.value = preferences.getInt("controlOpacity", 50).toFloat()
textValue.setText(slider.value.toInt().toString()) textValue.setText(slider.value.toInt().toString())
textValue.addTextChangedListener( object : TextWatcher { textValue.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable) { override fun afterTextChanged(s: Editable) {
val value = s.toString().toIntOrNull() val value = s.toString().toIntOrNull()
if (value == null || value < slider.valueFrom || value > slider.valueTo) { if (value == null || value < slider.valueFrom || value > slider.valueTo) {
@ -1155,6 +1312,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
slider.value = value.toFloat() slider.value = value.toFloat()
} }
} }
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {} override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {} override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
}) })
@ -1163,11 +1321,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
slider.addOnChangeListener { _: Slider, value: Float, _: Boolean -> slider.addOnChangeListener { _: Slider, value: Float, _: Boolean ->
if (textValue.text.toString() != slider.value.toInt().toString()) { if (textValue.text.toString() != slider.value.toInt().toString()) {
textValue.setText(slider.value.toInt().toString()) textValue.setText(slider.value.toInt().toString())
textValue.setSelection(textValue.length()) textValue.setSelection(textValue.length())
setControlOpacity(slider.value.toInt()) setControlOpacity(slider.value.toInt())
}
} }
}
textInput.suffixText = "%" textInput.suffixText = "%"
} }
@ -1352,7 +1510,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
} }
private fun updateStatsPosition(position: Int) { private fun updateStatsPosition(position: Int) {
val params = binding.performanceOverlayShowText.layoutParams as CoordinatorLayout.LayoutParams val params =
binding.performanceOverlayShowText.layoutParams as CoordinatorLayout.LayoutParams
val padding = (20 * resources.displayMetrics.density).toInt() // 20dp val padding = (20 * resources.displayMetrics.density).toInt() // 20dp
params.setMargins(padding, 0, padding, 0) params.setMargins(padding, 0, padding, 0)
@ -1387,7 +1546,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
private fun getBatteryTemperature(): Float { private fun getBatteryTemperature(): Float {
try { try {
val batteryIntent = requireContext().registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED)) val batteryIntent =
requireContext().registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
// Temperature in tenths of a degree Celsius // Temperature in tenths of a degree Celsius
val temperature = batteryIntent?.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0) ?: 0 val temperature = batteryIntent?.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0) ?: 0
// Convert to degrees Celsius // Convert to degrees Celsius

View file

@ -354,6 +354,10 @@ static const char* android_config_default_file_content = (BOOST_HANA_STRING(R"(
# 0 (default): Off, 1: On # 0 (default): Off, 1: On
)") DECLARE_KEY(expand_to_cutout_area) BOOST_HANA_STRING(R"( )") DECLARE_KEY(expand_to_cutout_area) BOOST_HANA_STRING(R"(
# Allows Azahar to use externally connected displays
# 0: Off, 1: On (default)
)") DECLARE_KEY(enable_secondary_display) BOOST_HANA_STRING(R"(
# Secondary Display Layout # Secondary Display Layout
# What the game should do if a secondary display is connected physically or using # What the game should do if a secondary display is connected physically or using
# Miracast / Chromecast screen mirroring # Miracast / Chromecast screen mirroring

View file

@ -18,17 +18,16 @@
#include "video_core/renderer_base.h" #include "video_core/renderer_base.h"
bool EmuWindow_Android::OnSurfaceChanged(ANativeWindow* surface) { bool EmuWindow_Android::OnSurfaceChanged(ANativeWindow* surface) {
if (render_window == surface) { int temp_width = surface == nullptr ? 0 : ANativeWindow_getWidth(surface);
int temp_height = surface == nullptr ? 0 : ANativeWindow_getHeight(surface);
if (render_window == surface && temp_width == window_width && temp_height == window_height) {
return false; return false;
} }
window_width = temp_width;
window_height = temp_height;
render_window = surface; render_window = surface;
window_info.type = Frontend::WindowSystemType::Android; window_info.type = Frontend::WindowSystemType::Android;
window_info.render_surface = surface; window_info.render_surface = surface;
if (surface != nullptr) {
window_width = ANativeWindow_getWidth(surface);
window_height = ANativeWindow_getHeight(surface);
}
StopPresenting(); StopPresenting();
OnFramebufferSizeChanged(); OnFramebufferSizeChanged();
return true; return true;
@ -48,15 +47,9 @@ void EmuWindow_Android::OnTouchMoved(int x, int y) {
} }
void EmuWindow_Android::OnFramebufferSizeChanged() { void EmuWindow_Android::OnFramebufferSizeChanged() {
const bool is_portrait_mode{IsPortraitMode()}; const bool is_portrait_mode = IsPortraitMode() && !is_secondary;
const int bigger{window_width > window_height ? window_width : window_height}; UpdateCurrentFramebufferLayout(window_width, window_height, is_portrait_mode);
const int smaller{window_width < window_height ? window_width : window_height};
if (is_portrait_mode && !is_secondary) {
UpdateCurrentFramebufferLayout(smaller, bigger, is_portrait_mode);
} else {
UpdateCurrentFramebufferLayout(bigger, smaller, is_portrait_mode);
}
} }
EmuWindow_Android::EmuWindow_Android(ANativeWindow* surface, bool is_secondary) EmuWindow_Android::EmuWindow_Android(ANativeWindow* surface, bool is_secondary)

View file

@ -391,6 +391,11 @@ void Java_org_citra_citra_1emu_NativeLibrary_secondarySurfaceChanged(JNIEnv* env
if (secondary_window) { if (secondary_window) {
// Second window already created, so update it // Second window already created, so update it
notify = secondary_window->OnSurfaceChanged(s_secondary_surface); notify = secondary_window->OnSurfaceChanged(s_secondary_surface);
// Log the dimensions for debugging
int32_t width = ANativeWindow_getWidth(s_secondary_surface);
int32_t height = ANativeWindow_getHeight(s_secondary_surface);
LOG_INFO(Frontend, "Secondary Surface changed to {}x{}", width, height);
} else { } else {
LOG_WARNING(Frontend, LOG_WARNING(Frontend,
"Second Window does not exist in native.cpp but surface changed. Ignoring."); "Second Window does not exist in native.cpp but surface changed. Ignoring.");

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorOnSurface">
<path
android:fillColor="@android:color/black"
android:fillType="evenOdd"
android:pathData="M17 4h3c1.1 0 2 0.9 2 2v2h-2V6h-3ZM4 8V6h3V4H4C2.9 4 2 4.9 2 6v2z m16 8v2h-3v2h3c1.1 0 2-0.9 2-2v-2ZM7 18H4v-2H2v2c0 1.1 0.9 2 2 2h3ZM18 8H6v8h12z M12.6 10.23q-0.07-0.17-0.21-0.28-0.13-0.12-0.26-0.14-0.42 0-0.77 0.23-0.35 0.22-0.64 0.53-0.13 0.1-0.29 0.1-0.16 0-0.27-0.1-0.1-0.12-0.1-0.3 0-0.15 0.1-0.3 0.18-0.19 0.39-0.37 0.21-0.2 0.45-0.34 0.24-0.14 0.5-0.23 0.25-0.09 0.52-0.09 0.31 0 0.58 0.14 0.27 0.12 0.47 0.36 0.2 0.24 0.31 0.56 0.12 0.33 0.12 0.72 0 0.12-0.03 0.26-0.1 0.44-0.33 0.72-0.21 0.28-0.48 0.48-0.26 0.2-0.55 0.36-0.28 0.15-0.53 0.35-0.24 0.2-0.42 0.47-0.19 0.27-0.25 0.7h1.84q0.11 0 0.17-0.03l0.09-0.06 0.1-0.12q0.08-0.1 0.12-0.11l0.1-0.04 0.12-0.01q0.18 0 0.28 0.12 0.1 0.12 0.1 0.28 0 0.11-0.02 0.17l-0.04 0.08q-0.18 0.26-0.43 0.42-0.25 0.15-0.56 0.15h-2.3q-0.38-0.03-0.42-0.45 0.03-0.32 0.1-0.62 0.05-0.3 0.16-0.59 0.1-0.28 0.27-0.53 0.16-0.25 0.41-0.45 0.22-0.17 0.51-0.33 0.3-0.15 0.55-0.34 0.26-0.19 0.45-0.44 0.18-0.25 0.18-0.6 0-0.1-0.04-0.21l-0.05-0.12z"/>
</vector>

View file

@ -32,6 +32,11 @@
android:icon="@drawable/ic_portrait_fit_screen" android:icon="@drawable/ic_portrait_fit_screen"
android:title="@string/emulation_switch_portrait_layout" /> android:title="@string/emulation_switch_portrait_layout" />
<item
android:id="@+id/menu_secondary_screen_layout"
android:icon="@drawable/ic_secondary_fit_screen"
android:title="@string/emulation_secondary_display_management" />
<item <item
android:id="@+id/menu_swap_screens" android:id="@+id/menu_swap_screens"
android:icon="@drawable/ic_splitscreen" android:icon="@drawable/ic_splitscreen"

View file

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/menu_enable_secondary_layout"
android:title="@string/emulation_secondary_display_enable"
android:checkable="true"
android:checked="true"/>
<item
android:id="@+id/menu_secondary_choose"
android:title="@string/emulation_select_secondary_display">
<menu>
<group
android:id="@+id/menu_secondary_management_display_group"
android:checkableBehavior="single">
</group>
</menu>
</item>
<item
android:title="@string/preferences_layout"
android:enabled="false"/>
<group
android:checkableBehavior="single"
android:id="@+id/menu_secondary_layout_group">
<item
android:id="@+id/menu_secondary_layout_opposite"
android:title="@string/emulation_secondary_display_opposite" />
<item
android:id="@+id/menu_secondary_layout_top"
android:title="@string/emulation_top_screen" />
<item
android:id="@+id/menu_secondary_layout_bottom"
android:title="@string/emulation_bottom_screen" />
<item
android:id="@+id/menu_secondary_layout_side_by_side"
android:title="@string/emulation_screen_layout_sidebyside" />
<item
android:id="@+id/menu_secondary_layout_original"
android:title="@string/emulation_screen_layout_original" />
<item
android:id="@+id/menu_secondary_layout_hybrid"
android:title="@string/emulation_screen_layout_hybrid" />
<item
android:id="@+id/menu_secondary_layout_largescreen"
android:title="@string/emulation_screen_layout_largescreen" />
</group>
</menu>

View file

@ -36,10 +36,14 @@
</string-array> </string-array>
<string-array name="secondaryLayouts"> <string-array name="secondaryLayouts">
<item>@string/emulation_secondary_display_default</item> <item>@string/emulation_secondary_display_opposite</item>
<item>@string/emulation_top_screen</item> <item>@string/emulation_top_screen</item>
<item>@string/emulation_bottom_screen</item> <item>@string/emulation_bottom_screen</item>
<item>@string/emulation_screen_layout_sidebyside</item> <item>@string/emulation_screen_layout_sidebyside</item>
<item>@string/emulation_screen_layout_original</item>
<item>@string/emulation_screen_layout_hybrid</item>
<item>@string/emulation_screen_layout_largescreen</item>
</string-array> </string-array>
<integer-array name="portraitLayoutValues"> <integer-array name="portraitLayoutValues">
@ -49,10 +53,13 @@
</integer-array> </integer-array>
<integer-array name="secondaryLayoutValues"> <integer-array name="secondaryLayoutValues">
<item>0</item> <item>4</item>
<item>1</item> <item>1</item>
<item>2</item> <item>2</item>
<item>3</item> <item>3</item>
<item>5</item>
<item>6</item>
<item>7</item>
</integer-array> </integer-array>
<string-array name="smallScreenPositions"> <string-array name="smallScreenPositions">

View file

@ -500,7 +500,11 @@
<string name="emulation_aspect_ratio">Aspect Ratio</string> <string name="emulation_aspect_ratio">Aspect Ratio</string>
<string name="emulation_switch_screen_layout">Landscape Screen Layout</string> <string name="emulation_switch_screen_layout">Landscape Screen Layout</string>
<string name="emulation_switch_portrait_layout">Portrait Screen Layout</string> <string name="emulation_switch_portrait_layout">Portrait Screen Layout</string>
<string name="emulation_switch_secondary_layout">Secondary Display Screen Layout</string> <string name="emulation_secondary_display_enable">Enable Secondary Display</string>
<string name="emulation_secondary_display_enable_description">If disabled, Azahar will let Android manage connected displays. If this is enabled and multiple displays are connected, you can select which one Azahar will use in the Emulation Quick Menu</string>
<string name="emulation_switch_secondary_layout">Secondary Display Layout</string>
<string name="emulation_secondary_display_management">Secondary Display</string>
<string name="emulation_select_secondary_display">Choose Display</string>
<string name="emulation_switch_secondary_layout_description">The layout used by a connected secondary screen, wired or wireless (Chromecast, Miracast)</string> <string name="emulation_switch_secondary_layout_description">The layout used by a connected secondary screen, wired or wireless (Chromecast, Miracast)</string>
<string name="emulation_screen_layout_largescreen">Large Screen</string> <string name="emulation_screen_layout_largescreen">Large Screen</string>
<string name="emulation_screen_layout_portrait">Portrait</string> <string name="emulation_screen_layout_portrait">Portrait</string>
@ -509,7 +513,7 @@
<string name="emulation_screen_layout_hybrid">Hybrid Screens</string> <string name="emulation_screen_layout_hybrid">Hybrid Screens</string>
<string name="emulation_screen_layout_original">Original</string> <string name="emulation_screen_layout_original">Original</string>
<string name="emulation_portrait_layout_top_full">Default</string> <string name="emulation_portrait_layout_top_full">Default</string>
<string name="emulation_secondary_display_default">System Default (mirror)</string> <string name="emulation_secondary_display_opposite">Opposite Screen</string>
<string name="emulation_screen_layout_custom">Custom Layout</string> <string name="emulation_screen_layout_custom">Custom Layout</string>
<string name="bg_color">Background Color</string> <string name="bg_color">Background Color</string>
<string name="bg_color_description">The color which appears behind the screens during emulation, represented as an RGB value.</string> <string name="bg_color_description">The color which appears behind the screens during emulation, represented as an RGB value.</string>

View file

@ -57,7 +57,16 @@ enum class PortraitLayoutOption : u32 {
PortraitOriginal PortraitOriginal
}; };
enum class SecondaryDisplayLayout : u32 { None, TopScreenOnly, BottomScreenOnly, SideBySide }; enum class SecondaryDisplayLayout : u32 {
None,
TopScreenOnly,
BottomScreenOnly,
SideBySide,
OppositeScreenOnly,
Original,
Hybrid,
LargeScreen
};
/** Defines where the small screen will appear relative to the large screen /** Defines where the small screen will appear relative to the large screen
* when in Large Screen mode * when in Large Screen mode
*/ */
@ -549,7 +558,7 @@ struct Values {
SwitchableSetting<bool> swap_screen{false, Keys::swap_screen}; SwitchableSetting<bool> swap_screen{false, Keys::swap_screen};
SwitchableSetting<bool> upright_screen{false, Keys::upright_screen}; SwitchableSetting<bool> upright_screen{false, Keys::upright_screen};
SwitchableSetting<SecondaryDisplayLayout> secondary_display_layout{ SwitchableSetting<SecondaryDisplayLayout> secondary_display_layout{
SecondaryDisplayLayout::None, Keys::secondary_display_layout}; SecondaryDisplayLayout::OppositeScreenOnly, Keys::secondary_display_layout};
SwitchableSetting<std::vector<LayoutOption>> layouts_to_cycle{ SwitchableSetting<std::vector<LayoutOption>> layouts_to_cycle{
{LayoutOption::Default, LayoutOption::SingleScreen, LayoutOption::LargeScreen, {LayoutOption::Default, LayoutOption::SingleScreen, LayoutOption::LargeScreen,
LayoutOption::SideScreen, LayoutOption::SideScreen,

View file

@ -275,18 +275,19 @@ FramebufferLayout HybridScreenLayout(u32 width, u32 height, bool swapped, bool u
std::swap(width, height); std::swap(width, height);
} }
// Split the window into two parts. Give 2.25x width to the main screen, // use Large Screen layout with these specific ratios to get two of the pieces
// and make a bar on the right side with 1x width top screen and 1.25x width bottom screen
// To do that, find the total emulation box and maximize that based on window size
const float scale_factor = swapped ? 2.25 : 1.8; const float scale_factor = swapped ? 2.25 : 1.8;
const Settings::SmallScreenPosition pos = swapped ? Settings::SmallScreenPosition::TopRight const Settings::SmallScreenPosition pos = swapped ? Settings::SmallScreenPosition::TopRight
: Settings::SmallScreenPosition::BottomRight; : Settings::SmallScreenPosition::BottomRight;
FramebufferLayout res = LargeFrameLayout(width, height, swapped, upright, scale_factor, pos); // always pass false as the upright value here, as it is being handled here not there
FramebufferLayout res = LargeFrameLayout(width, height, swapped, false, scale_factor, pos);
const Common::Rectangle<u32> main = swapped ? res.bottom_screen : res.top_screen; const Common::Rectangle<u32> main = swapped ? res.bottom_screen : res.top_screen;
const Common::Rectangle<u32> small = swapped ? res.top_screen : res.bottom_screen; const Common::Rectangle<u32> small = swapped ? res.top_screen : res.bottom_screen;
res.additional_screen = Common::Rectangle<u32>{small.left, swapped ? small.bottom : main.top, res.additional_screen = Common::Rectangle<u32>{small.left, swapped ? small.bottom : main.top,
small.right, swapped ? main.bottom : small.top}; small.right, swapped ? main.bottom : small.top};
res.additional_screen_is_bottom = swapped;
res.additional_screen_enabled = true; res.additional_screen_enabled = true;
res.is_rotated = !upright;
if (upright) { if (upright) {
return reverseLayout(res); return reverseLayout(res);
} else { } else {
@ -305,17 +306,30 @@ FramebufferLayout AndroidSecondaryLayout(u32 width, u32 height) {
const Settings::SecondaryDisplayLayout layout = const Settings::SecondaryDisplayLayout layout =
Settings::values.secondary_display_layout.GetValue(); Settings::values.secondary_display_layout.GetValue();
switch (layout) { switch (layout) {
case Settings::SecondaryDisplayLayout::TopScreenOnly:
return SingleFrameLayout(width, height, false, Settings::values.upright_screen.GetValue());
case Settings::SecondaryDisplayLayout::BottomScreenOnly: case Settings::SecondaryDisplayLayout::BottomScreenOnly:
return SingleFrameLayout(width, height, true, Settings::values.upright_screen.GetValue()); return SingleFrameLayout(width, height, true, Settings::values.upright_screen.GetValue());
case Settings::SecondaryDisplayLayout::SideBySide: case Settings::SecondaryDisplayLayout::SideBySide:
return LargeFrameLayout(width, height, false, Settings::values.upright_screen.GetValue(), return LargeFrameLayout(width, height, false, Settings::values.upright_screen.GetValue(),
1.0f, Settings::SmallScreenPosition::MiddleRight); 1.0f, Settings::SmallScreenPosition::MiddleRight);
case Settings::SecondaryDisplayLayout::LargeScreen:
return LargeFrameLayout(width, height, false, Settings::values.upright_screen.GetValue(),
Settings::values.large_screen_proportion.GetValue(),
Settings::values.small_screen_position.GetValue());
case Settings::SecondaryDisplayLayout::Original:
return LargeFrameLayout(width, height, false, Settings::values.upright_screen.GetValue(),
1.0f, Settings::SmallScreenPosition::BelowLarge);
case Settings::SecondaryDisplayLayout::Hybrid:
return HybridScreenLayout(width, height, false, Settings::values.upright_screen.GetValue());
case Settings::SecondaryDisplayLayout::None: case Settings::SecondaryDisplayLayout::None:
// this should never happen, but if it does, somehow, send the top screen // this should never happen - if "none" is set this method shouldn't run - but if it does,
case Settings::SecondaryDisplayLayout::TopScreenOnly: // somehow, use OppositeScreenOnly
case Settings::SecondaryDisplayLayout::OppositeScreenOnly:
default: default:
return SingleFrameLayout(width, height, false, Settings::values.upright_screen.GetValue()); return SingleFrameLayout(width, height, !Settings::values.swap_screen.GetValue(),
Settings::values.upright_screen.GetValue());
} }
} }

View file

@ -32,13 +32,14 @@ struct FramebufferLayout {
bool bottom_screen_enabled; bool bottom_screen_enabled;
Common::Rectangle<u32> top_screen; Common::Rectangle<u32> top_screen;
Common::Rectangle<u32> bottom_screen; Common::Rectangle<u32> bottom_screen;
// is_rotated is true when the screen is in landscape mode - not sure why!
bool is_rotated = true; bool is_rotated = true;
bool is_portrait = false; bool additional_screen_enabled = false;
bool additional_screen_enabled; // top_opacity is currently not used but could be used in the future
float top_opacity = 1.0f; float top_opacity = 1.0f;
float bottom_opacity = 1.0f; float bottom_opacity = 1.0f;
bool additional_screen_is_bottom = false;
Common::Rectangle<u32> additional_screen; Common::Rectangle<u32> additional_screen;
CardboardSettings cardboard; CardboardSettings cardboard;
/** /**

View file

@ -706,7 +706,7 @@ void RendererOpenGL::DrawScreens(const Layout::FramebufferLayout& layout, bool f
if (layout.additional_screen_enabled) { if (layout.additional_screen_enabled) {
const auto& additional_screen = layout.additional_screen; const auto& additional_screen = layout.additional_screen;
if (!Settings::values.swap_screen.GetValue()) { if (!layout.additional_screen_is_bottom) {
DrawTopScreen(layout, additional_screen); DrawTopScreen(layout, additional_screen);
} else { } else {
DrawBottomScreen(layout, additional_screen); DrawBottomScreen(layout, additional_screen);

View file

@ -1037,7 +1037,7 @@ void RendererVulkan::DrawScreens(Frame* frame, const Layout::FramebufferLayout&
if (layout.additional_screen_enabled) { if (layout.additional_screen_enabled) {
const auto& additional_screen = layout.additional_screen; const auto& additional_screen = layout.additional_screen;
if (!Settings::values.swap_screen.GetValue()) { if (!layout.additional_screen_is_bottom) {
DrawTopScreen(layout, additional_screen); DrawTopScreen(layout, additional_screen);
} else { } else {
DrawBottomScreen(layout, additional_screen); DrawBottomScreen(layout, additional_screen);