mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-18 03:35:56 +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),
|
||||
`src/ui/SkinStudio.lua` (the desktop editor). Tests:
|
||||
`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`.
|
||||
|
||||
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
|
||||
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
|
||||
per page. The cutout is itself a draggable element with a 10:9 lock. Drop a PNG
|
||||
or JPG on the window to import art into the skin.
|
||||
per page. The cutout is itself a draggable element with a 10:9 lock.
|
||||
|
||||
**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
|
||||
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
|
||||
|
||||
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 Studio then return Studio.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
|
||||
-- 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 Studio then return Studio.keypressed(key) end
|
||||
if Importer then return Importer:keypressed(key) end
|
||||
if not Game then return end
|
||||
Game:keypressed(key)
|
||||
end
|
||||
|
||||
function love.keyreleased(key)
|
||||
if editorMode or TouchEditor or Studio then return end
|
||||
if Importer then return end
|
||||
if not Game then return end
|
||||
Game:keyreleased(key)
|
||||
end
|
||||
|
||||
@@ -625,7 +628,9 @@ function love.gamepadpressed(joystick, button)
|
||||
end
|
||||
return
|
||||
end
|
||||
if Studio then return end
|
||||
if Importer then return Importer:gamepadpressed(joystick, button) end
|
||||
if not Game then return end
|
||||
Game:gamepadpressed(joystick, button)
|
||||
end
|
||||
|
||||
@@ -643,7 +648,9 @@ function love.gamepadreleased(joystick, button)
|
||||
end
|
||||
return
|
||||
end
|
||||
if Studio then return end
|
||||
if Importer then return Importer:gamepadreleased(joystick, button) end
|
||||
if not Game then return end
|
||||
Game:gamepadreleased(joystick, button)
|
||||
end
|
||||
|
||||
@@ -661,7 +668,9 @@ function love.gamepadaxis(joystick, axis, value)
|
||||
end
|
||||
return
|
||||
end
|
||||
if Studio then return end
|
||||
if Importer then return Importer:gamepadaxis(joystick, axis, value) end
|
||||
if not Game then return end
|
||||
Game:gamepadaxis(joystick, axis, value)
|
||||
end
|
||||
|
||||
@@ -679,7 +688,9 @@ function love.joystickpressed(joystick, button)
|
||||
end
|
||||
return
|
||||
end
|
||||
if Studio then return end
|
||||
if Importer then return Importer:joystickpressed(joystick, button) end
|
||||
if not Game then return end
|
||||
Game:joystickpressed(joystick, button)
|
||||
end
|
||||
|
||||
@@ -697,7 +708,9 @@ function love.joystickreleased(joystick, button)
|
||||
end
|
||||
return
|
||||
end
|
||||
if Studio then return end
|
||||
if Importer then return Importer:joystickreleased(joystick, button) end
|
||||
if not Game then return end
|
||||
Game:joystickreleased(joystick, button)
|
||||
end
|
||||
|
||||
@@ -715,7 +728,9 @@ function love.joystickaxis(joystick, axis, value)
|
||||
end
|
||||
return
|
||||
end
|
||||
if Studio then return end
|
||||
if Importer then return Importer:joystickaxis(joystick, axis, value) end
|
||||
if not Game then return end
|
||||
Game:joystickaxis(joystick, axis, value)
|
||||
end
|
||||
|
||||
@@ -733,21 +748,25 @@ function love.joystickhat(joystick, hat, direction)
|
||||
end
|
||||
return
|
||||
end
|
||||
if Studio then return end
|
||||
if Importer then return Importer:joystickhat(joystick, hat, direction) end
|
||||
if not Game then return end
|
||||
Game:joystickhat(joystick, hat, direction)
|
||||
end
|
||||
|
||||
function love.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 not Game then return end
|
||||
Game:joystickadded(joystick)
|
||||
end
|
||||
|
||||
function love.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 not Game then return end
|
||||
Game:joystickremoved(joystick)
|
||||
end
|
||||
|
||||
@@ -756,26 +775,36 @@ end
|
||||
-- unfocused, so reset input on either transition rather than trust it.
|
||||
function love.focus(f)
|
||||
if editorMode or TouchEditor then return end
|
||||
if Studio then
|
||||
if Studio.focus then Studio.focus(f) end
|
||||
return
|
||||
end
|
||||
if Importer then
|
||||
require("src.core.Input"):reset()
|
||||
if Importer.focus then Importer:focus(f) end
|
||||
return
|
||||
end
|
||||
if not Game then return end
|
||||
Game:focus(f)
|
||||
end
|
||||
|
||||
-- v is true when the window becomes visible again, false on minimize.
|
||||
function love.visible(v)
|
||||
if editorMode or TouchEditor then return end
|
||||
if Studio then
|
||||
if Studio.visible then Studio.visible(v) end
|
||||
return
|
||||
end
|
||||
if Importer then
|
||||
require("src.core.Input"):reset()
|
||||
return
|
||||
end
|
||||
if not Game then return end
|
||||
Game:visible(v)
|
||||
end
|
||||
|
||||
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
|
||||
end
|
||||
|
||||
@@ -796,12 +825,14 @@ function love.touchpressed(id, x, y, dx, dy, pressure)
|
||||
if love.system.getOS() == "iOS" then return end
|
||||
return TouchEditor.touchpressed(id, x, y)
|
||||
end
|
||||
if Studio then return end
|
||||
if Importer then
|
||||
-- Both mobiles: FlexLove scroll needs the real touch stream. Clicks are
|
||||
-- polled inside the view; the istouch filter on mousepressed still drops
|
||||
-- Android's synthesized mouse twin so Import cannot double-fire (#553).
|
||||
return Importer:touchpressed(id, x, y, dx, dy, pressure)
|
||||
end
|
||||
if not Game then return end
|
||||
Game:touchpressed(id, x, y, dx, dy, pressure)
|
||||
end
|
||||
|
||||
@@ -811,9 +842,11 @@ function love.touchmoved(id, x, y, dx, dy, pressure)
|
||||
if love.system.getOS() == "iOS" then return end
|
||||
return TouchEditor.touchmoved(id, x, y)
|
||||
end
|
||||
if Studio then return end
|
||||
if Importer then
|
||||
return Importer:touchmoved(id, x, y, dx, dy, pressure)
|
||||
end
|
||||
if not Game then return end
|
||||
Game:touchmoved(id, x, y, dx, dy, pressure)
|
||||
end
|
||||
|
||||
@@ -823,9 +856,11 @@ function love.touchreleased(id, x, y, dx, dy, pressure)
|
||||
if love.system.getOS() == "iOS" then return end
|
||||
return TouchEditor.touchreleased(id, x, y)
|
||||
end
|
||||
if Studio then return end
|
||||
if Importer then
|
||||
return Importer:touchreleased(id, x, y, dx, dy, pressure)
|
||||
end
|
||||
if not Game then return end
|
||||
Game:touchreleased(id, x, y, dx, dy, pressure)
|
||||
end
|
||||
|
||||
@@ -837,6 +872,7 @@ function love.wheelmoved(x, y)
|
||||
if TouchEditor then return end
|
||||
if Studio then return Studio.wheelmoved(x, y) end
|
||||
if Importer then return end
|
||||
if not Game then return end
|
||||
Game:wheelmoved(x, y)
|
||||
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 TouchControls = require("src.core.TouchControls")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local FilePicker = require("src.core.FilePicker")
|
||||
|
||||
local Studio = {}
|
||||
|
||||
@@ -214,12 +215,19 @@ function Studio.cycleImage(dir)
|
||||
markDirty()
|
||||
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)
|
||||
local page, ctl = Studio.page(), Studio.selectedControl()
|
||||
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
|
||||
elseif ctl and Studio.imageTarget == "idle" then
|
||||
elseif ctl and target == "idle" then
|
||||
ctl.imagePath, ctl.image = rel, img
|
||||
elseif page then
|
||||
page.imagePath, page.image = rel, img
|
||||
@@ -228,11 +236,69 @@ function Studio.assignImage(rel)
|
||||
Studio.dirty = true
|
||||
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)
|
||||
if not Studio.skin then return end
|
||||
local path = (file.getFilename and file:getFilename()) or ""
|
||||
local base = path:match("([^/\\]+)$") or path
|
||||
if not base:lower():match("%.png$") and not base:lower():match("%.jpe?g$") then
|
||||
local base = FilePicker.basename(path)
|
||||
if not FilePicker.matches(base, FilePicker.IMAGE) then
|
||||
Studio.status = "Drop a PNG or JPG to use it as art."
|
||||
return
|
||||
end
|
||||
@@ -246,17 +312,7 @@ function Studio.filedropped(file)
|
||||
Studio.status = "Could not read " .. base
|
||||
return
|
||||
end
|
||||
local rel, err = TouchSkin.importImage(Studio.skin, 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
|
||||
Studio.adoptImage(base, data)
|
||||
end
|
||||
|
||||
function Studio.detectViewport()
|
||||
@@ -617,10 +673,16 @@ local function inspectorBody(x, y, w)
|
||||
|
||||
if page then
|
||||
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.cycleImage(1)
|
||||
end
|
||||
if Kit.button(x + cycleW + gap, cy, pickW, rowH, "Import",
|
||||
{ id = "bezelpick" }) then
|
||||
Studio.importImageFile("bezel")
|
||||
end
|
||||
cy = cy + rowH + gap
|
||||
local vpLabel = page.viewport and "Screen cutout: ON" or "Screen cutout: OFF"
|
||||
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)
|
||||
cy = cy + Kit.textHeight("small") + gap
|
||||
|
||||
local pickW = 82 * Kit.scale
|
||||
local artW = w - pickW - gap
|
||||
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.cycleImage(1)
|
||||
end
|
||||
if Kit.button(x + artW + gap, cy, pickW, rowH, "Import", { id = "imgpick" }) then
|
||||
Studio.importImageFile("idle")
|
||||
end
|
||||
cy = cy + rowH + gap
|
||||
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.cycleImage(1)
|
||||
end
|
||||
if Kit.button(x + artW + gap, cy, pickW, rowH, "Import", { id = "imgppick" }) then
|
||||
Studio.importImageFile("pressed")
|
||||
end
|
||||
return cy + rowH
|
||||
end
|
||||
|
||||
@@ -885,6 +955,16 @@ function Studio.wheelmoved(_, dy)
|
||||
Studio.wheel = dy
|
||||
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)
|
||||
Kit.textinput(text)
|
||||
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