diff --git a/main.lua b/main.lua index c33d63d4..8d3a368c 100644 --- a/main.lua +++ b/main.lua @@ -476,10 +476,14 @@ function love.mousepressed(x, y, button, istouch) -- meant two choose() calls and two stacked SAF picker activities: the -- player picked their ROM, the top picker closed, and the second was still -- underneath asking for it again, which is the "import the file twice" - -- in #553. Filtering on istouch rather than on the OS keeps a real mouse - -- (DeX, a Chromebook, a USB mouse) working, which an Android-wide return - -- would have broken. - if istouch then return end + -- in #553. Filtering on istouch keeps a real mouse (DeX, a Chromebook, a + -- USB mouse) working, which an Android-wide return would have broken. + -- + -- ANDROID ONLY, and the OS test is load bearing: love.touchpressed above + -- returns early on iOS and never forwards, so there the synthesized mouse + -- press is the ONLY event the launcher gets. Filtering istouch on both + -- killed every tap on iOS outright. + if istouch and love.system.getOS() == "Android" then return end return Importer:mousepressed(x, y, button) end if editorMode and EditorApp.mousepressed then diff --git a/src/import/RomImporter.lua b/src/import/RomImporter.lua index eea9a618..65f37419 100644 --- a/src/import/RomImporter.lua +++ b/src/import/RomImporter.lua @@ -1017,7 +1017,6 @@ function RomImporter:chooseMod() else self.pickPending = true self.pickTimer = 0 - self.pickElapsed = 0 end return end @@ -1082,7 +1081,6 @@ function RomImporter:chooseSaveImport(version) else self.pickPending = true self.pickTimer = 0 - self.pickElapsed = 0 end return end @@ -1121,7 +1119,6 @@ function RomImporter:exportSave(version) if love.system.createFile and love.system.createFile(suggested) then self.pickPending = true self.pickTimer = 0 - self.pickElapsed = 0 self.saveNotice[version] = { ok = true, text = "Pick where to save " .. suggested .. "..." } else @@ -1177,7 +1174,6 @@ function RomImporter:choose(version) else self.pickPending = true self.pickTimer = 0 - self.pickElapsed = 0 end return end @@ -1225,18 +1221,18 @@ end -- for the next tap to find, which is what made users import twice and -- what made it look random: it depends on memory pressure (#553). -- --- Disarms after PICK_TIMEOUT so a cancelled picker (which delivers nothing, --- ever) does not leave this scanning the save directory for the whole session. -local PICK_TIMEOUT = 120 +-- Deliberately NO timeout. A version of this disarmed the poll after 120s so a +-- cancelled picker would stop scanning, which was wrong on iOS: the picker there +-- is an in-process modal sheet, so update() keeps running while it is open and +-- the window burned down while the player was still browsing Files. The pick +-- then landed with nothing armed to consume it, and because every path here is +-- silent on success the import just did not happen, with no error shown. A +-- half-second directory listing on a menu screen is far cheaper than an import +-- that vanishes, so the poll stays armed until something is actually consumed. function RomImporter:_pollPickedFiles(dt) if not self.pickPending then return end if self.workState == "working" then return end - self.pickElapsed = (self.pickElapsed or 0) + dt - if self.pickElapsed > PICK_TIMEOUT then - self.pickPending, self.pickElapsed = nil, nil - return - end self.pickTimer = (self.pickTimer or 0) + dt if self.pickTimer < 0.5 then return end self.pickTimer = 0 @@ -1246,7 +1242,7 @@ function RomImporter:_pollPickedFiles(dt) local pickError = love.filesystem.read("pick_error.txt") if pickError then love.filesystem.remove("pick_error.txt") - self.pickPending, self.pickElapsed = nil, nil + self.pickPending = nil self.modNotice = { ok = false, text = pickError } self.notice = { version = self.chooseVersion or "red", status = "File import failed:", detail = pickError } @@ -1263,7 +1259,7 @@ function RomImporter:_pollPickedFiles(dt) end end if found then - self.pickPending, self.pickElapsed = nil, nil + self.pickPending = nil self:focus(true) end end diff --git a/tests/rom_importer_double_pick_test.lua b/tests/rom_importer_double_pick_test.lua index 3e1efe54..97b2fca0 100644 --- a/tests/rom_importer_double_pick_test.lua +++ b/tests/rom_importer_double_pick_test.lua @@ -96,9 +96,13 @@ local fired = false ri3.focus = function(self, f) if f then fired = true end end ri3:_pollPickedFiles(0.6) check(not fired, "an empty save dir consumes nothing") -check(ri3.pickPending, "and stays armed while it is still within the timeout") -ri3:_pollPickedFiles(200) -check(not ri3.pickPending, "a cancelled pick disarms instead of polling forever") +check(ri3.pickPending, "and stays armed, waiting for the pick to land") +-- No timeout on purpose: a 120s disarm silently dropped iOS imports, because the +-- picker there is an in-process sheet and update() keeps running while the +-- player browses Files. Staying armed costs a directory listing; disarming cost +-- the import, with no error shown. +ri3:_pollPickedFiles(600) +check(ri3.pickPending, "and is still armed after a long browse in the picker") -- 5. A pick that is still importing must not be double-started. saveDir = { ["picked_mod.zip"] = "PK\003\004" } @@ -131,6 +135,21 @@ contract:choose("red") check(picks == 2, "choose() still reopens the picker per call (#420/#442 contract, got " .. picks .. ")") +-- 7. iOS taps must survive the Android double-fire guard. love.touchpressed in +-- main.lua returns early on iOS and never forwards, so the synthesized +-- love.mousepressed is the ONLY event the launcher gets there. Filtering +-- istouch on both platforms killed every tap on iOS. The guard is Android +-- only, and this pins the asymmetry the guard depends on. +local touchForwardsToImporter = { + Android = true, -- love.touchpressed -> Importer:mousepressed + iOS = false, -- returns early; mousepressed(istouch=true) is the only path +} +for os, forwards in pairs(touchForwardsToImporter) do + local dropSynthesized = (os == "Android") + check(dropSynthesized == forwards, + os .. ": the synthesized mouse press is dropped only where touch already forwarded") +end + love.system.getOS = saved.getOS love.system.pickFile = saved.pickFile love.filesystem.getDirectoryItems = saved.getDirectoryItems