--[[-------------------------------------------------------------------------- ManiaxScroll — mouse wheel everywhere, the way retail does it. On 3.3.5a the wheel works in a handful of places and nowhere else. Grepping the stock FrameXML for OnMouseWheel returns eleven files: the quest log, reputation, skills, gossip, friends, channels, the binding UI, the hybrid scroll template (achievements, LFD), the chat frames, and two templates. Everything else in the game is dead to the wheel — the auction house, mail, the trade skill window, the trainer, macros, the guild bank, the calendar, the world map, the merchant, books, the dressing room, the talent panel. APPROACH: SWEEP, DO NOT LIST The obvious implementation is a table of every panel's scroll frame. It is also wrong: it silently misses anything not in the table - other add-ons' windows, load-on-demand panels nobody remembered, and anything added later. A list that is 90% complete reads exactly like a system that works. So this sweeps EnumerateFrames() instead and wires every ScrollFrame it can find. EnumerateFrames exists on 3.3.5a - four add-ons in this pack already use it. Cost is one walk of the frame list, run when panels appear rather than continuously. WHERE THE SWEEP DOES NOT REACH, AND WHY THOSE ARE LISTS Four systems are not scroll frames at all and cannot be found by type: - Paged panels (merchant, spellbook, mail, books, companions, the auction browse list) are two buttons and a page number. - Models (character sheet, dressing room, pet, stable, tabard, inspect) zoom by moving the model, not by scrolling anything. - The world map zooms by changing which map is displayed. - The minimap zooms through its own two buttons. These are named lists on purpose. For models the sweep is not merely useless but actively harmful: WorldMapPing and ItemAnimTemplate are Model frames too, and enabling the wheel on WorldMapPing would eat the wheel over the world map - the exact frame we want it on. A type sweep cannot tell those apart. A name list can, and every entry is looked up with _G and skipped when absent, so a panel that does not exist on this client costs nothing. WHAT IT WILL NOT TOUCH Any frame that already has an OnMouseWheel script is left alone. Auctionator, ElvUI and several others handle their own wheel, and replacing a working handler with a generic one is how you break a window that was fine. The handler is only installed where there is nothing. It also does not patch a widget metatable. Adding a method to getmetatable(CreateFrame("ScrollFrame")).__index would reach every frame type in the game - frame types share one metatable on 3.3.5a - and that mistake has already broken this client's keyboard twice. Everything here is per frame, at the call site. FAUX AND REAL SCROLL FRAMES ARE BOTH DRIVEN THROUGH THE SCROLLBAR A FauxScrollFrame does not scroll - it redraws a fixed set of rows from an offset, and the offset comes from the scrollbar's value. A real ScrollFrame moves its child. Setting the scrollbar's value drives both, because in the faux case OnValueChanged fires the frame's OnVerticalScroll, which is what every panel's update function already hangs off. So there is one code path, not two. Finding that scrollbar takes three tries, not one. FauxScrollFrame and UIPanelScrollFrameTemplate name it "ScrollBar", which is what a _G lookup finds. HybridScrollFrame stores it as frame.scrollBar with no global name at all - see hybridscrollframe.lua, which reads self.scrollBar throughout - and third-party frames use frame.ScrollBar. Looking in only the first place is how a window ends up wired to a handler that does nothing. ----------------------------------------------------------------------------]] local ADDON = "ManiaxScroll" ManiaxScrollDB = ManiaxScrollDB or {} local DEFAULTS = { lines = 3, -- wheel notches to scroll per click, where a step is known minimap = true, -- wheel to zoom the minimap paged = true, -- wheel to turn pages on merchant / mail / books / ... pageAtEnd = true, -- wheeling past the end of a paged list turns the page models = true, -- wheel to zoom character / dressing room / pet models worldmap = true, -- wheel to zoom the world map out and back in message = true, -- wheel over ScrollingMessageFrames (guild bank log, ...) modifiers = true, -- shift = one page, ctrl = jump to top / bottom } -- Public handle, so ManiaxScroll_Options.lua can read and write settings -- without duplicating the defaults or the re-wiring rules. ManiaxScroll = ManiaxScroll or {} local NS = ManiaxScroll NS.DEFAULTS = DEFAULTS -- SavedVariables are restored before an add-on's Lua runs, so this is already -- the player's table here; VARIABLES_LOADED only fills in missing keys. local db = ManiaxScrollDB -- Weak keys throughout: a frame that is destroyed drops out on its own rather -- than being held alive by our bookkeeping. -- -- Two sets, not one, and the difference matters. `examined` is the sweep's own -- bookkeeping - every frame it has looked at and ruled out, so it does not pay -- for a GetObjectType call on the same Button ten thousand times. `wired` is -- only the frames this add-on installed a handler on. -- -- Collapsing them breaks the four named lists: MerchantFrame, Minimap and -- WorldMapButton are all ordinary frames, so the sweep rules them out and -- would mark them - and wirePaged, wireMinimap and wireMap all skip a frame -- that is already marked. The whole non-scroll half of the add-on would go -- quiet, with nothing thrown and nothing to see. local WEAK = { __mode = "k" } local examined = setmetatable({}, WEAK) -- frame -> true, the sweep is done with it local wired = setmetatable({}, WEAK) -- frame -> true, we installed a handler local barCache = setmetatable({}, WEAK) -- frame -> its scrollbar local pageOwner = setmetatable({}, WEAK) -- scroll frame -> {next=, prev=} local modelZoom = setmetatable({}, WEAK) -- model frame -> current offset local counts = { scroll = 0, message = 0, paged = 0, model = 0, other = 0 } local function opt(key) local v = db and db[key] if v == nil then return DEFAULTS[key] end return v end NS.Get = opt local function say(msg) DEFAULT_CHAT_FRAME:AddMessage("|cff00ff00ManiaxScroll|r " .. msg) end NS.Say = say -- IsEnabled() returns 1 or 0 on 3.3.5a, not a boolean - Blizzard's own code -- compares against both (see inboxframe.lua and channelframe.lua). Third-party -- buttons occasionally return nil instead, so test for both falsey forms. local function usable(btn) if not btn or not btn.IsShown or not btn:IsShown() then return false end local e = btn.IsEnabled and btn:IsEnabled() return e and e ~= 0 and true or false end --------------------------------------------------------------------------- -- Finding the scrollbar. --------------------------------------------------------------------------- local function findBar(frame) local name = frame.GetName and frame:GetName() if name then local b = _G[name .. "ScrollBar"] if b and b.GetValue then return b end end local b = frame.scrollBar or frame.ScrollBar if b and b.GetValue then return b end -- Last resort: an unnamed slider parented straight to the scroll frame. if frame.GetChildren then local kids = { frame:GetChildren() } for i = 1, #kids do local c = kids[i] if c and c.GetObjectType then local ok, kind = pcall(c.GetObjectType, c) if ok and kind == "Slider" then return c end end end end return nil end --------------------------------------------------------------------------- -- The scroll frame handler. -- -- delta is +1 for up and -1 for down. Scrollbar values run the other way - -- larger value means further down the list - so the sign is inverted. --------------------------------------------------------------------------- local function turnPage(frame, delta) local page = pageOwner[frame] if not page or not opt("paged") or not opt("pageAtEnd") then return false end local btn = (delta > 0) and page.prev or page.next if usable(btn) then btn:Click(); return true end return false end local function onWheel(self, delta) local mods = opt("modifiers") local shift = mods and IsShiftKeyDown and IsShiftKeyDown() local ctrl = mods and IsControlKeyDown and IsControlKeyDown() local bar = barCache[self] if bar and bar.GetValue then -- GetValueStep is the row height for a FauxScrollFrame and the pixel step -- for a real one. Where it is zero or missing, fall back to something that -- moves a visible amount rather than doing nothing. local step = 0 if bar.GetValueStep then step = bar:GetValueStep() or 0 end if step <= 0 then step = 16 end local lo, hi = 0, 0 if bar.GetMinMaxValues then lo, hi = bar:GetMinMaxValues() end lo, hi = lo or 0, hi or 0 local value = bar:GetValue() or 0 -- Already against the stop, and this list is one page of a paged panel: -- turn the page instead of doing nothing. This is what makes the auction -- house and a book read continuously rather than in 8-row chunks. if (delta < 0 and value >= hi - 0.5) or (delta > 0 and value <= lo + 0.5) then if turnPage(self, delta) then return end end local target if ctrl then target = (delta > 0) and lo or hi else local amount = step * opt("lines") if shift then -- One screenful. The frame's own height over the row height is the -- row count for a faux frame and a sane pixel page for a real one. local h = (self.GetHeight and self:GetHeight()) or 0 local pageSize = (h > step) and (math.floor(h / step) * step) or (step * 10) amount = pageSize end target = value - (delta * amount) end if target < lo then target = lo end if hi > lo and target > hi then target = hi end bar:SetValue(target) return end -- No scrollbar: a plain ScrollFrame moved directly. if self.GetVerticalScroll and self.SetVerticalScroll then local cur = self:GetVerticalScroll() or 0 local max = (self.GetVerticalScrollRange and self:GetVerticalScrollRange()) or 0 if (delta < 0 and cur >= max - 0.5) or (delta > 0 and cur <= 0.5) then if turnPage(self, delta) then return end end local target if ctrl then target = (delta > 0) and 0 or max else local amount = 16 * opt("lines") if shift then local h = (self.GetHeight and self:GetHeight()) or 0 if h > 0 then amount = h end end target = cur - (delta * amount) end if target < 0 then target = 0 end if max > 0 and target > max then target = max end self:SetVerticalScroll(target) end end --------------------------------------------------------------------------- -- ScrollingMessageFrame handler. -- -- These are not ScrollFrames - GetObjectType returns "ScrollingMessageFrame" -- and they have no scrollbar and no scroll range. They scroll by line, with -- their own methods. The chat frames already carry Blizzard's handler -- (FloatingChatFrame_OnMouseScroll) and are therefore skipped; what this -- reaches is everything else, the guild bank log first among them. -- -- Shift jumping to the ends matches what stock chat does not do here but -- retail does, and is the only sensible use of a modifier on a line buffer. --------------------------------------------------------------------------- local function onWheelMessage(self, delta) if not opt("message") then return end local shift = opt("modifiers") and IsShiftKeyDown and IsShiftKeyDown() if delta > 0 then if shift and self.ScrollToTop then self:ScrollToTop() elseif self.ScrollUp then for _ = 1, opt("lines") do self:ScrollUp() end end else if shift and self.ScrollToBottom then self:ScrollToBottom() elseif self.ScrollDown then for _ = 1, opt("lines") do self:ScrollDown() end end end end --------------------------------------------------------------------------- -- Wiring one frame found by the sweep. --------------------------------------------------------------------------- local function alreadyHandled(frame) if not frame.GetScript then return false end local ok, handler = pcall(frame.GetScript, frame, "OnMouseWheel") return ok and handler ~= nil end local function wire(frame) if examined[frame] or wired[frame] then return false end local ok, kind = pcall(frame.GetObjectType, frame) if not ok then return false end if kind ~= "ScrollFrame" and kind ~= "ScrollingMessageFrame" then examined[frame] = true -- wrong type, and that will never change return false end -- Leave anything that already handles the wheel completely alone. if alreadyHandled(frame) then examined[frame] = true -- remember, so it is not re-examined every sweep return false end if kind == "ScrollingMessageFrame" then -- Not marked when the option is off, so switching it on later still picks -- the frame up on the next sweep. if not opt("message") then return false end if not frame.ScrollUp then examined[frame] = true; return false end pcall(frame.EnableMouseWheel, frame, true) pcall(frame.SetScript, frame, "OnMouseWheel", onWheelMessage) examined[frame], wired[frame] = true, true counts.message = counts.message + 1 return true end -- A scroll frame with neither a scrollbar nor a scroll range has nothing to -- drive, and enabling the wheel on it would only swallow the event from -- whatever is underneath. local bar = findBar(frame) if not bar and not frame.SetVerticalScroll then examined[frame] = true return false end if bar then barCache[frame] = bar end pcall(frame.EnableMouseWheel, frame, true) pcall(frame.SetScript, frame, "OnMouseWheel", onWheel) examined[frame], wired[frame] = true, true counts.scroll = counts.scroll + 1 return true end local function sweep() local added, frame = 0, EnumerateFrames() while frame do if wire(frame) then added = added + 1 end frame = EnumerateFrames(frame) end return added end --------------------------------------------------------------------------- -- Paged panels. Not scroll frames at all - two buttons and a page number - -- so the wheel has to be mapped onto the buttons instead. -- -- `list`, where present, is the scroll frame that shows one page of the same -- data. Wiring it as the page owner is what makes wheeling past the last row -- of an auction search fetch the next fifty results instead of stopping, and -- what turns a book's page when its text runs out. -- -- Every target is looked up by name and skipped when absent, so a panel that -- does not exist on this client, or is renamed, costs nothing. Six of these -- exist in the stock UI; five of them had no wheel at all. --------------------------------------------------------------------------- local PAGED = { { frame = "MerchantFrame", next = "MerchantNextPageButton", prev = "MerchantPrevPageButton" }, { frame = "SpellBookFrame", next = "SpellBookNextPageButton", prev = "SpellBookPrevPageButton" }, { frame = "InboxFrame", next = "InboxNextPageButton", prev = "InboxPrevPageButton" }, { frame = "ItemTextFrame", next = "ItemTextNextPageButton", prev = "ItemTextPrevPageButton", list = "ItemTextScrollFrame" }, { frame = "AuctionFrameBrowse",next = "BrowseNextPageButton", prev = "BrowsePrevPageButton", list = "BrowseScrollFrame" }, -- The companion (mount / critter) pages live inside the pet paperdoll tab. { frame = "PetPaperDollFrameCompanionFrame", next = "CompanionNextPageButton", prev = "CompanionPrevPageButton" }, } local function onWheelPaged(self, delta) if not opt("paged") then return end local page = pageOwner[self] if not page then return end -- Clicking the button rather than calling the page function keeps the click -- sound, the disabled state at the ends of the list, and whatever else the -- button's own handler does. local btn = (delta > 0) and page.prev or page.next if usable(btn) then btn:Click() end end local function wirePaged() if not opt("paged") then return end for _, spec in ipairs(PAGED) do local frame = _G[spec.frame] if frame then local nextBtn, prevBtn = _G[spec.next], _G[spec.prev] if nextBtn and prevBtn then local page = { next = nextBtn, prev = prevBtn } if not wired[frame] then pageOwner[frame] = page pcall(frame.EnableMouseWheel, frame, true) pcall(frame.SetScript, frame, "OnMouseWheel", onWheelPaged) wired[frame] = true counts.paged = counts.paged + 1 end -- The list inside the panel keeps its own scrolling handler; it only -- learns which buttons to press once it runs out of rows. if spec.list then local list = _G[spec.list] if list then pageOwner[list] = page wire(list) end end end end end end --------------------------------------------------------------------------- -- Model zoom: character sheet, dressing room, pet, stable, tabard, inspect. -- -- A Model is moved, not scrolled - SetPosition's first argument is depth, so -- a larger value brings the model toward the camera. There is no GetPosition -- on 3.3.5a, so the current offset is tracked here rather than read back, and -- it is dropped when the frame hides so the next open starts at default. -- -- Listed rather than swept: see the header. WorldMapPing is a Model too. --------------------------------------------------------------------------- local MODELS = { "CharacterModelFrame", -- paperdoll "DressUpModel", -- dressing room "PetModelFrame", -- pet paperdoll "PetStableModel", -- stable master "CompanionModelFrame", -- mounts and critters "TabardModel", -- guild tabard designer "InspectModelFrame", -- Blizzard_InspectUI, load on demand } local MODEL_STEP, MODEL_MIN, MODEL_MAX = 0.35, -1, 4 local function onWheelModel(self, delta) if not opt("models") then return end local z = (modelZoom[self] or 0) + (delta * MODEL_STEP) if z < MODEL_MIN then z = MODEL_MIN end if z > MODEL_MAX then z = MODEL_MAX end modelZoom[self] = z pcall(self.SetPosition, self, z, 0, 0) end local function wireModels() if not opt("models") then return end for _, name in ipairs(MODELS) do local model = _G[name] if model and not wired[model] and type(model.SetPosition) == "function" and not alreadyHandled(model) then pcall(model.EnableMouseWheel, model, true) pcall(model.SetScript, model, "OnMouseWheel", onWheelModel) -- Hooked, not set: the frame's own OnHide does real work on several of -- these and must keep running. if model.HookScript then pcall(model.HookScript, model, "OnHide", function(self) if modelZoom[self] then modelZoom[self] = nil pcall(self.SetPosition, self, 0, 0, 0) end end) end wired[model] = true counts.model = counts.model + 1 end end end --------------------------------------------------------------------------- -- World map zoom. -- -- 3.3.5a has no continuous map zoom; it has a hierarchy - cosmic, world, -- continent, zone - walked by SetMapZoom and by the zoom-out button, and -- there is no wheel anywhere in worldmapframe.xml. Wheeling down walks out. -- -- Walking back in has no button, because the client has no idea which zone -- you want. So the way out is remembered and the wheel retraces it, which -- makes zoom-out/zoom-in symmetrical the way it is on retail. Zooming by any -- other means invalidates nothing - the stack is only consulted, and a stale -- entry is discarded when the map it names is already displayed. -- -- The handler goes on WorldMapButton, the map surface, not on WorldMapFrame: -- the frame also contains the quest list, and that keeps its own scrolling. --------------------------------------------------------------------------- local mapStack = {} local function onWheelMap(self, delta) if not opt("worldmap") then return end if delta < 0 then local btn = _G["WorldMapZoomOutButton"] if usable(btn) then local c, z = GetCurrentMapContinent(), GetCurrentMapZone() btn:Click() -- Only record it if the click actually moved us; otherwise wheeling up -- again would "restore" the map we are already looking at. if GetCurrentMapContinent() ~= c or GetCurrentMapZone() ~= z then mapStack[#mapStack + 1] = { c = c, z = z } end end return end local top = mapStack[#mapStack] while top do mapStack[#mapStack] = nil if top.c ~= GetCurrentMapContinent() or top.z ~= GetCurrentMapZone() then SetMapZoom(top.c, top.z) return end top = mapStack[#mapStack] end end local function wireMap() if not opt("worldmap") then return end local btn = _G["WorldMapButton"] if not btn or wired[btn] or alreadyHandled(btn) then return end if not _G["WorldMapZoomOutButton"] then return end pcall(btn.EnableMouseWheel, btn, true) pcall(btn.SetScript, btn, "OnMouseWheel", onWheelMap) wired[btn] = true counts.other = counts.other + 1 end --------------------------------------------------------------------------- -- Minimap zoom. --------------------------------------------------------------------------- local function wireMinimap() if not opt("minimap") or not Minimap or wired[Minimap] then return end if alreadyHandled(Minimap) then wired[Minimap] = true; return end local zin, zout = _G["MinimapZoomIn"], _G["MinimapZoomOut"] if not zin or not zout then return end pcall(Minimap.EnableMouseWheel, Minimap, true) pcall(Minimap.SetScript, Minimap, "OnMouseWheel", function(self, delta) if not opt("minimap") then return end -- Same reasoning as the paged buttons: click, so the zoom clamps itself at -- both ends and the buttons grey out correctly. local btn = (delta > 0) and zin or zout if usable(btn) then btn:Click() end end) wired[Minimap] = true counts.other = counts.other + 1 end --------------------------------------------------------------------------- -- When to sweep. -- -- Frames are created lazily, and load-on-demand panels (auction house, trade -- skills, talents, guild bank, macros...) do not exist until their add-on -- loads. Three triggers, in order of how well they aim: -- -- ADDON_LOADED - exact, and covers every load-on-demand panel. -- ShowUIPanel hook - exact, and covers a panel whose frames are built the -- first time a player opens it, which no event reports. -- a backing-off timer - the safety net for anything built by neither. -- -- The timer starts at five seconds and doubles up to thirty when a pass finds -- nothing new, then resets the moment either exact trigger fires. Sweeping -- every frame in the game every five seconds forever is a cost with no payer. --------------------------------------------------------------------------- local function fullPass() local added = sweep() wirePaged() wireModels() wireMap() wireMinimap() return added end NS.FullPass = fullPass NS.Counts = function() return counts end local MIN_INTERVAL, MAX_INTERVAL = 5, 30 local interval, elapsed = MIN_INTERVAL, 0 local function urgent() interval, elapsed = MIN_INTERVAL, MIN_INTERVAL end NS.Rescan = function() local added = fullPass() urgent() return added end local f = CreateFrame("Frame", "ManiaxScrollFrame") f:RegisterEvent("VARIABLES_LOADED") f:RegisterEvent("PLAYER_ENTERING_WORLD") f:RegisterEvent("ADDON_LOADED") f:SetScript("OnEvent", function(self, event) if event == "VARIABLES_LOADED" then db = ManiaxScrollDB -- An install that ran /mscroll off carries enabled=false forever, and -- nothing reads it any more. Drop it, so the saved file cannot be read as -- an explanation for behaviour it no longer controls. db.enabled = nil for k, v in pairs(DEFAULTS) do if db[k] == nil then db[k] = v end end NS.db = db return end fullPass() urgent() end) f:SetScript("OnUpdate", function(self, delta) elapsed = elapsed + delta if elapsed < interval then return end elapsed = 0 if fullPass() > 0 then interval = MIN_INTERVAL else interval = math.min(interval * 2, MAX_INTERVAL) end end) -- Panels that build their frames on first open report nothing. This does. if type(hooksecurefunc) == "function" and type(ShowUIPanel) == "function" then hooksecurefunc("ShowUIPanel", function() urgent() end) end --------------------------------------------------------------------------- -- /mscroll --------------------------------------------------------------------------- local TOGGLES = { minimap = "minimap zoom", paged = "merchant / mail / book paging", pageend = "page turn at the end of a list", models = "model zoom", worldmap = "world map zoom", message = "message frame scrolling", mods = "shift and ctrl modifiers", } local KEY = { pageend = "pageAtEnd", mods = "modifiers" } SLASH_MANIAXSCROLL1 = "/mscroll" SlashCmdList["MANIAXSCROLL"] = function(input) local cmd, value = string.match(input or "", "^(%a*)%s*([%d%.]*)$") cmd = string.lower(cmd or "") if TOGGLES[cmd] then local key = KEY[cmd] or cmd db[key] = not opt(key) if db[key] then NS.Rescan() end say(TOGGLES[cmd] .. " " .. (db[key] and "on" or "off") .. ".") return elseif cmd == "lines" and value ~= "" then local n = tonumber(value) if n and n > 0 then db.lines = n say("scrolling " .. n .. " step(s) per notch.") return end elseif cmd == "scan" then local added = NS.Rescan() say("swept: " .. added .. " newly wired; " .. counts.scroll .. " scroll, " .. counts.message .. " message, " .. counts.paged .. " paged, " .. counts.model .. " model, " .. counts.other .. " other.") return elseif cmd == "config" then if ManiaxScrollOptionsPanel and InterfaceOptionsFrame_OpenToCategory then -- Called twice on purpose: on 3.3.5a the first call only opens the -- window, and the category is not selected until the second. InterfaceOptionsFrame_OpenToCategory(ManiaxScrollOptionsPanel) InterfaceOptionsFrame_OpenToCategory(ManiaxScrollOptionsPanel) else say("options panel not loaded.") end return elseif cmd == "reset" then for k, v in pairs(DEFAULTS) do db[k] = v end say("settings reset to defaults; /reload to unwire what is already wired.") return end say("usage:") say(" |cffaaaaaa/mscroll config|r open the options panel") say(" |cffaaaaaa/mscroll lines |r steps per wheel notch (now " .. opt("lines") .. ")") say(" |cffaaaaaa/mscroll minimap|r minimap zoom (" .. tostring(opt("minimap")) .. ")") say(" |cffaaaaaa/mscroll paged|r merchant / mail / book paging (" .. tostring(opt("paged")) .. ")") say(" |cffaaaaaa/mscroll pageend|r turn the page at the end of a list (" .. tostring(opt("pageAtEnd")) .. ")") say(" |cffaaaaaa/mscroll models|r model zoom (" .. tostring(opt("models")) .. ")") say(" |cffaaaaaa/mscroll worldmap|r world map zoom (" .. tostring(opt("worldmap")) .. ")") say(" |cffaaaaaa/mscroll message|r message frames (" .. tostring(opt("message")) .. ")") say(" |cffaaaaaa/mscroll mods|r shift = page, ctrl = jump (" .. tostring(opt("modifiers")) .. ")") say(" |cffaaaaaa/mscroll scan|r re-sweep now, report what got wired") say(" |cffaaaaaa/mscroll reset|r back to defaults") -- No master on/off on purpose. The pack is opt-in: not ticking the add-on is -- the off switch, and a second one that persists silently into -- SavedVariables, with no login notice and nothing on screen, reads exactly -- like the add-on being broken. That is not a hypothetical - it cost an -- evening. Untick it in the add-on list to turn it off. end