This commit is contained in:
NovaChild 2026-03-31 10:17:47 +00:00 committed by GitHub
commit 4ba0aa0134
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 352 additions and 58 deletions

View File

@ -63,7 +63,7 @@ class EmulationActivity : AppCompatActivity() {
private lateinit var binding: ActivityEmulationBinding
private lateinit var screenAdjustmentUtil: ScreenAdjustmentUtil
private lateinit var hotkeyUtility: HotkeyUtility
private lateinit var secondaryDisplay: SecondaryDisplay
lateinit var secondaryDisplay: SecondaryDisplay
private val onShutdown = Runnable {
if (intent.getBooleanExtra("launched_from_shortcut", false)) {

View File

@ -71,6 +71,13 @@ class ScreenAdjustmentUtil(
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 changeActivityOrientation(orientationOption: Int) {
val activity = context as? Activity ?: return
IntSetting.ORIENTATION_OPTION.int = orientationOption

View File

@ -56,7 +56,12 @@ enum class SecondaryDisplayLayout(val int: Int) {
NONE(0),
TOP_SCREEN(1),
BOTTOM_SCREEN(2),
SIDE_BY_SIDE(3);
SIDE_BY_SIDE(3),
REVERSE_PRIMARY(4),
ORIGINAL(5),
HYBRID(6),
LARGE_SCREEN(7)
;
companion object {
fun from(int: Int): SecondaryDisplayLayout {

View File

@ -6,21 +6,28 @@ package org.citra.citra_emu.display
import android.app.Presentation
import android.content.Context
import android.graphics.SurfaceTexture
import android.hardware.display.DisplayManager
import android.hardware.display.VirtualDisplay
import android.os.Build
import android.os.Bundle
import android.util.Log
import android.view.Display
import android.view.MotionEvent
import android.view.Surface
import android.view.SurfaceHolder
import android.view.SurfaceView
import android.view.WindowManager
import org.citra.citra_emu.features.settings.model.IntSetting
import org.citra.citra_emu.display.SecondaryDisplayLayout
import org.citra.citra_emu.NativeLibrary
class SecondaryDisplay(val context: Context) : DisplayManager.DisplayListener {
private var pres: SecondaryDisplayPresentation? = null
private val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
private val vd: VirtualDisplay
var preferredDisplayId = -1
var currentDisplayId = -1
init {
vd = displayManager.createVirtualDisplay(
@ -35,31 +42,38 @@ class SecondaryDisplay(val context: Context) : DisplayManager.DisplayListener {
}
fun updateSurface() {
NativeLibrary.secondarySurfaceChanged(pres!!.getSurfaceHolder().surface)
val surface = pres?.getSurfaceHolder()?.surface
if (surface != null && surface.isValid) {
NativeLibrary.secondarySurfaceChanged(surface)
} else {
Log.w("SecondaryDisplay", "Attempted to update null or invalid surface")
}
}
fun destroySurface() {
NativeLibrary.secondarySurfaceDestroyed()
}
private fun getExternalDisplay(context: Context): Display? {
val dm = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
val currentDisplayId = context.display.displayId
fun getSecondaryDisplays(context: Context): List<Display> {
val dm = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager
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 presDisplays = dm.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION);
val extDisplays = displays.filter {
return displays.filter {
val isPresentable = presDisplays.any { pd -> pd.displayId == it.displayId }
val isNotDefaultOrPresentable = it.displayId != Display.DEFAULT_DISPLAY || isPresentable
isNotDefaultOrPresentable &&
val isNotDefaultOrPresentable = it != null && it.displayId != Display.DEFAULT_DISPLAY || isPresentable
isNotDefaultOrPresentable &&
it.displayId != currentDisplayId &&
it.name != "HiddenDisplay" &&
it.state != Display.STATE_OFF &&
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() {
@ -67,12 +81,23 @@ class SecondaryDisplay(val context: Context) : DisplayManager.DisplayListener {
if (context is android.app.Activity && (context.isFinishing || context.isDestroyed)) {
return
}
// decide if we are going to the external display or the internal one
var display = getExternalDisplay(context)
if (display == null ||
IntSetting.SECONDARY_DISPLAY_LAYOUT.int == SecondaryDisplayLayout.NONE.int) {
display = vd.display
val displays = getSecondaryDisplays(context)
val display = if (displays.isEmpty() ||
IntSetting.SECONDARY_DISPLAY_LAYOUT.int == SecondaryDisplayLayout.NONE.int
) {
currentDisplayId = -1
vd.display
} else if (preferredDisplayId >=0 && displays.any { it.displayId == preferredDisplayId }) {
currentDisplayId = preferredDisplayId
displays.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 = displays.firstOrNull{it.name != default.name && !it.name.contains("Built",true)}?.displayId ?: displays[0].displayId
displays.first{ it.displayId == currentDisplayId }
}
// if our presentation is already on the right display, ignore
@ -137,16 +162,18 @@ class SecondaryDisplayPresentation(
surfaceView = SurfaceView(context)
surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
override fun surfaceCreated(holder: SurfaceHolder) {
Log.d("SecondaryDisplay", "Surface created")
}
override fun surfaceChanged(
holder: SurfaceHolder, format: Int, width: Int, height: Int
) {
Log.d("SecondaryDisplay", "Surface changed: ${width}x${height}")
parent.updateSurface()
}
override fun surfaceDestroyed(holder: SurfaceHolder) {
Log.d("SecondaryDisplay", "Surface destroyed")
parent.destroySurface()
}
})

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.ScreenAdjustmentUtil
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.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.ui.SettingsActivity
import org.citra.citra_emu.features.settings.utils.SettingsFile
@ -107,8 +107,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
private val settingsViewModel: SettingsViewModel by viewModels()
private val settings get() = settingsViewModel.settings
private val onPause = Runnable{ togglePause() }
private val onShutdown = Runnable{ emulationState.stop() }
private val onPause = Runnable { togglePause() }
private val onShutdown = Runnable { emulationState.stop() }
// Only used if a game is passed through intent on google play variant
private var gameFd: Int? = null
@ -184,7 +184,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
retainInstance = true
emulationState = EmulationState(game.path)
emulationActivity = requireActivity() as EmulationActivity
screenAdjustmentUtil = ScreenAdjustmentUtil(requireContext(), requireActivity().windowManager, settings)
screenAdjustmentUtil =
ScreenAdjustmentUtil(requireContext(), requireActivity().windowManager, settings)
EmulationLifecycleUtil.addPauseResumeHook(onPause)
EmulationLifecycleUtil.addShutdownHook(onShutdown)
}
@ -195,6 +196,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
savedInstanceState: Bundle?
): View {
_binding = FragmentEmulationBinding.inflate(inflater)
binding.inGameMenu.menu.findItem(R.id.menu_secondary_screen_layout).isVisible =
emulationActivity.secondaryDisplay.getSecondaryDisplays(emulationActivity).isNotEmpty()
binding.inGameMenu.menu.findItem(R.id.menu_landscape_screen_layout).isVisible =
CitraApplication.appContext.resources.configuration.orientation !=
Configuration.ORIENTATION_PORTRAIT
@ -336,6 +339,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
true
}
R.id.menu_secondary_screen_layout -> {
showSecondaryScreenLayoutMenu()
true
}
R.id.menu_swap_screens -> {
screenAdjustmentUtil.swapScreen()
true
@ -621,17 +629,21 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
}
add(text).setEnabled(enableClick).setOnMenuItemClickListener {
if(isSaving) {
if (isSaving) {
NativeLibrary.saveState(slot)
Toast.makeText(context,
Toast.makeText(
context,
getString(R.string.saving),
Toast.LENGTH_SHORT).show()
Toast.LENGTH_SHORT
).show()
} else {
NativeLibrary.loadState(slot)
binding.drawerLayout.close()
Toast.makeText(context,
Toast.makeText(
context,
getString(R.string.loading),
Toast.LENGTH_SHORT).show()
Toast.LENGTH_SHORT
).show()
}
true
}
@ -640,9 +652,9 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
savestates?.forEach {
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)
} else{
} else {
getString(R.string.emulation_occupied_state_slot, it.slot, it.time)
}
popupMenu.menu.getItem(it.slot).setTitle(text).setEnabled(enableClick)
@ -724,8 +736,12 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
}
R.id.menu_performance_overlay_show -> {
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 =
!BooleanSetting.PERF_OVERLAY_ENABLE.boolean
settings.saveSetting(
BooleanSetting.PERF_OVERLAY_ENABLE,
SettingsFile.FILE_NAME_CONFIG
)
updateShowPerformanceOverlay()
true
}
@ -996,10 +1012,13 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
val layoutOptionMenuItem = when (IntSetting.PORTRAIT_SCREEN_LAYOUT.int) {
PortraitScreenLayout.TOP_FULL_WIDTH.int ->
R.id.menu_portrait_layout_top_full
PortraitScreenLayout.ORIGINAL.int ->
R.id.menu_portrait_layout_original
PortraitScreenLayout.CUSTOM_PORTRAIT_LAYOUT.int ->
R.id.menu_portrait_layout_custom
else ->
R.id.menu_portrait_layout_top_full
@ -1036,6 +1055,140 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
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_secondary_layout_none)
chooserMenu?.subMenu?.removeGroup(R.id.menu_secondary_management_display_group)
val displays =
emulationActivity.secondaryDisplay.getSecondaryDisplays(emulationActivity)
if (selectedLayout == SecondaryDisplayLayout.NONE.int) {
enableSecondaryCheckbox.isChecked = false
chooserMenu.isVisible = false
popupMenu.menu.setGroupEnabled(R.id.menu_secondary_layout_group, false)
selectedLayout = SecondaryDisplayLayout.REVERSE_PRIMARY.int
} 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_reverse_primary
}
SecondaryDisplayLayout.REVERSE_PRIMARY.int ->
R.id.menu_secondary_layout_reverse_primary
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
if (displays.size > 1 && selectedLayout != SecondaryDisplayLayout.NONE.int) {
val current = emulationActivity.secondaryDisplay.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_secondary_layout_none -> {
if (!it.isChecked) {
screenAdjustmentUtil.changeSecondaryOrientation(selectedLayout)
} else {
screenAdjustmentUtil.changeSecondaryOrientation(SecondaryDisplayLayout.NONE.int)
}
emulationActivity.secondaryDisplay.updateDisplay()
showSecondaryScreenLayoutMenu() // reopen menu to get new behaviors
true
}
R.id.menu_secondary_layout_reverse_primary -> {
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
emulationActivity.secondaryDisplay.preferredDisplayId = it.itemId
emulationActivity.secondaryDisplay.updateDisplay()
true
}
}
}
popupMenu.show()
}
private fun editControlsPlacement() {
if (binding.surfaceInputOverlay.isInEditMode) {
binding.doneControlConfig.visibility = View.GONE
@ -1092,7 +1245,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
slider.valueFrom = 0f
slider.value = preferences.getInt(target, 50).toFloat()
textValue.setText((slider.value + 50).toInt().toString())
textValue.addTextChangedListener( object : TextWatcher {
textValue.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable) {
val value = s.toString().toIntOrNull()
if (value == null || value < 50 || value > 150) {
@ -1102,6 +1255,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
slider.value = value.toFloat() - 50
}
}
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
})
@ -1142,7 +1296,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
slider.value = preferences.getInt("controlOpacity", 50).toFloat()
textValue.setText(slider.value.toInt().toString())
textValue.addTextChangedListener( object : TextWatcher {
textValue.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable) {
val value = s.toString().toIntOrNull()
if (value == null || value < slider.valueFrom || value > slider.valueTo) {
@ -1152,6 +1306,7 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
slider.value = value.toFloat()
}
}
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
})
@ -1160,11 +1315,11 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
slider.addOnChangeListener { _: Slider, value: Float, _: Boolean ->
if (textValue.text.toString() != slider.value.toInt().toString()) {
textValue.setText(slider.value.toInt().toString())
textValue.setSelection(textValue.length())
setControlOpacity(slider.value.toInt())
}
textValue.setText(slider.value.toInt().toString())
textValue.setSelection(textValue.length())
setControlOpacity(slider.value.toInt())
}
}
textInput.suffixText = "%"
}
@ -1349,7 +1504,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
}
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
params.setMargins(padding, 0, padding, 0)
@ -1384,7 +1540,8 @@ class EmulationFragment : Fragment(), SurfaceHolder.Callback, Choreographer.Fram
private fun getBatteryTemperature(): Float {
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
val temperature = batteryIntent?.getIntExtra(BatteryManager.EXTRA_TEMPERATURE, 0) ?: 0
// Convert to degrees Celsius

View File

@ -157,7 +157,7 @@ void Config::ReadValues() {
ReadSetting("Renderer", Settings::values.turbo_limit);
// Workaround to map Android setting for enabling the frame limiter to the format Citra expects
if (android_config->GetBoolean("Renderer", "use_frame_limit", true)) {
ReadSetting("Renderer", Settings::values.frame_limit);
ReadSetting("Renderer", Settings::values.frame_limit);
} else {
Settings::values.frame_limit = 0;
}

View File

@ -18,10 +18,13 @@
#include "video_core/renderer_base.h"
bool EmuWindow_Android::OnSurfaceChanged(ANativeWindow* surface) {
if (render_window == surface) {
int w = surface== NULL ? 0 : ANativeWindow_getWidth(surface);
int h = surface== NULL ? 0 : ANativeWindow_getHeight(surface);
if (render_window == surface && w == window_width && h == window_height) {
return false;
}
window_width = w;
window_height = h;
render_window = surface;
window_info.type = Frontend::WindowSystemType::Android;
window_info.render_surface = surface;
@ -48,15 +51,9 @@ void EmuWindow_Android::OnTouchMoved(int x, int y) {
}
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};
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);
}
UpdateCurrentFramebufferLayout(window_width,window_height,is_portrait_mode);
}
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) {
// Second window already created, so update it
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 {
LOG_WARNING(Frontend,
"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: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
android:id="@+id/menu_swap_screens"
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_secondary_layout_none"
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_reverse_primary"
android:title="@string/emulation_secondary_display_reverse_primary" />
<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

@ -37,9 +37,14 @@
<string-array name="secondaryLayouts">
<item>@string/emulation_secondary_display_default</item>
<item>@string/emulation_secondary_display_reverse_primary</item>
<item>@string/emulation_top_screen</item>
<item>@string/emulation_bottom_screen</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>
<integer-array name="portraitLayoutValues">
@ -50,9 +55,13 @@
<integer-array name="secondaryLayoutValues">
<item>0</item>
<item>4</item>
<item>1</item>
<item>2</item>
<item>3</item>
<item>5</item>
<item>6</item>
<item>7</item>
</integer-array>
<string-array name="smallScreenPositions">

View File

@ -467,7 +467,10 @@
<string name="emulation_aspect_ratio">Aspect Ratio</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_secondary_layout">Secondary Display Screen Layout</string>
<string name="emulation_secondary_display_enable">Enable Secondary Display</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_screen_layout_largescreen">Large Screen</string>
<string name="emulation_screen_layout_portrait">Portrait</string>
@ -476,7 +479,8 @@
<string name="emulation_screen_layout_hybrid">Hybrid Screens</string>
<string name="emulation_screen_layout_original">Original</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_default">None (system default)</string>
<string name="emulation_secondary_display_reverse_primary">Opposite of Primary Display</string>
<string name="emulation_screen_layout_custom">Custom Layout</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>

View File

@ -57,7 +57,7 @@ enum class PortraitLayoutOption : u32 {
PortraitOriginal
};
enum class SecondaryDisplayLayout : u32 { None, TopScreenOnly, BottomScreenOnly, SideBySide };
enum class SecondaryDisplayLayout : u32 { None, TopScreenOnly, BottomScreenOnly, SideBySide, ReversePrimary, Original, Hybrid, LargeScreen };
/** Defines where the small screen will appear relative to the large screen
* when in Large Screen mode
*/

View File

@ -305,17 +305,30 @@ FramebufferLayout AndroidSecondaryLayout(u32 width, u32 height) {
const Settings::SecondaryDisplayLayout layout =
Settings::values.secondary_display_layout.GetValue();
switch (layout) {
case Settings::SecondaryDisplayLayout::TopScreenOnly:
return SingleFrameLayout(width, height, false, Settings::values.upright_screen.GetValue());
case Settings::SecondaryDisplayLayout::BottomScreenOnly:
return SingleFrameLayout(width, height, true, Settings::values.upright_screen.GetValue());
case Settings::SecondaryDisplayLayout::SideBySide:
return LargeFrameLayout(width, height, false, Settings::values.upright_screen.GetValue(),
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:
// this should never happen, but if it does, somehow, send the top screen
case Settings::SecondaryDisplayLayout::TopScreenOnly:
// this should never happen - if "none" is set this method shouldn't run - but if it does,
// somehow, use ReversePrimary
case Settings::SecondaryDisplayLayout::ReversePrimary:
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());
}
}