Round 7: presets system, file associations, plugin toggle

Major features:
- Presets system: save/load/clone/delete animation configurations
  - Each preset stored as .ini in ~/.config/aseprite/aniphallow_presets/
  - Master file tracks global prefs and file-to-preset associations
  - Config dialog has Presets section with combobox + Save/Load/Clone/Delete
  - Default preset created automatically on first run
  - Migration from old single prefs file to preset system
- File-to-preset association: each .aseprite file remembers its preset
  - Auto-detects file changes and loads appropriate preset
  - Preset association saved when loading a preset
- Plugin toggle: running the plugin again closes it (no duplicate windows)
- Preview title fixed to "AniPhallow Preview" (always)
- Animation name label shown below canvas in Show One mode
- Show One navigation (L/R click) no longer closes/reopens window
- Config dialog reorganized: Presets > Animations > Optimization > Preview
This commit is contained in:
Cidwel Highwind 2026-04-03 19:56:31 +02:00
parent 91af5f5691
commit 9e3a2702dc
1 changed files with 563 additions and 110 deletions

View File

@ -28,6 +28,13 @@ local DEFAULT_GB_TILE_W = 8 -- Default Game Boy tile width
local DEFAULT_GB_TILE_H = 8 -- Default Game Boy tile height
local GB_COLS = 16 -- tiles per row in optimized image (128px = GB standard)
----------------------------------------------------------------------
-- Presets paths
----------------------------------------------------------------------
local PRESETS_DIR = app.fs.joinPath(app.fs.userConfigPath, "aniphallow_presets")
local MASTER_FILE = app.fs.joinPath(app.fs.userConfigPath, "aniphallow_master.ini")
local OLD_PREFS_FILE = app.fs.joinPath(app.fs.userConfigPath, "aniphallow_prefs.ini")
----------------------------------------------------------------------
-- State
----------------------------------------------------------------------
@ -109,8 +116,17 @@ local S = {
previewLayoutValue = 2, -- number for fixed cols/rows
previewMode = "all", -- "all" or "single"
previewSingleIdx = 1, -- which animation to show in single mode
-- Current preset
currentPreset = "Default",
}
----------------------------------------------------------------------
-- File-to-preset association map
----------------------------------------------------------------------
local filePresetMap = {} -- filepath -> presetName
local knownPresetNames = { "Default" } -- list of all known preset names
local lastSpriteFilename = "" -- for auto-detect file change
----------------------------------------------------------------------
-- Tab widget IDs (for show/hide)
----------------------------------------------------------------------
@ -152,10 +168,8 @@ local openPreviewWindow
local openMainDialog
----------------------------------------------------------------------
-- Preferences
-- Serialize / Deserialize frames
----------------------------------------------------------------------
local PREFS_FILE = app.fs.joinPath(app.fs.userConfigPath, "aniphallow_prefs.ini")
-- Serialize frames list: "x1,y1,f,fv,ox,oy;x2,y2,f,fv,ox,oy;..."
local function serializeFrames(frames)
local parts = {}
@ -184,29 +198,122 @@ local function deserializeFrames(str)
return frames
end
local function loadPrefs()
local f = io.open(PREFS_FILE, "r")
----------------------------------------------------------------------
-- Reset to defaults
----------------------------------------------------------------------
local function resetToDefaults()
S.tileW = DEFAULT_TILE_W
S.tileH = DEFAULT_TILE_H
S.animSpeed = DEFAULT_ANIM_SPEED
S.sourceZoom = DEFAULT_SOURCE_ZOOM
S.useBgColor = false
S.bgColor = Color(0, 0, 0)
S.gbFlipOpt = false
S.gbOffsetOpt = false
S.gbCompress = true
S.gbSilhouette = false
S.gbSilhouetteColor = Color(0, 0, 0)
S.gbSilhouetteColorSet = false
S.gbAnalyzeMode = "pixel"
S.gbLayerName = "auto-optimized-tiles"
S.gbAlwaysOverwrite = false
S.gbTileW = DEFAULT_GB_TILE_W
S.gbTileH = DEFAULT_GB_TILE_H
S.gbSimilarThreshold = 80
S.animNames = {}
S.anims = {}
S.currentAnim = 1
S.selectedFrame = 0
end
----------------------------------------------------------------------
-- Preset system: save/load preset files and master file
----------------------------------------------------------------------
-- Get list of all known preset names
local function getAllPresetNames()
-- Ensure "Default" is always first
local seen = {}
local result = { "Default" }
seen["Default"] = true
for _, name in ipairs(knownPresetNames) do
if not seen[name] then
table.insert(result, name)
seen[name] = true
end
end
return result
end
-- Save current state to a preset file
local function savePreset(presetName)
-- Create presets dir if needed
app.fs.makeDirectory(PRESETS_DIR)
local path = app.fs.joinPath(PRESETS_DIR, presetName .. ".ini")
local f = io.open(path, "w")
if not f then return end
f:write("tileW=" .. S.tileW .. "\n")
f:write("tileH=" .. S.tileH .. "\n")
f:write("animSpeed=" .. S.animSpeed .. "\n")
f:write("sourceZoom=" .. S.sourceZoom .. "\n")
-- Save current animation name for robust resolution after sort
local curName = ""
if S.currentAnim >= 1 and S.currentAnim <= #S.animNames then
curName = S.animNames[S.currentAnim]
end
f:write("currentAnimName=" .. curName .. "\n")
f:write("currentTab=" .. S.currentTab .. "\n")
f:write("animNames=" .. table.concat(S.animNames, "|") .. "\n")
f:write("useBgColor=" .. tostring(S.useBgColor) .. "\n")
f:write("bgColorR=" .. S.bgColor.red .. "\n")
f:write("bgColorG=" .. S.bgColor.green .. "\n")
f:write("bgColorB=" .. S.bgColor.blue .. "\n")
f:write("gbFlipOpt=" .. tostring(S.gbFlipOpt) .. "\n")
f:write("gbOffsetOpt=" .. tostring(S.gbOffsetOpt) .. "\n")
f:write("gbCompress=" .. tostring(S.gbCompress) .. "\n")
f:write("gbSimilarThreshold=" .. S.gbSimilarThreshold .. "\n")
f:write("gbZoomOpt=" .. S.gbZoomOpt .. "\n")
f:write("gbZoomSrc=" .. S.gbZoomSrc .. "\n")
f:write("gbSilhouette=" .. tostring(S.gbSilhouette) .. "\n")
f:write("gbSilhouetteColorR=" .. S.gbSilhouetteColor.red .. "\n")
f:write("gbSilhouetteColorG=" .. S.gbSilhouetteColor.green .. "\n")
f:write("gbSilhouetteColorB=" .. S.gbSilhouetteColor.blue .. "\n")
f:write("gbSilhouetteColorSet=" .. tostring(S.gbSilhouetteColorSet) .. "\n")
f:write("gbAnalyzeMode=" .. S.gbAnalyzeMode .. "\n")
f:write("gbLastSavePath=" .. S.gbLastSavePath .. "\n")
f:write("gbLayerName=" .. S.gbLayerName .. "\n")
f:write("gbAlwaysOverwrite=" .. tostring(S.gbAlwaysOverwrite) .. "\n")
f:write("gbTileW=" .. S.gbTileW .. "\n")
f:write("gbTileH=" .. S.gbTileH .. "\n")
for i, name in ipairs(S.animNames) do
f:write("anim_" .. (i - 1) .. "=" .. serializeFrames(S.anims[name] or {}) .. "\n")
end
f:close()
end
-- Load a preset file into S
local function loadPreset(presetName)
local path = app.fs.joinPath(PRESETS_DIR, presetName .. ".ini")
local f = io.open(path, "r")
if not f then
-- If preset doesn't exist, reset to defaults
resetToDefaults()
S.currentPreset = presetName
return
end
-- Reset animation data before loading
S.animNames = {}
S.anims = {}
local savedAnimName = nil
for line in f:lines() do
local k, v = string.match(line, "^([%w_]+)=(.+)$")
if k == "tileW" then S.tileW = tonumber(v) or DEFAULT_TILE_W end
if k == "tileH" then S.tileH = tonumber(v) or DEFAULT_TILE_H end
if k == "animSpeed" then S.animSpeed = tonumber(v) or DEFAULT_ANIM_SPEED end
if k == "previewZoom" then
-- Legacy: load into both
local pz = tonumber(v) or DEFAULT_PREVIEW_ZOOM
S.previewZoomAll = pz
S.previewZoomOne = pz
end
if k == "previewZoomAll" then S.previewZoomAll = tonumber(v) or DEFAULT_PREVIEW_ZOOM end
if k == "previewZoomOne" then S.previewZoomOne = tonumber(v) or DEFAULT_PREVIEW_ZOOM end
if k == "sourceZoom" then S.sourceZoom = tonumber(v) or DEFAULT_SOURCE_ZOOM end
-- Legacy: currentAnim as index (for migration)
if k == "currentAnim" then S.currentAnim = tonumber(v) or 1 end
if k == "currentAnimName" then savedAnimName = v end
if k == "currentTab" and v ~= "" then
-- Migrate old tab names
if v == "Setup" then v = "Animations"
elseif v == "Render" then v = "Animations"
elseif v == "Preview" then v = "Animations"
@ -239,22 +346,13 @@ local function loadPrefs()
if k == "gbLastSavePath" then S.gbLastSavePath = v end
if k == "gbLayerName" then S.gbLayerName = v end
if k == "gbAlwaysOverwrite" then S.gbAlwaysOverwrite = (v == "true") end
-- Load gbTileW and gbTileH
if k == "gbTileW" then S.gbTileW = tonumber(v) or DEFAULT_GB_TILE_W end
if k == "gbTileH" then S.gbTileH = tonumber(v) or DEFAULT_GB_TILE_H end
-- Backwards compatibility: load old gbTileSize into both W and H
if k == "gbTileSize" then
local ts = tonumber(v) or DEFAULT_GB_TILE_W
S.gbTileW = ts
S.gbTileH = ts
end
-- Load previewWindowOpen
if k == "previewWindowOpen" then S.previewWindowOpen = (v == "true") end
-- Preview layout prefs
if k == "previewLayout" then S.previewLayout = v end
if k == "previewLayoutValue" then S.previewLayoutValue = tonumber(v) or 2 end
if k == "previewMode" then S.previewMode = v end
if k == "previewSingleIdx" then S.previewSingleIdx = tonumber(v) or 1 end
-- Dynamic anim frames: anim_0, anim_1, ...
local animIdx = k and string.match(k, "^anim_(%d+)$")
if animIdx then
@ -281,6 +379,215 @@ local function loadPrefs()
-- Clamp currentAnim to valid range
if S.currentAnim < 1 then S.currentAnim = 1 end
if S.currentAnim > #S.animNames then S.currentAnim = math.max(1, #S.animNames) end
S.currentPreset = presetName
end
-- Save master file (global prefs + file associations)
local function saveMaster()
local f = io.open(MASTER_FILE, "w")
if not f then return end
f:write("currentPreset=" .. S.currentPreset .. "\n")
f:write("previewWindowOpen=" .. tostring(S.previewWindowOpen) .. "\n")
f:write("previewLayout=" .. S.previewLayout .. "\n")
f:write("previewLayoutValue=" .. S.previewLayoutValue .. "\n")
f:write("previewMode=" .. S.previewMode .. "\n")
f:write("previewZoomAll=" .. S.previewZoomAll .. "\n")
f:write("previewZoomOne=" .. S.previewZoomOne .. "\n")
f:write("previewSingleIdx=" .. S.previewSingleIdx .. "\n")
-- File associations
for filepath, preset in pairs(filePresetMap) do
if preset ~= "Default" then
f:write("file_" .. filepath .. "=" .. preset .. "\n")
end
end
-- Preset names list (for discovery since we can't list dir)
f:write("presetNames=" .. table.concat(getAllPresetNames(), "|") .. "\n")
f:close()
end
-- Load master file
local function loadMaster()
local f = io.open(MASTER_FILE, "r")
if not f then return end
filePresetMap = {}
for line in f:lines() do
local k, v = string.match(line, "^(.-)=(.+)$")
if k and v then
if k == "currentPreset" then S.currentPreset = v end
if k == "previewWindowOpen" then S.previewWindowOpen = (v == "true") end
if k == "previewLayout" then S.previewLayout = v end
if k == "previewLayoutValue" then S.previewLayoutValue = tonumber(v) or 2 end
if k == "previewMode" then S.previewMode = v end
if k == "previewZoomAll" then S.previewZoomAll = tonumber(v) or DEFAULT_PREVIEW_ZOOM end
if k == "previewZoomOne" then S.previewZoomOne = tonumber(v) or DEFAULT_PREVIEW_ZOOM end
if k == "previewSingleIdx" then S.previewSingleIdx = tonumber(v) or 1 end
if k == "presetNames" and v ~= "" then
knownPresetNames = {}
for name in string.gmatch(v, "([^|]+)") do
table.insert(knownPresetNames, name)
end
end
-- File associations: keys starting with "file_"
local filePath = string.match(k, "^file_(.+)$")
if filePath then
filePresetMap[filePath] = v
end
end
end
f:close()
-- Clamp previewSingleIdx
if S.previewSingleIdx < 1 then S.previewSingleIdx = 1 end
end
-- Convenience: save both preset and master
local function saveAll()
savePreset(S.currentPreset)
saveMaster()
end
----------------------------------------------------------------------
-- Migration from old prefs file
----------------------------------------------------------------------
local function migrateOldPrefs()
-- Check if old prefs file exists and no presets exist yet
local oldF = io.open(OLD_PREFS_FILE, "r")
if not oldF then return false end
-- Check if Default preset already exists
local defaultPath = app.fs.joinPath(PRESETS_DIR, "Default.ini")
local defF = io.open(defaultPath, "r")
if defF then
defF:close()
oldF:close()
return false -- Already migrated
end
oldF:close()
-- Load the old prefs into S using the same parsing logic
local f = io.open(OLD_PREFS_FILE, "r")
if not f then return false end
local savedAnimName = nil
for line in f:lines() do
local k, v = string.match(line, "^([%w_]+)=(.+)$")
if k == "tileW" then S.tileW = tonumber(v) or DEFAULT_TILE_W end
if k == "tileH" then S.tileH = tonumber(v) or DEFAULT_TILE_H end
if k == "animSpeed" then S.animSpeed = tonumber(v) or DEFAULT_ANIM_SPEED end
if k == "previewZoom" then
local pz = tonumber(v) or DEFAULT_PREVIEW_ZOOM
S.previewZoomAll = pz
S.previewZoomOne = pz
end
if k == "previewZoomAll" then S.previewZoomAll = tonumber(v) or DEFAULT_PREVIEW_ZOOM end
if k == "previewZoomOne" then S.previewZoomOne = tonumber(v) or DEFAULT_PREVIEW_ZOOM end
if k == "sourceZoom" then S.sourceZoom = tonumber(v) or DEFAULT_SOURCE_ZOOM end
if k == "currentAnim" then S.currentAnim = tonumber(v) or 1 end
if k == "currentAnimName" then savedAnimName = v end
if k == "currentTab" and v ~= "" then
if v == "Setup" then v = "Animations"
elseif v == "Render" then v = "Animations"
elseif v == "Preview" then v = "Animations"
elseif v == "GB" then v = "Optimize"
end
S.currentTab = v
end
if k == "animNames" and v ~= "" then
S.animNames = {}
for name in string.gmatch(v, "([^|]+)") do
table.insert(S.animNames, name)
end
end
if k == "useBgColor" then S.useBgColor = (v == "true") end
if k == "bgColorR" then S.bgColor = Color(tonumber(v) or 0, S.bgColor.green, S.bgColor.blue) end
if k == "bgColorG" then S.bgColor = Color(S.bgColor.red, tonumber(v) or 0, S.bgColor.blue) end
if k == "bgColorB" then S.bgColor = Color(S.bgColor.red, S.bgColor.green, tonumber(v) or 0) end
if k == "gbFlipOpt" then S.gbFlipOpt = (v == "true") end
if k == "gbOffsetOpt" then S.gbOffsetOpt = (v == "true") end
if k == "gbCompress" then S.gbCompress = (v == "true") end
if k == "gbSimilarThreshold" then S.gbSimilarThreshold = tonumber(v) or 80 end
if k == "gbZoomOpt" then S.gbZoomOpt = tonumber(v) or 3 end
if k == "gbZoomSrc" then S.gbZoomSrc = tonumber(v) or 2 end
if k == "gbSilhouette" then S.gbSilhouette = (v == "true") end
if k == "gbSilhouetteColorR" then S.gbSilhouetteColor = Color(tonumber(v) or 0, S.gbSilhouetteColor.green, S.gbSilhouetteColor.blue) end
if k == "gbSilhouetteColorG" then S.gbSilhouetteColor = Color(S.gbSilhouetteColor.red, tonumber(v) or 0, S.gbSilhouetteColor.blue) end
if k == "gbSilhouetteColorB" then S.gbSilhouetteColor = Color(S.gbSilhouetteColor.red, S.gbSilhouetteColor.green, tonumber(v) or 0) end
if k == "gbSilhouetteColorSet" then S.gbSilhouetteColorSet = (v == "true") end
if k == "gbAnalyzeMode" then S.gbAnalyzeMode = v end
if k == "gbLastSavePath" then S.gbLastSavePath = v end
if k == "gbLayerName" then S.gbLayerName = v end
if k == "gbAlwaysOverwrite" then S.gbAlwaysOverwrite = (v == "true") end
if k == "gbTileW" then S.gbTileW = tonumber(v) or DEFAULT_GB_TILE_W end
if k == "gbTileH" then S.gbTileH = tonumber(v) or DEFAULT_GB_TILE_H end
if k == "gbTileSize" then
local ts = tonumber(v) or DEFAULT_GB_TILE_W
S.gbTileW = ts
S.gbTileH = ts
end
if k == "previewWindowOpen" then S.previewWindowOpen = (v == "true") end
if k == "previewLayout" then S.previewLayout = v end
if k == "previewLayoutValue" then S.previewLayoutValue = tonumber(v) or 2 end
if k == "previewMode" then S.previewMode = v end
if k == "previewSingleIdx" then S.previewSingleIdx = tonumber(v) or 1 end
local animIdx = k and string.match(k, "^anim_(%d+)$")
if animIdx then
local idx = tonumber(animIdx) + 1
if S.animNames[idx] then
S.anims[S.animNames[idx]] = deserializeFrames(v)
end
end
end
f:close()
-- Sort animation names alphabetically
table.sort(S.animNames, function(a, b) return a:lower() < b:lower() end)
-- Resolve current animation by name
if savedAnimName and savedAnimName ~= "" then
for i, name in ipairs(S.animNames) do
if name == savedAnimName then
S.currentAnim = i
break
end
end
end
if S.currentAnim < 1 then S.currentAnim = 1 end
if S.currentAnim > #S.animNames then S.currentAnim = math.max(1, #S.animNames) end
if S.previewSingleIdx < 1 then S.previewSingleIdx = 1 end
if #S.animNames > 0 and S.previewSingleIdx > #S.animNames then
S.previewSingleIdx = #S.animNames
end
-- Save as Default preset
S.currentPreset = "Default"
savePreset("Default")
saveMaster()
return true
end
----------------------------------------------------------------------
-- Initialize: load master, then load appropriate preset
----------------------------------------------------------------------
local function initPresets()
-- Try migration first
local migrated = migrateOldPrefs()
if migrated then return end
-- Load master file
loadMaster()
-- Determine which preset to load
local presetToLoad = S.currentPreset or "Default"
-- Check if current sprite has an associated preset
if app.sprite and app.sprite.filename and app.sprite.filename ~= "" then
local assoc = filePresetMap[app.sprite.filename]
if assoc then
presetToLoad = assoc
end
end
-- Load the preset
loadPreset(presetToLoad)
-- Clamp previewSingleIdx
if S.previewSingleIdx < 1 then S.previewSingleIdx = 1 end
if #S.animNames > 0 and S.previewSingleIdx > #S.animNames then
@ -288,58 +595,7 @@ local function loadPrefs()
end
end
local function savePrefs()
local f = io.open(PREFS_FILE, "w")
if not f then return end
f:write("tileW=" .. S.tileW .. "\n")
f:write("tileH=" .. S.tileH .. "\n")
f:write("animSpeed=" .. S.animSpeed .. "\n")
f:write("previewZoomAll=" .. S.previewZoomAll .. "\n")
f:write("previewZoomOne=" .. S.previewZoomOne .. "\n")
f:write("sourceZoom=" .. S.sourceZoom .. "\n")
-- Save current animation name for robust resolution after sort
local curName = ""
if S.currentAnim >= 1 and S.currentAnim <= #S.animNames then
curName = S.animNames[S.currentAnim]
end
f:write("currentAnimName=" .. curName .. "\n")
f:write("currentTab=" .. S.currentTab .. "\n")
f:write("animNames=" .. table.concat(S.animNames, "|") .. "\n")
f:write("useBgColor=" .. tostring(S.useBgColor) .. "\n")
f:write("bgColorR=" .. S.bgColor.red .. "\n")
f:write("bgColorG=" .. S.bgColor.green .. "\n")
f:write("bgColorB=" .. S.bgColor.blue .. "\n")
f:write("gbFlipOpt=" .. tostring(S.gbFlipOpt) .. "\n")
f:write("gbOffsetOpt=" .. tostring(S.gbOffsetOpt) .. "\n")
f:write("gbCompress=" .. tostring(S.gbCompress) .. "\n")
f:write("gbSimilarThreshold=" .. S.gbSimilarThreshold .. "\n")
f:write("gbZoomOpt=" .. S.gbZoomOpt .. "\n")
f:write("gbZoomSrc=" .. S.gbZoomSrc .. "\n")
f:write("gbSilhouette=" .. tostring(S.gbSilhouette) .. "\n")
f:write("gbSilhouetteColorR=" .. S.gbSilhouetteColor.red .. "\n")
f:write("gbSilhouetteColorG=" .. S.gbSilhouetteColor.green .. "\n")
f:write("gbSilhouetteColorB=" .. S.gbSilhouetteColor.blue .. "\n")
f:write("gbSilhouetteColorSet=" .. tostring(S.gbSilhouetteColorSet) .. "\n")
f:write("gbAnalyzeMode=" .. S.gbAnalyzeMode .. "\n")
f:write("gbLastSavePath=" .. S.gbLastSavePath .. "\n")
f:write("gbLayerName=" .. S.gbLayerName .. "\n")
f:write("gbAlwaysOverwrite=" .. tostring(S.gbAlwaysOverwrite) .. "\n")
f:write("gbTileW=" .. S.gbTileW .. "\n")
f:write("gbTileH=" .. S.gbTileH .. "\n")
-- Save previewWindowOpen
f:write("previewWindowOpen=" .. tostring(S.previewWindowOpen) .. "\n")
-- Save preview layout prefs
f:write("previewLayout=" .. S.previewLayout .. "\n")
f:write("previewLayoutValue=" .. S.previewLayoutValue .. "\n")
f:write("previewMode=" .. S.previewMode .. "\n")
f:write("previewSingleIdx=" .. S.previewSingleIdx .. "\n")
for i, name in ipairs(S.animNames) do
f:write("anim_" .. (i - 1) .. "=" .. serializeFrames(S.anims[name] or {}) .. "\n")
end
f:close()
end
loadPrefs()
initPresets()
----------------------------------------------------------------------
-- Helpers
@ -969,21 +1225,6 @@ local function buildOptimizedImage(img, tiles, compress, silhouette, silhouetteC
end
end
----------------------------------------------------------------------
-- Helper: get preview title based on current mode/animation
----------------------------------------------------------------------
local function getPreviewTitle()
if S.previewMode == "all" then
return "AniPhallow: All"
else
local name = ""
if S.previewSingleIdx >= 1 and S.previewSingleIdx <= #S.animNames then
name = S.animNames[S.previewSingleIdx]
end
return "AniPhallow: " .. name
end
end
----------------------------------------------------------------------
-- Preview Window (primary window)
----------------------------------------------------------------------
@ -1034,7 +1275,7 @@ openPreviewWindow = function()
pvHeight = math.max(pvHeight, 40)
previewDlg = Dialog{
title = getPreviewTitle(),
title = "AniPhallow Preview",
onclose = function()
if previewTimer then pcall(function() previewTimer:stop() end) end
if pvRefreshTimer then pcall(function() pvRefreshTimer:stop() end) end
@ -1042,7 +1283,7 @@ openPreviewWindow = function()
pvRefreshTimer = nil
previewDlg = nil
S.previewWindowOpen = false
savePrefs()
saveAll()
end
}
@ -1068,7 +1309,7 @@ openPreviewWindow = function()
else
S.previewMode = "all"
end
-- Close and reopen to update title
-- Close and reopen to resize canvas
pcall(function() previewDlg:close() end)
openPreviewWindow()
end
@ -1163,17 +1404,21 @@ openPreviewWindow = function()
if #S.animNames > 0 then
S.previewSingleIdx = S.previewSingleIdx - 1
if S.previewSingleIdx < 1 then S.previewSingleIdx = #S.animNames end
-- Close and reopen to update title
pcall(function() previewDlg:close() end)
openPreviewWindow()
-- Update label text and repaint without closing/reopening
pcall(function()
previewDlg:modify{ id = "pvAnimName", text = S.animNames[S.previewSingleIdx] or "" }
end)
previewDlg:repaint()
end
elseif ev.button == MouseButton.RIGHT then
if #S.animNames > 0 then
S.previewSingleIdx = S.previewSingleIdx + 1
if S.previewSingleIdx > #S.animNames then S.previewSingleIdx = 1 end
-- Close and reopen to update title
pcall(function() previewDlg:close() end)
openPreviewWindow()
-- Update label text and repaint without closing/reopening
pcall(function()
previewDlg:modify{ id = "pvAnimName", text = S.animNames[S.previewSingleIdx] or "" }
end)
previewDlg:repaint()
end
end
end
@ -1189,6 +1434,17 @@ openPreviewWindow = function()
end
}
-- Animation name label for single mode
local singleName = ""
if S.previewSingleIdx >= 1 and S.previewSingleIdx <= #S.animNames then
singleName = S.animNames[S.previewSingleIdx]
end
previewDlg:label{
id = "pvAnimName",
text = singleName,
visible = (S.previewMode == "single")
}
previewTimer = Timer{
interval = S.animSpeed / 1000.0,
ontick = function()
@ -1203,12 +1459,29 @@ openPreviewWindow = function()
ontick = function()
refreshSource()
pcall(function() previewDlg:repaint() end)
-- Auto-detect file change
local currentFile = app.sprite and app.sprite.filename or ""
if currentFile ~= lastSpriteFilename then
lastSpriteFilename = currentFile
-- Look up preset for this file
local presetForFile = filePresetMap[currentFile] or "Default"
if presetForFile ~= S.currentPreset then
loadPreset(presetForFile)
S.currentPreset = presetForFile
-- Refresh UI if main dialog is open
if mainDlg then
pcall(function() mainDlg:close() end)
openMainDialog()
end
end
end
end
}
pvRefreshTimer:start()
S.previewWindowOpen = true
savePrefs()
saveAll()
previewDlg:show{ wait = false }
end
@ -1294,7 +1567,7 @@ openMainDialog = function()
mainAnimTimer = nil
mainRefreshTimer = nil
mainDlg = nil
savePrefs()
saveAll()
end
}
mainDlg = dlg
@ -1321,11 +1594,171 @@ openMainDialog = function()
text = "Config",
onclick = function()
local d = Dialog{ title = "Config" }
--------------------------------------------------------
-- Presets section
--------------------------------------------------------
d:separator{ text = "Presets" }
local presetNames = getAllPresetNames()
d:combobox{ id = "presetList", label = "Preset:", option = S.currentPreset, options = presetNames }
d:button{ id = "btnPresetSave", text = "Save", onclick = function()
local saveDlg = Dialog{ title = "Save Preset" }
saveDlg:entry{ id = "name", label = "Name:", text = S.currentPreset }
saveDlg:button{ id = "ok", text = "Save" }
saveDlg:button{ text = "Cancel" }
saveDlg:show()
if saveDlg.data.ok then
local pname = saveDlg.data.name
if pname and pname ~= "" then
-- Check if name exists and differs from current
if pname ~= S.currentPreset then
-- Check if preset already exists
local exists = false
for _, n in ipairs(knownPresetNames) do
if n == pname then exists = true; break end
end
if exists then
local confirm = app.alert{
title = "Overwrite Preset",
text = "Preset '" .. pname .. "' already exists. Overwrite?",
buttons = { "Overwrite", "Cancel" }
}
if confirm ~= 1 then return end
end
end
S.currentPreset = pname
-- Add to known names if new
local found = false
for _, n in ipairs(knownPresetNames) do
if n == pname then found = true; break end
end
if not found then
table.insert(knownPresetNames, pname)
end
-- Associate with current file
if app.sprite and app.sprite.filename and app.sprite.filename ~= "" then
filePresetMap[app.sprite.filename] = pname
end
saveAll()
-- Update combobox
pcall(function()
d:modify{ id = "presetList", options = getAllPresetNames(), option = S.currentPreset }
end)
end
end
end }
d:button{ id = "btnPresetLoad", text = "Load", onclick = function()
local selectedPreset = d.data.presetList
if selectedPreset and selectedPreset ~= "" then
-- Save current preset first
savePreset(S.currentPreset)
-- Load the new preset
loadPreset(selectedPreset)
S.currentPreset = selectedPreset
-- Associate with current file
if app.sprite and app.sprite.filename and app.sprite.filename ~= "" then
filePresetMap[app.sprite.filename] = selectedPreset
end
saveMaster()
-- Close config dialog and reopen main dialog
d:close()
if mainDlg then
pcall(function() mainDlg:close() end)
end
openMainDialog()
end
end }
d:button{ id = "btnPresetClone", text = "Clone", onclick = function()
local cloneDlg = Dialog{ title = "Clone Preset" }
cloneDlg:entry{ id = "name", label = "New name:", text = S.currentPreset .. "_copy" }
cloneDlg:button{ id = "ok", text = "Clone" }
cloneDlg:button{ text = "Cancel" }
cloneDlg:show()
if cloneDlg.data.ok then
local newName = cloneDlg.data.name
if newName and newName ~= "" then
-- Check if name already exists
local exists = false
for _, n in ipairs(knownPresetNames) do
if n == newName then exists = true; break end
end
if exists then
app.alert("Preset '" .. newName .. "' already exists.")
return
end
-- Save current state as the new preset
savePreset(newName)
table.insert(knownPresetNames, newName)
S.currentPreset = newName
-- Associate with current file
if app.sprite and app.sprite.filename and app.sprite.filename ~= "" then
filePresetMap[app.sprite.filename] = newName
end
saveMaster()
-- Update combobox
pcall(function()
d:modify{ id = "presetList", options = getAllPresetNames(), option = S.currentPreset }
end)
end
end
end }
d:button{ id = "btnPresetDelete", text = "Delete", onclick = function()
local selectedPreset = d.data.presetList
if not selectedPreset or selectedPreset == "" then return end
if selectedPreset == "Default" then
app.alert("Cannot delete the Default preset.")
return
end
local confirm = app.alert{
title = "Delete Preset",
text = "Delete preset '" .. selectedPreset .. "'?",
buttons = { "Delete", "Cancel" }
}
if confirm ~= 1 then return end
-- Delete the preset file
local path = app.fs.joinPath(PRESETS_DIR, selectedPreset .. ".ini")
os.remove(path)
-- Remove from known names
for i, n in ipairs(knownPresetNames) do
if n == selectedPreset then
table.remove(knownPresetNames, i)
break
end
end
-- Remove file associations pointing to this preset
for filepath, preset in pairs(filePresetMap) do
if preset == selectedPreset then
filePresetMap[filepath] = nil
end
end
-- If current preset was deleted, switch to Default
if S.currentPreset == selectedPreset then
loadPreset("Default")
S.currentPreset = "Default"
if app.sprite and app.sprite.filename and app.sprite.filename ~= "" then
filePresetMap[app.sprite.filename] = nil
end
end
saveMaster()
-- Update combobox
pcall(function()
d:modify{ id = "presetList", options = getAllPresetNames(), option = S.currentPreset }
end)
end }
--------------------------------------------------------
-- Animations section
--------------------------------------------------------
d:separator{ text = "Animations" }
d:number{ id = "tileW", label = "Sprite W:", text = tostring(S.tileW), decimals = 0 }
d:number{ id = "tileH", label = "Sprite H:", text = tostring(S.tileH), decimals = 0 }
d:slider{ id = "animSpeed", label = "Speed (ms):", min = 50, max = 1000, value = S.animSpeed }
d:check{ id = "useBgColor", text = "Solid background", selected = S.useBgColor }
d:color{ id = "bgColor", label = "Bg Color:", color = S.bgColor }
--------------------------------------------------------
-- Optimization section
--------------------------------------------------------
d:separator{ text = "Optimization" }
d:number{ id = "gbTileW", label = "Tile W:", text = tostring(S.gbTileW), decimals = 0 }
d:number{ id = "gbTileH", label = "Tile H:", text = tostring(S.gbTileH), decimals = 0 }
@ -1336,6 +1769,10 @@ openMainDialog = function()
d:color{ id = "silhouetteColor", label = "Silhouette:", color = silColor }
d:entry{ id = "layerName", label = "Layer name:", text = S.gbLayerName }
d:check{ id = "alwaysOverwrite", text = "Always overwrite layer", selected = S.gbAlwaysOverwrite }
--------------------------------------------------------
-- Preview section
--------------------------------------------------------
d:separator{ text = "Preview" }
d:combobox{ id = "previewLayout", label = "Layout:", option = S.previewLayout, options = {"auto", "fixedCols", "fixedRows"} }
d:number{ id = "previewLayoutValue", label = "Value:", text = tostring(S.previewLayoutValue), decimals = 0 }
@ -1415,7 +1852,7 @@ openMainDialog = function()
for i, n in ipairs(S.animNames) do
if n == name then S.currentAnim = i; break end
end
savePrefs()
saveAll()
dlg:close()
openMainDialog()
end
@ -1439,7 +1876,7 @@ openMainDialog = function()
table.remove(S.animNames, S.currentAnim)
S.currentAnim = math.min(S.currentAnim, math.max(1, #S.animNames))
S.selectedFrame = 0
savePrefs()
saveAll()
dlg:close()
openMainDialog()
end
@ -1468,7 +1905,7 @@ openMainDialog = function()
for i, n in ipairs(S.animNames) do
if n == newName then S.currentAnim = i; break end
end
savePrefs()
saveAll()
dlg:close()
openMainDialog()
end
@ -1507,7 +1944,7 @@ openMainDialog = function()
for i, n in ipairs(S.animNames) do
if n == cloneName then S.currentAnim = i; break end
end
savePrefs()
saveAll()
dlg:close()
openMainDialog()
end
@ -1882,7 +2319,7 @@ openMainDialog = function()
table.remove(S.animNames, S.currentAnim)
S.currentAnim = math.min(S.currentAnim, math.max(1, #S.animNames))
S.selectedFrame = 0
savePrefs()
saveAll()
dlg:close()
openMainDialog()
return
@ -2484,9 +2921,6 @@ openMainDialog = function()
end
table.insert(candidates, { tileIdx = idx, opaque = isOpaque })
end
-- Also check silhouette areas in non-compress mode is N/A here,
-- but in compress mode with silhouette there's only one tile per grid cell
else
-- Non-compress mode: candidate-based click like Occurrences canvas
for i, tile in ipairs(S.gbTiles) do
@ -2726,6 +3160,19 @@ openMainDialog = function()
end
pcall(function() dlg:repaint() end)
end
-- Auto-detect file change (also in main dialog timer)
local currentFile = app.sprite and app.sprite.filename or ""
if currentFile ~= lastSpriteFilename then
lastSpriteFilename = currentFile
local presetForFile = filePresetMap[currentFile] or "Default"
if presetForFile ~= S.currentPreset then
loadPreset(presetForFile)
S.currentPreset = presetForFile
pcall(function() dlg:close() end)
openMainDialog()
end
end
end
}
mainRefreshTimer:start()
@ -2743,6 +3190,12 @@ local function run()
app.alert("No sprite is open.")
return
end
-- Toggle: if already running, close everything
if previewDlg then
pcall(function() previewDlg:close() end)
if mainDlg then pcall(function() mainDlg:close() end) end
return
end
S.currentTab = "Animations"
refreshSource()
-- Always open preview window on launch