mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-19 20:20:19 +02:00
updated skin studio
This commit is contained in:
+11
-4
@@ -6,6 +6,7 @@ control layout, and the rectangle the Game Boy screen is drawn into. Engine:
|
|||||||
(draw and input), `src/render/Renderer.lua` (the screen viewport),
|
(draw and input), `src/render/Renderer.lua` (the screen viewport),
|
||||||
`src/ui/SkinStudio.lua` (the desktop editor). Tests:
|
`src/ui/SkinStudio.lua` (the desktop editor). Tests:
|
||||||
`tests/engine/touch_skin_test.lua`, `tests/engine/skin_studio_test.lua`,
|
`tests/engine/touch_skin_test.lua`, `tests/engine/skin_studio_test.lua`,
|
||||||
|
`tests/engine/skin_studio_image_import.lua`,
|
||||||
`tests/engine/launcher_skins_tab.lua`.
|
`tests/engine/launcher_skins_tab.lua`.
|
||||||
|
|
||||||
Skins are picked in the launcher's **Skins** tab, which also imports them and
|
Skins are picked in the launcher's **Skins** tab, which also imports them and
|
||||||
@@ -159,8 +160,16 @@ The Super Game Boy preset locks the viewport to the real screen window,
|
|||||||
resize. X / Y / W / H are in canvas pixels, so a control can be typed to the
|
resize. X / Y / W / H are in canvas pixels, so a control can be typed to the
|
||||||
coordinate its art was drawn at. Bind, hitbox shape, hit reach and idle and
|
coordinate its art was drawn at. Bind, hitbox shape, hit reach and idle and
|
||||||
pressed images are per control; the bezel, the pages and the screen cutout are
|
pressed images are per control; the bezel, the pages and the screen cutout are
|
||||||
per page. The cutout is itself a draggable element with a 10:9 lock. Drop a PNG
|
per page. The cutout is itself a draggable element with a 10:9 lock.
|
||||||
or JPG on the window to import art into the skin.
|
|
||||||
|
**Art.** The **Bezel**, **Idle art** and **Pressed art** rows cycle through the
|
||||||
|
images already in the skin folder; the **Import** button beside each one opens
|
||||||
|
the host file picker (`src/core/FilePicker.lua`: osascript, PowerShell,
|
||||||
|
zenity/kdialog) and copies the chosen PNG or JPG into `img/` under the name in
|
||||||
|
the SKIN field, then assigns it to that slot. Dropping a PNG or JPG on the
|
||||||
|
window does the same for whichever slot was last touched. A new bezel does not
|
||||||
|
move the screen cutout: press **Detect screen from bezel** to measure it out of
|
||||||
|
the art's alpha.
|
||||||
|
|
||||||
**Testing.** **Test** makes the canvas live: clicking presses real Game Boy
|
**Testing.** **Test** makes the canvas live: clicking presses real Game Boy
|
||||||
buttons and the footer reports what is held. **Play** saves the skin, selects
|
buttons and the footer reports what is held. **Play** saves the skin, selects
|
||||||
@@ -175,5 +184,3 @@ straight back into `skins/` and still opens in RetroArch.
|
|||||||
## Not implemented
|
## Not implemented
|
||||||
|
|
||||||
RetroArch's `analog_*`, `dpad_area`, `abxy_area` and `retrok_*` desc types.
|
RetroArch's `analog_*`, `dpad_area`, `abxy_area` and `retrok_*` desc types.
|
||||||
Image assignment cycles through art already in the skin folder; there is no
|
|
||||||
file browser, so new art arrives by drag and drop.
|
|
||||||
|
|||||||
@@ -506,6 +506,7 @@ function love.update(dt)
|
|||||||
if TouchEditor then return TouchEditor.update(dt) end
|
if TouchEditor then return TouchEditor.update(dt) end
|
||||||
if Studio then return Studio.update(dt) end
|
if Studio then return Studio.update(dt) end
|
||||||
if Importer then return Importer:update(dt) end
|
if Importer then return Importer:update(dt) end
|
||||||
|
if not Game then return end
|
||||||
|
|
||||||
-- Scripted runs (autopilot / POKEPORT_DRIVER) observe and act exactly
|
-- Scripted runs (autopilot / POKEPORT_DRIVER) observe and act exactly
|
||||||
-- once per Game:update, so they must keep a 1:1 relationship with the
|
-- once per Game:update, so they must keep a 1:1 relationship with the
|
||||||
@@ -602,12 +603,14 @@ function love.keypressed(key, scancode, isrepeat)
|
|||||||
if TouchEditor then return TouchEditor.keypressed(key) end
|
if TouchEditor then return TouchEditor.keypressed(key) end
|
||||||
if Studio then return Studio.keypressed(key) end
|
if Studio then return Studio.keypressed(key) end
|
||||||
if Importer then return Importer:keypressed(key) end
|
if Importer then return Importer:keypressed(key) end
|
||||||
|
if not Game then return end
|
||||||
Game:keypressed(key)
|
Game:keypressed(key)
|
||||||
end
|
end
|
||||||
|
|
||||||
function love.keyreleased(key)
|
function love.keyreleased(key)
|
||||||
if editorMode or TouchEditor or Studio then return end
|
if editorMode or TouchEditor or Studio then return end
|
||||||
if Importer then return end
|
if Importer then return end
|
||||||
|
if not Game then return end
|
||||||
Game:keyreleased(key)
|
Game:keyreleased(key)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -625,7 +628,9 @@ function love.gamepadpressed(joystick, button)
|
|||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if Studio then return end
|
||||||
if Importer then return Importer:gamepadpressed(joystick, button) end
|
if Importer then return Importer:gamepadpressed(joystick, button) end
|
||||||
|
if not Game then return end
|
||||||
Game:gamepadpressed(joystick, button)
|
Game:gamepadpressed(joystick, button)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -643,7 +648,9 @@ function love.gamepadreleased(joystick, button)
|
|||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if Studio then return end
|
||||||
if Importer then return Importer:gamepadreleased(joystick, button) end
|
if Importer then return Importer:gamepadreleased(joystick, button) end
|
||||||
|
if not Game then return end
|
||||||
Game:gamepadreleased(joystick, button)
|
Game:gamepadreleased(joystick, button)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -661,7 +668,9 @@ function love.gamepadaxis(joystick, axis, value)
|
|||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if Studio then return end
|
||||||
if Importer then return Importer:gamepadaxis(joystick, axis, value) end
|
if Importer then return Importer:gamepadaxis(joystick, axis, value) end
|
||||||
|
if not Game then return end
|
||||||
Game:gamepadaxis(joystick, axis, value)
|
Game:gamepadaxis(joystick, axis, value)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -679,7 +688,9 @@ function love.joystickpressed(joystick, button)
|
|||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if Studio then return end
|
||||||
if Importer then return Importer:joystickpressed(joystick, button) end
|
if Importer then return Importer:joystickpressed(joystick, button) end
|
||||||
|
if not Game then return end
|
||||||
Game:joystickpressed(joystick, button)
|
Game:joystickpressed(joystick, button)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -697,7 +708,9 @@ function love.joystickreleased(joystick, button)
|
|||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if Studio then return end
|
||||||
if Importer then return Importer:joystickreleased(joystick, button) end
|
if Importer then return Importer:joystickreleased(joystick, button) end
|
||||||
|
if not Game then return end
|
||||||
Game:joystickreleased(joystick, button)
|
Game:joystickreleased(joystick, button)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -715,7 +728,9 @@ function love.joystickaxis(joystick, axis, value)
|
|||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if Studio then return end
|
||||||
if Importer then return Importer:joystickaxis(joystick, axis, value) end
|
if Importer then return Importer:joystickaxis(joystick, axis, value) end
|
||||||
|
if not Game then return end
|
||||||
Game:joystickaxis(joystick, axis, value)
|
Game:joystickaxis(joystick, axis, value)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -733,21 +748,25 @@ function love.joystickhat(joystick, hat, direction)
|
|||||||
end
|
end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if Studio then return end
|
||||||
if Importer then return Importer:joystickhat(joystick, hat, direction) end
|
if Importer then return Importer:joystickhat(joystick, hat, direction) end
|
||||||
|
if not Game then return end
|
||||||
Game:joystickhat(joystick, hat, direction)
|
Game:joystickhat(joystick, hat, direction)
|
||||||
end
|
end
|
||||||
|
|
||||||
function love.joystickadded(joystick)
|
function love.joystickadded(joystick)
|
||||||
SwitchDiagnostics.onJoystickEvent("joystickadded", joystick)
|
SwitchDiagnostics.onJoystickEvent("joystickadded", joystick)
|
||||||
if editorMode or TouchEditor then return end
|
if editorMode or TouchEditor or Studio then return end
|
||||||
if Importer then return end
|
if Importer then return end
|
||||||
|
if not Game then return end
|
||||||
Game:joystickadded(joystick)
|
Game:joystickadded(joystick)
|
||||||
end
|
end
|
||||||
|
|
||||||
function love.joystickremoved(joystick)
|
function love.joystickremoved(joystick)
|
||||||
SwitchDiagnostics.onJoystickEvent("joystickremoved", joystick)
|
SwitchDiagnostics.onJoystickEvent("joystickremoved", joystick)
|
||||||
if editorMode or TouchEditor then return end
|
if editorMode or TouchEditor or Studio then return end
|
||||||
if Importer then return end
|
if Importer then return end
|
||||||
|
if not Game then return end
|
||||||
Game:joystickremoved(joystick)
|
Game:joystickremoved(joystick)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -756,26 +775,36 @@ end
|
|||||||
-- unfocused, so reset input on either transition rather than trust it.
|
-- unfocused, so reset input on either transition rather than trust it.
|
||||||
function love.focus(f)
|
function love.focus(f)
|
||||||
if editorMode or TouchEditor then return end
|
if editorMode or TouchEditor then return end
|
||||||
|
if Studio then
|
||||||
|
if Studio.focus then Studio.focus(f) end
|
||||||
|
return
|
||||||
|
end
|
||||||
if Importer then
|
if Importer then
|
||||||
require("src.core.Input"):reset()
|
require("src.core.Input"):reset()
|
||||||
if Importer.focus then Importer:focus(f) end
|
if Importer.focus then Importer:focus(f) end
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if not Game then return end
|
||||||
Game:focus(f)
|
Game:focus(f)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- v is true when the window becomes visible again, false on minimize.
|
-- v is true when the window becomes visible again, false on minimize.
|
||||||
function love.visible(v)
|
function love.visible(v)
|
||||||
if editorMode or TouchEditor then return end
|
if editorMode or TouchEditor then return end
|
||||||
|
if Studio then
|
||||||
|
if Studio.visible then Studio.visible(v) end
|
||||||
|
return
|
||||||
|
end
|
||||||
if Importer then
|
if Importer then
|
||||||
require("src.core.Input"):reset()
|
require("src.core.Input"):reset()
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
if not Game then return end
|
||||||
Game:visible(v)
|
Game:visible(v)
|
||||||
end
|
end
|
||||||
|
|
||||||
function love.lowmemory()
|
function love.lowmemory()
|
||||||
if editorMode or TouchEditor or Importer then return end
|
if editorMode or TouchEditor or Studio or Importer then return end
|
||||||
if Game then Game:onResume() end
|
if Game then Game:onResume() end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -796,12 +825,14 @@ function love.touchpressed(id, x, y, dx, dy, pressure)
|
|||||||
if love.system.getOS() == "iOS" then return end
|
if love.system.getOS() == "iOS" then return end
|
||||||
return TouchEditor.touchpressed(id, x, y)
|
return TouchEditor.touchpressed(id, x, y)
|
||||||
end
|
end
|
||||||
|
if Studio then return end
|
||||||
if Importer then
|
if Importer then
|
||||||
-- Both mobiles: FlexLove scroll needs the real touch stream. Clicks are
|
-- Both mobiles: FlexLove scroll needs the real touch stream. Clicks are
|
||||||
-- polled inside the view; the istouch filter on mousepressed still drops
|
-- polled inside the view; the istouch filter on mousepressed still drops
|
||||||
-- Android's synthesized mouse twin so Import cannot double-fire (#553).
|
-- Android's synthesized mouse twin so Import cannot double-fire (#553).
|
||||||
return Importer:touchpressed(id, x, y, dx, dy, pressure)
|
return Importer:touchpressed(id, x, y, dx, dy, pressure)
|
||||||
end
|
end
|
||||||
|
if not Game then return end
|
||||||
Game:touchpressed(id, x, y, dx, dy, pressure)
|
Game:touchpressed(id, x, y, dx, dy, pressure)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -811,9 +842,11 @@ function love.touchmoved(id, x, y, dx, dy, pressure)
|
|||||||
if love.system.getOS() == "iOS" then return end
|
if love.system.getOS() == "iOS" then return end
|
||||||
return TouchEditor.touchmoved(id, x, y)
|
return TouchEditor.touchmoved(id, x, y)
|
||||||
end
|
end
|
||||||
|
if Studio then return end
|
||||||
if Importer then
|
if Importer then
|
||||||
return Importer:touchmoved(id, x, y, dx, dy, pressure)
|
return Importer:touchmoved(id, x, y, dx, dy, pressure)
|
||||||
end
|
end
|
||||||
|
if not Game then return end
|
||||||
Game:touchmoved(id, x, y, dx, dy, pressure)
|
Game:touchmoved(id, x, y, dx, dy, pressure)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -823,9 +856,11 @@ function love.touchreleased(id, x, y, dx, dy, pressure)
|
|||||||
if love.system.getOS() == "iOS" then return end
|
if love.system.getOS() == "iOS" then return end
|
||||||
return TouchEditor.touchreleased(id, x, y)
|
return TouchEditor.touchreleased(id, x, y)
|
||||||
end
|
end
|
||||||
|
if Studio then return end
|
||||||
if Importer then
|
if Importer then
|
||||||
return Importer:touchreleased(id, x, y, dx, dy, pressure)
|
return Importer:touchreleased(id, x, y, dx, dy, pressure)
|
||||||
end
|
end
|
||||||
|
if not Game then return end
|
||||||
Game:touchreleased(id, x, y, dx, dy, pressure)
|
Game:touchreleased(id, x, y, dx, dy, pressure)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -837,6 +872,7 @@ function love.wheelmoved(x, y)
|
|||||||
if TouchEditor then return end
|
if TouchEditor then return end
|
||||||
if Studio then return Studio.wheelmoved(x, y) end
|
if Studio then return Studio.wheelmoved(x, y) end
|
||||||
if Importer then return end
|
if Importer then return end
|
||||||
|
if not Game then return end
|
||||||
Game:wheelmoved(x, y)
|
Game:wheelmoved(x, y)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
local Platform = require("src.core.Platform")
|
||||||
|
local HostShell = require("src.core.HostShell")
|
||||||
|
|
||||||
|
local FilePicker = {}
|
||||||
|
|
||||||
|
FilePicker.IMAGE = {
|
||||||
|
label = "Image",
|
||||||
|
exts = { "png", "jpg", "jpeg" },
|
||||||
|
tempName = "pokeport_image_pick",
|
||||||
|
}
|
||||||
|
|
||||||
|
local function trim(value)
|
||||||
|
return value and value:gsub("^%s+", ""):gsub("%s+$", "") or ""
|
||||||
|
end
|
||||||
|
|
||||||
|
local function shellSafe(s)
|
||||||
|
s = tostring(s):gsub("%%", "%%%%")
|
||||||
|
return s:gsub('"', '\\"'):gsub("'", "''")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function commandOutput(command)
|
||||||
|
if not Platform.canSpawnProcess() then return nil end
|
||||||
|
local pipe = HostShell.popen(command)
|
||||||
|
if not pipe then return nil end
|
||||||
|
local result = pipe:read("*a")
|
||||||
|
HostShell.pclose(pipe)
|
||||||
|
result = trim(result)
|
||||||
|
return result ~= "" and result or nil
|
||||||
|
end
|
||||||
|
|
||||||
|
local function appleTypes(exts)
|
||||||
|
local out = {}
|
||||||
|
for _, ext in ipairs(exts) do out[#out + 1] = '"' .. ext .. '"' end
|
||||||
|
return table.concat(out, ", ")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function windowsPatterns(exts)
|
||||||
|
local out = {}
|
||||||
|
for _, ext in ipairs(exts) do out[#out + 1] = "*." .. ext end
|
||||||
|
return table.concat(out, ";")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function globPatterns(exts)
|
||||||
|
local out = {}
|
||||||
|
for _, ext in ipairs(exts) do out[#out + 1] = "*." .. ext end
|
||||||
|
return table.concat(out, " ")
|
||||||
|
end
|
||||||
|
|
||||||
|
function FilePicker.available()
|
||||||
|
return Platform.canSpawnProcess()
|
||||||
|
end
|
||||||
|
|
||||||
|
function FilePicker.matches(name, kind)
|
||||||
|
local lower = tostring(name or ""):lower()
|
||||||
|
for _, ext in ipairs(kind.exts) do
|
||||||
|
if lower:match("%." .. ext .. "$") then return true end
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
function FilePicker.open(prompt, kind)
|
||||||
|
if not Platform.canSpawnProcess() then return nil end
|
||||||
|
local title = shellSafe(prompt)
|
||||||
|
local platform = love.system.getOS()
|
||||||
|
if platform == "OS X" then
|
||||||
|
return commandOutput(
|
||||||
|
([[osascript -e 'POSIX path of (choose file with prompt "%s" of type {%s})' 2>/dev/null]])
|
||||||
|
:format(title, appleTypes(kind.exts)))
|
||||||
|
elseif platform == "Windows" then
|
||||||
|
local script = table.concat({
|
||||||
|
"Add-Type -AssemblyName System.Windows.Forms;",
|
||||||
|
"$d=New-Object System.Windows.Forms.OpenFileDialog;",
|
||||||
|
"$d.Title='" .. title .. "';",
|
||||||
|
"$d.Filter='" .. kind.label .. " (" .. windowsPatterns(kind.exts) .. ")|"
|
||||||
|
.. windowsPatterns(kind.exts) .. "|All files (*.*)|*.*';",
|
||||||
|
"if($d.ShowDialog() -eq 'OK'){",
|
||||||
|
"$n=[IO.Path]::GetFileName($d.FileName) -replace '[^\\x20-\\x7E]','_';",
|
||||||
|
"$t=Join-Path $env:TEMP $n;",
|
||||||
|
"Copy-Item -LiteralPath $d.FileName -Destination $t -Force;",
|
||||||
|
"[Console]::OutputEncoding=[Text.Encoding]::UTF8;",
|
||||||
|
"[Console]::Write($t)}",
|
||||||
|
})
|
||||||
|
return commandOutput(
|
||||||
|
'powershell -NoProfile -STA -Command "' .. script .. '"')
|
||||||
|
elseif platform == "Linux" then
|
||||||
|
local path = commandOutput(
|
||||||
|
([[zenity --file-selection --title="%s" --file-filter="%s | %s" 2>/dev/null]])
|
||||||
|
:format(title, kind.label, globPatterns(kind.exts)))
|
||||||
|
if path then return path end
|
||||||
|
return commandOutput(([[kdialog --getopenfilename "$HOME" "%s|%s" 2>/dev/null]])
|
||||||
|
:format(globPatterns(kind.exts), kind.label))
|
||||||
|
end
|
||||||
|
return nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function FilePicker.read(path)
|
||||||
|
local file, openError = io.open(path, "rb")
|
||||||
|
if not file then return nil, openError end
|
||||||
|
local data = file:read("*a")
|
||||||
|
file:close()
|
||||||
|
if not data or data == "" then return nil, "empty file" end
|
||||||
|
return data
|
||||||
|
end
|
||||||
|
|
||||||
|
function FilePicker.basename(path)
|
||||||
|
return tostring(path or ""):match("([^/\\]+)$") or tostring(path or "")
|
||||||
|
end
|
||||||
|
|
||||||
|
return FilePicker
|
||||||
+98
-18
@@ -4,6 +4,7 @@ local PAL = Theme.PAL
|
|||||||
local TouchSkin = require("src.core.TouchSkin")
|
local TouchSkin = require("src.core.TouchSkin")
|
||||||
local TouchControls = require("src.core.TouchControls")
|
local TouchControls = require("src.core.TouchControls")
|
||||||
local SaveData = require("src.core.SaveData")
|
local SaveData = require("src.core.SaveData")
|
||||||
|
local FilePicker = require("src.core.FilePicker")
|
||||||
|
|
||||||
local Studio = {}
|
local Studio = {}
|
||||||
|
|
||||||
@@ -214,12 +215,19 @@ function Studio.cycleImage(dir)
|
|||||||
markDirty()
|
markDirty()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Studio.imageTargetLabel()
|
||||||
|
local target = Studio.imageTarget
|
||||||
|
if target == "bezel" or not Studio.selectedControl() then return "bezel" end
|
||||||
|
return target == "pressed" and "pressed art" or "idle art"
|
||||||
|
end
|
||||||
|
|
||||||
function Studio.assignImage(rel)
|
function Studio.assignImage(rel)
|
||||||
local page, ctl = Studio.page(), Studio.selectedControl()
|
local page, ctl = Studio.page(), Studio.selectedControl()
|
||||||
local img = TouchSkin.resolveImage(Studio.skin.root, rel)
|
local img = TouchSkin.resolveImage(Studio.skin.root, rel)
|
||||||
if ctl and Studio.imageTarget == "pressed" then
|
local target = Studio.imageTarget
|
||||||
|
if ctl and target == "pressed" then
|
||||||
ctl.pressedImagePath, ctl.pressedImage = rel, img
|
ctl.pressedImagePath, ctl.pressedImage = rel, img
|
||||||
elseif ctl and Studio.imageTarget == "idle" then
|
elseif ctl and target == "idle" then
|
||||||
ctl.imagePath, ctl.image = rel, img
|
ctl.imagePath, ctl.image = rel, img
|
||||||
elseif page then
|
elseif page then
|
||||||
page.imagePath, page.image = rel, img
|
page.imagePath, page.image = rel, img
|
||||||
@@ -228,11 +236,69 @@ function Studio.assignImage(rel)
|
|||||||
Studio.dirty = true
|
Studio.dirty = true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function commitSkinId()
|
||||||
|
local skin = Studio.skin
|
||||||
|
if not skin then return end
|
||||||
|
local id = (Studio.skinIdField or ""):gsub("[^%w_%-]", "")
|
||||||
|
if id == "" or id == skin.id then return end
|
||||||
|
TouchSkin.saveTo(skin, id)
|
||||||
|
Studio.available = TouchSkin.list()
|
||||||
|
end
|
||||||
|
|
||||||
|
function Studio.adoptImage(name, data, target)
|
||||||
|
if not Studio.skin then return false end
|
||||||
|
if target then Studio.imageTarget = target end
|
||||||
|
if not FilePicker.matches(name, FilePicker.IMAGE) then
|
||||||
|
Studio.status = "Pick a PNG or JPG."
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
commitSkinId()
|
||||||
|
local rel, err = TouchSkin.importImage(Studio.skin, name, data)
|
||||||
|
if not rel then
|
||||||
|
Studio.status = "Import failed: " .. tostring(err)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
local where = Studio.imageTargetLabel()
|
||||||
|
Studio.assignImage(rel)
|
||||||
|
Studio.skinIdField = Studio.skin.id
|
||||||
|
Studio.status = "Imported " .. rel .. " as " .. where
|
||||||
|
if where == "bezel" and not Studio.canvas().lockViewport then
|
||||||
|
Studio.status = Studio.status
|
||||||
|
.. " -- use Detect screen from bezel to place the screen"
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function Studio.importImageFile(target)
|
||||||
|
if not Studio.skin then return end
|
||||||
|
target = target or Studio.imageTarget
|
||||||
|
Studio.imageTarget = target
|
||||||
|
if target ~= "bezel" and not Studio.selectedControl() then
|
||||||
|
Studio.status = "Select a control first, or import a bezel image."
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if not FilePicker.available() then
|
||||||
|
Studio.status = "No file picker here -- drag a PNG onto the window instead."
|
||||||
|
return
|
||||||
|
end
|
||||||
|
local prompt = (target == "bezel") and "Choose a bezel image"
|
||||||
|
or "Choose a button image"
|
||||||
|
local path = FilePicker.open(prompt, FilePicker.IMAGE)
|
||||||
|
if not path then return end
|
||||||
|
local base = FilePicker.basename(path)
|
||||||
|
local data, err = FilePicker.read(path)
|
||||||
|
if not data then
|
||||||
|
Studio.status = "Could not read " .. base .. ": " .. tostring(err)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
Studio.adoptImage(base, data, target)
|
||||||
|
end
|
||||||
|
|
||||||
function Studio.filedropped(file)
|
function Studio.filedropped(file)
|
||||||
if not Studio.skin then return end
|
if not Studio.skin then return end
|
||||||
local path = (file.getFilename and file:getFilename()) or ""
|
local path = (file.getFilename and file:getFilename()) or ""
|
||||||
local base = path:match("([^/\\]+)$") or path
|
local base = FilePicker.basename(path)
|
||||||
if not base:lower():match("%.png$") and not base:lower():match("%.jpe?g$") then
|
if not FilePicker.matches(base, FilePicker.IMAGE) then
|
||||||
Studio.status = "Drop a PNG or JPG to use it as art."
|
Studio.status = "Drop a PNG or JPG to use it as art."
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@@ -246,17 +312,7 @@ function Studio.filedropped(file)
|
|||||||
Studio.status = "Could not read " .. base
|
Studio.status = "Could not read " .. base
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local rel, err = TouchSkin.importImage(Studio.skin, base, data)
|
Studio.adoptImage(base, data)
|
||||||
if not rel then
|
|
||||||
Studio.status = "Import failed: " .. tostring(err)
|
|
||||||
return
|
|
||||||
end
|
|
||||||
Studio.assignImage(rel)
|
|
||||||
local where = Studio.selectedControl()
|
|
||||||
and (Studio.imageTarget == "pressed" and "pressed art" or "idle art")
|
|
||||||
or "bezel"
|
|
||||||
Studio.status = "Imported " .. rel .. " as " .. where
|
|
||||||
Studio.skinIdField = Studio.skin.id
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Studio.detectViewport()
|
function Studio.detectViewport()
|
||||||
@@ -617,10 +673,16 @@ local function inspectorBody(x, y, w)
|
|||||||
|
|
||||||
if page then
|
if page then
|
||||||
local bezel = page.imagePath or "(none)"
|
local bezel = page.imagePath or "(none)"
|
||||||
if Kit.button(x, cy, w, rowH, "Bezel: " .. bezel, { id = "bezel" }) then
|
local pickW = 82 * Kit.scale
|
||||||
|
local cycleW = w - pickW - gap
|
||||||
|
if Kit.button(x, cy, cycleW, rowH, "Bezel: " .. bezel, { id = "bezel" }) then
|
||||||
Studio.imageTarget = "bezel"
|
Studio.imageTarget = "bezel"
|
||||||
Studio.cycleImage(1)
|
Studio.cycleImage(1)
|
||||||
end
|
end
|
||||||
|
if Kit.button(x + cycleW + gap, cy, pickW, rowH, "Import",
|
||||||
|
{ id = "bezelpick" }) then
|
||||||
|
Studio.importImageFile("bezel")
|
||||||
|
end
|
||||||
cy = cy + rowH + gap
|
cy = cy + rowH + gap
|
||||||
local vpLabel = page.viewport and "Screen cutout: ON" or "Screen cutout: OFF"
|
local vpLabel = page.viewport and "Screen cutout: ON" or "Screen cutout: OFF"
|
||||||
if Kit.button(x, cy, half, rowH, vpLabel, { id = "vp",
|
if Kit.button(x, cy, half, rowH, vpLabel, { id = "vp",
|
||||||
@@ -701,17 +763,25 @@ local function inspectorBody(x, y, w)
|
|||||||
Kit.text("small", ("canvas %dx%d px"):format(canvas.w, canvas.h), x, cy, PAL.faint)
|
Kit.text("small", ("canvas %dx%d px"):format(canvas.w, canvas.h), x, cy, PAL.faint)
|
||||||
cy = cy + Kit.textHeight("small") + gap
|
cy = cy + Kit.textHeight("small") + gap
|
||||||
|
|
||||||
|
local pickW = 82 * Kit.scale
|
||||||
|
local artW = w - pickW - gap
|
||||||
local idle = ctl.imagePath or "(none)"
|
local idle = ctl.imagePath or "(none)"
|
||||||
if Kit.button(x, cy, w, rowH, "Idle art: " .. idle, { id = "img" }) then
|
if Kit.button(x, cy, artW, rowH, "Idle art: " .. idle, { id = "img" }) then
|
||||||
Studio.imageTarget = "idle"
|
Studio.imageTarget = "idle"
|
||||||
Studio.cycleImage(1)
|
Studio.cycleImage(1)
|
||||||
end
|
end
|
||||||
|
if Kit.button(x + artW + gap, cy, pickW, rowH, "Import", { id = "imgpick" }) then
|
||||||
|
Studio.importImageFile("idle")
|
||||||
|
end
|
||||||
cy = cy + rowH + gap
|
cy = cy + rowH + gap
|
||||||
local pressed = ctl.pressedImagePath or "(none)"
|
local pressed = ctl.pressedImagePath or "(none)"
|
||||||
if Kit.button(x, cy, w, rowH, "Pressed art: " .. pressed, { id = "imgp" }) then
|
if Kit.button(x, cy, artW, rowH, "Pressed art: " .. pressed, { id = "imgp" }) then
|
||||||
Studio.imageTarget = "pressed"
|
Studio.imageTarget = "pressed"
|
||||||
Studio.cycleImage(1)
|
Studio.cycleImage(1)
|
||||||
end
|
end
|
||||||
|
if Kit.button(x + artW + gap, cy, pickW, rowH, "Import", { id = "imgppick" }) then
|
||||||
|
Studio.importImageFile("pressed")
|
||||||
|
end
|
||||||
return cy + rowH
|
return cy + rowH
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -885,6 +955,16 @@ function Studio.wheelmoved(_, dy)
|
|||||||
Studio.wheel = dy
|
Studio.wheel = dy
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function Studio.focus()
|
||||||
|
Studio.drag = nil
|
||||||
|
Studio.clicked = false
|
||||||
|
if Studio.testing then TouchControls:reset() end
|
||||||
|
end
|
||||||
|
|
||||||
|
function Studio.visible()
|
||||||
|
Studio.focus()
|
||||||
|
end
|
||||||
|
|
||||||
function Studio.textinput(text)
|
function Studio.textinput(text)
|
||||||
Kit.textinput(text)
|
Kit.textinput(text)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
-- Driver: the studio's side of the alt-tab crash. Leaving the window mid-drag
|
||||||
|
-- must drop the drag and the queued click and still draw; main.lua's routing of
|
||||||
|
-- love.focus / love.visible / pad events while the studio owns the window is
|
||||||
|
-- pinned by tests/engine/skin_studio_image_import.lua (a driver run boots a
|
||||||
|
-- game, so main.lua's Studio branch is not live here).
|
||||||
|
-- POKEPORT_DRIVER=tests/drivers/skin_studio_focus_test.lua love .
|
||||||
|
return function(game)
|
||||||
|
local U = dofile("tests/drivers/util.lua")
|
||||||
|
local Studio = require("src.ui.SkinStudio")
|
||||||
|
local TouchControls = require("src.core.TouchControls")
|
||||||
|
|
||||||
|
love.window.setMode(1280, 720, { resizable = true, highdpi = true })
|
||||||
|
U.wait(2)
|
||||||
|
|
||||||
|
Studio.load({ version = "red", skinId = "gb_anim", onClose = function() end })
|
||||||
|
U.wait(2)
|
||||||
|
U.log("skin:", Studio.skin and Studio.skin.id)
|
||||||
|
|
||||||
|
Studio.drag = { kind = "control-move", mx = 1, my = 1, bx = 0, by = 0,
|
||||||
|
bw = 10, bh = 10 }
|
||||||
|
Studio.clicked = true
|
||||||
|
local okFocus, errFocus = pcall(Studio.focus, false)
|
||||||
|
U.log("focus lost:", okFocus, errFocus or "")
|
||||||
|
U.log("drag:", tostring(Studio.drag), "click:", tostring(Studio.clicked))
|
||||||
|
local droppedDrag = Studio.drag == nil and Studio.clicked == false
|
||||||
|
|
||||||
|
-- a held test press must not survive the trip out of the window either
|
||||||
|
Studio.testing = true
|
||||||
|
TouchControls:setPreview(false)
|
||||||
|
TouchControls.held = TouchControls.held or {}
|
||||||
|
TouchControls.held.start = true
|
||||||
|
pcall(Studio.focus, false)
|
||||||
|
local held = next(TouchControls.held or {})
|
||||||
|
U.log("held after focus loss:", tostring(held))
|
||||||
|
|
||||||
|
local okVisible = pcall(Studio.visible, false)
|
||||||
|
U.log("minimize:", okVisible)
|
||||||
|
|
||||||
|
local okDraw, errDraw = pcall(Studio.draw)
|
||||||
|
U.log("draw after alt-tab:", okDraw, errDraw or "")
|
||||||
|
|
||||||
|
Studio.unload()
|
||||||
|
|
||||||
|
if droppedDrag and held == nil and okVisible and okDraw then
|
||||||
|
U.log("RESULT pass")
|
||||||
|
else
|
||||||
|
U.log("RESULT FAIL")
|
||||||
|
end
|
||||||
|
love.event.quit()
|
||||||
|
while true do coroutine.yield() end
|
||||||
|
end
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
-- Driver: imports a bezel image into a fresh skin through the studio's Import
|
||||||
|
-- button and shoots the result, with the native dialog stubbed out (the real
|
||||||
|
-- one blocks on osascript / zenity / PowerShell). Also covers button art and
|
||||||
|
-- the drag-and-drop path.
|
||||||
|
-- SHOT_DIR=/tmp/studioimport POKEPORT_DRIVER=tests/drivers/skin_studio_import_shot.lua love .
|
||||||
|
return function(game)
|
||||||
|
local U = dofile("tests/drivers/util.lua")
|
||||||
|
local Studio = require("src.ui.SkinStudio")
|
||||||
|
local FilePicker = require("src.core.FilePicker")
|
||||||
|
local TouchSkin = require("src.core.TouchSkin")
|
||||||
|
|
||||||
|
local dir = os.getenv("SHOT_DIR") or "/tmp/studioimport"
|
||||||
|
os.execute('mkdir -p "' .. dir .. '" 2>/dev/null')
|
||||||
|
love.window.setMode(1440, 900, { resizable = true, highdpi = true })
|
||||||
|
U.wait(2)
|
||||||
|
|
||||||
|
local pending = nil
|
||||||
|
love.draw = function()
|
||||||
|
Studio.draw()
|
||||||
|
if pending then
|
||||||
|
local path = pending
|
||||||
|
pending = nil
|
||||||
|
love.graphics.captureScreenshot(function(imagedata)
|
||||||
|
local f = io.open(path, "wb")
|
||||||
|
if f then f:write(imagedata:encode("png"):getString()) f:close() end
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
local function shot(name)
|
||||||
|
pending = dir .. "/" .. name
|
||||||
|
for _ = 1, 90 do
|
||||||
|
if not pending then break end
|
||||||
|
coroutine.yield()
|
||||||
|
end
|
||||||
|
U.wait(3)
|
||||||
|
local f = io.open(dir .. "/" .. name, "rb")
|
||||||
|
U.log(f and "shot" or "FAIL shot", name)
|
||||||
|
if f then f:close() end
|
||||||
|
end
|
||||||
|
|
||||||
|
Studio.load({ version = "red", onClose = function() end })
|
||||||
|
U.wait(3)
|
||||||
|
-- start from New so the run does not depend on the saved skin choice
|
||||||
|
Studio.skin = TouchSkin.newSkin("import_probe")
|
||||||
|
Studio.skinIdField = "import_probe"
|
||||||
|
Studio.pageIndex, Studio.selected, Studio.images = 1, nil, {}
|
||||||
|
U.log("skin:", Studio.skin.id, "bezel:", tostring(Studio.page().imagePath))
|
||||||
|
|
||||||
|
-- what the native dialog would hand back
|
||||||
|
local picked = "assets/skins/gb_anim/img/gb_back.png"
|
||||||
|
local realOpen = FilePicker.open
|
||||||
|
FilePicker.open = function(prompt, kind)
|
||||||
|
U.log("picker prompt:", prompt, "exts:", table.concat(kind.exts, "/"))
|
||||||
|
return picked
|
||||||
|
end
|
||||||
|
|
||||||
|
Studio.importImageFile("bezel")
|
||||||
|
U.log("status:", tostring(Studio.status))
|
||||||
|
U.log("bezel now:", tostring(Studio.page().imagePath),
|
||||||
|
"loaded:", tostring(Studio.page().image ~= nil))
|
||||||
|
U.log("skin root:", Studio.skin.root)
|
||||||
|
U.log("file on disk:",
|
||||||
|
tostring(love.filesystem.getInfo(Studio.skin.root .. "/img/gb_back.png") ~= nil))
|
||||||
|
local viewport = Studio.page().viewport
|
||||||
|
U.log("viewport left alone:", viewport and
|
||||||
|
("%.3f %.3f %.3f %.3f"):format(viewport.x, viewport.y, viewport.w, viewport.h)
|
||||||
|
or "none")
|
||||||
|
shot("import_bezel.png")
|
||||||
|
|
||||||
|
-- button art goes onto the selected control, not the page
|
||||||
|
Studio.addControl()
|
||||||
|
FilePicker.open = function() return "assets/skins/gb_anim/img/gbc_a.png" end
|
||||||
|
Studio.importImageFile("idle")
|
||||||
|
local ctl = Studio.selectedControl()
|
||||||
|
U.log("idle art:", tostring(ctl.imagePath),
|
||||||
|
"bezel untouched:", tostring(Studio.page().imagePath))
|
||||||
|
FilePicker.open = function() return "assets/skins/gb_anim/img/gbc_b.png" end
|
||||||
|
Studio.importImageFile("pressed")
|
||||||
|
U.log("pressed art:", tostring(ctl.pressedImagePath))
|
||||||
|
shot("import_button_art.png")
|
||||||
|
|
||||||
|
-- cancelling the dialog changes nothing
|
||||||
|
local before = Studio.page().imagePath
|
||||||
|
FilePicker.open = function() return nil end
|
||||||
|
Studio.importImageFile("bezel")
|
||||||
|
U.log("after cancel:", tostring(Studio.page().imagePath),
|
||||||
|
"unchanged:", tostring(before == Studio.page().imagePath))
|
||||||
|
|
||||||
|
-- the drop path shares the import, and refuses non-art
|
||||||
|
Studio.imageTarget = "bezel"
|
||||||
|
local raw = love.filesystem.read("assets/skins/tv_crt/img/tv-integer.png")
|
||||||
|
Studio.filedropped({
|
||||||
|
getFilename = function() return "/tmp/tv_frame.png" end,
|
||||||
|
open = function() return true end,
|
||||||
|
read = function() return raw end,
|
||||||
|
close = function() return true end,
|
||||||
|
})
|
||||||
|
U.log("dropped bezel:", tostring(Studio.page().imagePath))
|
||||||
|
Studio.filedropped({
|
||||||
|
getFilename = function() return "/tmp/notes.txt" end,
|
||||||
|
open = function() return true end,
|
||||||
|
read = function() return "nope" end,
|
||||||
|
close = function() return true end,
|
||||||
|
})
|
||||||
|
U.log("after dropping a txt:", tostring(Studio.page().imagePath),
|
||||||
|
"status:", tostring(Studio.status))
|
||||||
|
shot("import_dropped.png")
|
||||||
|
|
||||||
|
love.window.setMode(760, 900, { resizable = true, highdpi = true })
|
||||||
|
U.wait(3)
|
||||||
|
shot("import_narrow.png")
|
||||||
|
|
||||||
|
FilePicker.open = realOpen
|
||||||
|
local saved = TouchSkin.find("import_probe")
|
||||||
|
U.log("skin discoverable:", tostring(saved ~= nil))
|
||||||
|
Studio.unload()
|
||||||
|
U.log("done")
|
||||||
|
love.event.quit()
|
||||||
|
while true do coroutine.yield() end
|
||||||
|
end
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
-- Skin Studio art import: the native image picker behind the "Import" buttons
|
||||||
|
-- (bezel / idle / pressed), and the alt-tab crash that took the studio down --
|
||||||
|
-- love.focus fell through to Game:focus with no game booted.
|
||||||
|
-- luajit tests/engine/skin_studio_image_import.lua
|
||||||
|
|
||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
if not _G.love then _G.love = require("tests.love_stub") end
|
||||||
|
|
||||||
|
local T = require("tests.harness")
|
||||||
|
local check, eq = T.check, T.eq
|
||||||
|
|
||||||
|
local FilePicker = require("src.core.FilePicker")
|
||||||
|
local TouchSkin = require("src.core.TouchSkin")
|
||||||
|
local Studio = require("src.ui.SkinStudio")
|
||||||
|
|
||||||
|
local function session()
|
||||||
|
Studio.skin = TouchSkin.newSkin("t")
|
||||||
|
Studio.skinIdField = "t"
|
||||||
|
Studio.pageIndex = 1
|
||||||
|
Studio.selected = nil
|
||||||
|
Studio.canvasIndex = 1
|
||||||
|
Studio.drag = nil
|
||||||
|
Studio.dirty = false
|
||||||
|
Studio.status = nil
|
||||||
|
Studio.testing = false
|
||||||
|
Studio.images = {}
|
||||||
|
Studio.imageTarget = "idle"
|
||||||
|
return Studio.skin
|
||||||
|
end
|
||||||
|
|
||||||
|
-- ------------------------------------------------------------ file picker
|
||||||
|
|
||||||
|
check(FilePicker.matches("bezel.png", FilePicker.IMAGE), "png is art")
|
||||||
|
check(FilePicker.matches("BEZEL.PNG", FilePicker.IMAGE), "case does not matter")
|
||||||
|
check(FilePicker.matches("shot.jpg", FilePicker.IMAGE), "jpg is art")
|
||||||
|
check(FilePicker.matches("shot.jpeg", FilePicker.IMAGE), "jpeg is art")
|
||||||
|
check(not FilePicker.matches("skin.zip", FilePicker.IMAGE), "a zip is not art")
|
||||||
|
eq(FilePicker.basename("/home/me/art/bezel.png"), "bezel.png", "posix basename")
|
||||||
|
eq(FilePicker.basename("C:\\Users\\me\\bezel.png"), "bezel.png", "windows basename")
|
||||||
|
eq(FilePicker.basename("bezel.png"), "bezel.png", "a bare name is its own basename")
|
||||||
|
|
||||||
|
-- ----------------------------------------------------------- import target
|
||||||
|
|
||||||
|
session()
|
||||||
|
Studio.addControl()
|
||||||
|
local ctl = Studio.selectedControl()
|
||||||
|
check(Studio.adoptImage("bezel.png", "PNGDATA", "bezel"), "bezel import succeeds")
|
||||||
|
eq(Studio.page().imagePath, "img/bezel.png", "bezel art lands on the page")
|
||||||
|
eq(ctl.imagePath, nil, "and not on the selected control")
|
||||||
|
check(Studio.dirty, "importing marks the skin dirty")
|
||||||
|
check(Studio.status:find("bezel", 1, true) ~= nil, "the status names the target")
|
||||||
|
|
||||||
|
check(Studio.adoptImage("a_button.png", "PNGDATA", "idle"), "idle import succeeds")
|
||||||
|
eq(ctl.imagePath, "img/a_button.png", "idle art lands on the control")
|
||||||
|
eq(Studio.page().imagePath, "img/bezel.png", "and leaves the bezel alone")
|
||||||
|
|
||||||
|
check(Studio.adoptImage("a_down.png", "PNGDATA", "pressed"), "pressed import succeeds")
|
||||||
|
eq(ctl.pressedImagePath, "img/a_down.png", "pressed art lands on the control")
|
||||||
|
eq(ctl.imagePath, "img/a_button.png", "idle art survives")
|
||||||
|
|
||||||
|
check(not Studio.adoptImage("notes.txt", "junk", "bezel"), "non-art is refused")
|
||||||
|
eq(Studio.page().imagePath, "img/bezel.png", "and changes nothing")
|
||||||
|
|
||||||
|
-- the imported files are listed for the cycle buttons
|
||||||
|
local listed = {}
|
||||||
|
for _, rel in ipairs(Studio.images or {}) do listed[rel] = true end
|
||||||
|
check(listed["img/bezel.png"], "the imported bezel joins the image list")
|
||||||
|
|
||||||
|
-- -------------------------------------------------------- picker plumbing
|
||||||
|
|
||||||
|
local realOpen, realRead = FilePicker.open, FilePicker.read
|
||||||
|
local asked
|
||||||
|
FilePicker.open = function(prompt, kind)
|
||||||
|
asked = { prompt = prompt, kind = kind }
|
||||||
|
return "/tmp/some folder/frame.png"
|
||||||
|
end
|
||||||
|
FilePicker.read = function() return "PNGDATA" end
|
||||||
|
|
||||||
|
session()
|
||||||
|
Studio.importImageFile("bezel")
|
||||||
|
check(asked ~= nil, "the bezel button opens a picker")
|
||||||
|
eq(asked.kind, FilePicker.IMAGE, "and asks for images, not archives")
|
||||||
|
check(asked.prompt:lower():find("bezel", 1, true) ~= nil, "with a bezel prompt")
|
||||||
|
eq(Studio.page().imagePath, "img/frame.png", "the pick becomes the bezel")
|
||||||
|
eq(Studio.imageTarget, "bezel", "and the target sticks for the cycle button")
|
||||||
|
|
||||||
|
-- no control selected: an art import says so instead of writing to the page
|
||||||
|
session()
|
||||||
|
asked = nil
|
||||||
|
Studio.importImageFile("idle")
|
||||||
|
check(asked == nil, "no picker opens without a control to paint")
|
||||||
|
eq(Studio.page().imagePath, nil, "and the bezel is not overwritten")
|
||||||
|
check(Studio.status ~= nil, "the studio explains why")
|
||||||
|
|
||||||
|
-- a cancelled dialog leaves the skin exactly as it was
|
||||||
|
session()
|
||||||
|
FilePicker.open = function() return nil end
|
||||||
|
Studio.importImageFile("bezel")
|
||||||
|
eq(Studio.page().imagePath, nil, "cancelling imports nothing")
|
||||||
|
check(not Studio.dirty, "and does not dirty the skin")
|
||||||
|
|
||||||
|
FilePicker.open, FilePicker.read = realOpen, realRead
|
||||||
|
|
||||||
|
-- --------------------------------------------------------- focus / alt-tab
|
||||||
|
|
||||||
|
session()
|
||||||
|
Studio.drag = { kind = "control-move", mx = 1, my = 1, bx = 0, by = 0, bw = 1, bh = 1 }
|
||||||
|
Studio.clicked = true
|
||||||
|
Studio.focus(false)
|
||||||
|
eq(Studio.drag, nil, "losing focus drops the in-flight drag")
|
||||||
|
eq(Studio.clicked, false, "and the queued click")
|
||||||
|
check(pcall(Studio.visible, false), "minimizing is survivable too")
|
||||||
|
|
||||||
|
-- main.lua used to hand focus / visibility / pad events straight to Game while
|
||||||
|
-- the studio owned the window, and the launcher has no Game -- alt-tab took the
|
||||||
|
-- app down with "attempt to index a nil value (global 'Game')".
|
||||||
|
local f = assert(io.open("main.lua", "r"))
|
||||||
|
local src = f:read("*a")
|
||||||
|
f:close()
|
||||||
|
|
||||||
|
local checked = 0
|
||||||
|
for name, body in ("\n" .. src):gmatch("\nfunction love%.([%w_]+)%b()\n(.-)\nend\n") do
|
||||||
|
if body:find("Game:") then
|
||||||
|
checked = checked + 1
|
||||||
|
check(body:find("Studio") ~= nil,
|
||||||
|
"love." .. name .. " checks Studio before dispatching to Game")
|
||||||
|
check(body:find("not Game then") ~= nil
|
||||||
|
or body:find("if Game then") ~= nil
|
||||||
|
or body:find("Game and ") ~= nil,
|
||||||
|
"love." .. name .. " tolerates a nil Game (launcher / studio session)")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
check(checked >= 10, "the handler scan actually found handlers (" .. checked .. ")")
|
||||||
|
check(src:find("Studio.focus", 1, true) ~= nil, "love.focus routes to the studio")
|
||||||
|
check(src:find("Studio.visible", 1, true) ~= nil, "so does love.visible")
|
||||||
|
|
||||||
|
T.finish("skin_studio_image_import")
|
||||||
Reference in New Issue
Block a user