-- @description Purge Inactive FX (bypassed/offline) with ReaImGui + per-row A/B toggle and GUI toggle button -- @author -- Johannes Ahlberg -- @version 3.2 -- @about -- English UX. Scans Master, Tracks and Item Takes for inactive FX (bypassed or offline). -- • Tick boxes = which FX to DELETE. If none are ticked → ALL currently INACTIVE will be deleted. -- • "GUI" button per row toggles the plugin window (open/close) and lights up when open. -- • Rightmost per-row toggle = (de)activate FX live for A/B (enabled=true + offline=false when ON). -- • "Activate/Deactivate Selected" buttons for bulk toggling of ticked items. -- • Window closes on X and after Delete. Uses ListBox + dynamic column layout. -- ========================================================= -- = KOMMENTARER (SV) = -- = • Listar alla FX som är inaktiva vid start. = -- = • "Delete?" = markerar för radering. = -- = • "GUI"-knapp på varje rad öppnar/stänger fönster = -- = och lyser grönt när fönstret är öppet. = -- = • FIX: Kombinerar flagga 2 (stäng) & 3 (öppna). = -- = • Frågar REAPER direkt om GUI-status varje frame. = -- = • Egen färgkonverterare för maximal kompabilitet. = -- = • Per-rad "Active"-toggle slår på/av FX direkt. = -- = • Radering: Om några är valda → endast valda. = -- = Om inga valda → alla som FORTF. inakt. = -- = • Knappar för "Activate/Deactivate Selected". = -- = • Efter Delete stängs fönstret. = -- = • Dynamic layout: kolumner beräknas efter bredd. = -- ========================================================= -- =========== -- Säkerhetskoll för ReaImGui -- =========== if not reaper or not reaper.ImGui_CreateContext then reaper.ShowMessageBox( "ReaImGui is not available. Please install/enable ReaImGui (bundled with modern REAPER or via ReaPack).", "Missing ReaImGui", 0 ) return end -- =========== -- Hjälpfunktioner (SV) -- =========== -- (SV) Konvertera RGBA (0-1) till ett packat heltal (U32) för maximal kompatibilitet. local function PackColorU32(r, g, b, a) local R = math.floor(r * 255 + 0.5) local G = math.floor(g * 255 + 0.5) local B = math.floor(b * 255 + 0.5) local A = math.floor(a * 255 + 0.5) -- ImGui's färgformat är AABBGGRR return R + G * 256 + B * 65536 + A * 16777216 end local function get_track_name(tr) if tr == reaper.GetMasterTrack(0) then return "MASTER" end local ok, name = reaper.GetSetMediaTrackInfo_String(tr, "P_NAME", "", false) if ok and name ~= "" then return name end local idx = reaper.GetMediaTrackInfo_Value(tr, "IP_TRACKNUMBER") return string.format("Track %d", idx) end local function get_take_name(tk) local _, nm = reaper.GetSetMediaItemTakeInfo_String(tk, "P_NAME", "", false) if nm and nm ~= "" then return nm end return "Unnamed take" end local function get_fx_name_track(tr, fxidx) local rv, buf = reaper.TrackFX_GetFXName(tr, fxidx, "") return rv and buf or ("FX " .. tostring(fxidx)) end local function get_fx_name_take(tk, fxidx) local rv, buf = reaper.TakeFX_GetFXName(tk, fxidx, "") return rv and buf or ("FX " .. tostring(fxidx)) end -- (SV) Status → text local function status_reason(enabled, offline) if enabled and not offline then return "active" end if (not enabled) and offline then return "bypassed + offline" end if not enabled then return "bypassed" end if offline then return "offline" end return "active" end -- (SV) Slå på/av FX och uppdatera postens status local function set_fx_active(entry, make_active) if entry.type == "take" then if make_active then reaper.TakeFX_SetEnabled(entry.take, entry.fx_index, true) reaper.TakeFX_SetOffline(entry.take, entry.fx_index, false) else reaper.TakeFX_SetEnabled(entry.take, entry.fx_index, false) end entry.enabled = reaper.TakeFX_GetEnabled(entry.take, entry.fx_index) entry.offline = reaper.TakeFX_GetOffline(entry.take, entry.fx_index) else if make_active then reaper.TrackFX_SetEnabled(entry.track, entry.fx_index, true) reaper.TrackFX_SetOffline(entry.track, entry.fx_index, false) else reaper.TrackFX_SetEnabled(entry.track, entry.fx_index, false) end entry.enabled = reaper.TrackFX_GetEnabled(entry.track, entry.fx_index) entry.offline = reaper.TrackFX_GetOffline(entry.track, entry.fx_index) end entry.reason = status_reason(entry.enabled, entry.offline) reaper.UpdateArrange() end -- Samla in alla inaktiva FX (initialt) local function collect_inactive_fx() local list = {} -- Master do local tr = reaper.GetMasterTrack(0) local fx_count = reaper.TrackFX_GetCount(tr) for fx = 0, fx_count-1 do local enabled = reaper.TrackFX_GetEnabled(tr, fx) local offline = reaper.TrackFX_GetOffline(tr, fx) if (not enabled) or offline then list[#list+1] = { type = "master", track = tr, fx_index = fx, name = get_fx_name_track(tr, fx), track_name = "MASTER", take_name = nil, enabled = enabled, offline = offline, reason = status_reason(enabled, offline), selected = false } end end end -- Tracks local track_cnt = reaper.CountTracks(0) for i = 0, track_cnt-1 do local tr = reaper.GetTrack(0, i) local tr_name = get_track_name(tr) local fx_count = reaper.TrackFX_GetCount(tr) for fx = 0, fx_count-1 do local enabled = reaper.TrackFX_GetEnabled(tr, fx) local offline = reaper.TrackFX_GetOffline(tr, fx) if (not enabled) or offline then list[#list+1] = { type = "track", track = tr, fx_index = fx, name = get_fx_name_track(tr, fx), track_name = tr_name, take_name = nil, enabled = enabled, offline = offline, reason = status_reason(enabled, offline), selected = false } end end end -- Item Takes local item_cnt = reaper.CountMediaItems(0) for i = 0, item_cnt-1 do local it = reaper.GetMediaItem(0, i) local takes = reaper.GetMediaItemNumTakes(it) or 0 for t = 0, takes-1 do local tk = reaper.GetMediaItemTake(it, t) if tk then local tk_name = get_take_name(tk) local fx_count = reaper.TakeFX_GetCount(tk) for fx = 0, fx_count-1 do local enabled = reaper.TakeFX_GetEnabled(tk, fx) local offline = reaper.TakeFX_GetOffline(tk, fx) if (not enabled) or offline then local tr = reaper.GetMediaItem_Track(it) local tr_name = tr and get_track_name(tr) or "Track ?" list[#list+1] = { type = "take", track = tr, take = tk, fx_index = fx, name = get_fx_name_take(tk, fx), track_name = tr_name, take_name = tk_name, enabled = enabled, offline = offline, reason = status_reason(enabled, offline), selected = false } end end end end end return list end -- Radera: om några är markerade → bara de; annars → alla som FORTFARANDE är inaktiva. local function delete_inactive_fx(entries) local any_selected = false for _, e in ipairs(entries) do if e.selected then any_selected = true break end end local groups_track, groups_take = {}, {} local total = 0 for _, e in ipairs(entries) do local is_inactive_now = (not e.enabled) or e.offline local include = any_selected and e.selected or (not any_selected and is_inactive_now) if include then total = total + 1 if e.type == "master" or e.type == "track" then local t = groups_track[e.track] ; if not t then t = {} ; groups_track[e.track] = t end t[#t+1] = e.fx_index elseif e.type == "take" then local t = groups_take[e.take] ; if not t then t = {} ; groups_take[e.take] = t end t[#t+1] = e.fx_index end end end if total == 0 then return {deleted = 0, total = 0} end reaper.Undo_BeginBlock2(0) local deleted = 0 for tr, arr in pairs(groups_track) do table.sort(arr, function(a,b) return a > b end) for _, fxidx in ipairs(arr) do if reaper.ValidatePtr(tr, "MediaTrack*") then if reaper.TrackFX_Delete(tr, fxidx) then deleted = deleted + 1 end end end end for tk, arr in pairs(groups_take) do table.sort(arr, function(a,b) return a > b end) for _, fxidx in ipairs(arr) do if reaper.ValidatePtr(tk, "MediaItem_Take*") then if reaper.TakeFX_Delete(tk, fxidx) then deleted = deleted + 1 end end end end reaper.Undo_EndBlock2(0, "Purge inactive FX", -1) reaper.UpdateArrange() return {deleted = deleted, total = total} end -- =========== -- GUI / ReaImGui (ListBox + dynamiska kolumner) -- =========== local ctx = reaper.ImGui_CreateContext('Purge Inactive FX') local FONT = reaper.ImGui_CreateFont('sans-serif', 15) reaper.ImGui_Attach(ctx, FONT) local state = { entries = {}, initialized = false, select_all_toggle = false, filter_text = "", request_close = false, -- stäng efter Delete } local function pass_filter(e, f) if not f or f == "" then return true end f = f:lower() local function has(s) return s and s:lower():find(f, 1, true) ~= nil end return has(e.name) or has(e.track_name) or has(e.take_name) or has(e.reason) end local function refresh_entries() state.entries = collect_inactive_fx() end local function safe_destroy_ctx() if reaper.ImGui_DestroyContext then reaper.ImGui_DestroyContext(ctx) end end -- (SV) Rita rubrikrad med samma kolumnlogik som listan local function draw_headers(total_w) -- Kolumn-ankare local col_name = 80 local col_loc = math.floor(total_w * 0.40) local col_gui = math.floor(total_w * 0.68) local col_reason = math.floor(total_w * 0.78) local col_toggle = math.max(math.floor(total_w - 70), col_reason + 120) reaper.ImGui_Text(ctx, "Delete?") reaper.ImGui_SameLine(ctx, col_name) reaper.ImGui_Text(ctx, "FX Name") reaper.ImGui_SameLine(ctx, col_loc) reaper.ImGui_Text(ctx, "Location") reaper.ImGui_SameLine(ctx, col_gui) reaper.ImGui_Text(ctx, "Show") reaper.ImGui_SameLine(ctx, col_reason) reaper.ImGui_Text(ctx, "State") reaper.ImGui_SameLine(ctx, col_toggle) reaper.ImGui_Text(ctx, "Active") end local function loop() if not state.initialized then refresh_entries() state.initialized = true end if state.request_close then safe_destroy_ctx() return end reaper.ImGui_SetNextWindowSize(ctx, 1050, 640, reaper.ImGui_Cond_FirstUseEver()) local visible, open = reaper.ImGui_Begin(ctx, 'Purge Inactive FX', true, reaper.ImGui_WindowFlags_NoCollapse()) if visible then reaper.ImGui_Text(ctx, "Tick boxes to DELETE. If none are ticked, ALL currently INACTIVE will be deleted. Use the Active switch to A/B.") if #state.entries == 0 then reaper.ImGui_Separator(ctx) reaper.ImGui_Text(ctx, "No inactive FX found.") reaper.ImGui_Spacing(ctx) if reaper.ImGui_Button(ctx, "Close", 100, 0) then open = false end reaper.ImGui_End(ctx) else reaper.ImGui_Separator(ctx) reaper.ImGui_PushItemWidth(ctx, 320) local changed, txt = reaper.ImGui_InputText(ctx, "Filter (name/track/take/reason)", state.filter_text or "") if changed then state.filter_text = txt end reaper.ImGui_PopItemWidth(ctx) reaper.ImGui_SameLine(ctx) local chk_changed, togg = reaper.ImGui_Checkbox(ctx, "Select/Deselect all (visible)", state.select_all_toggle) if chk_changed then state.select_all_toggle = togg for _, e in ipairs(state.entries) do if pass_filter(e, state.filter_text) then e.selected = togg end end end reaper.ImGui_SameLine(ctx) if reaper.ImGui_Button(ctx, "Refresh", 90, 0) then refresh_entries() end reaper.ImGui_Separator(ctx) -- Hämta tillgänglig bredd och rita rubriker local list_w = select(1, reaper.ImGui_GetContentRegionAvail(ctx)) draw_headers(list_w) -- Själva listan if reaper.ImGui_BeginListBox(ctx, "##fx_list", -1, 460) then local start_x = reaper.ImGui_GetCursorPosX(ctx) for i, e in ipairs(state.entries) do if pass_filter(e, state.filter_text) then local row_start_x = reaper.ImGui_GetCursorPosX(ctx) local avail_w = select(1, reaper.ImGui_GetContentRegionAvail(ctx)) local col_name = row_start_x - start_x + 80 local col_loc = row_start_x - start_x + math.floor(avail_w * 0.40) local col_gui = row_start_x - start_x + math.floor(avail_w * 0.68) local col_reason = row_start_x - start_x + math.floor(avail_w * 0.78) local col_toggle = row_start_x - start_x + math.max(math.floor(avail_w - 70), col_reason + 120) -- Delete-kryss local c_changed, c_val = reaper.ImGui_Checkbox(ctx, "##del_"..i, e.selected) if c_changed then e.selected = c_val end -- FX-namn reaper.ImGui_SameLine(ctx, col_name) reaper.ImGui_Text(ctx, e.name or "(FX)") -- Plats reaper.ImGui_SameLine(ctx, col_loc) local loc if e.type == "master" then loc = "MASTER" elseif e.type == "track" then loc = string.format("%s (FX #%d)", e.track_name or "Track", e.fx_index+1) else loc = string.format("%s / Take: %s (FX #%d)", e.track_name or "Track", e.take_name or "Take", e.fx_index+1) end reaper.ImGui_Text(ctx, loc) -- GUI-knapp med färgindikator reaper.ImGui_SameLine(ctx, col_gui) local is_gui_open_now if e.type == 'take' then is_gui_open_now = reaper.TakeFX_GetOpen(e.take, e.fx_index) else is_gui_open_now = reaper.TrackFX_GetOpen(e.track, e.fx_index) end if is_gui_open_now then local color = PackColorU32(0.2, 0.7, 0.3, 1.0) reaper.ImGui_PushStyleColor(ctx, reaper.ImGui_Col_Button(), color) end if reaper.ImGui_Button(ctx, "GUI##"..i, 50, 0) then -- KORRIGERING: Använd den metod som fungerar för respektive tillstånd. local show_flag if is_gui_open_now then show_flag = 2 -- Använd "toggle" för att STÄNGA (mer pålitligt) else show_flag = 3 -- Använd "open" för att ÖPPNA (mer pålitligt) end if e.type == 'take' and reaper.ValidatePtr(e.take, "MediaItem_Take*") then reaper.TakeFX_Show(e.take, e.fx_index, show_flag) elseif (e.type == 'track' or e.type == 'master') and reaper.ValidatePtr(e.track, "MediaTrack*") then reaper.TrackFX_Show(e.track, e.fx_index, show_flag) end end if is_gui_open_now then reaper.ImGui_PopStyleColor(ctx, 1) end -- Reason (status) reaper.ImGui_SameLine(ctx, col_reason) reaper.ImGui_Text(ctx, e.reason or "") -- Aktiv-toggle (höger) reaper.ImGui_SameLine(ctx, col_toggle) local is_active = (e.enabled and not e.offline) local sw_changed, sw_val = reaper.ImGui_Checkbox(ctx, "##act_"..i, is_active) if sw_changed then set_fx_active(e, sw_val) end end end reaper.ImGui_EndListBox(ctx) end reaper.ImGui_Separator(ctx) if reaper.ImGui_Button(ctx, "Delete (selected; none = ALL inactive)", 290, 0) then delete_inactive_fx(state.entries) state.request_close = true end reaper.ImGui_SameLine(ctx) if reaper.ImGui_Button(ctx, "Close", 120, 0) then open = false end reaper.ImGui_SameLine(ctx, 0, 20) if reaper.ImGui_Button(ctx, "Activate Selected", 140, 0) then reaper.Undo_BeginBlock2(0) for _, e in ipairs(state.entries) do if e.selected then set_fx_active(e, true) end end reaper.Undo_EndBlock2(0, "Activate selected FX", -1) end reaper.ImGui_SameLine(ctx) if reaper.ImGui_Button(ctx, "Deactivate Selected", 150, 0) then reaper.Undo_BeginBlock2(0) for _, e in ipairs(state.entries) do if e.selected then set_fx_active(e, false) end end reaper.Undo_EndBlock2(0, "Deactivate selected FX", -1) end reaper.ImGui_End(ctx) end end if open then reaper.defer(loop) else safe_destroy_ctx() end end reaper.defer(loop)