From 9e3a2702dc74be39f62678b97aa243a7ce768885 Mon Sep 17 00:00:00 2001 From: Cidwel Highwind Date: Fri, 3 Apr 2026 19:56:31 +0200 Subject: [PATCH] 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 --- aniphallow.lua | 673 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 563 insertions(+), 110 deletions(-) diff --git a/aniphallow.lua b/aniphallow.lua index c817b77..29a44fe 100644 --- a/aniphallow.lua +++ b/aniphallow.lua @@ -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