diff --git a/CHANGELOG.md b/CHANGELOG.md index 6067cc50f5..6d30e9fa83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,21 @@ # Changelog +## 1.0-test243 (2026-10-22) +- Touch screen (Quick menu, PC), found by Saleh on test242: + - The PC's pointer stayed in the picture with touch Off. With Off, MoonCast now moves it to the middle of the right edge, where it's off the screen, when the stream starts and when you switch to Off. (Not a corner: Windows' bottom-right corner is Show desktop.) + - New: Settings, Handheld, **Touch screen** (Off, Mouse, Touch, default Off) sets how each PC stream starts. The Quick menu still changes it for the game you're in. +- Quick menu: with more items than fit (the PC menu now has up to 9), the last ones were out of reach with a controller, so Hibernate PC looked gone. The list now scrolls with the selection, on the handheld and the TV (the TV's list didn't scroll at all). + ## 1.0-test242 (2026-10-22) - Hibernate PC worked on Ahmed's PC (test240/241), but MoonCast stayed on the last picture, frozen: a hibernating PC just stops sending video, and Moonlight only notices after a long timeout. After Hibernate PC, MoonCast now watches the video: after 3 seconds without a new frame it checks whether the PC still answers (its http port), and if not it leaves the stream with " is hibernating". The game isn't quit; it stays as it was in the hibernated PC. +## 1.0-test241 (2026-10-22) +- In-game Quick menu, PC: **Touch screen** (Off, Mouse, Touch), for the stream it's in. Every stream starts Off, so a hand on a handheld's screen can't click on the PC. + - Mouse: the screen is a trackpad (Moonlight's relative touch: drag moves the pointer, tap clicks, two fingers right-click and scroll). + - Touch: the pointer goes where the finger is and a tap clicks there (Moonlight's absolute touch). + - A stylus and a real mouse work in every mode. The on-screen controller isn't affected. +- Moonlight's "Use the touchscreen as a trackpad" (All settings) is gone; the Quick menu's switch replaces it. + ## 1.0-test240 (2026-10-22) - Hibernate PC still didn't work on Ahmed's PC: the Run box never opened, so Win + R didn't arrive (games often turn the Windows key off while they play). It now opens Start with Ctrl + Esc, which doesn't use the Windows key, types `shutdown /h` into Start's search and presses Enter 2 seconds later. diff --git a/app/src/handheld/java/com/limelight/handheld/HandheldHomeActivity.kt b/app/src/handheld/java/com/limelight/handheld/HandheldHomeActivity.kt index 5220942c51..4282d270b5 100644 --- a/app/src/handheld/java/com/limelight/handheld/HandheldHomeActivity.kt +++ b/app/src/handheld/java/com/limelight/handheld/HandheldHomeActivity.kt @@ -1202,6 +1202,9 @@ class HandheldHomeActivity : Activity() { choice(list, prefs, "hh_screen_size", "Screen size", "Auto fits this screen. Compact and Large make everything bigger", listOf("Auto" to "auto", "Compact" to "compact", "Normal" to "normal", "Large" to "large"), "auto") { recreate() } appToggle(list, "hh_rotate", "Screen turns with the device", "Portrait when you hold it upright", true) { applyRotation() } + // PC games: what a finger on the picture does when a game starts (the Quick menu changes it for that game) + choice(list, stream, com.limelight.Game.TOUCH_DEFAULT_PREF, "Touch screen", "In PC games. Mouse is a trackpad, Touch clicks where you tap. The Quick menu changes it while you play", + listOf("Off" to "off", "Mouse" to "mouse", "Touch" to "touch"), "off") choice(list, prefs, com.limelight.home.BatteryCap.PREF, "On battery, stream at", "Unplugged, PC and PS5 streams go no higher than this. Plugged in, the Picture settings apply", listOf("Same" to "same", "1080p" to "1080", "720p" to "720", "540p" to "540"), "same") appToggle(list, "hh_dim_on_tv", "Dim the screen on a TV", "While this ${deviceShort()} is the controller for a TV", true) diff --git a/app/src/main/java/com/limelight/Game.java b/app/src/main/java/com/limelight/Game.java index a75c25f74e..3a8c1e7f44 100644 --- a/app/src/main/java/com/limelight/Game.java +++ b/app/src/main/java/com/limelight/Game.java @@ -527,17 +527,9 @@ public void notifyCrash(Exception e) { InputManager inputManager = (InputManager) getSystemService(Context.INPUT_SERVICE); inputManager.registerInputDeviceListener(keyboardTranslator, null); - // Initialize touch contexts - for (int i = 0; i < touchContextMap.length; i++) { - if (!prefConfig.touchscreenTrackpad) { - touchContextMap[i] = new AbsoluteTouchContext(conn, i, streamView); - } - else { - touchContextMap[i] = new RelativeTouchContext(conn, i, - REFERENCE_HORIZ_RES, REFERENCE_VERT_RES, - streamView, prefConfig); - } - } + // Touch screen starts as Settings, Handheld, Touch screen say (Off unless changed there); the Quick menu + // switches it for this stream + setTouchMode(defaultTouchMode()); if (prefConfig.onscreenController) { // create virtual onscreen controller @@ -1603,6 +1595,8 @@ public void openQuickMenu() { java.util.List items = new java.util.ArrayList<>(); items.add(new com.limelight.home.QuickMenu.Item("⌨", "Keyboard", "On-screen", false, this::toggleKeyboard).asToggle()); items.add(new com.limelight.home.QuickMenu.Item("ⓘ", "Streaming status", prefConfig.enablePerfOverlay ? "On" : "Off", false, this::toggleStatus).asToggle()); + items.add(new com.limelight.home.QuickMenu.Item("☝", "Touch screen", TOUCH_LABELS[touchMode], false, + () -> setTouchMode((touchMode + 1) % TOUCH_LABELS.length)).asToggle()); items.add(new com.limelight.home.QuickMenu.Item("✚", "Controllers", com.limelight.home.QuickMenu.pads().size() + " connected", false, this::showControllers)); items.add(new com.limelight.home.QuickMenu.Item("▤", "Task Manager", "Windows: Ctrl + Shift + Esc", false, this::openTaskManager)); // handheld only: send the game to the last MoonCast TV (it ends this stream, the game keeps running) @@ -1625,6 +1619,44 @@ public void openQuickMenu() { }); } + // Quick menu, Touch screen: what a finger on the picture does, for this stream only (every stream starts Off, + // so a hand on a handheld's screen can't click on the PC). Mouse is Moonlight's trackpad, Touch puts the pointer + // where the finger is. A stylus and a real mouse work in every mode. + private static final int TOUCH_OFF = 0, TOUCH_MOUSE = 1, TOUCH_DIRECT = 2; + private static final String[] TOUCH_LABELS = {"Off", "Mouse", "Touch"}; + /** Settings, Handheld, Touch screen: how each stream starts ("off", "mouse" or "touch"). */ + public static final String TOUCH_DEFAULT_PREF = "mc_touch_default"; + private int touchMode = TOUCH_OFF; + + private int defaultTouchMode() { + String v = android.preference.PreferenceManager.getDefaultSharedPreferences(this).getString(TOUCH_DEFAULT_PREF, "off"); + return "mouse".equals(v) ? TOUCH_MOUSE : "touch".equals(v) ? TOUCH_DIRECT : TOUCH_OFF; + } + + /** + * Touch screen Off: the PC's pointer is drawn into the picture, so move it to the middle of the right edge, where + * the arrow (drawn right and down from its tip) is off the screen. Not a corner: Windows' bottom-right corner is + * Show desktop. + */ + private void parkPointer() { + if (conn == null || !connected) return; + conn.sendMousePosition((short) (REFERENCE_HORIZ_RES - 1), (short) (REFERENCE_VERT_RES / 2), + (short) REFERENCE_HORIZ_RES, (short) REFERENCE_VERT_RES); + } + + private void setTouchMode(int mode) { + for (TouchContext c : touchContextMap) { + if (c != null) c.cancelTouch(); + } + touchMode = mode; + for (int i = 0; i < touchContextMap.length; i++) { + touchContextMap[i] = mode == TOUCH_DIRECT ? new AbsoluteTouchContext(conn, i, streamView) + : new RelativeTouchContext(conn, i, REFERENCE_HORIZ_RES, REFERENCE_VERT_RES, streamView, prefConfig); + } + LimeLog.info("Touch screen: " + TOUCH_LABELS[mode]); + if (mode == TOUCH_OFF) parkPointer(); + } + // Quick menu, Streaming status: Moonlight's stats corner on or off right away, saved like Settings' "Stats while playing" private void toggleStatus() { prefConfig.enablePerfOverlay = !prefConfig.enablePerfOverlay; @@ -2303,10 +2335,15 @@ else if (event.getActionMasked() == MotionEvent.ACTION_UP || event.getActionMask return true; } + // Touch screen Off (the Quick menu's default): a finger on the picture does nothing on the PC + if (touchMode == TOUCH_OFF) { + return true; + } + // If this is the parent view, we'll offset our coordinates to appear as if they // are relative to the StreamView like our StreamView touch events are. float xOffset, yOffset; - if (view != streamView && !prefConfig.touchscreenTrackpad) { + if (view != streamView && touchMode == TOUCH_DIRECT) { xOffset = -streamView.getX(); yOffset = -streamView.getY(); } @@ -2338,7 +2375,7 @@ else if (event.getActionMasked() == MotionEvent.ACTION_UP || event.getActionMask // TODO: Re-enable native touch when have a better solution for handling // cancelled touches from Android gestures and 3 finger taps to activate // the software keyboard. - /*if (!prefConfig.touchscreenTrackpad && trySendTouchEvent(view, event)) { + /*if (touchMode == TOUCH_DIRECT && trySendTouchEvent(view, event)) { // If this host supports touch events and absolute touch is enabled, // send it directly as a touch event. return true; @@ -2762,6 +2799,8 @@ public void run() { connected = true; connecting = false; updatePipAutoEnter(); + // Touch screen Off: the PC's pointer out of the way once the picture is up + if (touchMode == TOUCH_OFF) new Handler(getMainLooper()).postDelayed(Game.this::parkPointer, 1500); // The phone drives this stream as the controller (native receiver flow): feed its pad into our conn. PhoneGamepad.start(conn, null); diff --git a/app/src/main/java/com/limelight/home/QuickMenu.kt b/app/src/main/java/com/limelight/home/QuickMenu.kt index 351a8925d5..2948ccb29a 100644 --- a/app/src/main/java/com/limelight/home/QuickMenu.kt +++ b/app/src/main/java/com/limelight/home/QuickMenu.kt @@ -93,8 +93,9 @@ class QuickMenu(private val act: Activity) { list.addView(this, tv.lp(-1, 80)) } } - panel.addView(list, tv.lp(-1, -2).apply { topMargin = tv.px(16) }) - panel.addView(View(act), LinearLayout.LayoutParams(-1, 0, 1f)) + // the list takes the room between the stats and the hints, and scrolls when the items don't fit + val scroll = android.widget.ScrollView(act).apply { isVerticalScrollBarEnabled = false; addView(list) } + panel.addView(scroll, LinearLayout.LayoutParams(-1, 0, 1f).apply { topMargin = tv.px(16) }) panel.addView(tv.row().apply { addView(tv.hint("A", "Select")); addView(tv.hint("B", "Back to the game")) }) @@ -276,6 +277,14 @@ class QuickMenu(private val act: Activity) { rows.forEachIndexed { i, v -> v.background = if (switchStyle) switchRow(i == sel) else if (i == sel) tv.round(tv.p.raise, 16, Tv.CORONA, 3) else null } + // keep the selected row in view: with more items than fit, the last ones were out of reach (test242) + val row = rows.getOrNull(sel) ?: return + val sc = row.parent?.parent as? android.widget.ScrollView ?: return + sc.post { + val gap = row.height / 2 + if (row.top - gap < sc.scrollY) sc.smoothScrollTo(0, maxOf(0, row.top - gap)) + else if (row.bottom + gap > sc.scrollY + sc.height) sc.smoothScrollTo(0, row.bottom + gap - sc.height) + } } private fun move(d: Int) { diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 5b09082b35..1b7dbee17a 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -129,11 +129,7 @@ - +