diff gui.lua @ 1:822b6ca3ef89

Import of 2.15, moving to wowace svn.
author Farmbuyer of US-Kilrogg <farmbuyer@gmail.com>
date Sat, 16 Apr 2011 06:03:29 +0000
parents
children fe437e761ef8
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/gui.lua	Sat Apr 16 06:03:29 2011 +0000
@@ -0,0 +1,1966 @@
+local addon = select(2,...)
+
+--[[
+Purely the AceGUI-related routines, and the subroutines needed for support.
+------ Constants
+------ Locals
+------ Behind the scenes routines
+------ Main GUI Window
+------ Popup dialogs
+]]
+
+------ Constants
+local eoi_st_rowheight			= 20
+local eoi_st_displayed_rows		= math.floor(366/eoi_st_rowheight)
+local eoi_st_textured_item_format = "|T%s:"..(eoi_st_rowheight-2).."|t %s[%s]|r%s"
+local eoi_st_otherrow_bgcolortable = {
+	wipe	= { ["r"] = 0.3, ["g"] = 0.3, ["b"] = 0.3},
+	kill	= { ["r"] = 0.2, ["g"] = 0.2, ["b"] = 0.2},
+	time	= { ["r"] = 0x0/255, ["g"] = 0x0/255, ["b"] = 1, ["a"] = 0.3},
+}
+eoi_st_otherrow_bgcolortable[""] = eoi_st_otherrow_bgcolortable["kill"]
+eoi_st_otherrow_bgcolortable["realm"] = eoi_st_otherrow_bgcolortable["time"]
+local eoi_st_otherrow_bgcolortable_default
+local eoi_st_lootrow_col3_colortable = {
+	[""]	= { text = "", r = 1.0, g = 1.0, b = 1.0, a = 1.0 },
+	shard	= { text = "shard", r = 0xa3/255, g = 0x35/255, b = 0xee/255, a = 1.0 },
+	offspec	= { text = "offspec", r = 0.78, g = 0.61, b = 0.43, a = 1.0 },
+	gvault	= { text = "guild vault", r = 0x33/255, g = 1.0, b = 0x99/255, a = 1.0 },
+}
+local function eoi_st_lootrow_col3_colortable_func (data, cols, realrow, column, table)
+	local disp = data[realrow].disposition
+	return eoi_st_lootrow_col3_colortable[disp or ""]
+end
+addon.time_column1_used_mt = { __index = {
+	[2] = {value=""},
+	[3] = {value=""},
+} }
+local time_column1_used_mt = addon.time_column1_used_mt
+
+
+------ Locals
+local GUI = LibStub("AceGUI-3.0")
+local flib = LibStub("LibFarmbuyer")
+
+local g_loot			= nil
+local g_generated		= nil
+
+local pairs, ipairs, tinsert, tremove, tonumber = pairs, ipairs, table.insert, table.remove, tonumber
+
+local pprint, tabledump = addon.pprint, flib.tabledump
+local GetItemInfo = GetItemInfo 
+
+-- En masse forward decls of symbols defined inside local blocks
+local _generate_text, _populate_text_specials
+local _tabtexts, _taborder -- filled out in gui block scope
+
+-- Working around this bug:
+-- http://forums.wowace.com/showpost.php?p=295202&postcount=31
+do
+	local function FixFrameLevel (level, ...)
+		for i = 1, select("#", ...) do
+			local button = select(i, ...)
+			button:SetFrameLevel(level)
+		end
+	end
+
+	local function FixMenuFrameLevels()
+		local f = DropDownList1
+		local i = 1
+		while f do
+			FixFrameLevel (f:GetFrameLevel() + 2, f:GetChildren())
+			i = i + 1
+			f = _G["DropDownList"..i]
+		end
+	end
+
+	-- To fix Blizzard's bug caused by the new "self:SetFrameLevel(2);"
+	hooksecurefunc("UIDropDownMenu_CreateFrames", FixMenuFrameLevels)
+end
+
+
+------ Behind the scenes routines
+-- Text generation
+do
+	local next_insertion_position = 2   -- position in _taborder
+	local text_gen_funcs, specials_gen_funcs = {}, {}
+	local accumulator = {}
+
+	-- Can do clever things by passing other halting points as zero
+	function addon:zero_printed_fenceposts(zero)
+		for t in pairs(text_gen_funcs) do
+			g_loot.printed[t] = zero or g_loot.printed[t] or 0
+		end
+	end
+
+	-- This function is called during load, so be careful!
+	function addon:register_text_generator (text_type, title, description, generator, opt_specgen)
+		if type(generator) ~= 'function' then
+			error(("Generator for text type '%s' must be a function!"):format(text_type))
+		end
+		_tabtexts[text_type] = { title=title, desc=description }
+		tinsert (_taborder, next_insertion_position, text_type)
+		next_insertion_position = next_insertion_position + 1
+		text_gen_funcs[text_type] = generator
+		specials_gen_funcs[text_type] = opt_specgen
+	end
+
+	-- Called by tabs_generated_text_OGS
+	function _generate_text (text_type)
+		local f = text_gen_funcs[text_type]
+		if not f then
+			error(("Generator called for unregistered text type '%s'."):format(text_type))
+		end
+		g_generated = g_generated or {}
+		g_loot[text_type] = g_loot[text_type] or ""
+
+		if g_loot.printed[text_type] >= #g_loot then return false end
+		assert(addon.loot_clean == #g_loot, tostring(addon.loot_clean) .. " ~= " .. #g_loot)
+		-- if glc is nil, #==0 test already returned
+
+		local ok,ret = pcall (f, text_type, g_loot, g_loot.printed[text_type], g_generated, accumulator)
+		if not ok then
+			error(("ERROR:  text generator '%s' failed:  %s"):format(text_type, ret))
+			return false
+		end
+		if ret then
+			g_loot.printed[text_type] = #g_loot
+			g_generated[text_type] = (g_generated[text_type] or "") .. table.concat(accumulator,'\n') .. '\n'
+		end
+		wipe(accumulator)
+		return ret
+	end
+	function _populate_text_specials (editbox, specials, mkb, text_type)
+		local f = specials_gen_funcs[text_type]
+		if not f then return end
+		pcall (f, text_type, editbox, specials, mkb)
+	end
+end
+
+--[[
+The g_loot table is populated only with "behavior-relevant" data (names,
+links, etc).  This function runs through it and fills out the "display-
+relevant" bits (icons, user-friendly labels, etc).  Everything from the
+loot_clean index to the end of the table is filled out, loot_clean is
+updated.  Override the starting point with the argument.
+
+XXX blizzard's scrolling update and lib-st keep finding some way of displaying
+the grid without ever calling the hooked refresh, thereby skipping this
+function and erroring on missing columnar data.  fuckit.  from now on
+this function gets called everywhere, all the time, and loops over the
+entire goddamn table each time.  If we can't find blizz's scrollframe bugs,
+we'll just work around them.  Sorry for your smoking CPU.
+
+FIXME just move this functionality to a per-entry function and call it once
+in _addlootentry.  --actually no, then the columnar data won't be updated once
+the backend data is changed on the fly.
+]]
+do
+	local grammar = { -- not worth making a mt for this
+		[2] = "nd",
+		[3] = "rd",
+	}
+
+	function addon:_fill_out_eoi_data (opt_starting_index)
+		if #g_loot < 1 then
+			--pprint('_f_o_e_d', "#g_loot<1")
+			self.loot_clean = nil
+			opt_starting_index = nil
+		end
+		for i = (opt_starting_index or self.loot_clean or 1), #g_loot do
+			local e = g_loot[i]
+			if e == nil then
+				self.loot_clean = nil
+				pprint('_f_o_e_d', "index",i,"somehow still in loop past",#g_loot,"bailing")
+				return
+			end
+
+			-- XXX FIXME a major weakness here is that we're constantly replacing
+			-- what's already been created.  Lots of garbage.  Trying to detect what
+			-- actually needs to be replaced is even worse.  We'll live with
+			-- garbage for now.
+			if e.kind == 'loot' then
+				local textured = eoi_st_textured_item_format:format (e.itexture, self.quality_hexes[e.quality], e.itemname, e.count or "")
+				e.cols = {
+					{value = textured},
+					{value = e.person},
+					{ color = eoi_st_lootrow_col3_colortable_func }
+				}
+				-- This is horrible. Must do better.
+				if e.extratext then for k,v in pairs(eoi_st_lootrow_col3_colortable) do
+					if v.text == e.extratext then
+						e.disposition = k
+						--e.extratext = nil, not feasible
+						break
+					end
+				end end
+				local ex = eoi_st_lootrow_col3_colortable[e.disposition or ""].text
+				if e.bcast_from and e.extratext then
+					ex = e.extratext .. " (from " .. e.bcast_from .. ")"
+				elseif e.bcast_from then
+					ex = "(from " .. e.bcast_from .. ")"
+				elseif e.extratext then
+					ex = e.extratext
+				end
+				e.cols[3].value = ex
+
+			elseif e.kind == 'boss' then
+				local v
+				if e.reason == 'kill' then
+					if e.attempts == 1 then
+						v = "one-shot"
+					else
+						v = ("kill on %d%s attempt"):format(e.attempts, grammar[e.attempts] or "th")
+					end
+					v = ("%s (%d:%.2d)"):format(v, math.floor(e.duration/60), math.floor(e.duration%60))
+				elseif e.reason == 'wipe' then
+					v = ("wipe (%d:%.2d)"):format(math.floor(e.duration/60), math.floor(e.duration%60))
+				end
+				e.cols = {
+					{value = e.bosskill},
+					{value = e.instance},
+					{value = v or ""},
+				}
+
+			elseif e.kind == 'time' then
+				e.cols = setmetatable({
+					{value=e.startday.text},
+				}, time_column1_used_mt)
+				--[[e.cols = {
+					{value=e.startday.text},
+					{value=""},
+					{value=""},
+				}]]
+
+			end
+		end
+		self.loot_clean = #g_loot
+	end
+end
+
+do
+	local offset
+	function addon:_fill_out_hist_data (opt_starting_index)
+		-- Clearing history finishes this function with #hist==0 and hist_clean==0.
+		-- The next call typically detects this (#<1) and handles it.  If loot is
+		-- recorded before then, it results in hist_clean==0 and #hist==1, which
+		-- breaks the first iteration of the loop.  Thus, the "extra" test here:
+		if #self.history < 1 or self.hist_clean == 0 then
+			self.hist_clean = nil
+			opt_starting_index = nil
+		end
+		if not self.history.st then
+			self.history.st = {
+				{ kind = "realm",
+				  cols = setmetatable({
+					{ value = self.history.realm },
+				  }, time_column1_used_mt)
+			    }
+			}
+			offset = #self.history.st
+		end
+		local st = self.history.st
+		for i = (opt_starting_index or self.hist_clean or 1), #self.history do
+			local h = self.history[i]
+			local sti = i+offset
+			if not st[sti] then
+				st[sti] = { kind = "history" }
+			end
+			local sth = st[sti]   -- corresponding ST entry for h
+
+			sth.cols = sth.cols or {
+				{ value = h.name },
+				{},--{ value = h[1].id },
+				{},--{ value = h[1].when },
+			}
+
+			if sth.shown ~= h[1].id then
+				-- things have changed, redo the row with new data
+				local iname, ilink, iquality, _,_,_,_,_,_, itexture = GetItemInfo(h[1].id)
+				local textured = eoi_st_textured_item_format:format (itexture, self.quality_hexes[iquality], iname, h[1].count or "")
+				sth.cols[2].value = textured
+				sth.cols[3].value = h[1].when
+				sth.shown = h[1].id
+			end
+
+		end
+		self.hist_clean = #self.history
+	end
+end
+
+
+------ Main GUI Window
+-- Lots of shared data here, kept in a large local scope.  For readability,
+-- indentation of the scope as a whole is kicked left a notch.
+do
+local _d
+local function setstatus(txt) _d:SetStatusText(txt) end
+local function statusy_OnLeave() setstatus("") end
+local tabgroup_tabs
+
+--[[
+Controls for the tabs on the left side of the main display.
+]]
+_tabtexts = {
+	["eoi"] = {title=[[Loot]], desc=[[Observed loot, plus boss kills and other events of interest]]},
+	["hist"] = {title=[[History]], desc=[[A short semi-permanent record]]},
+	["help"] = {title=[[Help]], desc=[[Instructions, reminders, and tips for non-obvious features]]},
+	["opt"] = {title=[[Options]], desc=[[Options for fine-tuning behavior]]},
+	--["adv"] = {title=[[Advanced]], desc=[[Debugging and testing]]},
+}
+if addon.author_debug then
+_taborder = { "eoi", "hist", "help", "opt" }
+else _taborder = { "eoi", "help", "opt" } end
+
+--[[
+This is a table of callback functions, each responsible for drawing a tab
+into the container passed in the first argument.  Special-purpose buttons
+can optionally be created (mkbutton) and added to the container in the second
+argument.
+]]
+local tabs_OnGroupSelected = {}
+local mkbutton
+local tabs_OnGroupSelected_func, tabs_generated_text_OGS
+
+function addon:gui_init (loot_pointer)
+	g_loot = loot_pointer
+	g_generated = nil
+	tabgroup_tabs = {}
+	for _,v in ipairs(_taborder) do
+		tinsert (tabgroup_tabs, {value=v, text=_tabtexts[v].title})
+		-- By default, tabs are editboxes with generated text
+		if not tabs_OnGroupSelected[v] then
+			tabs_OnGroupSelected[v] = tabs_generated_text_OGS
+		end
+	end
+end
+
+-- Tab 1:  Events Of Interest
+-- This actually takes up quite a bit of the file.
+local eoi_editcell
+
+local function dropdownmenu_handler (ddbutton, subfunc, arg)
+	local i = _d:GetUserData("DD loot index")
+	subfunc(i,arg)
+	_d:GetUserData("eoiST"):OuroLoot_Refresh(i)
+end
+
+local function gen_easymenu_table (initial, list, funcs)
+	for _,tag in ipairs(list) do
+		local name, arg, tiptext
+		name, tiptext = strsplit('|',tag)
+		name, arg = strsplit('%',name)
+		if name == "--" then
+			tinsert (initial, {
+				disabled = true, text = "",
+			})
+		else
+			if not funcs[name] then
+				error(("'%s' not defined as a dropdown function"):format(name))
+			end
+			tinsert (initial, {
+				text = name,
+				func = dropdownmenu_handler,
+				arg1 = funcs[name],
+				arg2 = arg,
+				notCheckable = true,
+				tooltipTitle = tiptext and name or nil,
+				tooltipText = tiptext,
+			})
+		end
+	end
+	return initial
+end
+
+local dropdownmenuframe = CreateFrame("Frame", "OuroLootDropDownMenu", nil, "UIDropDownMenuTemplate")
+local dropdownfuncs
+dropdownfuncs = {
+	[CLOSE] = function() CloseDropDownMenus() end,
+
+	df_INSERT = function(rowi,text)
+		local which = (text == 'loot') and "OUROL_EOI_INSERT_LOOT" or "OUROL_EOI_INSERT"
+		local dialog = StaticPopup_Show(which,text)
+		dialog.wideEditBox:SetScript("OnTextChanged",StaticPopup_EditBoxOnTextChanged)
+		dialog.data = {rowindex=rowi, display=_d, kind=text}
+	end,
+
+	df_DELETE = function(rowi)
+		local gone = tremove(g_loot,rowi)
+		addon:Print("Removed %s.",
+			gone.itemlink or gone.bosskill or gone.startday.text)
+	end,
+
+	-- if kind is boss, also need to stop at new timestamp
+	["Delete remaining entries for this day"] = function(rowi,kind)
+		local fencepost
+		local closest_time = addon._find_next_after('time',rowi)
+		if kind == 'time' then
+			fencepost = closest_time
+		elseif kind == 'boss' then
+			local closest_boss = addon._find_next_after('boss',rowi)
+			if not closest_boss then
+				fencepost = closest_time
+			elseif not closest_time then
+				fencepost = closest_boss
+			else
+				fencepost = math.min(closest_time,closest_boss)
+			end
+		end
+		local count = fencepost and (fencepost-rowi) or (#g_loot-rowi+1)
+		repeat
+			dropdownfuncs.df_DELETE(rowi)
+			count = count - 1
+		until count < 1
+	end,
+
+	["Rebroadcast this loot entry"] = function(rowi)
+		local e = g_loot[rowi]
+		-- This only works because GetItemInfo accepts multiple argument formats
+		addon:broadcast('loot', e.person, e.itemlink, e.count, e.cols[3].value)
+		addon:Print("Rebroadcast entry",rowi,e.itemlink)
+	end,
+
+	["Rebroadcast this boss"] = function(rowi,kind)
+		addon:Print("not implemented yet") -- TODO
+	end,
+
+	["Mark as normal"] = function(rowi,disp) -- broadcast the change?  ugh
+		g_loot[rowi].disposition = disp
+		g_loot[rowi].bcast_from = nil
+		g_loot[rowi].extratext = nil
+	end,
+
+	["Show only this player"] = function(rowi)
+		local st = _d:GetUserData("eoiST")
+		_d:SetUserData("player filter name", g_loot[rowi].person)
+		st:SetFilter(_d:GetUserData("player filter by name"))
+		_d:GetUserData("eoi_filter_reset"):SetDisabled(false)
+	end,
+
+	["Change from 'wipe' to 'kill'"] = function(rowi)
+		addon:_mark_boss_kill(rowi)
+		-- the fillout function called automatically will start too far down the list
+		_d:GetUserData("eoiST"):OuroLoot_Refresh()
+	end,
+
+	["Edit note"] = function(rowi)
+		eoi_editcell (rowi, _d:GetUserData("DD loot cell"))
+	end,
+
+	df_REASSIGN = function(rowi,to_whom)
+		g_loot[rowi].person = to_whom
+		g_loot[rowi].person_class = select(2,UnitClass(to_whom))
+		CloseDropDownMenus()  -- also need to close parent menu
+	end,
+	["Enter name..."] = function(rowi)
+		local dialog = StaticPopup_Show "OUROL_REASSIGN_ENTER"
+		dialog.data = {index=rowi, display=_d}
+	end,
+}
+-- Would be better to move the %arg to this list rather than below, but
+-- that's a lot of extra effort that doesn't buy much in return.
+dropdownfuncs["Delete this loot event"] = dropdownfuncs.df_DELETE
+dropdownfuncs["Delete this boss event"] = dropdownfuncs.df_DELETE
+dropdownfuncs["Insert new loot entry"] = dropdownfuncs.df_INSERT
+dropdownfuncs["Insert new boss kill event"] = dropdownfuncs.df_INSERT
+dropdownfuncs["Mark as disenchanted"] = dropdownfuncs["Mark as normal"]
+dropdownfuncs["Mark as guild vault"] = dropdownfuncs["Mark as normal"]
+dropdownfuncs["Mark as offspec"] = dropdownfuncs["Mark as normal"]
+dropdownfuncs["Delete remaining entries for this boss"] = dropdownfuncs["Delete remaining entries for this day"]
+dropdownfuncs["Rebroadcast this day"] = dropdownfuncs["Rebroadcast this boss"]
+local eoi_time_dropdown = gen_easymenu_table(
+	{{
+		-- this is the dropdown title, text filled in on the fly
+		isTitle = true,
+		notClickable = true,
+		notCheckable = true,
+	}},
+	{
+		"Rebroadcast this day%time|Broadcasts everything from here down until a new day",
+		"Delete remaining entries for this day%time|Erases everything from here down until a new day",
+		"Insert new loot entry%loot|Inserts new loot above this one, prompting you for information",
+		"Insert new boss kill event%boss|Inserts new event above this one, prompting you for information",
+		CLOSE
+	}, dropdownfuncs)
+local eoi_loot_dropdown = gen_easymenu_table(
+	{{
+		-- this is the dropdown title, text filled in on the fly
+		notClickable = true,
+		notCheckable = true,
+	}},
+	{
+		"Mark as disenchanted%shard",
+		"Mark as offspec%offspec",
+		"Mark as guild vault%gvault",
+		"Mark as normal|This is the default. Selecting any 'Mark as <x>' action blanks out extra notes about who broadcast this entry, etc.",
+		"--",
+		"Rebroadcast this loot entry|Sends this loot event, including special notes, as if it just happened.",
+		"Delete this loot event|Permanent, no going back!",
+		"Delete remaining entries for this boss%boss|Erases everything from here down until a new boss/day",
+		"Insert new loot entry%loot|Inserts new loot above this one, prompting you for information",
+		"Insert new boss kill event%boss|Inserts new event above this one, prompting you for information",
+		"Edit note|Same as double-clicking in the notes column",
+		"--",
+		CLOSE
+	}, dropdownfuncs)
+local eoi_player_dropdown = gen_easymenu_table(
+	{
+		{
+			-- this is the dropdown title, text filled in on the fly
+			isTitle = true,
+			notClickable = true,
+			notCheckable = true,
+		},
+		{
+			text = "Reassign to...",
+			hasArrow = true,
+			--menuList = filled in in the fly,
+		},
+	},
+	{
+		"Show only this player",
+		CLOSE
+	}, dropdownfuncs)
+local eoi_boss_dropdown = gen_easymenu_table(
+	{{
+		-- this is the dropdown title, text filled in on the fly
+		isTitle = true,
+		notClickable = true,
+		notCheckable = true,
+	}},
+	{
+		"Change from 'wipe' to 'kill'|Also collapses other wipe entries",
+		"Rebroadcast this boss|Broadcasts the kill event and all subsequent loot until next boss",
+		"Delete this boss event|Permanent, no going back!",
+		"Delete remaining entries for this boss%boss|Erases everything from here down until a new boss/day",
+		"Insert new loot entry%loot|Inserts new loot above this one, prompting you for information",
+		"Insert new boss kill event%boss|Inserts new event above this one, prompting you for information",
+		"--",
+		CLOSE
+	}, dropdownfuncs)
+
+--[[ quoted verbatim from lib-st docs:
+rowFrame This is the UI Frame table for the row.
+cellFrame This is the UI Frame table for the cell in the row.
+data This is the data table supplied to the scrolling table (in case you lost it :) )
+cols This is the cols table supplied to the scrolling table (again, in case you lost it :) )
+row This is the number of the UI row that the event was triggered for.<br/> ex. If your scrolling table only shows ten rows, this number will be a number between 1 and 10.
+realrow This is the exact row index (after sorting and filtering) in the data table of what data is displayed in the row you triggered the event in. (NOT the UI row!)
+column This is the index of which column the event was triggered in.
+table This is a reference to the scrollingtable table.
+...  Any arguments generated by the '''NORMAL''' Blizzard event triggered by the frame are passed as is.
+]]
+local function eoi_st_OnEnter (rowFrame, cellFrame, data, cols, row, realrow, column, table, button, ...)
+	if (row == nil) or (realrow == nil) then return end  -- mouseover column header
+	local e = data[realrow]
+	local kind = e.kind
+
+	if kind == 'loot' and column == 1 then
+		GameTooltip:SetOwner (cellFrame, "ANCHOR_RIGHT", -20, 0)
+		GameTooltip:SetHyperlink (e.itemlink)
+
+	elseif kind == 'loot' and column == 2 then
+		GameTooltip:SetOwner (cellFrame, "ANCHOR_BOTTOMRIGHT", -50, 5)
+		GameTooltip:ClearLines()
+		GameTooltip:AddLine(e.person.." Loot:")
+		local counter = 0
+		for i,e2 in ipairs(data) do
+			if e2.person == e.person then  -- would be awesome to test for alts
+				if counter > 10 then
+					GameTooltip:AddLine("...")
+					break
+				else
+					-- textures screw up too badly, strip them
+					local textured = e2.cols[1].value
+					local space = textured:find(" ")
+					GameTooltip:AddLine(textured:sub(space+1))
+					counter = counter + 1
+				end
+			end
+		end
+		GameTooltip:Show()
+
+	elseif kind == 'loot' and column == 3 then
+		setstatus(e.cols[column].value)
+
+	end
+
+	return false  -- continue with default highlighting behavior
+end
+local function eoi_st_OnLeave (rowFrame, cellFrame, data, cols, row, realrow, column, table, button, ...)
+	GameTooltip:Hide()
+	if row and realrow and data[realrow].kind ~= 'loot' then
+		table:SetHighLightColor (rowFrame, eoi_st_otherrow_bgcolortable[data[realrow].reason or data[realrow].kind])
+		return true   -- do not do anything further
+	else
+		--setstatus("")
+		return false  -- continue with default un-highlighting behavior
+	end
+end
+
+local function eoi_st_OnClick (rowFrame, cellFrame, data, cols, row, realrow, column, stable, button, ...)
+	if (row == nil) or (realrow == nil) then return true end  -- click column header, suppress reordering
+	local e = data[realrow]
+	local kind = e.kind
+
+	-- Check for shift-clicking a loot line
+	if IsModifiedClick("CHATLINK") and kind == 'loot' and column == 1 then
+		ChatEdit_InsertLink (e.itemlink)
+		return true  -- do not do anything further
+	end
+
+	-- Remaining actions are all right-click
+	if button ~= "RightButton" then return true end
+	_d:SetUserData("DD loot index", realrow)
+
+	if kind == 'loot' and (column == 1 or column == 3) then
+		_d:SetUserData("DD loot cell", cellFrame)
+		eoi_loot_dropdown[1].text = e.itemlink
+		EasyMenu (eoi_loot_dropdown, dropdownmenuframe, cellFrame, 0, 0, "MENU")
+
+	elseif kind == 'loot' and column == 2 then
+		eoi_player_dropdown[1].text = e.person
+		local raiders = {}
+		for i = 1, GetNumRaidMembers() do
+			tinsert (raiders, (GetRaidRosterInfo(i)))
+		end
+		table.sort(raiders)
+		for i = 1, #raiders do
+			local name = raiders[i]
+			raiders[i] = {
+				text = name,
+				func = dropdownmenu_handler,
+				arg1 = dropdownfuncs.df_REASSIGN,
+				arg2 = name,
+				notCheckable = true,
+			}
+		end
+		eoi_player_dropdown[2].menuList =
+			gen_easymenu_table (raiders, {"Enter name...",CLOSE}, dropdownfuncs)
+		--tabledump(eoi_player_dropdown)
+		EasyMenu (eoi_player_dropdown, dropdownmenuframe, cellFrame, 0, 0, "MENU")
+
+	elseif kind == 'boss' then
+		eoi_boss_dropdown[1].text = e.bosskill
+		EasyMenu (eoi_boss_dropdown, dropdownmenuframe, cellFrame, 0, 0, "MENU")
+
+	elseif kind == 'time' then
+		eoi_time_dropdown[1].text = e.startday.text
+		EasyMenu (eoi_time_dropdown, dropdownmenuframe, cellFrame, 0, 0, "MENU")
+
+	end
+
+	return true  -- do not do anything further
+end
+
+function eoi_editcell (row_index, cell_frame)
+	local e = g_loot[row_index]
+	if not e then return end   -- how the hell could we get this far?
+	local celldata = e.cols[3]
+	local box = GUI:Create("EditBox")
+	box:SetText(celldata.value)
+	box:SetUserData("old show", box.editbox:GetScript("OnShow"))
+	box:SetUserData("old escape", box.editbox:GetScript("OnEscapePressed"))
+	box.editbox:SetScript("OnShow", box.editbox.SetFocus)
+	box.editbox:SetScript("OnEscapePressed", function(_be)
+		_be:ClearFocus()
+		_be.obj:Release()
+	end)
+	box:SetCallback("OnEnterPressed", function(_b,event,value)
+		e.extratext = value
+		celldata.value = value
+		e.bcast_from = nil  -- things get screwy if this field is still present. sigh.
+		e.extratext_byhand = true
+		value = value and value:match("^(x%d+)")
+		if value then e.count = value end
+		_b:Release()
+		return _d:GetUserData("eoiST"):OuroLoot_Refresh(row_index)
+	end)
+	box:SetCallback("OnRelease", function(_b)
+		_b.editbox:ClearFocus()
+		_b.editbox:SetScript("OnShow", _b:GetUserData("old show"))
+		_b.editbox:SetScript("OnEscapePressed", _b:GetUserData("old escape"))
+		setstatus("")
+	end)
+	box.frame:SetAllPoints(cell_frame)
+	box.frame:SetParent(cell_frame)
+	box.frame:SetFrameLevel(cell_frame:GetFrameLevel()+1)
+	box.frame:Show()
+	setstatus("Press Enter or click Okay to accept changes, or press Escape to cancel them.")
+end
+
+local function eoi_st_OnDoubleClick (rowFrame, cellFrame, data, cols, row, realrow, column, stable, button, ...)
+	if (row == nil) or (realrow == nil) then return true end  -- they clicked on column header, suppress reordering
+	local e = data[realrow]
+	local kind = e.kind
+
+	--_d:SetUserData("DD loot index", realrow)
+	if kind == 'loot' and column == 3 and button == "LeftButton" then
+		eoi_editcell (realrow, cellFrame)
+	end
+
+	return true  -- do not do anything further
+end
+
+local function hist_st_OnClick (rowFrame, cellFrame, data, cols, row, realrow, column, stable, button, ...)
+	if (row == nil) or (realrow == nil) then return true end  -- click column header, suppress reordering FOR NOW
+	local h = data[realrow]
+	local kind = h.kind
+
+
+	return true  -- do not do anything further
+end
+
+local function hist_st_OnDoubleClick (rowFrame, cellFrame, data, cols, row, realrow, column, stable, button, ...)
+	if (row == nil) or (realrow == nil) then return true end  -- they clicked on column header, suppress reordering
+	local h = data[realrow]
+	local kind = h.kind
+
+	return true  -- do not do anything further
+end
+
+-- Used for EOI column 2 and Hist column 1.  Both are player name columns.
+local function eoi_st_col2_DoCellUpdate (rowFrame, cellFrame, data, cols, row, realrow, column, fShow, stable, ...) 
+	if not fShow then
+		cellFrame.text:SetText("")
+		if cellFrame.icontexture then
+			cellFrame.icontexture:Hide()
+		end
+		return
+	end
+
+	local e = data[realrow]
+	local cell = e.cols[column]
+
+	cellFrame.text:SetText(cell.value)
+	cellFrame.text:SetTextColor(1,1,1,1)
+
+	if e.person_class then
+		local icon
+		if cellFrame.icontexture then
+			icon = cellFrame.icontexture
+		else
+			icon = cellFrame:CreateTexture(nil,"BACKGROUND")
+			icon:SetPoint("LEFT", cellFrame, "LEFT")
+			icon:SetHeight(eoi_st_rowheight-4)
+			icon:SetWidth(eoi_st_rowheight-4)
+			icon:SetTexture("Interface\\Glues\\CharacterCreate\\UI-CharacterCreate-Classes")
+			cellFrame.icontexture = icon
+		end
+		icon:SetTexCoord(unpack(CLASS_ICON_TCOORDS[e.person_class]))
+		icon:Show()
+		cellFrame.text:SetPoint("LEFT", icon, "RIGHT", 1, 0)
+	else
+		if cellFrame.icontexture then
+			cellFrame.icontexture:Hide()
+			cellFrame.text:SetPoint("LEFT", cellFrame, "LEFT")
+		end
+	end
+
+	--if e.kind ~= 'loot' then
+		stable:SetHighLightColor (rowFrame, eoi_st_otherrow_bgcolortable[e.reason or e.kind or ""])
+	--else
+	--	table:SetHighLightColor (rowFrame, table:GetDefaultHighlightBlank())
+	--end
+end
+
+local eoi_st_cols = {
+	{  -- col 1
+		name	= "Item",
+		width	= 250,
+	},
+	{  -- col 2
+		name	= "Player",
+		width	= 130,
+		DoCellUpdate = eoi_st_col2_DoCellUpdate,
+	},
+	{  -- col 3
+		name	= "Notes",
+		width	= 160,
+	},
+}
+
+local rowfilter_all
+local rowfilter_by_name = function (st, e)
+	if e.kind ~= 'loot' then return true end
+	return e.person == _d:GetUserData("player filter name")
+end
+
+-- Tab 1:  Events Of Interest (implementation)
+tabs_OnGroupSelected["eoi"] = function(ocontainer,specials)
+	if (not addon.rebroadcast) and (not addon.enabled) and (#g_loot < 1) then
+		return addon.dprint('flow', "Nothing to show in first tab, skipping creation")
+	end
+
+	-- The first time this function is called, we set up a persistent ST
+	-- object and store it.  Any other delayed setup work is done, and then
+	-- this function replaces itself with a smaller, sleeker, sexier one.
+	-- This function will later be garbage collected.
+	local ST = LibStub("ScrollingTable"):CreateST(eoi_st_cols,eoi_st_displayed_rows,eoi_st_rowheight)
+	_d:SetUserData("eoiST",ST)
+	if addon.author_debug then
+		_G.OLST = ST
+	end
+
+	if not eoi_st_otherrow_bgcolortable_default then
+		eoi_st_otherrow_bgcolortable_default = ST:GetDefaultHighlightBlank()
+		setmetatable(eoi_st_otherrow_bgcolortable, {__index = function (bg, key)
+			return eoi_st_otherrow_bgcolortable_default
+		end})
+	end
+
+	-- Calling SetData breaks (trying to call Refresh) if g_loot hasn't gone
+	-- through this loop.
+	addon:_fill_out_eoi_data(1)
+	-- safety check  begin
+	for i,e in ipairs(g_loot) do
+		if type(e.cols) ~= 'table' then
+			addon:Print("ARGH, index",i,"bad in eoi_OGS, type",type(e.cols),
+				"entry kind", e.kind, "data", e.itemname or e.bosskill or e.startday.text,
+				"-- please take a screenshot and send to Farmbuyer.")
+			tabledump(e)
+		end
+	end
+	-- safety check  end
+	ST:SetData(g_loot)
+	ST:RegisterEvents{
+		OnEnter = eoi_st_OnEnter,
+		OnLeave = eoi_st_OnLeave,
+		OnClick = eoi_st_OnClick,
+		OnDoubleClick = eoi_st_OnDoubleClick,
+	}
+
+	-- We want a single "update and redraw" function for the ST.  Also, the
+	-- given refresh function is badly named and does nothing; the actual
+	-- function is SortData (also badly named when no sorting is being done),
+	-- which unconditionally calls the *hooked* Refresh.
+	local oldrefresh = ST.Refresh
+	ST.Refresh = function (self, opt_index)
+		addon:_fill_out_eoi_data(opt_index)
+		return oldrefresh(self)
+	end
+	ST.OuroLoot_Refresh = function (self, opt_index)
+		addon:_fill_out_eoi_data(opt_index)
+		-- safety check  begin
+		for i,e in ipairs(g_loot) do
+			if type(e.cols) ~= 'table' then
+				addon:Print("ARGH, index",i,"bad in refresh, refreshed at", opt_index, "type",type(e.cols),
+					"entry kind", e.kind, "data", e.itemname or e.bosskill or e.startday.text,
+					"-- please take a screenshot and send to Farmbuyer.")
+				tabledump(e)
+			end
+		end
+		-- safety check  end
+		self:SortData()  -- calls hooked refresh
+	end
+
+	-- No need to keep creating function closures that all just "return true",
+	-- instead we grab the one made inside lib-st.  There's no "get filter" API
+	-- so we just reach inside.
+	rowfilter_all = ST.Filter
+
+	-- Now set up the future drawing function...
+	tabs_OnGroupSelected["eoi"] = function(container,specials)
+		local st_widget = GUI:Create("lib-st")
+		local st = _d:GetUserData("eoiST")
+
+		-- This is actually required each time
+		_d:SetUserData ("player filter clear", rowfilter_all)
+		_d:SetUserData ("player filter by name", rowfilter_by_name)
+
+		st:OuroLoot_Refresh()
+		st_widget:WrapST(st)
+
+		if OuroLootSV_opts.scroll_to_bottom then
+			local scrollbar = _G[st.scrollframe:GetName().."ScrollBar"]
+			if scrollbar then
+				local _,max = scrollbar:GetMinMaxValues()
+				scrollbar:SetValue(max)   -- also calls hooked Refresh
+			end
+		end
+
+		container:SetLayout("Fill")
+		container:AddChild(st_widget)
+
+		local b = mkbutton('eoi_filter_reset', "Reset Player Filter",
+			[[Return to showing complete loot information.]])
+		b:SetFullWidth(true)
+		b:SetCallback("OnClick", function (_b)
+			local st = _d:GetUserData("eoiST")
+			st:SetFilter(rowfilter_all)
+			_b:SetDisabled(true)
+		end)
+		b:SetDisabled(st.Filter == rowfilter_all)
+		specials:AddChild(b)
+
+		local people = { "<nobody>" }
+		for i=1,GetNumRaidMembers() do
+			tinsert(people,(GetRaidRosterInfo(i)))
+		end
+		table.sort(people)
+		local initial
+		for i,n in ipairs(people) do
+			if n == addon.sharder then initial = i end
+		end
+		b = mkbutton("Dropdown", nil, "",
+			[[If set, items received by this person will be automatically marked as disenchanted.]])
+		b:SetFullWidth(true)
+		b:SetLabel("Auto-mark as shard:")
+		b:SetList(people)
+		b:SetValue(initial or 1)
+		b:SetCallback("OnValueChanged", function(_dd,event,choice)
+			addon.sharder = (choice ~= 1) and people[choice] or nil
+		end)
+		specials:AddChild(b)
+
+		local b = mkbutton('eoi_bcast_req', "Request B'casters",
+			[[Sends out a request for others to enable loot rebroadcasting if they have not already done so.]])
+		b:SetFullWidth(true)
+		b:SetCallback("OnClick", function ()
+			addon:Print("Sending request!")
+			addon.requesting = true
+			addon:broadcast('bcast_req')
+		end)
+		b:SetDisabled(not addon.enabled)
+		specials:AddChild(b)
+	end
+	-- ...and call it.
+	return tabs_OnGroupSelected["eoi"](ocontainer,specials)
+end
+
+-- Tab 2/3 (generated text)
+function tabs_generated_text_OGS (container, specials, text_kind)
+	container:SetLayout("Fill")
+	local box = GUI:Create("MultiLineEditBox")
+	box:SetFullWidth(true)
+	box:SetFullHeight(true)
+	box:SetLabel("Pressing the Escape key while typing will return keystroke control to the usual chat window.")
+	box:DisableButton(true)
+	addon:_fill_out_eoi_data(1)
+
+	-- Update the savedvar copy of the text before presenting it for editing,
+	-- then save it again when editing finishes.  This way if the user goes
+	-- offline while editing, at least the unedited version is saved instead
+	-- of all the new text being lost entirely.  (Yes, it's happened.)
+	--
+	-- No good local-ish place to store the cursor position that will also
+	-- survive the entire display being released.  Abuse the generated text
+	-- cache for this purpose.
+	local pos = text_kind.."_pos"
+	if _generate_text(text_kind) then
+		g_loot[text_kind] = g_loot[text_kind] .. g_generated[text_kind]
+		g_generated[text_kind] = nil
+	end
+	box:SetText(g_loot[text_kind])
+	box.editBox:SetCursorPosition(g_generated[pos] or 0)
+	box.editBox:SetScript("OnShow", box.editBox.SetFocus)
+	box:SetCallback("OnRelease", function(_box)
+		box.editBox:ClearFocus()
+		g_loot[text_kind] = _box:GetText()
+		g_generated[pos] = _box.editBox:GetCursorPosition()
+	end)
+	container:AddChild(box)
+
+	local w = mkbutton("Regenerate",
+		[[+DISCARD> all text in this tab, and regenerate it from the current loot information.]])
+	w:SetFullWidth(true)
+	w:SetDisabled ((#g_loot == 0) and (box:GetText() == ""))
+	w:SetCallback("OnClick", function(_w)
+		box:SetText("")
+		g_loot[text_kind] = ""
+		g_loot.printed[text_kind] = 0
+		g_generated.last_instance = nil
+		g_generated[pos] = nil
+		addon:Print("'%s' has been regenerated.", _tabtexts[text_kind].title)
+		return addon:redisplay()
+		--return tabs_OnGroupSelected_func(container,"OnGroupSelected",text_kind)
+	end)
+	specials:AddChild(w)
+	_populate_text_specials (box, specials, mkbutton, text_kind)
+end
+
+-- Tab 4:  History
+-- Much of the implementation here follows a similar desgin for the first
+-- tab's handling of ST objects.
+do
+	local histST
+	local hist_st_cols = {
+		{  -- col 1
+			name	= "Player",
+			width	= 130,
+			DoCellUpdate = eoi_st_col2_DoCellUpdate,
+		},
+		{  -- col 2
+			name	= "Most Recent Loot",
+			width	= 250,
+			DoCellUpdate = hist_st_col2_DoCellUpdate,
+		},
+		{  -- col 3
+			name	= "When",
+			width	= 160,
+			DoCellUpdate = hist_st_col3_DoCellUpdate,
+		},
+	}
+
+	-- Loot column
+	local function hist_st_col2_DoCellUpdate (rowFrame, cellFrame, data, cols, row, realrow, column, fShow, stable, ...) 
+	end
+
+	-- Formatted timestamp column
+	local function hist_st_col3_DoCellUpdate (rowFrame, cellFrame, data, cols, row, realrow, column, fShow, stable, ...) 
+		if not fShow then
+			cellFrame.text:SetText("")
+			return
+		end
+
+		local d = data[realrow]
+		local cell = d.cols[column]
+
+		cellFrame.text:SetText(cell.value)
+		cellFrame.text:SetTextColor(1,1,1,1)
+
+		--if d.kind ~= 'loot' then
+			stable:SetHighLightColor (rowFrame, eoi_st_otherrow_bgcolortable[d.kind])
+		--else
+		--	table:SetHighLightColor (rowFrame, table:GetDefaultHighlightBlank())
+		--end
+	end
+
+	tabs_OnGroupSelected["hist"] = function(container,specials)
+		histST = LibStub("ScrollingTable"):CreateST(hist_st_cols,eoi_st_displayed_rows,eoi_st_rowheight)
+		if addon.author_debug then
+			_G.OLHST = histST
+		end
+
+		if not eoi_st_otherrow_bgcolortable_default then
+			eoi_st_otherrow_bgcolortable_default = histST:GetDefaultHighlightBlank()
+			setmetatable(eoi_st_otherrow_bgcolortable, {__index = function (bg, key)
+				return eoi_st_otherrow_bgcolortable_default
+			end})
+		end
+
+		addon:_build_history_names()
+		addon:_fill_out_hist_data(1)
+		histST:SetData(addon.history.st)
+		histST:RegisterEvents{
+			OnEnter = eoi_st_OnEnter,
+			OnLeave = eoi_st_OnLeave,
+			OnClick = hist_st_OnClick,
+			--OnDoubleClick = eoi_st_OnDoubleClick,
+		}
+		local oldrefresh = histST.Refresh
+		histST.Refresh = function (self, opt_index)
+			addon:_fill_out_hist_data(opt_index)
+			return oldrefresh(self)
+		end
+		histST.OuroLoot_Refresh = function (self, opt_index)
+			addon:_fill_out_hist_data(opt_index)
+			self:SortData()  -- calls hooked refresh
+		end
+
+		tabs_OnGroupSelected["hist"] = function(container,specials)
+			local st_widget = GUI:Create("lib-st")
+			histST:OuroLoot_Refresh()
+			st_widget:WrapST(histST)
+			container:SetLayout("Fill")
+			container:AddChild(st_widget)
+
+			local b = mkbutton("Regenerate",
+				[[Erases all history entries from current realm, and regenerate it from current loot information.]])
+			b:SetFullWidth(true)
+			b:SetDisabled (#addon.history == 0)
+			b:SetCallback("OnClick", function(_b)
+				addon:Print("%s history has been regenerated.", addon.history.realm)
+				return addon:redisplay()
+				--return tabs_OnGroupSelected_func(container,"OnGroupSelected","hist")
+			end)
+			specials:AddChild(b)
+
+			b = mkbutton('hist_clear_all', "Clear All History",
+				[[Erases ALL history entries from all realms.]])
+			b:SetFullWidth(true)
+			b:SetCallback("OnClick", function (_b)
+				local r = GetRealmName()
+				-- new .history table:
+				addon.history_all[r] = addon:_prep_new_history_category (nil, r)
+				addon.history = addon.history_all[r]
+				addon.hist_clean = nil
+				-- new .history.st table:
+				histST:OuroLoot_Refresh()
+				histST:SetData(addon.history.st)
+				addon:Print("You've clicked the history erase button!")
+				return addon:redisplay()
+				--return tabs_OnGroupSelected_func(container,"OnGroupSelected","hist")
+			end)
+			specials:AddChild(b)
+
+			b = mkbutton('hist_clear_old', "Clear Older",
+				[[Preserves only the latest loot entry for each player, removing all others.]])
+			b:SetFullWidth(true)
+			b:SetCallback("OnClick", function (_b)
+				addon:Print("All loot prior to the most recent entries has been erased.")
+				return addon:redisplay()
+				--return tabs_OnGroupSelected_func(container,"OnGroupSelected","hist")
+			end)
+			specials:AddChild(b)
+		end
+		return tabs_OnGroupSelected["hist"](container,specials)
+	end
+end
+
+-- Tab 5:  Help (content in lootaux.lua)
+do
+	local tabs_help_OnGroupSelected_func = function (treeg,event,category)
+		treeg:ReleaseChildren()
+		local txt = GUI:Create("Label")
+		txt:SetFullWidth(true)
+		txt:SetFontObject(GameFontNormal)--Highlight)
+		txt:SetText(addon.helptext[category])
+		local sf = GUI:Create("ScrollFrame")
+		local sfstat = _d:GetUserData("help tab scroll status") or {}
+		sf:SetStatusTable(sfstat)
+		_d:SetUserData("help tab scroll status",sfstat)
+		sf:SetLayout("Fill")
+		-- This forces the scrolling area to be bigger than the visible area; else
+		-- some of the text gets cut off.
+		sf.content:SetHeight(700)
+		sf:AddChild(txt)
+		treeg:AddChild(sf)
+		if treeg:GetUserData("help restore scroll") then
+			sfstat = sfstat.scrollvalue
+			if sfstat then sf:SetScroll(sfstat) end
+			treeg:SetUserData("help restore scroll", false)
+		else
+			sf:SetScroll(0)
+		end
+	end
+	tabs_OnGroupSelected["help"] = function(container,specials)
+		container:SetLayout("Fill")
+		local left = GUI:Create("TreeGroup")
+		local leftstat = _d:GetUserData("help tab select status")
+						 or {treewidth=145}
+		left:SetStatusTable(leftstat)
+		_d:SetUserData("help tab select status",leftstat)
+		left:SetLayout("Fill")
+		left:SetFullWidth(true)
+		left:SetFullHeight(true)
+		left:EnableButtonTooltips(false)
+		left:SetTree(addon.helptree)
+		left:SetCallback("OnGroupSelected", tabs_help_OnGroupSelected_func)
+		container:AddChild(left)
+		leftstat = leftstat.selected
+		if leftstat then
+			left:SetUserData("help restore scroll", true)
+			left:SelectByValue(leftstat)
+		else
+			left:SelectByValue("basic")
+		end
+	end
+end
+
+-- Tab 6:  Options / Advanced
+do
+	local function mkoption (opt, label, width, desc, opt_func)
+		local w = mkbutton("CheckBoxSmallLabel", nil, "", desc)
+		w:SetRelativeWidth(width)
+		w:SetType("checkbox")
+		w:SetLabel(label)
+		if opt then
+			w:SetValue(OuroLootSV_opts[opt])
+			w:SetCallback("OnValueChanged", opt_func or (function(_w,event,value)
+				OuroLootSV_opts[opt] = value
+			end))
+		end
+		return w
+	end
+
+	local function adv_careful_OnTextChanged (ebox,event,value)
+		-- The EditBox widget's code will call an internal ShowButton routine
+		-- after this callback returns.  ShowButton will test for this flag:
+		ebox:DisableButton (value == "")
+	end
+
+	-- Like the first tab, we use a pair of functions; first and repeating.
+	local function adv_real (container, specials)
+		local grp, w
+
+		grp = GUI:Create("InlineGroup")
+		grp:SetLayout("Flow")
+		grp:PauseLayout()
+		grp:SetFullWidth(true)
+		grp:SetTitle("Debugging/Testing Options      [not saved across sessions]")
+
+		w = mkbutton("EditBox", 'comm_ident', addon.ident,
+			[[Disable the addon, change this field (click Okay or press Enter), then re-enable the addon.]])
+		w:SetRelativeWidth(0.2)
+		w:SetLabel("Addon channel ID")
+		w:SetCallback("OnTextChanged", adv_careful_OnTextChanged)
+		w:SetCallback("OnEnterPressed", function(_w,event,value)
+			-- if they set it to blank spaces, they're boned.  oh well.
+			-- Re-enabling will take care of propogating this new value.
+			addon.ident = (value == "") and "OuroLoot2" or value
+			_w:SetText(addon.ident)
+			addon:Print("Addon channel ID set to '".. addon.ident.. "' for rebroadcasting and listening.")
+		end)
+		w:SetDisabled(addon.enabled or addon.rebroadcast)
+		grp:AddChild(w)
+
+		w = mkbutton("EditBox", nil, addon.recent_messages.ttl, [[comm cache (only) ttl]])
+		w:SetRelativeWidth(0.05)
+		w:SetLabel("ttl")
+		w:SetCallback("OnTextChanged", adv_careful_OnTextChanged)
+		w:SetCallback("OnEnterPressed", function(_w,event,value)
+			value = tonumber(value) or addon.recent_messages.ttl
+			addon.recent_messages.ttl = value
+			_w:SetText(tostring(value))
+		end)
+		grp:AddChild(w)
+
+		w = mkbutton("load nsaab1548", [[Cursed Darkhound]])
+		w:SetRelativeWidth(0.25)
+		w:SetCallback("OnClick", function()
+			for i, v in ipairs(DBM.AddOns) do
+				if v.modId == "DBM-NotScaryAtAll" then
+					DBM:LoadMod(v)
+					break
+				end
+			end
+			local mod = DBM:GetModByName("NotScaryAtAll")
+			if mod then
+				mod:EnableMod()
+				addon:Print("Now tracking ID",mod.creatureId)
+			else addon:Print("Can do nothing; DBM testing mod wasn't loaded.") end
+		end)
+		w:SetDisabled(not addon.dbm_registered)
+		grp:AddChild(w)
+
+		w = mkbutton("GC", [[full GC cycle]])
+		w:SetRelativeWidth(0.1)
+		w:SetCallback("OnClick", function() collectgarbage() end)
+		grp:AddChild(w)
+
+		w = mkbutton("EditBox", nil, addon.loot_pattern:sub(17), [[]])
+		w:SetRelativeWidth(0.35)
+		w:SetLabel("CML pattern suffix")
+		w:SetCallback("OnEnterPressed", function(_w,event,value)
+			addon.loot_pattern = addon.loot_pattern:sub(1,16) .. value
+		end)
+		grp:AddChild(w)
+
+		local simple = GUI:Create("SimpleGroup")
+		simple:SetLayout("List")
+		simple:SetRelativeWidth(0.3)
+		w = GUI:Create("CheckBoxSmallLabel")
+		w:SetFullWidth(true)
+		w:SetType("checkbox")
+		w:SetLabel("master dtoggle")
+		w:SetValue(addon.DEBUG_PRINT)
+		w:SetCallback("OnValueChanged", function(_w,event,value) addon.DEBUG_PRINT = value end)
+		simple:AddChild(w)
+		w = mkbutton("Clear All & Reload",
+			[[No confirmation!  |cffff1010Erases absolutely all> Ouro Loot saved variables and reloads the UI.]])
+		w:SetFullWidth(true)
+		w:SetCallback("OnClick", function()
+			addon:_clear_SVs()
+			ReloadUI()
+		end)
+		simple:AddChild(w)
+		grp:AddChild(simple)
+
+		simple = GUI:Create("SimpleGroup")
+		simple:SetLayout("List")
+		simple:SetRelativeWidth(0.5)
+		for d,v in pairs(addon.debug) do
+			w = GUI:Create("CheckBoxSmallLabel")
+			w:SetFullWidth(true)
+			w:SetType("checkbox")
+			w:SetLabel(d)
+			if d == "notraid" then
+				w:SetDescription("Tick this before enabling to make the addon work outside of raid groups")
+			end
+			w:SetValue(v)
+			w:SetCallback("OnValueChanged", function(_w,event,value) addon.debug[d] = value end)
+			simple:AddChild(w)
+		end
+		grp:AddChild(simple)
+		grp:ResumeLayout()
+
+		container:AddChild(grp)
+		GUI:ClearFocus()
+	end
+
+	-- Initial lower panel function
+	local function adv_lower (container, specials)
+		local speedbump = GUI:Create("InteractiveLabel")
+		speedbump:SetFullWidth(true)
+		speedbump:SetFontObject(GameFontHighlightLarge)
+		speedbump:SetImage("Interface\\DialogFrame\\DialogAlertIcon")
+		speedbump:SetImageSize(50,50)
+		speedbump:SetText("The debugging/testing settings on the rest of this panel can"
+			.." seriously bork up the addon if you make a mistake.  If you're okay"
+			.." with the possibility of losing data, click this warning to load the panel.")
+		speedbump:SetCallback("OnClick", function (_sb)
+			adv_lower = adv_real
+			return addon:redisplay()
+			--return tabs_OnGroupSelected_func(container.parent,"OnGroupSelected","opt")
+		end)
+		container:AddChild(speedbump)
+	end
+
+	tabs_OnGroupSelected["opt"] = function(container,specials)
+		--container:SetLayout("List")
+		container:SetLayout("Fill")
+		local scroll, grp, w
+
+		scroll = GUI:Create("ScrollFrame")
+		scroll:SetLayout("Flow")
+
+		grp = GUI:Create("InlineGroup")
+		grp:SetLayout("Flow")
+		grp:SetFullWidth(true)
+		grp:SetTitle("User Options     [these are saved across sessions]")
+
+		-- reminder popup
+		w = mkoption ('popup_on_join', "Show reminder popup", 0.35,
+			[[When joining a raid and not already tracking, display a dialog asking for instructions.]])
+		grp:AddChild(w)
+
+		-- toggle scroll-to-bottom on first tab
+		w = mkoption('scroll_to_bottom', "Scroll to bottom when opening display", 0.60,
+			[[Scroll to the bottom of the loot window (most recent entries) when displaying the GUI.]])
+		grp:AddChild(w)
+
+		-- /loot option
+		w = mkoption('register_slashloot', "Register /loot slash command on login", 0.45,
+			[[Register "/loot" as a slash command in addition to the normal "/ouroloot".  Relog to take effect.]])
+		grp:AddChild(w)
+
+		-- chatty mode
+		w = mkoption('chatty_on_kill', "Be chatty on boss kill", 0.30,
+			[[Print something to chat output when DBM tells Ouro Loot about a successful boss kill.]])
+		grp:AddChild(w)
+
+		-- less noise in main panel
+		w = mkoption('no_tracking_wipes', "Do not track wipes", 0.25,
+			[[Do not add 'wipe' entries on the main loot grid, or generate any text for them.]])
+		grp:AddChild(w)
+
+		-- cutesy abbrevs
+		w = mkoption('snarky_boss', "Use snarky boss names", 0.35,
+			[[Irreverent replacement names for boss events.]])
+		grp:AddChild(w)
+
+		-- possible keybindings
+		do
+			local pair = GUI:Create("SimpleGroup")
+			pair:SetLayout("Flow")
+			pair:SetRelativeWidth(0.6)
+			local editbox, checkbox
+			editbox = mkbutton("EditBox", nil, OuroLootSV_opts.keybinding_text,
+				[[Keybinding text format is fragile!  Relog to take effect.]])
+			editbox:SetRelativeWidth(0.5)
+			editbox:SetLabel("Keybinding text")
+			editbox:SetCallback("OnEnterPressed", function(_w,event,value)
+				OuroLootSV_opts.keybinding_text = value
+			end)
+			editbox:SetDisabled(not OuroLootSV_opts.keybinding)
+			checkbox = mkoption('keybinding', "Register keybinding", 0.5,
+				[[Register a keybinding to toggle the loot display.  Relog to take effect.]],
+				function (_w,_,value)
+					OuroLootSV_opts.keybinding = value
+					editbox:SetDisabled(not OuroLootSV_opts.keybinding)
+				end)
+			pair:AddChild(checkbox)
+			pair:AddChild(editbox)
+			grp:AddChild(pair)
+		end
+
+		-- item filter
+		w = GUI:Create("Spacer")
+		w:SetFullWidth(true)
+		w:SetHeight(20)
+		grp:AddChild(w)
+		do
+			local list = {}
+			for id in pairs(OuroLootSV_opts.itemfilter) do
+				local iname, _, iquality = GetItemInfo(id)
+				list[id] = addon.quality_hexes[iquality] .. iname .. "|r"
+			end
+			w = GUI:Create("EditBoxDropDown")
+			w:SetRelativeWidth(0.4)
+			w:SetText("Item filter")
+			w:SetEditBoxTooltip("Link items which should no longer be tracked.")
+			w:SetList(list)
+			w:SetCallback("OnTextEnterPressed", function(_w, _, text)
+				local iname, ilink, iquality = GetItemInfo(strtrim(text))
+				if not iname then
+					return addon:Print("Error:  %s is not a valid item name/link!", text)
+				end
+				local id = tonumber(ilink:match("item:(%d+)"))
+				list[id] = addon.quality_hexes[iquality] .. iname .. "|r"
+				OuroLootSV_opts.itemfilter[id] = true
+				addon:Print("Now filtering out", ilink)
+			end)
+			w:SetCallback("OnListItemClicked", function(_w, _, key_id, val_name)
+				--local ilink = select(2,GetItemInfo(key_id))
+				OuroLootSV_opts.itemfilter[tonumber(key_id)] = nil
+				--addon:Print("No longer filtering out", ilink)
+				addon:Print("No longer filtering out", val_name)
+			end)
+			grp:AddChild(w)
+		end
+
+		scroll:AddChild(grp)
+
+		addon.sender_list.sort()
+		if #addon.sender_list.namesI > 0 then
+			local senders = table.concat(addon.sender_list.namesI,'\n')   -- sigh
+			-- If 39 other people in the raid are running this, the label will
+			-- explode... is it likely enough to care about?  No.
+			w = GUI:Create("Spacer")
+			w:SetFullWidth(true)
+			w:SetHeight(20)
+			grp:AddChild(w)
+			w = GUI:Create("Label")
+			w:SetRelativeWidth(0.4)
+			w:SetText(addon.quality_hexes[3].."Echo from latest ping:|r\n"..senders)
+			grp:AddChild(w)
+		end
+
+		w = mkbutton("ReloadUI", [[Does what you think it does.  Loot information is written out and restored.]])
+		w:SetFullWidth(true)
+		w:SetCallback("OnClick", ReloadUI)
+		specials:AddChild(w)
+
+		w = mkbutton("Ping!",
+			[[Asks other raid users for their addon version and current status.  Results displayed on User Options panel.]])
+		w:SetFullWidth(true)
+		w:SetCallback("OnClick", function(_w)
+			addon:Print("Give me a ping, Vasili. One ping only, please.")
+			addon.sender_list.active = {}
+			addon.sender_list.names = {}
+			_w:SetText("5... 4... 3...")
+			_w:SetDisabled(true)
+			addon:broadcast('ping')
+			addon:ScheduleTimer(function(b)
+				if b:IsVisible() then
+					return addon:redisplay()
+					--return tabs_OnGroupSelected_func(container,"OnGroupSelected","opt")
+				end
+			end, 5, _w)
+		end)
+		specials:AddChild(w)
+
+		-- Add appropriate lower panel
+		adv_lower (scroll, specials)
+
+		-- Finish up
+		container:AddChild(scroll)
+	end
+end
+
+
+-- Simply to avoid recreating the same function over and over
+local tabs_OnGroupSelected_func_args = { [2] = "OnGroupSelected" }
+tabs_OnGroupSelected_func = function (tabs,event,group)
+	tabs_OnGroupSelected_func_args[1] = tabs
+	tabs_OnGroupSelected_func_args[3] = group
+	tabs:ReleaseChildren()
+	local spec = tabs:GetUserData("special buttons group")
+	spec:ReleaseChildren()
+	local h = GUI:Create("Heading")
+	h:SetFullWidth(true)
+	h:SetText(_tabtexts[group].title)
+	spec:AddChild(h)
+	return tabs_OnGroupSelected[group](tabs,spec,group)
+	--[====[
+	Unfortunately, :GetHeight() called on anything useful out of a TabGroup
+	returns the static default size (about 50 pixels) until the refresh
+	cycle *after* all the frames are shown.  Trying to fix it up after a
+	single OnUpdate doesn't work either.  So for now it's all hardcoded.
+	
+	Using this to determine the actual height of the usable area.
+	366 pixels
+	if group == "eoi" then
+		local stframe = tabs.children[1].frame
+		print(stframe:GetTop(),"-",stframe:GetBottom(),"=",
+		      stframe:GetTop()-stframe:GetBottom())
+		print(stframe:GetRight(),"-",stframe:GetLeft(),"=",
+		      stframe:GetRight()-stframe:GetLeft())
+	end
+	]====]
+end
+
+--[[
+mkbutton ("WidgetType", 'display key', "Text On Widget", "the mouseover display text")
+mkbutton ( [Button]     'display key', "Text On Widget", "the mouseover display text")
+mkbutton ( [Button]      [text]        "Text On Widget", "the mouseover display text")
+]]
+do
+	local replacement_colors = { ["+"]="|cffffffff", ["<"]="|cff00ff00", [">"]="|r" }
+	function mkbutton (opt_widget_type, opt_key, label, status)
+		if not label then
+			opt_widget_type, opt_key, label, status = "Button", opt_widget_type, opt_widget_type, opt_key
+		elseif not status then
+			opt_widget_type, opt_key, label, status = "Button", opt_widget_type, opt_key, label
+		end
+		local button = GUI:Create(opt_widget_type)
+		if button.SetText then button:SetText(tostring(label)) end
+		status = status:gsub("[%+<>]",replacement_colors)
+		button:SetCallback("OnEnter", function() setstatus(status) end) -- maybe factor that closure out
+		button:SetCallback("OnLeave", statusy_OnLeave)
+		-- retrieval key may be specified as nil if all the parameters are given
+		if opt_key then _d:SetUserData (opt_key, button) end
+		return button
+	end
+end
+
+--[[
+Creates the main window.
+]]
+function addon:BuildMainDisplay (opt_tabselect)
+	if self.display then
+		-- try to get everything to update, rebuild, refresh... ugh, no
+		self.display:Hide()
+	end
+
+	local display = GUI:Create("Frame")
+	if _d then
+		display:SetUserData("eoiST",_d)   -- warning! warning! kludge detected!
+	end
+	_d = display
+	self.display = display
+	display:SetTitle("Ouro Loot")
+	display:SetStatusText(self.status_text)
+	display:SetLayout("Flow")
+	display:SetStatusTable{width=800}
+	-- prevent resizing, also see ace3 ticket #80
+	--[[
+	display.sizer_se:SetScript("OnMouseDown",nil)
+	display.sizer_se:SetScript("OnMouseUp",nil)
+	display.sizer_s:SetScript("OnMouseDown",nil)
+	display.sizer_s:SetScript("OnMouseUp",nil)
+	display.sizer_e:SetScript("OnMouseDown",nil)
+	display.sizer_e:SetScript("OnMouseUp",nil)
+	]]
+	display:SetCallback("OnClose", function(_display)
+		_d = _display:GetUserData("eoiST")
+		self.display = nil
+		GUI:Release(_display)
+		collectgarbage()
+	end)
+
+	----- Right-hand panel
+	local rhs_width = 0.20
+	local control = GUI:Create("SimpleGroup")
+	control:SetLayout("Flow")
+	control:SetRelativeWidth(rhs_width)
+	control.alignoffset = 25
+	control:PauseLayout()
+	local h,b
+
+	--- Main ---
+	h = GUI:Create("Heading")
+	h:SetFullWidth(true)
+	h:SetText("Main")
+	control:AddChild(h)
+
+	do
+		b = mkbutton("Dropdown", nil, "",
+			[[Enable full tracking, only rebroadcasting, or disable activity altogether.]])
+		b:SetFullWidth(true)
+		b:SetLabel("On/Off:")
+		b:SetList{"Full Tracking", "Broadcasting", "Disabled"}
+		b:SetValue(self.enabled and 1 or (self.rebroadcast and 2 or 3))
+		b:SetCallback("OnValueChanged", function(_w,event,choice)
+			if choice == 1 then       self:Activate()
+			elseif choice == 2 then   self:Activate(nil,true)
+			else                      self:Deactivate()
+			end
+			_w = display:GetUserData('comm_ident')
+			if _w and _w:IsVisible() then
+				_w:SetDisabled(self.enabled or self.rebroadcast)
+			end
+			_w = display:GetUserData('eoi_bcast_req')
+			if _w and _w:IsVisible() then
+				_w:SetDisabled(not self.enabled)
+			end
+		end)
+		control:AddChild(b)
+	end
+
+	b = mkbutton("Dropdown", 'threshold', "",
+		[[Items greater than or equal to this quality will be tracked/rebroadcast.]])
+	b:SetFullWidth(true)
+	b:SetLabel("Threshold:")
+	b:SetList(self.thresholds)
+	b:SetValue(self.threshold)
+	b:SetCallback("OnValueChanged", function(_dd,event,choice)
+		self:SetThreshold(choice)
+	end)
+	control:AddChild(b)
+
+	b = mkbutton("Clear",
+		[[+Erases> all current loot information and generated text (but not saved texts).]])
+	b:SetFullWidth(true)
+	b:SetCallback("OnClick", function()
+		StaticPopup_Show("OUROL_CLEAR").data = self
+	end)
+	control:AddChild(b)
+
+	b = GUI:Create("Spacer")
+	b:SetFullWidth(true)
+	b:SetHeight(15)
+	control:AddChild(b)
+
+	--[[
+	--- Saved Texts ---
+	 [ Save Current As... ]
+	   saved1
+	   saved2
+	   ...
+	 [ Load ]  [ Delete ]
+	]]
+	h = GUI:Create("Heading")
+	h:SetFullWidth(true)
+	h:SetText("Saved Texts")
+	control:AddChild(h)
+	b = mkbutton("Save Current As...",
+		[[Save forum/attendance/etc texts for later retrieval.  Main loot information not included.]])
+	b:SetFullWidth(true)
+	b:SetCallback("OnClick", function()
+		StaticPopup_Show "OUROL_SAVE_SAVEAS"
+		_d:Hide()
+	end)
+	control:AddChild(b)
+
+	local saved = self:check_saved_table(--[[silent_on_empty=]]true)
+	if saved then for i,s in ipairs(saved) do
+		local il = GUI:Create("InteractiveLabel")
+		il:SetFullWidth(true)
+		il:SetText(s.name)
+		il:SetUserData("num",i)
+		il:SetHighlight(1,1,1,0.4)
+		local str = ("%s    %d entries     %s"):format(s.date,s.count,s.name)
+		il:SetCallback("OnEnter", function() setstatus(str) end)
+		il:SetCallback("OnLeave", statusy_OnLeave)
+		il:SetCallback("OnClick", function(_il)
+			local prev = _d:GetUserData("saved selection")
+			if prev then
+				prev.highlight:Hide()
+				prev:SetColor()
+			end
+			_il:SetColor(0,1,0)
+			_il.highlight:Show()
+			_d:SetUserData("saved selection",_il)
+			_d:GetUserData("Load"):SetDisabled(false)
+			_d:GetUserData("Delete"):SetDisabled(false)
+		end)
+		control:AddChild(il)
+	end end
+
+	b = mkbutton("Load",
+		[[Load previously saved text.  +REPLACES> all current loot information!]])
+	b:SetRelativeWidth(0.5)
+	b:SetCallback("OnClick", function()
+		local num = _d:GetUserData("saved selection"):GetUserData("num")
+		self:save_restore(num)
+		self:BuildMainDisplay()
+	end)
+	b:SetDisabled(true)
+	control:AddChild(b)
+	b = mkbutton("Delete",
+		[[Delete previously saved text.]])
+	b:SetRelativeWidth(0.5)
+	b:SetCallback("OnClick", function()
+		local num = _d:GetUserData("saved selection"):GetUserData("num")
+		self:save_delete(num)
+		self:BuildMainDisplay()
+	end)
+	b:SetDisabled(true)
+	control:AddChild(b)
+
+	b = GUI:Create("Spacer")
+	b:SetFullWidth(true)
+	b:SetHeight(15)
+	control:AddChild(b)
+
+	-- Other stuff on right-hand side
+	local tab_specials = GUI:Create("SimpleGroup")
+	tab_specials:SetLayout("Flow")
+	tab_specials:SetFullWidth(true)
+	control:AddChild(tab_specials)
+	control:ResumeLayout()
+
+	----- Left-hand group
+	local tabs = GUI:Create("TabGroup")
+	tabs:SetLayout("Flow")
+	tabs.alignoffset = 25
+	tabs.titletext:SetFontObject(GameFontNormalSmall) -- XXX
+	do
+		self.sender_list.sort()
+		tabs.titletext:SetFormattedText("Received broadcast data from %d |4player:players;.",
+			self.sender_list.activeI)
+	end
+	tabs:SetRelativeWidth(0.99-rhs_width)
+	tabs:SetFullHeight(true)
+	tabs:SetTabs(tabgroup_tabs)
+	tabs:SetCallback("OnGroupSelected", tabs_OnGroupSelected_func)
+	tabs:SetCallback("OnTabEnter", function(_tabs,event,value,tab)
+		setstatus(_tabtexts[value].desc)
+	end)
+	tabs:SetCallback("OnTabLeave", statusy_OnLeave)
+	tabs:SetUserData("special buttons group",tab_specials)
+	tabs:SelectTab(opt_tabselect or "eoi")
+
+	display:AddChildren (tabs, control)
+	display:ApplyStatus()
+
+	display:Show() -- without this, only appears every *other* function call
+	return display
+end
+
+function addon:OpenMainDisplayToTab (text)
+	text = '^'..text
+	for tab,v in pairs(_tabtexts) do
+		if v.title:lower():find(text) then
+			self:BuildMainDisplay(tab)
+			return true
+		end
+	end
+end
+
+-- Essentially a re-click on the current tab (if the current tab were clickable).
+function addon:redisplay ()
+	tabs_OnGroupSelected_func (unpack(tabs_OnGroupSelected_func_args))
+end
+
+end -- local 'do' scope
+
+
+------ Popup dialogs
+-- Callback for each Next/Accept stage of inserting a new loot row via dropdown
+local function eoi_st_insert_OnAccept_boss (dialog, data)
+	if data.all_done then
+		-- It'll probably be the final entry in the table, but there might have
+		-- been real loot happening at the same time.
+		local boss_index = addon._addLootEntry{
+			kind		= 'boss',
+			bosskill	= (OuroLootSV_opts.snarky_boss and addon.boss_abbrev[data.name] or data.name) or data.name,
+			reason		= 'kill',
+			instance	= data.instance,
+			duration	= 0,
+		}
+		local entry = tremove(g_loot,boss_index)
+		tinsert(g_loot,data.rowindex,entry)
+		addon:_mark_boss_kill(data.rowindex)
+		data.display:GetUserData("eoiST"):OuroLoot_Refresh(data.rowindex)
+		dialog.data = nil   -- free up memory
+		addon:Print("Inserted %s %s (entry %d).", data.kind, data.name, data.rowindex)
+		return
+	end
+
+	local text = dialog.wideEditBox:GetText()
+
+	-- second click
+	if data.name and text then
+		data.instance = text
+		data.all_done = true
+		-- in future do one more thing, for now just jump to the check
+		return eoi_st_insert_OnAccept_boss (dialog, data)
+	end
+
+	-- first click
+	if text then
+		data.name = text
+		local getinstance = StaticPopup_Show("OUROL_EOI_INSERT","instance")
+		getinstance.data = data
+		getinstance.wideEditBox:SetText(addon.instance_tag())
+		-- This suppresses auto-hide (which would case the getinstance dialog
+		-- to go away), but only when mouse clicking.  OnEnter is on its own.
+		return true
+	end
+end
+
+local function eoi_st_insert_OnAccept_loot (dialog, data)
+	if data.all_done then
+		--local real_rebroadcast, real_enabled = addon.rebroadcast, addon.enabled
+		--g_rebroadcast, g_enabled = false, true
+		data.display:Hide()
+		local loot_index = addon:CHAT_MSG_LOOT ("manual", data.recipient, data.name, data.notes)
+		--g_rebroadcast, g_enabled = real_g_rebroadcast, real_g_enabled
+		local entry = tremove(g_loot,loot_index)
+		tinsert(g_loot,data.rowindex,entry)
+		--data.display:GetUserData("eoiST"):OuroLoot_Refresh(data.rowindex)
+		addon:_fill_out_eoi_data(data.rowindex)
+		addon:BuildMainDisplay()
+		dialog.data = nil
+		addon:Print("Inserted %s %s (entry %d).", data.kind, data.name, data.rowindex)
+		return
+	end
+
+	local text = dialog.wideEditBox:GetText():trim()
+
+	-- third click
+	if data.name and data.recipient and text then
+		data.notes = (text ~= "<none>") and text or nil
+		data.all_done = true
+		return eoi_st_insert_OnAccept_loot (dialog, data)
+	end
+
+	-- second click
+	if data.name and text then
+		data.recipient = text
+		local getnotes = StaticPopup_Show("OUROL_EOI_INSERT","notes")
+		getnotes.data = data
+		getnotes.wideEditBox:SetText("<none>")
+		getnotes.wideEditBox:HighlightText()
+		return true
+	end
+
+	-- first click
+	if text then
+		data.name = text
+		dialog:Hide()  -- technically a "different" one about to be shown
+		local getrecipient = StaticPopup_Show("OUROL_EOI_INSERT","recipient")
+		getrecipient.data = data
+		getrecipient.wideEditBox:SetText("")
+		return true
+	end
+end
+
+local function eoi_st_insert_OnAccept (dialog, data)
+	if data.kind == 'boss' then
+		return eoi_st_insert_OnAccept_boss (dialog, data)
+	elseif data.kind == 'loot' then
+		return eoi_st_insert_OnAccept_loot (dialog, data)
+	end
+end
+
+StaticPopupDialogs["OUROL_CLEAR"] = flib.StaticPopup{
+	text = "Clear current loot information and text?",
+	button1 = ACCEPT,
+	button2 = CANCEL,
+	OnAccept = function (dialog, addon)
+		addon:Clear(--[[verbose_p=]]true)
+	end,
+}
+
+StaticPopupDialogs["OUROL_REMIND"] = flib.StaticPopup{
+	text = "Do you wish to activate Ouro Loot?\n\n(Hit the Escape key to close this window without clicking)",
+	button1 = "Activate recording",  -- "accept", left
+	button3 = "Broadcast only",      -- "alt", middle
+	button2 = "Help",                -- "cancel", right
+	OnAccept = function (dialog, addon)
+		addon:Activate()
+	end,
+	OnAlt = function (dialog, addon)
+		addon:Activate(nil,true)
+	end,
+	OnCancel = function (dialog, addon)
+		-- hitting escape also calls this, but the 3rd arg would be "clicked"
+		-- in both cases, not useful here.
+		local helpbutton = dialog.button2
+		local ismousing = MouseIsOver(helpbutton)
+		if ismousing then
+			-- they actually clicked the button (or at least the mouse was over "Help"
+			-- when they hit escape... sigh)
+			addon:BuildMainDisplay('help')
+		else
+			addon.popped = true
+		end
+	end,
+}
+
+-- The data member here is a table built with:
+-- {rowindex=<GUI row receiving click>, display=_d, kind=<loot/boss>}
+do
+	local t = flib.StaticPopup{
+		text = "Enter name of new %s, then click Next or press Enter:",
+		button1 = "Next >",
+		button2 = CANCEL,
+		hasEditBox = true,
+		hasWideEditBox = true,
+		maxLetters = 50,
+		noCancelOnReuse = true,
+		--[[ XXX still needed?
+		OnShow = function(dialog)
+			dialog.wideEditBox:SetText("")
+			dialog.wideEditBox:SetFocus()
+		end,]]
+	}
+	t.EditBoxOnEnterPressed = function(editbox)
+		local dialog = editbox:GetParent()
+		if not eoi_st_insert_OnAccept (dialog, dialog.data) then
+			dialog:Hide()  -- replicate OnAccept click behavior
+		end
+	end
+	t.enterClicksFirstButton = nil  -- no effect with editbox focused
+	t.OnAccept = eoi_st_insert_OnAccept
+	StaticPopupDialogs["OUROL_EOI_INSERT"] = t
+end
+
+-- This seems to be gratuitous use of metatables, really.
+do
+	local OEIL = {
+		text = "Paste the new item into here, then click Next or press Enter:",
+		__index = StaticPopupDialogs["OUROL_EOI_INSERT"]
+	}
+	StaticPopupDialogs["OUROL_EOI_INSERT_LOOT"] = setmetatable(OEIL,OEIL)
+
+	hooksecurefunc("ChatEdit_InsertLink", function (link,...)
+		local dialogname = StaticPopup_Visible "OUROL_EOI_INSERT_LOOT"
+		if dialogname then
+			_G[dialogname.."WideEditBox"]:SetText(link)
+			return true
+		end
+	end)
+end
+
+StaticPopupDialogs["OUROL_REASSIGN_ENTER"] = flib.StaticPopup{
+	text = "Enter the player name:",
+	button1 = ACCEPT,
+	button2 = CANCEL,
+	hasEditBox = true,
+	--[[ XXX needed?
+	OnShow = function(dialog)
+		dialog.editBox:SetText("")
+		dialog.editBox:SetFocus()
+	end,]]
+	OnAccept = function(dialog, data)
+		local name = dialog.usertext --editBox:GetText()
+		g_loot[data.index].person = name
+		g_loot[data.index].person_class = select(2,UnitClass(name))
+		addon:Print("Reassigned entry %d to '%s'.", data.index, name)
+		data.display:GetUserData("eoiST"):OuroLoot_Refresh(data.index)
+	end,
+}
+
+StaticPopupDialogs["OUROL_SAVE_SAVEAS"] = flib.StaticPopup{
+	text = "Enter a name for the loot collection:",
+	button1 = ACCEPT,
+	button2 = CANCEL,
+	hasEditBox = true,
+	maxLetters = 30,
+	--[[ XXX
+	OnShow = function(dialog)
+		dialog.editBox:SetText("")
+		dialog.editBox:SetFocus()
+	end,]]
+	OnAccept = function(dialog)--, data)
+		local name = dialog.usertext --editBox:GetText()
+		addon:save_saveas(name)
+		addon:BuildMainDisplay()
+	end,
+	OnCancel = function(dialog)--, data, reason)
+		addon:BuildMainDisplay()
+	end,
+	--[[XXX
+	EditBoxOnEnterPressed = function(editbox)
+		local dialog = editbox:GetParent()
+		StaticPopupDialogs["OUROL_SAVE_SAVEAS"].OnAccept (dialog, dialog.data)
+		dialog:Hide()
+	end,]]
+}
+
+-- vim:noet