diff --git a/app/src/main/java/app/gamenative/PrefManager.kt b/app/src/main/java/app/gamenative/PrefManager.kt index a3bb1f3ad..b4137a295 100644 --- a/app/src/main/java/app/gamenative/PrefManager.kt +++ b/app/src/main/java/app/gamenative/PrefManager.kt @@ -423,6 +423,17 @@ object PrefManager { setPref(DINPUT_MAPPER_TYPE, value) } + // External display input mode (off|touchpad|keyboard|hybrid) + private val EXTERNAL_DISPLAY_INPUT_MODE = stringPreferencesKey("external_display_input_mode") + var externalDisplayInputMode: String + get() = getPref(EXTERNAL_DISPLAY_INPUT_MODE, Container.DEFAULT_EXTERNAL_DISPLAY_MODE) + set(value) { setPref(EXTERNAL_DISPLAY_INPUT_MODE, value) } + + private val EXTERNAL_DISPLAY_SWAP = booleanPreferencesKey("external_display_swap") + var externalDisplaySwap: Boolean + get() = getPref(EXTERNAL_DISPLAY_SWAP, false) + set(value) { setPref(EXTERNAL_DISPLAY_SWAP, value) } + // Disable Mouse Input (prevents external mouse events) private val DISABLE_MOUSE_INPUT = booleanPreferencesKey("disable_mouse_input") var disableMouseInput: Boolean diff --git a/app/src/main/java/app/gamenative/externaldisplay/ExternalDisplayInputController.kt b/app/src/main/java/app/gamenative/externaldisplay/ExternalDisplayInputController.kt new file mode 100644 index 000000000..6b6f65c5f --- /dev/null +++ b/app/src/main/java/app/gamenative/externaldisplay/ExternalDisplayInputController.kt @@ -0,0 +1,279 @@ +package app.gamenative.externaldisplay + +import android.app.Presentation +import android.content.Context +import android.graphics.drawable.GradientDrawable +import android.hardware.display.DisplayManager +import android.os.Handler +import android.os.Looper +import android.view.Display +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import android.widget.FrameLayout +import android.widget.ImageButton +import android.widget.ImageView +import androidx.core.content.ContextCompat +import app.gamenative.R +import com.winlator.container.Container +import com.winlator.widget.TouchpadView +import com.winlator.xserver.XServer + +private const val EXTERNAL_SURFACE_BG_RES: Int = R.color.external_display_surface_background +private const val EXTERNAL_KEY_BG_RES: Int = R.color.external_display_key_background + +class ExternalDisplayInputController( + private val context: Context, + private val xServer: XServer, + private val touchpadViewProvider: () -> TouchpadView?, +) { + enum class Mode { OFF, TOUCHPAD, KEYBOARD, HYBRID } + + companion object { + fun fromConfig(value: String?): Mode = when (value?.lowercase()) { + Container.EXTERNAL_DISPLAY_MODE_TOUCHPAD -> Mode.TOUCHPAD + Container.EXTERNAL_DISPLAY_MODE_KEYBOARD -> Mode.KEYBOARD + Container.EXTERNAL_DISPLAY_MODE_HYBRID -> Mode.HYBRID + else -> Mode.OFF + } + } + + private val displayManager = context.getSystemService(DisplayManager::class.java) + private var presentation: ExternalInputPresentation? = null + private var mode: Mode = Mode.OFF + + private val displayListener = object : DisplayManager.DisplayListener { + override fun onDisplayAdded(displayId: Int) { + updatePresentation() + } + + override fun onDisplayRemoved(displayId: Int) { + if (presentation?.display?.displayId == displayId) { + dismissPresentation() + } + updatePresentation() + } + + override fun onDisplayChanged(displayId: Int) { + if (presentation?.display?.displayId == displayId) { + updatePresentation() + } + } + } + + fun start() { + displayManager?.registerDisplayListener(displayListener, Handler(Looper.getMainLooper())) + updatePresentation() + } + + fun stop() { + dismissPresentation() + try { + displayManager?.unregisterDisplayListener(displayListener) + } catch (_: Exception) { + } + } + + fun setMode(mode: Mode) { + this.mode = mode + updatePresentation() + } + + private fun updatePresentation() { + if (mode == Mode.OFF) { + dismissPresentation() + return + } + + val targetDisplay = findPresentationDisplay() ?: run { + dismissPresentation() + return + } + + val needsNewPresentation = presentation?.display?.displayId != targetDisplay.displayId + if (presentation == null || needsNewPresentation) { + dismissPresentation() + presentation = ExternalInputPresentation( + context = context, + display = targetDisplay, + mode = mode, + xServer = xServer, + touchpadViewProvider = touchpadViewProvider, + ) + presentation?.show() + } else { + presentation?.updateMode(mode) + } + } + + private fun dismissPresentation() { + presentation?.dismiss() + presentation = null + } + + private fun findPresentationDisplay(): Display? { + val currentDisplay = context.display ?: return null + // Required detection logic for external presentation displays + return displayManager + ?.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION) + ?.firstOrNull { display -> + display.displayId != currentDisplay.displayId && display.name != "HiddenDisplay" + } + } +} + +private class ExternalInputPresentation( + context: Context, + display: Display, + private var mode: ExternalDisplayInputController.Mode, + private val xServer: XServer, + private val touchpadViewProvider: () -> TouchpadView?, +) : Presentation(context, display) { + + override fun onCreate(savedInstanceState: android.os.Bundle?) { + super.onCreate(savedInstanceState) + window?.setFlags( + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, + ) + renderContent() + } + + fun updateMode(newMode: ExternalDisplayInputController.Mode) { + if (mode != newMode) { + mode = newMode + renderContent() + } + } + + private fun renderContent() { + when (mode) { + ExternalDisplayInputController.Mode.TOUCHPAD -> { + val pad = TouchpadView(context, xServer, false).apply { + layoutParams = FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ) + setBackgroundColor(ContextCompat.getColor(context, EXTERNAL_SURFACE_BG_RES)) + touchpadViewProvider()?.let { primary -> + setSimTouchScreen(primary.isSimTouchScreen) + } + } + setContentView(pad) + } + ExternalDisplayInputController.Mode.KEYBOARD -> { + val root = FrameLayout(context).apply { + layoutParams = FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ) + setBackgroundColor(ContextCompat.getColor(context, EXTERNAL_SURFACE_BG_RES)) + } + + val hintIcon = ImageView(context).apply { + val density = resources.displayMetrics.density + val sizePx = (128 * density).toInt() + layoutParams = FrameLayout.LayoutParams(sizePx, sizePx).apply { + gravity = Gravity.CENTER + } + setImageResource(R.drawable.icon_keyboard) + alpha = 0.35f + scaleType = ImageView.ScaleType.FIT_CENTER + } + + val keyboardView = ExternalOnScreenKeyboardView(context, xServer).apply { + layoutParams = FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ).apply { + gravity = Gravity.BOTTOM + } + } + + root.addView(hintIcon) + root.addView(keyboardView) + setContentView(root) + } + ExternalDisplayInputController.Mode.HYBRID -> { + val hybrid = HybridInputLayout( + context = context, + xServer = xServer, + touchpadViewProvider = touchpadViewProvider, + ) + setContentView(hybrid) + } + else -> { + setContentView(FrameLayout(context)) + } + } + } +} + +private class HybridInputLayout( + context: Context, + xServer: XServer, + touchpadViewProvider: () -> TouchpadView?, +) : FrameLayout(context) { + + private val touchpad = TouchpadView(context, xServer, false).apply { + layoutParams = LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ) + setBackgroundColor(ContextCompat.getColor(context, EXTERNAL_SURFACE_BG_RES)) + touchpadViewProvider()?.let { primary -> + setSimTouchScreen(primary.isSimTouchScreen) + } + } + private val keyboardView = ExternalOnScreenKeyboardView(context, xServer).apply { + layoutParams = LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ).apply { + gravity = Gravity.BOTTOM + } + visibility = View.GONE + } + + private val keyboardToggleButton = ImageButton(context).apply { + val density = resources.displayMetrics.density + val sizePx = (56 * density).toInt() + val marginPx = (16 * density).toInt() + layoutParams = LayoutParams(sizePx, sizePx).apply { + gravity = Gravity.BOTTOM or Gravity.END + setMargins(marginPx, marginPx, marginPx, marginPx) + } + background = GradientDrawable().apply { + shape = GradientDrawable.OVAL + setColor(ContextCompat.getColor(context, EXTERNAL_KEY_BG_RES)) + } + setImageResource(R.drawable.icon_keyboard) + scaleType = ImageView.ScaleType.CENTER_INSIDE + setPadding(marginPx / 2, marginPx / 2, marginPx / 2, marginPx / 2) + setOnClickListener { toggleKeyboard() } + } + + init { + addView(touchpad) + addView(keyboardView) + addView(keyboardToggleButton) + + keyboardView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> + updateToggleButtonPosition() + } + } + + private fun toggleKeyboard() { + val shouldShow = keyboardView.visibility != View.VISIBLE + keyboardView.visibility = if (shouldShow) View.VISIBLE else View.GONE + post { updateToggleButtonPosition() } + } + private fun updateToggleButtonPosition() { + keyboardToggleButton.translationY = if (keyboardView.visibility == View.VISIBLE) { + -keyboardView.height.toFloat() + } else { + 0f + } + } +} diff --git a/app/src/main/java/app/gamenative/externaldisplay/ExternalDisplaySwapController.kt b/app/src/main/java/app/gamenative/externaldisplay/ExternalDisplaySwapController.kt new file mode 100644 index 000000000..816c0ad90 --- /dev/null +++ b/app/src/main/java/app/gamenative/externaldisplay/ExternalDisplaySwapController.kt @@ -0,0 +1,150 @@ +package app.gamenative.externaldisplay + +import android.app.Presentation +import android.content.Context +import android.hardware.display.DisplayManager +import android.os.Handler +import android.os.Looper +import android.view.Display +import android.view.ViewGroup +import android.view.WindowManager +import android.widget.FrameLayout +import com.winlator.widget.XServerView + +class ExternalDisplaySwapController( + private val context: Context, + private val xServerViewProvider: () -> XServerView?, + private val internalGameHostProvider: () -> ViewGroup?, + private val onGameOnExternalChanged: (Boolean) -> Unit = {}, +) { + private val displayManager = context.getSystemService(DisplayManager::class.java) + private var presentation: GamePresentation? = null + private var swapEnabled: Boolean = false + private var gameOnExternal: Boolean = false + + private val displayListener = object : DisplayManager.DisplayListener { + override fun onDisplayAdded(displayId: Int) = updatePresentation() + + override fun onDisplayRemoved(displayId: Int) { + if (presentation?.display?.displayId == displayId) { + dismissPresentation() + } + updatePresentation() + } + + override fun onDisplayChanged(displayId: Int) { + if (presentation?.display?.displayId == displayId) { + updatePresentation() + } + } + } + + fun start() { + displayManager?.registerDisplayListener(displayListener, Handler(Looper.getMainLooper())) + updatePresentation() + } + + fun stop() { + dismissPresentation() + try { + displayManager?.unregisterDisplayListener(displayListener) + } catch (_: Exception) { + } + } + + fun setSwapEnabled(enabled: Boolean) { + if (swapEnabled == enabled) return + swapEnabled = enabled + updatePresentation() + } + + private fun updatePresentation() { + val targetDisplay = if (swapEnabled) findPresentationDisplay() else null + if (targetDisplay == null) { + moveGameToInternal() + dismissPresentation() + return + } + + val needsNewPresentation = presentation?.display?.displayId != targetDisplay.displayId + if (presentation == null || needsNewPresentation) { + dismissPresentation() + presentation = GamePresentation(context, targetDisplay).also { it.show() } + } + moveGameToExternal() + } + + private fun dismissPresentation() { + presentation?.dismiss() + presentation = null + setGameOnExternal(false) + } + + private fun moveGameToExternal() { + val xServerView = xServerViewProvider() ?: return + val root = presentation?.root ?: return + val parent = xServerView.parent as? ViewGroup + if (parent != null && parent != root) parent.removeView(xServerView) + if (xServerView.parent == null) { + xServerView.layoutParams = FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ) + root.addView(xServerView) + } + setGameOnExternal(true) + } + + private fun moveGameToInternal() { + val xServerView = xServerViewProvider() ?: return + val internalHost = internalGameHostProvider() ?: return + val parent = xServerView.parent as? ViewGroup + if (parent != null && parent != internalHost) parent.removeView(xServerView) + if (xServerView.parent == null) { + xServerView.layoutParams = FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ) + internalHost.addView(xServerView) + } + setGameOnExternal(false) + } + + private fun setGameOnExternal(value: Boolean) { + if (gameOnExternal == value) return + gameOnExternal = value + onGameOnExternalChanged(value) + } + + private fun findPresentationDisplay(): Display? { + val currentDisplay = context.display ?: return null + return displayManager + ?.getDisplays(DisplayManager.DISPLAY_CATEGORY_PRESENTATION) + ?.firstOrNull { display -> + display.displayId != currentDisplay.displayId && display.name != "HiddenDisplay" + } + } +} + +private class GamePresentation( + outerContext: Context, + display: Display, +) : Presentation(outerContext, display) { + val root: FrameLayout by lazy { + FrameLayout(context).apply { + layoutParams = FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ) + } + } + + override fun onCreate(savedInstanceState: android.os.Bundle?) { + super.onCreate(savedInstanceState) + window?.setFlags( + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, + ) + setContentView(root) + } +} diff --git a/app/src/main/java/app/gamenative/externaldisplay/ExternalOnScreenKeyboardView.kt b/app/src/main/java/app/gamenative/externaldisplay/ExternalOnScreenKeyboardView.kt new file mode 100644 index 000000000..c7c73848a --- /dev/null +++ b/app/src/main/java/app/gamenative/externaldisplay/ExternalOnScreenKeyboardView.kt @@ -0,0 +1,330 @@ +package app.gamenative.externaldisplay + +import android.content.Context +import android.graphics.Color +import android.graphics.Typeface +import android.graphics.drawable.GradientDrawable +import android.graphics.drawable.StateListDrawable +import android.view.Gravity +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.LinearLayout +import androidx.core.content.ContextCompat +import app.gamenative.R +import com.winlator.xserver.XKeycode +import com.winlator.xserver.XServer +import kotlin.math.roundToInt + +class ExternalOnScreenKeyboardView( + context: Context, + private val xServer: XServer, +) : LinearLayout(context) { + + private enum class ShiftState { OFF, ON, CAPS } + + private data class KeySpec( + val normalLabel: String, + val shiftedLabel: String? = null, + val keycode: XKeycode? = null, + val weight: Float = 1f, + val isLetter: Boolean = false, + val action: Action = Action.INPUT, + ) + + private enum class Action { INPUT, SHIFT, BACKSPACE, ENTER, SPACE, TAB, ESC, ARROW_LEFT, ARROW_DOWN, ARROW_RIGHT, ARROW_UP } + + private data class KeyButton( + val spec: KeySpec, + val button: Button, + ) + + private val keyButtons = mutableListOf() + private val downKeys = mutableSetOf() + private var shiftState: ShiftState = ShiftState.OFF + private val keyboardBackgroundColor: Int = ContextCompat.getColor(context, R.color.external_display_keyboard_background) + private val keyBackgroundColor: Int = ContextCompat.getColor(context, R.color.external_display_key_background) + private val keyHighlightColor: Int = ContextCompat.getColor(context, R.color.external_display_key_highlight_background) + private val keyHighlightStrongColor: Int = + ContextCompat.getColor(context, R.color.external_display_key_highlight_strong_background) + + init { + orientation = VERTICAL + setMotionEventSplittingEnabled(true) + val padding = dp(8) + setPadding(padding, padding, padding, padding) + setBackgroundColor(keyboardBackgroundColor) + buildLayout() + refreshLabels() + } + + private fun buildLayout() { + addRow( + listOf( + KeySpec("Esc", keycode = XKeycode.KEY_ESC, weight = 1.25f, action = Action.ESC), + KeySpec("1", "!", XKeycode.KEY_1), + KeySpec("2", "@", XKeycode.KEY_2), + KeySpec("3", "#", XKeycode.KEY_3), + KeySpec("4", "$", XKeycode.KEY_4), + KeySpec("5", "%", XKeycode.KEY_5), + KeySpec("6", "^", XKeycode.KEY_6), + KeySpec("7", "&", XKeycode.KEY_7), + KeySpec("8", "*", XKeycode.KEY_8), + KeySpec("9", "(", XKeycode.KEY_9), + KeySpec("0", ")", XKeycode.KEY_0), + KeySpec("-", "_", XKeycode.KEY_MINUS), + KeySpec("=", "+", XKeycode.KEY_EQUAL), + KeySpec("⌫", keycode = XKeycode.KEY_BKSP, weight = 1.75f, action = Action.BACKSPACE), + ), + ) + + addRow( + listOf( + KeySpec("Tab", keycode = XKeycode.KEY_TAB, weight = 1.5f, action = Action.TAB), + KeySpec("q", "Q", XKeycode.KEY_Q, isLetter = true), + KeySpec("w", "W", XKeycode.KEY_W, isLetter = true), + KeySpec("e", "E", XKeycode.KEY_E, isLetter = true), + KeySpec("r", "R", XKeycode.KEY_R, isLetter = true), + KeySpec("t", "T", XKeycode.KEY_T, isLetter = true), + KeySpec("y", "Y", XKeycode.KEY_Y, isLetter = true), + KeySpec("u", "U", XKeycode.KEY_U, isLetter = true), + KeySpec("i", "I", XKeycode.KEY_I, isLetter = true), + KeySpec("o", "O", XKeycode.KEY_O, isLetter = true), + KeySpec("p", "P", XKeycode.KEY_P, isLetter = true), + KeySpec("[", "{", XKeycode.KEY_BRACKET_LEFT), + KeySpec("]", "}", XKeycode.KEY_BRACKET_RIGHT), + KeySpec("\\", "|", XKeycode.KEY_BACKSLASH, weight = 1.25f), + ), + ) + + addRow( + listOf( + KeySpec("Shift", weight = 1.75f, action = Action.SHIFT), + KeySpec("a", "A", XKeycode.KEY_A, isLetter = true), + KeySpec("s", "S", XKeycode.KEY_S, isLetter = true), + KeySpec("d", "D", XKeycode.KEY_D, isLetter = true), + KeySpec("f", "F", XKeycode.KEY_F, isLetter = true), + KeySpec("g", "G", XKeycode.KEY_G, isLetter = true), + KeySpec("h", "H", XKeycode.KEY_H, isLetter = true), + KeySpec("j", "J", XKeycode.KEY_J, isLetter = true), + KeySpec("k", "K", XKeycode.KEY_K, isLetter = true), + KeySpec("l", "L", XKeycode.KEY_L, isLetter = true), + KeySpec(";", ":", XKeycode.KEY_SEMICOLON), + KeySpec("'", "\"", XKeycode.KEY_APOSTROPHE), + KeySpec("Enter", keycode = XKeycode.KEY_ENTER, weight = 2.0f, action = Action.ENTER), + ), + ) + + addRow( + listOf( + KeySpec("`", "~", XKeycode.KEY_GRAVE, weight = 1.25f), + KeySpec("z", "Z", XKeycode.KEY_Z, isLetter = true), + KeySpec("x", "X", XKeycode.KEY_X, isLetter = true), + KeySpec("c", "C", XKeycode.KEY_C, isLetter = true), + KeySpec("v", "V", XKeycode.KEY_V, isLetter = true), + KeySpec("b", "B", XKeycode.KEY_B, isLetter = true), + KeySpec("n", "N", XKeycode.KEY_N, isLetter = true), + KeySpec("m", "M", XKeycode.KEY_M, isLetter = true), + KeySpec(",", "<", XKeycode.KEY_COMMA), + KeySpec(".", ">", XKeycode.KEY_PERIOD), + KeySpec("/", "?", XKeycode.KEY_SLASH), + KeySpec("↑", keycode = XKeycode.KEY_UP, weight = 1.25f, action = Action.ARROW_UP), + ), + ) + + addRow( + listOf( + KeySpec("Space", keycode = XKeycode.KEY_SPACE, weight = 6f, action = Action.SPACE), + KeySpec("←", keycode = XKeycode.KEY_LEFT, weight = 1.25f, action = Action.ARROW_LEFT), + KeySpec("↓", keycode = XKeycode.KEY_DOWN, weight = 1.25f, action = Action.ARROW_DOWN), + KeySpec("→", keycode = XKeycode.KEY_RIGHT, weight = 1.25f, action = Action.ARROW_RIGHT), + ), + ) + } + + private fun addRow(keys: List) { + val row = LinearLayout(context).apply { + orientation = HORIZONTAL + gravity = Gravity.CENTER + layoutParams = LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ) + } + + val margin = dp(3) + val height = dp(48) + + keys.forEach { spec -> + val button = Button(context).apply { + isAllCaps = false + setTextColor(Color.WHITE) + setTextSize(16f) + typeface = Typeface.DEFAULT_BOLD + text = spec.normalLabel + background = createKeyBackground(normal = true) + setPadding(0, 0, 0, 0) + layoutParams = LayoutParams(0, height, spec.weight).apply { + setMargins(margin, margin, margin, margin) + } + setOnTouchListener { _, event -> + handleKeyTouch(spec, event) + false + } + } + keyButtons += KeyButton(spec, button) + row.addView(button) + } + + addView(row) + } + + private fun handleKeyTouch(spec: KeySpec, event: MotionEvent) { + when (event.actionMasked) { + MotionEvent.ACTION_DOWN -> onKeyDown(spec) + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> onKeyUp(spec, cancel = event.actionMasked == MotionEvent.ACTION_CANCEL) + } + } + + private fun onKeyDown(spec: KeySpec) { + when (spec.action) { + Action.SHIFT -> Unit + Action.BACKSPACE -> pressKey(XKeycode.KEY_BKSP) + Action.ENTER -> pressKey(XKeycode.KEY_ENTER) + Action.SPACE -> pressKey(XKeycode.KEY_SPACE) + Action.TAB -> pressKey(XKeycode.KEY_TAB) + Action.ESC -> pressKey(XKeycode.KEY_ESC) + Action.ARROW_LEFT -> pressKey(XKeycode.KEY_LEFT) + Action.ARROW_DOWN -> pressKey(XKeycode.KEY_DOWN) + Action.ARROW_RIGHT -> pressKey(XKeycode.KEY_RIGHT) + Action.ARROW_UP -> pressKey(XKeycode.KEY_UP) + Action.INPUT -> { + val keycode = spec.keycode ?: return + val useShift = when (shiftState) { + ShiftState.OFF -> false + ShiftState.ON -> true + ShiftState.CAPS -> spec.isLetter + } + pressKey(keycode, withShift = useShift) + if (shiftState == ShiftState.ON) { + shiftState = ShiftState.OFF + refreshLabels() + } + } + } + } + + private fun onKeyUp(spec: KeySpec, cancel: Boolean) { + when (spec.action) { + Action.SHIFT -> if (!cancel) cycleShift() + Action.BACKSPACE -> releaseKey(XKeycode.KEY_BKSP) + Action.ENTER -> releaseKey(XKeycode.KEY_ENTER) + Action.SPACE -> releaseKey(XKeycode.KEY_SPACE) + Action.TAB -> releaseKey(XKeycode.KEY_TAB) + Action.ESC -> releaseKey(XKeycode.KEY_ESC) + Action.ARROW_LEFT -> releaseKey(XKeycode.KEY_LEFT) + Action.ARROW_DOWN -> releaseKey(XKeycode.KEY_DOWN) + Action.ARROW_RIGHT -> releaseKey(XKeycode.KEY_RIGHT) + Action.ARROW_UP -> releaseKey(XKeycode.KEY_UP) + Action.INPUT -> spec.keycode?.let { releaseKey(it) } + } + } + + private fun cycleShift() { + shiftState = when (shiftState) { + ShiftState.OFF -> ShiftState.ON + ShiftState.ON -> ShiftState.CAPS + ShiftState.CAPS -> ShiftState.OFF + } + refreshLabels() + } + + private fun refreshLabels() { + val shiftForLetters = shiftState != ShiftState.OFF + keyButtons.forEach { (spec, button) -> + if (spec.action == Action.SHIFT) { + val label = when (shiftState) { + ShiftState.OFF -> "Shift" + ShiftState.ON -> "Shift" + ShiftState.CAPS -> "Caps" + } + button.text = label + button.background = when (shiftState) { + ShiftState.OFF -> createKeyBackground(normal = true) + ShiftState.ON -> createKeyBackground(highlight = true) + ShiftState.CAPS -> createKeyBackground(highlight = true, strong = true) + } + return@forEach + } + + val showShifted = when { + spec.isLetter -> shiftForLetters + shiftState == ShiftState.ON -> true + else -> false + } + + button.text = if (showShifted && spec.shiftedLabel != null) spec.shiftedLabel else spec.normalLabel + button.background = createKeyBackground(normal = true) + } + } + + private fun pressKey(key: XKeycode, withShift: Boolean = false) { + if (!downKeys.add(key)) return + val shiftWasDown = xServer.keyboard.modifiersMask.isSet(1) + if (withShift && !shiftWasDown) xServer.injectKeyPress(XKeycode.KEY_SHIFT_L) + xServer.injectKeyPress(key) + if (withShift && !shiftWasDown) xServer.injectKeyRelease(XKeycode.KEY_SHIFT_L) + } + + private fun releaseKey(key: XKeycode) { + if (!downKeys.remove(key)) return + xServer.injectKeyRelease(key) + } + + private fun dp(value: Int): Int = (value * resources.displayMetrics.density).toInt() + + override fun onDetachedFromWindow() { + downKeys.toList().forEach { key -> + xServer.injectKeyRelease(key) + } + downKeys.clear() + super.onDetachedFromWindow() + } + + private fun createKeyBackground( + normal: Boolean = false, + highlight: Boolean = false, + strong: Boolean = false, + ): StateListDrawable { + val radius = dp(8).toFloat() + val baseColor = when { + highlight && strong -> keyHighlightStrongColor + highlight -> keyHighlightColor + normal -> keyBackgroundColor + else -> keyBackgroundColor + } + + val pressedColor = blendColor(baseColor, Color.WHITE, 0.18f) + + fun shape(color: Int): GradientDrawable = GradientDrawable().apply { + cornerRadius = radius + setColor(color) + } + + return StateListDrawable().apply { + addState(intArrayOf(android.R.attr.state_pressed), shape(pressedColor)) + addState(intArrayOf(), shape(baseColor)) + } + } + + private fun blendColor(from: Int, to: Int, ratio: Float): Int { + val clamped = ratio.coerceIn(0f, 1f) + val inverse = 1f - clamped + val a = (Color.alpha(from) * inverse + Color.alpha(to) * clamped).roundToInt() + val r = (Color.red(from) * inverse + Color.red(to) * clamped).roundToInt() + val g = (Color.green(from) * inverse + Color.green(to) * clamped).roundToInt() + val b = (Color.blue(from) * inverse + Color.blue(to) * clamped).roundToInt() + return Color.argb(a, r, g, b) + } +} diff --git a/app/src/main/java/app/gamenative/externaldisplay/SwapInputOverlayView.kt b/app/src/main/java/app/gamenative/externaldisplay/SwapInputOverlayView.kt new file mode 100644 index 000000000..af457ea1f --- /dev/null +++ b/app/src/main/java/app/gamenative/externaldisplay/SwapInputOverlayView.kt @@ -0,0 +1,119 @@ +package app.gamenative.externaldisplay + +import android.content.Context +import android.graphics.drawable.GradientDrawable +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.ImageButton +import android.widget.ImageView +import androidx.core.content.ContextCompat +import app.gamenative.R +import com.winlator.xserver.XServer + +class SwapInputOverlayView( + context: Context, + private val xServer: XServer, +) : FrameLayout(context) { + + private var mode: ExternalDisplayInputController.Mode = ExternalDisplayInputController.Mode.OFF + + private val hintIcon: ImageView = ImageView(context).apply { + val density = resources.displayMetrics.density + val sizePx = (128 * density).toInt() + layoutParams = LayoutParams(sizePx, sizePx).apply { + gravity = Gravity.CENTER + } + setImageResource(R.drawable.icon_keyboard) + alpha = 0.35f + scaleType = ImageView.ScaleType.FIT_CENTER + visibility = View.GONE + isClickable = false + isFocusable = false + } + + private val keyboardView: ExternalOnScreenKeyboardView = ExternalOnScreenKeyboardView(context, xServer).apply { + layoutParams = LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ).apply { + gravity = Gravity.BOTTOM + } + visibility = View.GONE + } + + private val keyboardToggleButton: ImageButton = ImageButton(context).apply { + val density = resources.displayMetrics.density + val sizePx = (56 * density).toInt() + val marginPx = (16 * density).toInt() + layoutParams = LayoutParams(sizePx, sizePx).apply { + gravity = Gravity.BOTTOM or Gravity.END + setMargins(marginPx, marginPx, marginPx, marginPx) + } + background = GradientDrawable().apply { + shape = GradientDrawable.OVAL + setColor(ContextCompat.getColor(context, R.color.external_display_key_background)) + } + setImageResource(R.drawable.icon_keyboard) + scaleType = ImageView.ScaleType.CENTER_INSIDE + setPadding(marginPx / 2, marginPx / 2, marginPx / 2, marginPx / 2) + visibility = View.GONE + setOnClickListener { toggleKeyboard() } + } + + init { + layoutParams = LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ) + isClickable = false + isFocusable = false + + addView(hintIcon) + addView(keyboardView) + addView(keyboardToggleButton) + + keyboardView.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> + updateToggleButtonPosition() + } + } + + fun setMode(mode: ExternalDisplayInputController.Mode) { + this.mode = mode + when (mode) { + ExternalDisplayInputController.Mode.KEYBOARD -> { + hintIcon.visibility = View.VISIBLE + keyboardToggleButton.visibility = View.GONE + keyboardView.visibility = View.VISIBLE + updateToggleButtonPosition() + } + ExternalDisplayInputController.Mode.HYBRID -> { + hintIcon.visibility = View.GONE + keyboardToggleButton.visibility = View.VISIBLE + keyboardView.visibility = View.GONE + updateToggleButtonPosition() + } + else -> { + hintIcon.visibility = View.GONE + keyboardToggleButton.visibility = View.GONE + keyboardView.visibility = View.GONE + updateToggleButtonPosition() + } + } + } + + private fun toggleKeyboard() { + if (mode != ExternalDisplayInputController.Mode.HYBRID) return + keyboardView.visibility = if (keyboardView.visibility == View.VISIBLE) View.GONE else View.VISIBLE + post { updateToggleButtonPosition() } + } + + private fun updateToggleButtonPosition() { + keyboardToggleButton.translationY = if (keyboardView.visibility == View.VISIBLE) { + -keyboardView.height.toFloat() + } else { + 0f + } + } +} diff --git a/app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt b/app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt index 24e0ba689..44fdbe3b3 100644 --- a/app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt +++ b/app/src/main/java/app/gamenative/ui/component/dialog/ContainerConfigDialog.kt @@ -160,6 +160,12 @@ fun ContainerConfigDialog( val renderingModes = stringArrayResource(R.array.offscreen_rendering_modes).toList() val videoMemSizes = stringArrayResource(R.array.video_memory_size_entries).toList() val mouseWarps = stringArrayResource(R.array.mouse_warp_override_entries).toList() + val externalDisplayModes = listOf( + stringResource(R.string.external_display_mode_off), + stringResource(R.string.external_display_mode_touchpad), + stringResource(R.string.external_display_mode_keyboard), + stringResource(R.string.external_display_mode_hybrid), + ) val winCompOpts = stringArrayResource(R.array.win_component_entries).toList() val box64Versions = stringArrayResource(R.array.box64_version_entries).toList() val wowBox64VersionsBase = stringArrayResource(R.array.wowbox64_version_entries).toList() @@ -678,6 +684,15 @@ fun ContainerConfigDialog( val index = mouseWarps.indexOfFirst { it.lowercase() == config.mouseWarpOverride } mutableIntStateOf(if (index >= 0) index else 0) } + var externalDisplayModeIndex by rememberSaveable { + val index = when (config.externalDisplayMode.lowercase()) { + Container.EXTERNAL_DISPLAY_MODE_TOUCHPAD -> 1 + Container.EXTERNAL_DISPLAY_MODE_KEYBOARD -> 2 + Container.EXTERNAL_DISPLAY_MODE_HYBRID -> 3 + else -> 0 + } + mutableIntStateOf(index) + } var languageIndex by rememberSaveable { val idx = languages.indexOfFirst { it == config.language.lowercase() } mutableIntStateOf(if (idx >= 0) idx else languages.indexOf("english")) @@ -1704,6 +1719,32 @@ fun ContainerConfigDialog( state = config.touchscreenMode, onCheckedChange = { config = config.copy(touchscreenMode = it) } ) + // External display handling + SettingsListDropdown( + colors = settingsTileColors(), + title = { Text(text = stringResource(R.string.external_display_input)) }, + subtitle = { Text(text = stringResource(R.string.external_display_input_subtitle)) }, + value = externalDisplayModeIndex, + items = externalDisplayModes, + onItemSelected = { index -> + externalDisplayModeIndex = index + config = config.copy( + externalDisplayMode = when (index) { + 1 -> Container.EXTERNAL_DISPLAY_MODE_TOUCHPAD + 2 -> Container.EXTERNAL_DISPLAY_MODE_KEYBOARD + 3 -> Container.EXTERNAL_DISPLAY_MODE_HYBRID + else -> Container.EXTERNAL_DISPLAY_MODE_OFF + }, + ) + }, + ) + SettingsSwitch( + colors = settingsTileColorsAlt(), + title = { Text(text = stringResource(R.string.external_display_swap)) }, + subtitle = { Text(text = stringResource(R.string.external_display_swap_subtitle)) }, + state = config.externalDisplaySwap, + onCheckedChange = { config = config.copy(externalDisplaySwap = it) } + ) } if (selectedTab == 4) SettingsGroup() { // TODO: add desktop settings @@ -2069,4 +2110,3 @@ private fun ExecutablePathDropdown( } } } - diff --git a/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt b/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt index 6925dba65..94223e583 100644 --- a/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt +++ b/app/src/main/java/app/gamenative/ui/screen/xserver/XServerScreen.kt @@ -2,9 +2,11 @@ package app.gamenative.ui.screen.xserver import android.app.Activity import android.content.Context +import android.graphics.Color import android.os.Build import android.util.Log import android.view.View +import android.view.ViewGroup import android.view.WindowInsets import android.view.inputmethod.InputMethodManager import android.widget.FrameLayout @@ -48,6 +50,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import app.gamenative.R +import androidx.core.content.ContextCompat import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.lifecycle.LifecycleOwner @@ -60,6 +63,9 @@ import app.gamenative.data.LibraryItem import app.gamenative.data.SteamApp import app.gamenative.events.AndroidEvent import app.gamenative.events.SteamEvent +import app.gamenative.externaldisplay.ExternalDisplayInputController +import app.gamenative.externaldisplay.ExternalDisplaySwapController +import app.gamenative.externaldisplay.SwapInputOverlayView import app.gamenative.service.SteamService import app.gamenative.service.gog.GOGService import app.gamenative.ui.component.settings.SettingsListDropdown @@ -242,6 +248,8 @@ fun XServerScreen( result } + var swapInputOverlay: SwapInputOverlayView? by remember { mutableStateOf(null) } + var win32AppWorkarounds: Win32AppWorkarounds? by remember { mutableStateOf(null) } var physicalControllerHandler: PhysicalControllerHandler? by remember { mutableStateOf(null) } @@ -504,17 +512,22 @@ fun XServerScreen( modifier = Modifier .fillMaxSize() .pointerHoverIcon(PointerIcon(0)) - .pointerInteropFilter { + .pointerInteropFilter { event -> + val overlayHandled = swapInputOverlay + ?.takeIf { it.visibility == View.VISIBLE } + ?.dispatchTouchEvent(event) == true + if (overlayHandled) return@pointerInteropFilter true + // If controls are visible, let them handle it first val controlsHandled = if (areControlsVisible) { - PluviaApp.inputControlsView?.onTouchEvent(it) ?: false + PluviaApp.inputControlsView?.onTouchEvent(event) ?: false } else { false } // If controls didn't handle it or aren't visible, send to touchMouse if (!controlsHandled) { - PluviaApp.touchpadView?.onTouchEvent(it) + PluviaApp.touchpadView?.onTouchEvent(event) } true @@ -747,9 +760,16 @@ fun XServerScreen( } } } - PluviaApp.xServerView = xServerView; + PluviaApp.xServerView = xServerView - frameLayout.addView(xServerView) + val gameHost = FrameLayout(context).apply { + layoutParams = FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ) + } + frameLayout.addView(gameHost) + gameHost.addView(xServerView) PluviaApp.inputControlsManager = InputControlsManager(context) @@ -816,6 +836,73 @@ fun XServerScreen( // Add InputControlsView on top of XServerView frameLayout.addView(icView) + val configuredExternalMode = ExternalDisplayInputController.fromConfig(container.externalDisplayMode) + val swapEnabled = container.isExternalDisplaySwap + + val overlay = SwapInputOverlayView(context, xServerView.getxServer()).apply { + visibility = View.GONE + setMode(ExternalDisplayInputController.Mode.OFF) + } + frameLayout.addView(overlay) + swapInputOverlay = overlay + + val externalDisplayController = + if (!swapEnabled && configuredExternalMode != ExternalDisplayInputController.Mode.OFF) { + ExternalDisplayInputController( + context = context, + xServer = xServerView.getxServer(), + touchpadViewProvider = { PluviaApp.touchpadView }, + ).apply { + setMode(configuredExternalMode) + start() + } + } else { + null + } + + val swapController = + if (swapEnabled) { + val surfaceBg = ContextCompat.getColor(context, R.color.external_display_surface_background) + ExternalDisplaySwapController( + context = context, + xServerViewProvider = { xServerView }, + internalGameHostProvider = { gameHost }, + onGameOnExternalChanged = { gameOnExternal -> + if (gameOnExternal) { + PluviaApp.touchpadView?.setBackgroundColor(surfaceBg) + when (configuredExternalMode) { + ExternalDisplayInputController.Mode.KEYBOARD, + ExternalDisplayInputController.Mode.HYBRID, + -> { + overlay.visibility = View.VISIBLE + overlay.setMode(configuredExternalMode) + } + else -> { + overlay.visibility = View.GONE + overlay.setMode(ExternalDisplayInputController.Mode.OFF) + } + } + } else { + PluviaApp.touchpadView?.setBackgroundColor(Color.TRANSPARENT) + overlay.visibility = View.GONE + overlay.setMode(ExternalDisplayInputController.Mode.OFF) + } + }, + ).apply { + setSwapEnabled(true) + start() + } + } else { + null + } + frameLayout.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) {} + + override fun onViewDetachedFromWindow(v: View) { + externalDisplayController?.stop() + swapController?.stop() + } + }) // Don't call hideInputControls() here - let the auto-show logic below handle visibility // so that the view gets measured/laid out and has valid dimensions for element loading diff --git a/app/src/main/java/app/gamenative/utils/ContainerUtils.kt b/app/src/main/java/app/gamenative/utils/ContainerUtils.kt index 8bf1fe715..84d95cfcf 100644 --- a/app/src/main/java/app/gamenative/utils/ContainerUtils.kt +++ b/app/src/main/java/app/gamenative/utils/ContainerUtils.kt @@ -119,8 +119,10 @@ object ContainerUtils { useDRI3 = PrefManager.useDRI3, enableXInput = PrefManager.xinputEnabled, enableDInput = PrefManager.dinputEnabled, - dinputMapperType = PrefManager.dinputMapperType.toByte(), + dinputMapperType = PrefManager.dinputMapperType.toByte(), disableMouseInput = PrefManager.disableMouseInput, + externalDisplayMode = PrefManager.externalDisplayInputMode, + externalDisplaySwap = PrefManager.externalDisplaySwap, sharpnessEffect = PrefManager.sharpnessEffect, sharpnessLevel = PrefManager.sharpnessLevel, sharpnessDenoise = PrefManager.sharpnessDenoise, @@ -158,6 +160,8 @@ object ContainerUtils { PrefManager.mouseWarpOverride = containerData.mouseWarpOverride PrefManager.useDRI3 = containerData.useDRI3 PrefManager.disableMouseInput = containerData.disableMouseInput + PrefManager.externalDisplayInputMode = containerData.externalDisplayMode + PrefManager.externalDisplaySwap = containerData.externalDisplaySwap PrefManager.containerLanguage = containerData.language PrefManager.containerVariant = containerData.containerVariant PrefManager.wineVersion = containerData.wineVersion @@ -222,6 +226,8 @@ object ContainerUtils { val disableMouse = container.isDisableMouseInput() // Read touchscreen-mode flag from container val touchscreenMode = container.isTouchscreenMode() + val externalDisplayMode = container.getExternalDisplayMode() + val externalDisplaySwap = container.isExternalDisplaySwap() return ContainerData( name = container.name, @@ -264,6 +270,8 @@ object ContainerUtils { dinputMapperType = mapperType, disableMouseInput = disableMouse, touchscreenMode = touchscreenMode, + externalDisplayMode = externalDisplayMode, + externalDisplaySwap = externalDisplaySwap, csmt = csmt, videoPciDeviceID = videoPciDeviceID, offScreenRenderingMode = offScreenRenderingMode, @@ -384,6 +392,8 @@ object ContainerUtils { container.setFEXCorePreset(containerData.fexcorePreset) container.setDisableMouseInput(containerData.disableMouseInput) container.setTouchscreenMode(containerData.touchscreenMode) + container.setExternalDisplayMode(containerData.externalDisplayMode) + container.setExternalDisplaySwap(containerData.externalDisplaySwap) container.setForceDlc(containerData.forceDlc) container.setUseLegacyDRM(containerData.useLegacyDRM) container.putExtra("sharpnessEffect", containerData.sharpnessEffect) @@ -1083,4 +1093,3 @@ object ContainerUtils { return systemKeywords.any { fileName.contains(it) } } } - diff --git a/app/src/main/java/com/winlator/container/Container.java b/app/src/main/java/com/winlator/container/Container.java index 21dc53d7d..61feb778b 100644 --- a/app/src/main/java/com/winlator/container/Container.java +++ b/app/src/main/java/com/winlator/container/Container.java @@ -26,6 +26,13 @@ public enum XrControllerMapping { THUMBSTICK_UP, THUMBSTICK_DOWN, THUMBSTICK_LEFT, THUMBSTICK_RIGHT } + // External display modes + public static final String EXTERNAL_DISPLAY_MODE_OFF = "off"; + public static final String EXTERNAL_DISPLAY_MODE_TOUCHPAD = "touchpad"; + public static final String EXTERNAL_DISPLAY_MODE_KEYBOARD = "keyboard"; + public static final String EXTERNAL_DISPLAY_MODE_HYBRID = "hybrid"; + public static final String DEFAULT_EXTERNAL_DISPLAY_MODE = EXTERNAL_DISPLAY_MODE_OFF; + public static final String DEFAULT_ENV_VARS = "WRAPPER_MAX_IMAGE_COUNT=0 ZINK_DESCRIPTORS=lazy ZINK_DEBUG=compact MESA_SHADER_CACHE_DISABLE=false MESA_SHADER_CACHE_MAX_SIZE=512MB mesa_glthread=true WINEESYNC=1 MESA_VK_WSI_PRESENT_MODE=mailbox TU_DEBUG=noconform DXVK_FRAME_RATE=60 PULSE_LATENCY_MSEC=144"; public static final String DEFAULT_SCREEN_SIZE = "1280x720"; public static final String DEFAULT_GRAPHICS_DRIVER = DefaultVersion.DEFAULT_GRAPHICS_DRIVER; @@ -112,6 +119,10 @@ public enum XrControllerMapping { private boolean disableMouseInput = false; // Touchscreen mode private boolean touchscreenMode = false; + // External display input handling + private String externalDisplayMode = DEFAULT_EXTERNAL_DISPLAY_MODE; + // Swap game/input between internal and external displays + private boolean externalDisplaySwap = false; // Prefer DRI3 WSI path private boolean useDRI3 = true; // Steam client type for selecting appropriate Box64 RC config: normal, light, ultralight @@ -646,6 +657,8 @@ public void saveData() { data.put("disableMouseInput", disableMouseInput); // Touchscreen mode flag data.put("touchscreenMode", touchscreenMode); + data.put("externalDisplayMode", externalDisplayMode); + data.put("externalDisplaySwap", externalDisplaySwap); data.put("useDRI3", useDRI3); data.put("installPath", installPath); data.put("steamType", steamType); @@ -816,6 +829,12 @@ public void loadData(JSONObject data) throws JSONException { case "touchscreenMode" : setTouchscreenMode(data.getBoolean(key)); break; + case "externalDisplayMode" : + setExternalDisplayMode(data.getString(key)); + break; + case "externalDisplaySwap" : + setExternalDisplaySwap(data.getBoolean(key)); + break; case "useDRI3" : setUseDRI3(data.getBoolean(key)); break; @@ -943,6 +962,23 @@ public void setTouchscreenMode(boolean touchscreenMode) { this.touchscreenMode = touchscreenMode; } + // External display mode + public String getExternalDisplayMode() { + return externalDisplayMode != null ? externalDisplayMode : DEFAULT_EXTERNAL_DISPLAY_MODE; + } + + public void setExternalDisplayMode(String externalDisplayMode) { + this.externalDisplayMode = externalDisplayMode != null ? externalDisplayMode : DEFAULT_EXTERNAL_DISPLAY_MODE; + } + + public boolean isExternalDisplaySwap() { + return externalDisplaySwap; + } + + public void setExternalDisplaySwap(boolean externalDisplaySwap) { + this.externalDisplaySwap = externalDisplaySwap; + } + // Use DRI3 WSI public boolean isUseDRI3() { return useDRI3; diff --git a/app/src/main/java/com/winlator/container/ContainerData.kt b/app/src/main/java/com/winlator/container/ContainerData.kt index ef0f2e9df..6e3c35b57 100644 --- a/app/src/main/java/com/winlator/container/ContainerData.kt +++ b/app/src/main/java/com/winlator/container/ContainerData.kt @@ -72,6 +72,10 @@ data class ContainerData( val disableMouseInput: Boolean = false, /** Touchscreen mode **/ val touchscreenMode: Boolean = false, + /** External display input handling: off|touchpad|keyboard|hybrid **/ + val externalDisplayMode: String = Container.DEFAULT_EXTERNAL_DISPLAY_MODE, + /** Swap game/input between internal and external displays **/ + val externalDisplaySwap: Boolean = false, /** Preferred game language (Goldberg) **/ val language: String = "english", val forceDlc: Boolean = false, @@ -125,6 +129,8 @@ data class ContainerData( "dinputMapperType" to state.dinputMapperType, "disableMouseInput" to state.disableMouseInput, "touchscreenMode" to state.touchscreenMode, + "externalDisplayMode" to state.externalDisplayMode, + "externalDisplaySwap" to state.externalDisplaySwap, "useDRI3" to state.useDRI3, "language" to state.language, "forceDlc" to state.forceDlc, @@ -177,6 +183,8 @@ data class ContainerData( dinputMapperType = savedMap["dinputMapperType"] as Byte, disableMouseInput = savedMap["disableMouseInput"] as Boolean, touchscreenMode = savedMap["touchscreenMode"] as Boolean, + externalDisplayMode = (savedMap["externalDisplayMode"] as? String) ?: Container.DEFAULT_EXTERNAL_DISPLAY_MODE, + externalDisplaySwap = (savedMap["externalDisplaySwap"] as? Boolean) ?: false, useDRI3 = (savedMap["useDRI3"] as? Boolean) ?: true, language = (savedMap["language"] as? String) ?: "english", forceDlc = (savedMap["forceDlc"] as? Boolean) ?: false, diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 1fca92946..c7d273905 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -355,6 +355,14 @@ Deaktivér musinput Touchskærmstilstand Direkte touch-til-cursor-bevægelse (TIL) vs touchpad-stil relativ bevægelse (FRA) + Ekstern skærm-input + Vælg hvordan en tilsluttet præsentationsskærm skal opføre sig + Brug ikke ekstern skærm + Brug som touchpad-overflade + Brug som fuldt tastatur + Hybrid (touchpad + tastaturknap) + Vis spil på ekstern skærm + Byt skærme, så spillet gengives på den eksterne skærm og denne enhed bliver controlleroverfladen Start med on-screen-kontroller skjult On-screen-kontroller vil være skjult når spillet starter. Skift via navigationsmenuen. Emulér tastatur og mus diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 8d211c403..de26c88b3 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -490,6 +490,14 @@ Mauseingabe deaktivieren Touchscreen-Modus Direkte Zeigerbewegung (AN) vs. Touchpad-ähnliche relative Bewegung (AUS) + Externe Bildschirm-Eingabe + Wähle, wie ein angeschlossener Präsentationsbildschirm sich verhalten soll + Externen Bildschirm nicht verwenden + Als Touchpad-Oberfläche verwenden + Als vollständige Tastatur verwenden + Hybrid (Touchpad + Tastaturtaste) + Spiel auf externem Bildschirm anzeigen + Bildschirme tauschen, damit das Spiel auf dem externen Bildschirm gerendert wird und dieses Gerät zur Controller-Oberfläche wird Mit versteckten On-Screen-Controls starten On-Screen-Controls sind beim Spielstart ausgeblendet. Über das Menü ein-/ausblendbar. Tastatur und Maus emulieren diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 43114ad95..381985145 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -518,6 +518,14 @@ Désactiver l\'entrée souris Mode écran tactile Mouvement tactile direct vers le curseur (ON) vs mouvement relatif style pavé tactile (OFF) + Entrée d\'écran externe + Choisissez comment un écran de présentation connecté doit se comporter + Ne pas utiliser l\'écran externe + Utiliser comme surface de pavé tactile + Utiliser comme clavier complet + Hybride (pavé tactile + bouton clavier) + Afficher le jeu sur l\'écran externe + Permuter les écrans pour que le jeu s\'affiche sur l\'écran externe et que cet appareil devienne la surface de contrôle Démarrer avec les contrôles à l\'écran masqués Les contrôles à l\'écran seront masqués au démarrage du jeu. Basculez via le menu de navigation. Émuler clavier et souris diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 0cf9c8116..f3cb5da66 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -355,6 +355,14 @@ Desabilitar entrada do mouse Modo Touchscreen Movimento direto de toque para cursor (LIGADO) vs movimento relativo estilo touchpad (DESLIGADO) + Entrada de exibição externa + Escolha como uma tela de apresentação conectada deve se comportar + Não usar exibição externa + Usar como superfície de touchpad + Usar como teclado completo + Híbrido (touchpad + botão de teclado) + Mostrar o jogo na exibição externa + Trocar as telas para que o jogo seja renderizado na exibição externa e este dispositivo se torne a superfície de controle Iniciar com Controles On-screen Ocultos Controles on-screen estarão ocultos quando o jogo iniciar. Alterne via o menu de navegação. Emular teclado e mouse diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 6c7416ea8..0c40d3890 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -523,6 +523,14 @@ Dezactivează input-ul de mouse Mod touchscreen Mișcare directă touch-la-cursor (ON) vs mișcare relativă tip touchpad (OFF) + Intrare afișaj extern + Alege cum ar trebui să se comporte un afișaj de prezentare conectat + Nu utiliza afișaj extern + Folosește ca suprafață de touchpad + Folosește ca tastatură completă + Hibrid (touchpad + buton tastatură) + Afișează jocul pe afișajul extern + Schimbă ecranele astfel încât jocul să fie redat pe afișajul extern și acest dispozitiv să devină suprafața de control Pornește cu controalele pe ecran ascunse Controalele pe ecran vor fi ascunse la pornirea jocului. Le poți comuta din meniul de navigare. Emulează tastatura și mouse-ul diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index d56e51ba4..2f94932d1 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -524,6 +524,14 @@ Пряме переміщення курсора (УВІМК) або відносне переміщення, як на тачпаді (ВИМК) Запускати з прихованими елементами керування Екранні елементи будуть приховані під час запуску гри. Перемикайте через меню навігації. + Ввід зовнішнього дисплея + Виберіть, як має працювати підключений презентаційний дисплей + Не використовувати зовнішній дисплей + Використовувати як поверхню тачпада + Використовувати як повну клавіатуру + Гібрид (тачпад + кнопка клавіатури) + Показувати гру на зовнішньому дисплеї + Поміняти екрани місцями, щоб гра відображалася на зовнішньому дисплеї, а цей пристрій став поверхнею керування Емуляція клавіатури та миші Лівий стік = WASD, Правий стік = Миша. L2 = ЛКМ, R2 = ПКМ. @@ -876,7 +884,7 @@ Вміст успішно інстальовано Не вдалося інсталювати вміст Помилка інсталяції: %1$s - + Готова diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index d5c295e2a..997f046bd 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -514,6 +514,14 @@ 禁用鼠标输入 触屏模式 直接触控到光标移动(开启) vs 触摸板式相对移动(关闭) + 外接显示器输入 + 选择已连接的演示显示器的行为方式 + 不使用外接显示器 + 用作触摸板表面 + 用作完整键盘 + 混合(触摸板 + 键盘按钮) + 在外接显示器上显示游戏 + 交换屏幕,使游戏在外接显示器上渲染,此设备成为控制表面 开始时隐藏屏幕控制器 游戏开始时屏幕控制器将被隐藏。可通过导航菜单切换。 模拟键盘与鼠标 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 483e7aaf5..affc060c0 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -517,6 +517,14 @@ 停用滑鼠輸入 觸屏模式 直接觸控到游標移動 (開啟) vs 觸控板式相對移動 (關閉) + 外接顯示器輸入 + 選擇已連接的簡報顯示器應如何運作 + 不使用外接顯示器 + 作為觸控板表面 + 作為完整鍵盤 + 混合(觸控板 + 鍵盤按鈕) + 在外接顯示器上顯示遊戲 + 交換螢幕,讓遊戲在外接顯示器上渲染,此裝置成為控制表面 開始時隱藏螢幕控制器 遊戲開始時螢幕控制器將被隱藏。可透過導航選單切換。 模擬鍵盤與滑鼠 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 9831c9b71..07c06509b 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -5,4 +5,9 @@ #455a64 #06B6D4 #FAFAFA + #2B2B2B + #1F1F1F + #3A3A3A + #3D6CC4 + #2E5AAC diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7f81d67e5..2aacf06b5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -522,6 +522,14 @@ Disable Mouse Input Touchscreen Mode Direct touch-to-cursor movement (ON) vs touchpad-style relative movement (OFF) + External Display Input + Choose how a connected presentation display should behave + Do not use external display + Use as touchpad surface + Use as full keyboard + Hybrid (touchpad + keyboard button) + Show Game on External Display + Swap screens so the game renders on the external display and this device becomes the controller surface Start With On-Screen Controls Hidden On-screen controls will be hidden when the game starts. Toggle via the navigation menu. Emulate keyboard and mouse @@ -970,4 +978,3 @@ Failed to logout: %s Logging out from GOG… -