Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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 "<PC> 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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
65 changes: 52 additions & 13 deletions app/src/main/java/com/limelight/Game.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1603,6 +1595,8 @@ public void openQuickMenu() {
java.util.List<com.limelight.home.QuickMenu.Item> 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)
Expand All @@ -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;
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
13 changes: 11 additions & 2 deletions app/src/main/java/com/limelight/home/QuickMenu.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
})
Expand Down Expand Up @@ -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) {
Expand Down
6 changes: 1 addition & 5 deletions app/src/main/res/xml/preferences.xml
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,7 @@
</PreferenceCategory>
<PreferenceCategory android:title="@string/category_input_settings"
android:key="category_input_settings">
<CheckBoxPreference
android:key="checkbox_touchscreen_trackpad"
android:title="@string/title_checkbox_touchscreen_trackpad"
android:summary="@string/summary_checkbox_touchscreen_trackpad"
android:defaultValue="true" />
<!-- MoonCast: "touchscreen as a trackpad" is the in-game Quick menu's Touch screen (Off, Mouse, Touch) now -->
<CheckBoxPreference
android:key="checkbox_mouse_nav_buttons"
android:title="@string/title_checkbox_mouse_nav_buttons"
Expand Down
Loading