changeset 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 0f14a1e5364d
children fe437e761ef8
files .pkgmeta AceGUIWidget-CheckBoxSmallLabel.lua AceGUIWidget-DoTimerEditBoxDropDown.lua AceGUIWidget-Spacer.lua AceGUIWidget-lib-st.lua LibFarmbuyer.lua Ouro_Loot.toc abbreviations.lua core.lua gui.lua mleqdkp.lua text_tabs.lua verbage.lua
diffstat 13 files changed, 5597 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.pkgmeta	Sat Apr 16 06:03:29 2011 +0000
@@ -0,0 +1,28 @@
+package-as: Ouro_Loot
+
+externals:
+  libs/LibStub:
+    url: svn://svn.wowace.com/wow/libstub/mainline/trunk
+    tag: latest
+  libs/CallbackHandler-1.0:
+    url: svn://svn.wowace.com/wow/callbackhandler/mainline/trunk/CallbackHandler-1.0
+    tag: latest
+  libs/AceAddon-3.0:
+    url: svn://svn.wowace.com/wow/ace3/mainline/trunk/AceAddon-3.0
+  libs/AceEvent-3.0:
+    url: svn://svn.wowace.com/wow/ace3/mainline/trunk/AceEvent-3.0
+  libs/AceConsole-3.0:
+    url: svn://svn.wowace.com/wow/ace3/mainline/trunk/AceConsole-3.0
+  libs/AceTimer-3.0:
+    url: svn://svn.wowace.com/wow/ace3/mainline/trunk/AceTimer-3.0
+  libs/AceLocale-3.0:
+    url: svn://svn.wowace.com/wow/ace3/mainline/trunk/AceLocale-3.0
+  libs/AceGUI-3.0:
+    url: svn://svn.wowace.com/wow/ace3/mainline/trunk/AceGUI-3.0
+  libs/AceComm-3.0:
+    url: svn://svn.wowace.com/wow/ace3/mainline/trunk/AceComm-3.0
+  libs/lib-st:
+    url: svn://svn.wowace.com/wow/lib-st/mainline/trunk
+    tag: latest
+
+# vim: et
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/AceGUIWidget-CheckBoxSmallLabel.lua	Sat Apr 16 06:03:29 2011 +0000
@@ -0,0 +1,299 @@
+--[[
+This is exactly the same as the normal CheckBox widget, rev 21, but with a
+different font string choice for the text label.  (The same one as used for
+the "description" string, by coincidence.)  There's no way to cleanly change
+*and restore* the font strings without a pile of (possibly chained) OnRelease
+hooks, which just makes more noise in the problem domain.
+- farmbuyer
+]]
+
+--[[-----------------------------------------------------------------------------
+Checkbox Widget
+-------------------------------------------------------------------------------]]
+local Type, Version = "CheckBoxSmallLabel", 21
+local AceGUI = LibStub and LibStub("AceGUI-3.0", true)
+if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end
+
+-- Lua APIs
+local select, pairs = select, pairs
+
+-- WoW APIs
+local PlaySound = PlaySound
+local CreateFrame, UIParent = CreateFrame, UIParent
+
+-- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
+-- List them here for Mikk's FindGlobals script
+-- GLOBALS: SetDesaturation, GameFontHighlight
+
+--[[-----------------------------------------------------------------------------
+Support functions
+-------------------------------------------------------------------------------]]
+local function AlignImage(self)
+	local img = self.image:GetTexture()
+	self.text:ClearAllPoints()
+	if not img then
+		self.text:SetPoint("LEFT", self.checkbg, "RIGHT")
+		self.text:SetPoint("RIGHT")
+	else
+		self.text:SetPoint("LEFT", self.image,"RIGHT", 1, 0)
+		self.text:SetPoint("RIGHT")
+	end
+end
+
+--[[-----------------------------------------------------------------------------
+Scripts
+-------------------------------------------------------------------------------]]
+local function Control_OnEnter(frame)
+	frame.obj:Fire("OnEnter")
+end
+
+local function Control_OnLeave(frame)
+	frame.obj:Fire("OnLeave")
+end
+
+local function CheckBox_OnMouseDown(frame)
+	local self = frame.obj
+	if not self.disabled then
+		if self.image:GetTexture() then
+			self.text:SetPoint("LEFT", self.image,"RIGHT", 2, -1)
+		else
+			self.text:SetPoint("LEFT", self.checkbg, "RIGHT", 1, -1)
+		end
+	end
+	AceGUI:ClearFocus()
+end
+
+local function CheckBox_OnMouseUp(frame)
+	local self = frame.obj
+	if not self.disabled then
+		self:ToggleChecked()
+
+		if self.checked then
+			PlaySound("igMainMenuOptionCheckBoxOn")
+		else -- for both nil and false (tristate)
+			PlaySound("igMainMenuOptionCheckBoxOff")
+		end
+
+		self:Fire("OnValueChanged", self.checked)
+		AlignImage(self)
+	end
+end
+
+--[[-----------------------------------------------------------------------------
+Methods
+-------------------------------------------------------------------------------]]
+local methods = {
+	["OnAcquire"] = function(self)
+		self:SetType()
+		self:SetValue(false)
+		self:SetTriState(nil)
+		-- height is calculated from the width and required space for the description
+		self:SetWidth(200)
+		self:SetImage()
+		self:SetDisabled(nil)
+		self:SetDescription(nil)
+	end,
+
+	-- ["OnRelease"] = nil,
+
+	["OnWidthSet"] = function(self, width)
+		if self.desc then
+			self.desc:SetWidth(width - 30)
+			if self.desc:GetText() and self.desc:GetText() ~= "" then
+				self:SetHeight(28 + self.desc:GetHeight())
+			end
+		end
+	end,
+
+	["SetDisabled"] = function(self, disabled)
+		self.disabled = disabled
+		if disabled then
+			self.frame:Disable()
+			self.text:SetTextColor(0.5, 0.5, 0.5)
+			SetDesaturation(self.check, true)
+		else
+			self.frame:Enable()
+			self.text:SetTextColor(1, 1, 1)
+			if self.tristate and self.checked == nil then
+				SetDesaturation(self.check, true)
+			else
+				SetDesaturation(self.check, false)
+			end
+		end
+	end,
+
+	["SetValue"] = function(self,value)
+		local check = self.check
+		self.checked = value
+		if value then
+			SetDesaturation(self.check, false)
+			self.check:Show()
+		else
+			--Nil is the unknown tristate value
+			if self.tristate and value == nil then
+				SetDesaturation(self.check, true)
+				self.check:Show()
+			else
+				SetDesaturation(self.check, false)
+				self.check:Hide()
+			end
+		end
+		self:SetDisabled(self.disabled)
+	end,
+
+	["GetValue"] = function(self)
+		return self.checked
+	end,
+
+	["SetTriState"] = function(self, enabled)
+		self.tristate = enabled
+		self:SetValue(self:GetValue())
+	end,
+
+	["SetType"] = function(self, type)
+		local checkbg = self.checkbg
+		local check = self.check
+		local highlight = self.highlight
+
+		local size
+		if type == "radio" then
+			size = 16
+			checkbg:SetTexture("Interface\\Buttons\\UI-RadioButton")
+			checkbg:SetTexCoord(0, 0.25, 0, 1)
+			check:SetTexture("Interface\\Buttons\\UI-RadioButton")
+			check:SetTexCoord(0.25, 0.5, 0, 1)
+			check:SetBlendMode("ADD")
+			highlight:SetTexture("Interface\\Buttons\\UI-RadioButton")
+			highlight:SetTexCoord(0.5, 0.75, 0, 1)
+		else
+			size = 24
+			checkbg:SetTexture("Interface\\Buttons\\UI-CheckBox-Up")
+			checkbg:SetTexCoord(0, 1, 0, 1)
+			check:SetTexture("Interface\\Buttons\\UI-CheckBox-Check")
+			check:SetTexCoord(0, 1, 0, 1)
+			check:SetBlendMode("BLEND")
+			highlight:SetTexture("Interface\\Buttons\\UI-CheckBox-Highlight")
+			highlight:SetTexCoord(0, 1, 0, 1)
+		end
+		checkbg:SetHeight(size)
+		checkbg:SetWidth(size)
+	end,
+
+	["ToggleChecked"] = function(self)
+		local value = self:GetValue()
+		if self.tristate then
+			--cycle in true, nil, false order
+			if value then
+				self:SetValue(nil)
+			elseif value == nil then
+				self:SetValue(false)
+			else
+				self:SetValue(true)
+			end
+		else
+			self:SetValue(not self:GetValue())
+		end
+	end,
+
+	["SetLabel"] = function(self, label)
+		self.text:SetText(label)
+	end,
+
+	["SetDescription"] = function(self, desc)
+		if desc then
+			if not self.desc then
+				local desc = self.frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
+				desc:ClearAllPoints()
+				desc:SetPoint("TOPLEFT", self.checkbg, "TOPRIGHT", 5, -21)
+				desc:SetWidth(self.frame.width - 30)
+				desc:SetJustifyH("LEFT")
+				desc:SetJustifyV("TOP")
+				self.desc = desc
+			end
+			self.desc:Show()
+			--self.text:SetFontObject(GameFontNormal)
+			self.desc:SetText(desc)
+			self:SetHeight(28 + self.desc:GetHeight())
+		else
+			if self.desc then
+				self.desc:SetText("")
+				self.desc:Hide()
+			end
+			--self.text:SetFontObject(GameFontHighlight)
+			self:SetHeight(24)
+		end
+	end,
+	
+	["SetImage"] = function(self, path, ...)
+		local image = self.image
+		image:SetTexture(path)
+		
+		if image:GetTexture() then
+			local n = select("#", ...)
+			if n == 4 or n == 8 then
+				image:SetTexCoord(...)
+			else
+				image:SetTexCoord(0, 1, 0, 1)
+			end
+		end
+		AlignImage(self)
+	end
+}
+
+--[[-----------------------------------------------------------------------------
+Constructor
+-------------------------------------------------------------------------------]]
+local function Constructor()
+	local frame = CreateFrame("Button", nil, UIParent)
+	frame:Hide()
+
+	frame:EnableMouse(true)
+	frame:SetScript("OnEnter", Control_OnEnter)
+	frame:SetScript("OnLeave", Control_OnLeave)
+	frame:SetScript("OnMouseDown", CheckBox_OnMouseDown)
+	frame:SetScript("OnMouseUp", CheckBox_OnMouseUp)
+
+	local checkbg = frame:CreateTexture(nil, "ARTWORK")
+	checkbg:SetWidth(24)
+	checkbg:SetHeight(24)
+	checkbg:SetPoint("TOPLEFT")
+	checkbg:SetTexture("Interface\\Buttons\\UI-CheckBox-Up")
+
+	local check = frame:CreateTexture(nil, "OVERLAY")
+	check:SetAllPoints(checkbg)
+	check:SetTexture("Interface\\Buttons\\UI-CheckBox-Check")
+
+	--local text = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
+	local text = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
+	text:SetJustifyH("LEFT")
+	text:SetHeight(18)
+	text:SetPoint("LEFT", checkbg, "RIGHT")
+	text:SetPoint("RIGHT")
+
+	local highlight = frame:CreateTexture(nil, "HIGHLIGHT")
+	highlight:SetTexture("Interface\\Buttons\\UI-CheckBox-Highlight")
+	highlight:SetBlendMode("ADD")
+	highlight:SetAllPoints(checkbg)
+
+	local image = frame:CreateTexture(nil, "OVERLAY")
+	image:SetHeight(16)
+	image:SetWidth(16)
+	image:SetPoint("LEFT", checkbg, "RIGHT", 1, 0)
+
+	local widget = {
+		checkbg   = checkbg,
+		check     = check,
+		text      = text,
+		highlight = highlight,
+		image     = image,
+		frame     = frame,
+		type      = Type
+	}
+	for method, func in pairs(methods) do
+		widget[method] = func
+	end
+
+	return AceGUI:RegisterAsWidget(widget)
+end
+
+AceGUI:RegisterWidgetType(Type, Constructor, Version)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/AceGUIWidget-DoTimerEditBoxDropDown.lua	Sat Apr 16 06:03:29 2011 +0000
@@ -0,0 +1,289 @@
+local Type, Version = "EditBoxDropDown", 3
+local AceGUI = LibStub and LibStub("AceGUI-3.0", true)
+if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end
+
+--[[
+This is a from-scratch AceGUI-style implementation of the combination drop-
+down editboxes seen in DoTimer's option menus.  Typing in the editbox adds
+entries to a list; clicking the scrolldown button displays the list in a
+dropdown; clicking on an entry in the dropdown removes it from the list.
+
+(This type of widget may actually have a more formal name already.  I first
+encountered it in DoTimer, so that's what I named it after.)
+
+The implementation does not borrow any of DoTimer's actual code.  I've tried
+writing this to imitate the look-and-feel of an interface that many players
+will find familiar and easy to use.
+
+Version 2 is a rehash to follow the same form of the rest of the AceGUI widgets
+after their rewrite, and to work with the new EditBox behavior.
+
+Version 3 adds the EditBoxDropDownOptionControl variant, specifically for use
+as a 'dialogControl' in AceConfig option tables.  Details follow; the more
+disappointing restrictions are because AceConfig never gives the end user any
+direct link to the widgets in use.
+- 'desc' option field ignored
+- 'name' field may contain an embedded tab ('\t') followed by more text to be
+  used as the tooltip text when hovering over the editbox field
+- 'get' field must be a function, returning a function to be used as the
+  OnTextEnterPressed callback; this is typically how new entries should be
+  added to data
+- 'values' field must be a function, returning the usual list of entries, PLUS
+  the callback used for 'get', e.g., 
+    values = function()
+        local ret = build_real_dropdown_values()
+        ret[get_callback] = true  -- assuming "function get_callback (widget, event, text)"
+        return ret
+    end
+  The callback will be immediately removed from the table, but is required to
+  be present to pass tests in the AceConfig source.
+- 'set' receives the key of the dropdown table, but that entry will already be
+  removed by the time the 'set' function is called
+
+
+EditBoxDropDown API
+
+:SetLabel(txt)
+   forwards to the editbox's SetLabel
+
+:SetText(txt)
+   forwards to the editbox's SetText
+
+:SetEditBoxTooltip(txt)
+   sets text for the tooltip shown when hovering over the editbox
+   no default
+
+:SetButtonTooltip(txt)
+   sets text for the tooltip shown when hovering over the dropdown button
+   default "Click on entries to remove them."
+
+:SetList(t)
+   T is a table to be shown in the dropdown list; the values will be displayed
+   in the dropdown
+   When entries are clicked, they will be removed from T.  T is not copied,
+   the table is edited "live" before firing the callback below.
+
+
+EditBoxDropDown Callbacks
+
+OnTextEnterPressed
+   same as the editbox's OnEnterPressed
+
+OnListItemClicked
+   similar to a Dropdown widget's OnValueChanged, the key and value from the
+   table given to :SetList are passed
+
+
+farmbuyer
+]]
+
+local button_hover_text_default = "Click on entries to remove them."
+local maps  -- from actual editbox frame back to this widget
+
+
+local function Menu_OnClick (button, userlist_key, widget)
+	local v = widget.list[userlist_key]
+	widget.list[userlist_key] = nil
+	widget.dropdown.is_on = nil
+    -- firing these off changes widget contents, must be done last :-(
+	widget:Fire("OnListItemClicked", userlist_key, v)
+	widget:Fire("OnValueChanged", userlist_key)
+end
+
+local function BuildList (widget)
+	local ret = {}
+	if widget.list then for k,v in pairs(widget.list) do
+		table.insert (ret, {
+			text = tostring(v) or tostring(k),
+			func = Menu_OnClick,
+			arg1 = k,
+			arg2 = widget,
+			notCheckable = true,
+		})
+	end end
+	return ret
+end
+
+
+local function ddEditBox_OnMouseEnter (editbox)
+	if editbox.tooltip_text then
+		GameTooltip:SetOwner(editbox.frame, "ANCHOR_RIGHT")
+		GameTooltip:SetText(editbox.tooltip_text, nil, nil, nil, nil, 1)
+	end
+end
+
+local function ddEditBox_OnMouseLeave (editbox_or_button)
+	GameTooltip:Hide()
+end
+
+local function ddEditBox_Clear (editboxframe)
+	editboxframe:SetText("")
+end
+
+local function ddEditBox_Reset (editboxframe)  -- :ClearFocus triggers this
+	editboxframe:SetText(maps[editboxframe].editbox_basetext or "")
+end
+
+local function ddEditBox_OnEnterPressed (editbox, _, text)
+	editbox.obj:Fire("OnTextEnterPressed", text)
+	ddEditBox_Reset(editbox.editbox)
+	editbox.editbox:ClearFocus()  -- cursor still blinking in there, one more time
+end
+
+local function Button_OnClick (button)
+	local dd = button.obj.dropdown
+	-- Annoyingly, it's very tedious to find the correct nested frame to use
+	-- for :IsShown() here.  We'll avoid it all by using our own flag.
+	if dd.is_on then
+		dd.is_on = nil
+		HideDropDownMenu(--[[level=]]1)   -- EasyMenu always uses top/1 level
+	else
+		dd.is_on = true
+		local t = BuildList(button.obj)
+		EasyMenu (t, button.obj.dropdown, button.obj.frame, 0, 0, "MENU")
+		PlaySound("igMainMenuOptionCheckBoxOn")
+	end
+end
+
+local function Button_OnEnter (button)
+	if button.tooltip_text then
+		GameTooltip:SetOwner(button, "ANCHOR_RIGHT")
+		GameTooltip:SetText(button.tooltip_text, nil, nil, nil, nil, 1)
+	end
+end
+
+
+local methods = {
+	["OnAcquire"] = function (self)
+		self:SetHeight(20)
+		self:SetWidth(100)
+		self.button.tooltip_text = button_hover_text_default
+		self:SetList(nil)
+		self.editbox:DisableButton(true)
+
+		maps = maps or {}
+		maps[self.editbox.editbox] = self
+	end,
+
+	["OnRelease"] = function (self)
+		self.frame:ClearAllPoints()
+		self.frame:Hide()
+		self.editbox.tooltip_text = nil
+		self.button.tooltip_text = nil
+		self:SetList(nil)
+		maps[self.editbox.editbox] = nil
+	end,
+
+	["SetParent"] = function (self, parent)
+		self.frame:SetParent(nil)
+		self.frame:SetParent(parent.content)
+		self.parent = parent
+		self.editbox:SetParent(parent)
+	end,
+
+	["SetText"] = function (self, text)
+		self.editbox_basetext = text
+		return self.editbox:SetText(text)
+	end,
+
+	["SetLabel"] = function (self, text)
+		return self.editbox:SetLabel(text)
+	end,
+
+	["SetDisabled"] = function (self, disabled)
+		self.editbox:SetDisabled(disabled)
+		if disabled then
+			self.button:Disable()
+		else
+			self.button:Enable()
+		end
+	end,
+
+	["SetList"] = function (self, list)
+		self.list = list
+	end,
+
+	["SetEditBoxTooltip"] = function (self, text)
+		self.editbox.tooltip_text = text
+	end,
+
+	["SetButtonTooltip"] = function (self, text)
+		self.button.tooltip_text = text
+	end,
+}
+
+-- called with the 'name' entry
+local function optcontrol_SetLabel (self, text)
+    local name, desc = ('\t'):split(text)
+    if desc then
+        self:SetEditBoxTooltip(desc)
+    end
+    self.editbox:SetLabel(name)
+end
+
+local function optcontrol_SetValue (self, epcallback)
+    -- set the callback
+    self:SetCallback("OnTextEnterPressed", epcallback)
+    -- remove the fake entry from the values table
+    self.list[epcallback] = nil
+end
+
+
+local function Constructor(is_option_control)
+	local num = AceGUI:GetNextWidgetNum(Type)
+
+	-- Its frame becomes our widget frame, else its frame is never shown.  Gluing
+	-- them together seems a little evil, but it beats making this widget into a
+	-- formal containter.  Inspired by new-style InteractiveLabel.
+	local editbox = AceGUI:Create("EditBox")
+	local frame = editbox.frame
+	editbox:SetHeight(20)
+	editbox:SetWidth(100)
+	frame:SetWidth(frame:GetWidth()+6)
+	editbox:SetCallback("OnEnter", ddEditBox_OnMouseEnter)
+	editbox:SetCallback("OnLeave", ddEditBox_OnMouseLeave)
+	editbox:SetCallback("OnEnterPressed", ddEditBox_OnEnterPressed)
+	editbox.editbox:SetScript("OnEditFocusGained", ddEditBox_Clear)
+	editbox.editbox:SetScript("OnEditFocusLost", ddEditBox_Reset)
+	--editbox.editbox:SetScript("OnEscapePressed", ddEditBox_Reset)
+
+	local button = CreateFrame("Button", nil, frame)
+	button:SetHeight(20)
+	button:SetWidth(24)
+	button:SetPoint("LEFT", editbox.frame, "RIGHT", 0, 0)
+	button:SetNormalTexture("Interface\\ChatFrame\\UI-ChatIcon-ScrollDown-Up")
+	button:SetPushedTexture("Interface\\ChatFrame\\UI-ChatIcon-ScrollDown-Down")
+	button:SetDisabledTexture("Interface\\ChatFrame\\UI-ChatIcon-ScrollDown-Disabled")
+	button:SetHighlightTexture("Interface\\Buttons\\UI-Common-MouseHilight","ADD")
+	button:SetScript("OnClick", Button_OnClick)
+	button:SetScript("OnEnter", Button_OnEnter)
+	button:SetScript("OnLeave", ddEditBox_OnMouseLeave)
+	button.parent = frame
+
+	local dropdown = CreateFrame("Frame", "AceGUI-3.0EditBoxDropDownMenu"..num, nil, "UIDropDownMenuTemplate")
+	dropdown:Hide()
+
+    local widget = {
+        editbox   = editbox,
+        button    = button,
+        dropdown  = dropdown,
+        frame     = frame,
+        type      = Type
+    }
+	for method, func in pairs(methods) do
+		widget[method] = func
+	end
+    editbox.obj, button.obj, dropdown.obj = widget, widget, widget
+    if is_option_control then
+        widget.is_option_control = true
+        widget.SetLabel = optcontrol_SetLabel
+        widget.SetValue = optcontrol_SetValue
+    end
+
+	return AceGUI:RegisterAsWidget(widget)
+end
+
+AceGUI:RegisterWidgetType (Type, Constructor, Version)
+
+AceGUI:RegisterWidgetType (Type.."OptionControl", function() return Constructor(true) end, Version)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/AceGUIWidget-Spacer.lua	Sat Apr 16 06:03:29 2011 +0000
@@ -0,0 +1,91 @@
+--[[-----------------------------------------------------------------------------
+Spacer Widget
+
+Spacer API
+
+:SetImage(path,...)
+   same as Label:SetImage
+
+:SetImageSize(w,h)
+   same as Label:SetImageSize
+
+Because the whole point is to take up space, the most-used functions will
+probably be the sizing routines from the base widget API.
+
+-farmbuyer
+-------------------------------------------------------------------------------]]
+local Type, Version = "Spacer", 2
+local AceGUI = LibStub and LibStub("AceGUI-3.0", true)
+if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end
+
+-- Lua APIs
+
+-- WoW APIs
+local CreateFrame = CreateFrame
+
+
+--[[-----------------------------------------------------------------------------
+Support functions
+-------------------------------------------------------------------------------]]
+
+
+--[[-----------------------------------------------------------------------------
+Scripts
+-------------------------------------------------------------------------------]]
+
+
+--[[-----------------------------------------------------------------------------
+Methods
+-------------------------------------------------------------------------------]]
+local methods = {
+	["OnAcquire"] = function (self)
+		self:SetHeight(110)
+		self:SetWidth(110)
+		self:SetImage(nil)
+		self.frame:EnableMouse(true)  -- needed?
+	end,
+
+	--["OnRelease"] = function (self) end,
+
+	["SetImage"] = function (self, path, ...)
+		local space = self.space
+
+		space:SetTexture (path or "Interface\\GLUES\\COMMON\\Glue-Tooltip-Background")
+		local n = select('#', ...)
+		if n == 4 or n == 8 then
+			space:SetTexCoord(...)
+		end
+	end,
+
+	["SetImageSize"] = function (self, width, height)
+		self.frame:SetWidth(width)
+		self.frame:SetHeight(height)
+	end,
+}
+
+
+--[[-----------------------------------------------------------------------------
+Constructor
+-------------------------------------------------------------------------------]]
+local function Constructor()
+	local frame = CreateFrame("Frame",nil,UIParent)
+
+	local space = frame:CreateTexture(nil,"BACKGROUND")
+	space:SetAllPoints(frame)
+	space:SetTexture("Interface\\GLUES\\COMMON\\Glue-Tooltip-Background")
+	space:SetBlendMode("ADD")
+
+	local widget = {
+		space   = space,
+		frame   = frame,
+		type    = Type
+	}
+	for method, func in pairs(methods) do
+		widget[method] = func
+	end
+
+	return AceGUI:RegisterAsWidget(widget)
+end
+
+AceGUI:RegisterWidgetType(Type,Constructor,Version)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/AceGUIWidget-lib-st.lua	Sat Apr 16 06:03:29 2011 +0000
@@ -0,0 +1,249 @@
+--[[-----------------------------------------------------------------------------
+lib-st Beta Wrapper Widget
+
+lib-st does not recycle the objects (called "ST" here) that it creates and
+returns.  We therefore do not try to hold onto an ST when the widget is
+being recycled.  This means that Constructor() does very little work, and
+does not actually construct an ST.
+
+OnAcquire cannot construct an ST either, because we don't yet have any
+creation parameters from the user to feed to CreateST.  (Allowing such to
+be passed along from AceGUI:Create() would require changes to core AceGUI
+code, and I don't feel like trying to overcome that inertia.)
+
+The upshot is that the widget returned from Create is broken and useless
+until its CreateST member has been called.  This means that correct behavior
+depends entirely on the user remembering to do so.
+
+"The gods do not protect fools.  Fools are protected by more capable fools."
+- Ringworld
+
+
+Version 1 initial functioning implementation
+Version 2 reshuffle to follow new AceGUI widget coding style
+Version 3 add .tail_offset, defaulting to same absolute value as .head_offset
+-farmbuyer
+-------------------------------------------------------------------------------]]
+local Type, Version = "lib-st", 3
+local AceGUI = LibStub and LibStub("AceGUI-3.0", true)
+if not AceGUI or (AceGUI:GetWidgetVersion(Type) or 0) >= Version then return end
+
+-- Lua APIs
+local ipairs, error = ipairs, error
+
+-- WoW APIs
+local debugstack = debugstack
+local CreateFrame = CreateFrame
+
+
+--[[-----------------------------------------------------------------------------
+Support functions
+-------------------------------------------------------------------------------]]
+
+-- Some AceGUI functions simply Won't Work in this context.  Name them
+-- here, and code calling them will get a somewhat informative error().
+local oopsfuncs = {
+	'SetRelativeWidth', 'SetRelativeHeight',
+	'SetFullWidth', 'SetFullHeight',
+}
+local err = "Oops!  The AceGUI function you tried to call (%s) does not "
+            .. "make sense with lib-st and has not been implemented."
+
+local function Oops(self)
+	-- if you ever wanted an example of "brown paper bag" code, here it is
+	local ds = debugstack(0)
+	local func = ds:match("AceGUIWidget%-lib%-st%.lua:%d+:%s+in function `(%a+)'")
+	error(err:format(func or "?"))
+end
+
+
+--[[
+	Users with an ST already constructed can drop it into a widget directly
+	using this routine.  It must be safe to call this more than once with
+	new widgets on the same ST.
+
+	This is where most of the intelligence of the wrapper is located.  That
+	is, if you can call my code "intelligent" with a straight face.  Lemme
+	try again.
+
+	Think of the widget wrapper as a brain.  When ALL THREE neurons manage
+	to fire at the same time and produce a thought, this function represents
+	the thought.  Sigh.
+]]
+local ShiftingSetPoint, ShiftingSetAllPoints
+local function WrapST (self, st)
+	if not st.frame then
+		error"lib-st instance has no '.frame' field... wtf did you pass to this function?"
+	end
+	if st.frame.obj and (st.frame.obj ~= self) then
+		error"lib-st instance already has an '.obj' field from a different widget, cannot use with AceGUI!"
+	end
+	self.st = st
+	if not st.head then
+		error"lib-st instance has no '.head' field, must use either ScrollingTable:CreateST or this widget's CreatST first"
+	end
+	self.frame = st.frame   -- gutsy, but looks doable
+
+	-- Possibly have already wrapped this ST in a previous widget, careful.
+	if st.frame.obj ~= self then
+		self.frame.realSetPoint = self.frame.SetPoint
+		self.frame.SetPoint = ShiftingSetPoint
+		self.frame.SetAllPoints = ShiftingSetAllPoints
+	end
+
+	-- This needs the .frame field.  This also creates .obj inside that
+	-- field and calls a SetScript as well.
+	return AceGUI:RegisterAsWidget(self)
+end
+
+
+--[[-----------------------------------------------------------------------------
+Scripts
+-------------------------------------------------------------------------------]]
+--[[
+	All of an ST's subframes are attached to its main frame, which we have in
+	the st.frame link, and that's what AceGUI uses for all positioning.  Except
+	that ST:SetDisplayCols creates its "head" row /above/ the main frame, and
+	so the row of labels eats into whatever upper border space AceGUI calculates,
+	often overlapping other elements.
+
+	We get around this by replacing ST's main frame's SetPoint with a custom
+	version that just moves everything down a few pixels to allow room for the
+	head row.
+
+	FIXME this may need to be a secure hook (ugh, would end up calling the real
+	setpoint twice) rather than a replacement.
+]]
+local DEFAULT_OFFSET = 7
+function ShiftingSetPoint(frame,anchor,other,otheranchor,xoff,yoff)
+	local ho,to = frame.obj.head_offset, frame.obj.tail_offset
+	yoff = yoff or 0
+	if anchor:sub(1,3) == "TOP" then
+		yoff = yoff - ho
+	elseif anchor:sub(1,6) == "BOTTOM" then
+		yoff = yoff + to
+	end
+	return frame.realSetPoint(frame,anchor,other,otheranchor,xoff,yoff)
+end
+function ShiftingSetAllPoints(frame,other)
+	ShiftingSetPoint(frame,"TOPLEFT",other,"TOPLEFT",0,0)
+	ShiftingSetPoint(frame,"BOTTOMRIGHT",other,"BOTTOMRIGHT",0,0)
+end
+
+
+--[[-----------------------------------------------------------------------------
+Methods
+-------------------------------------------------------------------------------]]
+local methods = {
+	-- --------------------------------------------------------------
+	-- These are expected by AceGUI containers (and AceGUI users)
+	--
+	["OnAcquire"] = function (self)
+		-- Almost nothing can usefully be done here.
+		self.head_offset = DEFAULT_OFFSET
+		self.tail_offset = DEFAULT_OFFSET
+	end,
+
+	["OnRelease"] = function (self)
+		if self.st then
+			self.st.frame:ClearAllPoints()
+			self.st:Hide()
+		end
+		self.st = nil
+		-- XXX should also undo the frame hooks.  would most likely be wasted
+		-- cycles.  if somebody actually wants to make an ST, include it inside
+		-- an ace container, then hide the container and continue displaying
+		-- the ST by other means, they can file a ticket
+	end,
+
+	--[[
+		STs don't use a "normal" SetWidth, if we define "normal" to be the
+		behavior of the blizzard :SetWidth.  Column width is passed in during
+		creation of the whole ST.  The SetWidth defined by an ST takes no
+		arguments; "ReCalculateWidth" would be a more precise description of
+		what it does.
+		
+		Parts of AceGUI look for a .width field because a widget's SetWidth
+		sets such.  ST calculates a total width and dispatches it to its member
+		frame...  but doesn't store a local copy.  We need to bridge these
+		differences.
+
+		This widget wrapper does not make use of On{Width,Height}Set hooks,
+		but the acegui widget base functions do.  Since we're not inheriting
+		them, we may as well supply them.
+	]]
+	["SetWidth"] = function (self)
+		self.st:SetWidth()                    -- re-total the columns
+		local w = self.st.frame:GetWidth()    -- fetch the answer back
+		self.frame.width = w                  -- store it for acegui
+		if self.OnWidthSet then
+			self:OnWidthSet(w)
+		end
+	end,
+
+	-- Everything said about SetWidth applies here too.
+	["SetHeight"] = function (self)
+		self.st:SetHeight()
+		local h = self.st.frame:GetHeight()
+		self.frame.height = h
+		if self.OnHeightSet then
+			self:OnHeightSet(h)
+		end
+	end,
+
+	-- Some of the container layouts call Show/Hide on the innermost frame
+	-- directly.  We need to make sure the slightly-higher-level routine is
+	-- also called.
+	["LayoutFinished"] = function (self)
+		if self.frame:IsShown() then
+			self.st:Show()
+		else
+			self.st:Hide()
+		end
+	end,
+
+	-- --------------------------------------------------------------
+	-- Functions specific to this widget
+	--
+
+	["GetSTLibrary"] = function (self)   -- Purely for convenience
+		return LibST
+	end,
+
+	--[[
+		Replacement wrapper, so that instead of
+		   st = ScrollingTable:CreateST( args )
+		the user should be able to do
+		   st = AceGUI:Create("lib-st"):CreateST( args )
+		instead, without needing to get a lib-st handle.
+	]]
+	["CreateST"] = function (self, ...)
+		return self:WrapST( LibST:CreateST(...) )
+	end,
+
+	["WrapST"] = WrapST,
+}
+
+
+--[[-----------------------------------------------------------------------------
+Constructor
+-------------------------------------------------------------------------------]]
+local function Constructor()
+	-- .frame not done here, see WrapST
+	local widget = {
+		type   = Type
+	}
+	for method, func in pairs(methods) do
+		widget[method] = func
+	end
+
+	for _,func in ipairs(oopsfuncs) do
+		widget[func] = Oops
+	end
+
+	-- AceGUI:RegisterAsWidget needs .frame
+	return widget
+end
+
+AceGUI:RegisterWidgetType(Type,Constructor,Version)
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/LibFarmbuyer.lua	Sat Apr 16 06:03:29 2011 +0000
@@ -0,0 +1,248 @@
+--[[
+Not really meant for public use.  Stuff that I keep using everywhere and
+got tired of reimplementing, or even copy-and-pasting.  The notes here are
+reminders to myself.
+
+Library contents:
+- author_debug
+  Evaluates to true if I'm hacking on something.
+
+- tableprint(t)
+  A single print() call to the contents of T, including nils; strings are
+  cleaned up with respect to embedded '|'/control chars.  If there is a
+  global LIBFARMPRINT function, it is called with the cleaned-up T instead
+  of directly calling print.
+
+- safeprint(...)
+  Same as tableprint() on the argument list.
+
+- safeiprint(...)
+  Same as safeprint() but with index numbers inserted.  Ex:
+  safeiprint(a,b,c)  -->  <1>,a,<2>,b,<3>,c
+
+- CHAT(n)
+  Returns a function suitable for assigning to LIBFARMPRINT, directing all
+  output to builtin chat frame N.
+
+- t = StaticPopup(t)
+  Fills out "typical" settings inside T, especially if T contains any kind
+  of editbox:
+   + cannot accept an empty editbox
+   + pressing Enter runs OnAccept
+   + editbox grabs keyboard focus
+   + OnAccept runs with editbox's text in dialog.usertext
+  Returns T.
+
+- nullfunc()
+  Empty placeholder function.  Will be less useful if WoW ever moves to Lua 5.2.
+  This is a fascinating and educational place to insert print calls...
+
+- tabledump(t)/dumptable(t)
+  If author_debug, this runs the builtin /dump command on T.  Otherwise nothing.
+
+- DoOnceNextUpdate(f)
+  Runs F on the next frame refresh cycle.  Multiple calls in one cycle will
+  stack LIFO.  Calls *while* processing the stack are safe, and will be stacked
+  up for the next cycle.
+]]
+
+local MAJOR, MINOR = "LibFarmbuyer", 7
+assert(LibStub,MAJOR.." requires LibStub")
+local lib = LibStub:NewLibrary(MAJOR, MINOR)
+if not lib then return end
+
+_G[MAJOR] = lib
+
+----------------------------------------------------------------------
+local new, del, copy, clear
+
+
+----------------------------------------------------------------------
+--[[
+	safeprint
+]]
+local function undocontrol(c)
+	return ("\\%.3d"):format(c:byte())
+end
+function lib.CHAT(n)
+	local cf = _G["ChatFrame"..n]
+	return function(t)
+		local msg = table.concat(t,' ', i, tonumber(t.n) or #t)
+		cf:AddMessage(msg)
+	end
+end
+function lib.safeprint(...)
+	local args = { n=select('#',...), ... }
+	lib.tableprint(args)
+end
+function lib.safeiprint(...)
+	local args = { n=select('#',...), ... }
+	local last = args.n
+	while last > 0 do
+		table.insert (args, last, "<"..last..">")
+		last = last - 1
+	end
+	args.n = 2 * args.n
+	lib.tableprint(args)
+end
+function lib.tableprint(t)
+	for i = 1, (tonumber(t.n) or #t) do
+		t[i] = tostring(t[i]):gsub('\124','\124\124')
+		                     :gsub('(%c)', undocontrol)
+	end
+	if type(_G.LIBFARMPRINT) == 'function' then
+		return _G.LIBFARMPRINT(t)
+	else
+		return print(unpack(t))
+	end
+end
+
+-- See below for global versions.
+
+
+----------------------------------------------------------------------
+local StaticPopupDialogs = _G.StaticPopupDialogs
+
+local function EditBoxOnTextChanged_notempty (editbox) -- this is also called when first shown
+	if editbox:GetText() ~= "" then
+		editbox:GetParent().button1:Enable()
+	else
+		editbox:GetParent().button1:Disable()
+	end
+end
+local function EditBoxOnEnterPressed_accept (editbox)
+    local dialog = editbox:GetParent()
+    StaticPopupDialogs[dialog.which].OnAccept (dialog, dialog.data, dialog.data2)
+    dialog:Hide()
+end
+local function OnShow_witheditbox (dialog, data)
+	local info = StaticPopupDialogs[dialog.which]
+	dialog[info.hasWideEditBox and "wideEditBox" or "editBox"]:SetFocus()
+    if info.farm_OnShow then
+        return info.farm_OnShow (dialog, data)
+    end
+end
+local function OnAccept_witheditbox (dialog, data, data2)
+	local info = StaticPopupDialogs[dialog.which]
+	dialog.usertext = dialog[info.hasWideEditBox and "wideEditBox" or "editBox"]:GetText():trim()
+    if info.farm_OnAccept then
+        return info.farm_OnAccept (dialog, data, data2)
+    end
+end
+
+--[[
+	StaticPopup
+]]
+function lib.StaticPopup (t)
+    if t.hasEditBox then
+        t.EditBoxOnTextChanged = EditBoxOnTextChanged_notempty
+        t.EditBoxOnEnterPressed = EditBoxOnEnterPressed_accept
+		if t.OnShow then
+			t.farm_OnShow = t.OnShow
+		end
+        t.OnShow = OnShow_witheditbox
+		if t.OnAccept then
+			t.farm_OnAccept = t.OnAccept
+		end
+        t.OnAccept = OnAccept_witheditbox
+		-- this calls OnCancel with "clicked", unless noCancelOnEscape is set
+        t.EditBoxOnEscapePressed = StaticPopup_EscapePressed
+    end
+
+    t.timeout = 0
+    t.whileDead = true
+    t.hideOnEscape = true
+	t.enterClicksFirstButton = true
+
+    return t
+end
+
+
+----------------------------------------------------------------------
+--[[
+This is ugly, but at least it all gets GC'd almost immediately.
+]]
+function lib.nullfunc() end
+
+if ({
+	["Bandwagon"] = true, ["Kilvin"] = true, ["Waterfaucet"] = true,
+	["Farmbuyer"] = true, ["Oxdeadbeef"] = true, ["Pointystick"] = true,
+	["Angryhobbit"] = true, ["Malrubius"] = true, ["Hemogoblin"] = true,
+})[UnitName("player")] then
+	lib.author_debug = true
+	_G.safeprint = lib.safeprint
+	_G.safeiprint = lib.safeiprint
+	function lib.tabledump(t)
+		_G.UIParentLoadAddOn("Blizzard_DebugTools")
+		_G.LibF_DEBUG = t
+		_G.SlashCmdList.DUMP("LibF_DEBUG")
+	end
+else
+	lib.tabledump = lib.nullfunc
+end
+lib.dumptable = lib.tabledump
+
+
+----------------------------------------------------------------------
+--[[
+	DoOnceNextUpdate
+]]
+do
+	local frame = CreateFrame("Frame", "LibFarmbuyerDONUFrame")
+	frame:Hide()
+	frame:SetScript("OnUpdate", function()
+		frame:Hide()
+		local q = frame.nexttime
+		local tmp
+		frame.nexttime = nil
+		while q do
+			tmp = q
+			q.f(frame)
+			q = q.n
+			del(tmp)
+		end
+	end)
+
+	function lib.DoOnceNextUpdate (func)
+		local nextt = new()
+		nextt.f = func
+		nextt.n = frame.nexttime
+		frame.nexttime = nextt
+		frame:Show()
+	end
+end
+
+
+----------------------------------------------------------------------
+-- Recycling functions yoinked from AceConfigDialog and tweaked
+do
+	local pool = setmetatable({},{__mode="k"})
+	function clear()
+		wipe(pool)
+	end
+	function new(...)  -- slightly modified variant, takes optional initializers
+		local t = next(pool)
+		if t then
+			pool[t] = nil
+			for i = 1, select('#',...) do
+				t[i] = select(i,...)
+			end
+			return t
+		else
+			return {...}
+		end
+	end
+	function copy(t)
+		local c = new()
+		for k, v in pairs(t) do
+			c[k] = v
+		end
+		return c
+	end
+	function del(t)
+		wipe(t)
+		pool[t] = true
+	end
+end
+
+-- vim: noet
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Ouro_Loot.toc	Sat Apr 16 06:03:29 2011 +0000
@@ -0,0 +1,49 @@
+## Interface: 40000
+## Title: Ouro Loot
+## Version: @project-version@
+## Notes: Raid loot tracking
+## Author: Farmbuyer of Kilrogg
+# oldSavedVariables: sv_OLoot, sv_OLoot_hist, sv_OLoot_opts
+## SavedVariables: OuroLootSV, OuroLootSV_opts, OuroLootSV_hist
+## OptionalDeps: Ace3, DBM-Core, lib-st, LibFarmbuyer
+
+#@no-lib-strip@
+libs\LibStub\LibStub.lua
+libs\CallbackHandler-1.0\CallbackHandler-1.0.xml
+libs\AceAddon-3.0\AceAddon-3.0.xml
+libs\AceEvent-3.0\AceEvent-3.0.xml
+libs\AceTimer-3.0\AceTimer-3.0.xml
+libs\AceConsole-3.0\AceConsole-3.0.xml
+libs\AceGUI-3.0\AceGUI-3.0.xml
+libs\AceComm-3.0\AceComm-3.0.xml
+libs\lib-st\lib-st.xml
+#@end-no-lib-strip@
+
+#Libs\lib-st-Core-r118.lua
+#Libs\lib-st-Core-r137v38.lua
+
+# extra bits
+LibFarmbuyer.lua
+AceGUIWidget-Spacer.lua
+AceGUIWidget-lib-st.lua
+AceGUIWidget-DoTimerEditBoxDropDown.lua
+AceGUIWidget-CheckBoxSmallLabel.lua
+
+#locale-enUS.lua
+#locale-deDE.lua
+#locale-esES.lua
+#locale-esMX.lua
+#locale-frFR.lua
+#locale-koKR.lua
+#locale-ruRU.lua
+#locale-zhCN.lua
+#locale-zhTW.lua
+
+core.lua
+gui.lua
+verbage.lua
+text_tabs.lua
+abbreviations.lua
+
+mleqdkp.lua
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/abbreviations.lua	Sat Apr 16 06:03:29 2011 +0000
@@ -0,0 +1,42 @@
+local addon = select(2,...)
+
+addon.instance_abbrev = {
+	-------- Cata
+	["Baradin Hold"] = "BH",
+	["Bastion of Twilight"] = "BoT",
+	["Blackwing Descent"] = "BWD",
+	["Throne of the Four Winds"] = "To4W",
+
+	-------- WotLK
+	["Icecrown Citadel"] = "ICC",
+	["Trial of the Champion"] = "ToC",
+	["Trial of the Crusader"] = "ToC",
+	["Trial of the Grand Crusader"] = "ToGC",  -- does not actually trigger, need to test heroic
+	["Vault of Archavon"] = "VoA",
+}
+
+addon.boss_abbrev = {
+	-------- Cata
+	-- BoT
+	-- BWD
+	-- Throne
+	["Al'Akir"] = "Big Al",
+
+	-------- WotLK
+	-- ToC
+	["Northrend Beasts"] = "Animal House",
+	["Lord Jaraxxus"] = "latest DIAF demon",
+	["Faction Champions"] = "Alliance Clusterfuck",
+	["Valkyr Twins"] = "Salt'N'Pepa",
+	["Val'kyr Twins"] = "Salt'N'Pepa",
+	["Anub'arak"] = "Whack-a-Spider",
+	-- ICC
+	["Lady Deathwhisper"] = "Lady Won't-Stop-Yammering",
+	["Gunship"] = "Rocket Shirt Over the Side of the Damn Boat",
+	["Gunship Battle"] = "Rocket Shirt Over the Side of the Damn Boat",
+	["Deathbringer Saurfang"] = "Deathbringer Sauerkraut",
+	["Professor Putricide"] = "Professor Farnsworth",
+	["Valithria Dreamwalker"] = "Dreeeeaaamweaaaaaverrrr",
+}
+
+-- vim:noet
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/core.lua	Sat Apr 16 06:03:29 2011 +0000
@@ -0,0 +1,1328 @@
+local addon = select(2,...)
+
+--[==[
+g_loot's numeric indices are loot entries (including titles, separators,
+etc); its named indices are:
+- forum:		saved text from forum markup window, default nil
+- attend:		saved text from raid attendence window, default nil
+- printed.FOO:	last index formatted into text window FOO, default 0
+- saved:		table of copies of saved texts, default nil; keys are numeric
+				indices of tables, subkeys of those are name/forum/attend/date
+- autoshard:	optional name of disenchanting player, default nil
+- threshold:	optional loot threshold, default nil
+
+Functions arranged like this, with these lables (for jumping to).  As a
+rule, member functions with UpperCamelCase names are called directly by
+user-facing code, ones with lowercase names are "one step removed", and
+names with leading underscores are strictly internal helper functions.
+------ Saved variables
+------ Constants
+------ Addon member data
+------ Locals
+------ Expiring caches
+------ Ace3 framework stuff
+------ Event handlers
+------ Slash command handler
+------ On/off
+------ Behind the scenes routines
+------ Saved texts
+------ Loot histories
+------ Player communication
+
+This started off as part of a raid addon package written by somebody else.
+After he retired, I began modifying the code.  Eventually I set aside the
+entire package and rewrote the loot tracker module from scratch.  Many of the
+variable/function naming conventions (sv_*, g_*, and family) stayed across the
+rewrite.  Some variables are needlessly initialized to nil just to look uniform.
+
+]==]
+
+------ Saved variables
+OuroLootSV		= nil   -- possible copy of g_loot
+OuroLootSV_opts	= nil   -- same as option_defaults until changed
+OuroLootSV_hist	= nil
+
+
+------ Constants
+local option_defaults = {
+	['popup_on_join'] = true,
+	['register_slashloot'] = true,
+	['scroll_to_bottom'] = true,
+	['chatty_on_kill'] = false,
+	['no_tracking_wipes'] = false,
+	['snarky_boss'] = true,
+	['keybinding'] = false,
+	['keybinding_text'] = 'CTRL-SHIFT-O',
+	['forum'] = {
+		['[url]'] = '[url=http://www.wowhead.com/?item=$I]$N[/url]$X - $T',
+		['[item] by name'] = '[item]$N[/item]$X - $T',
+		['[item] by ID'] = '[item]$I[/item]$X - $T',
+		['Custom...'] = '',
+	},
+	['forum_current'] = '[item] by name',
+}
+local virgin = "First time loaded?  Hi!  Use the /ouroloot or /loot command"
+	.." to show the main display.  You should probably browse the instructions"
+	.." if you've never used this before; %s to display the help window.  This"
+	.." welcome message will not intrude again."
+local qualnames = {
+	['gray'] = 0, ['grey'] = 0, ['poor'] = 0, ['trash'] = 0,
+	['white'] = 1, ['common'] = 1,
+	['green'] = 2, ['uncommon'] = 2,
+	['blue'] = 3, ['rare'] = 3,
+	['epic'] = 4, ['purple'] = 4,
+	['legendary'] = 5, ['orange'] = 5,
+	['artifact'] = 6,
+	--['heirloom'] = 7,
+}
+local my_name				= UnitName('player')
+local comm_cleanup_ttl		= 5   -- seconds in the cache
+
+
+------ Addon member data
+local flib = LibStub("LibFarmbuyer")
+addon.author_debug = flib.author_debug
+
+-- Play cute games with namespaces here just to save typing.
+do local _G = _G setfenv (1, addon)
+
+	revision		= 15
+	ident			= "OuroLoot2"
+	identTg			= "OuroLoot2Tg"
+	status_text		= nil
+
+	DEBUG_PRINT		= false
+	debug = {
+		comm = false,
+		loot = false,
+		flow = false,
+		notraid = false,
+		cache = false,
+	}
+	function dprint (t,...)
+		if DEBUG_PRINT and debug[t] then return _G.print("<"..t.."> ",...) end
+	end
+
+	if author_debug then
+		function pprint(t,...)
+			return _G.print("<<"..t..">> ",...)
+		end
+	else
+		pprint = flib.nullfunc
+	end
+
+	enabled			= false
+	rebroadcast		= false
+	display			= nil   -- display frame, when visible
+	loot_clean		= nil   -- index of last GUI entry with known-current visual data
+	sender_list		= {active={},names={}}   -- this should be reworked
+	threshold		= debug.loot and 0 or 3     -- rare by default
+	sharder			= nil   -- name of person whose loot is marked as shards
+
+	-- The rest is also used in the GUI:
+
+	popped			= nil   -- non-nil when reminder has been shown, actual value unimportant
+
+	-- This is an amalgamation of all four LOOT_ITEM_* patterns.
+	-- Captures:   1 person/You, 2 itemstring, 3 rest of string after final |r until '.'
+	-- Can change 'loot' to 'item' to trigger on, e.g., extracting stuff from mail.
+	loot_pattern	= "(%S+) receives? loot:.*|cff%x+|H(.-)|h.*|r(.*)%.$"
+
+	dbm_registered	= nil
+	requesting		= nil   -- for prompting for additional rebroadcasters
+
+	thresholds, quality_hexes = {}, {}
+	for i = 0,6 do
+		local hex = _G.select(4,_G.GetItemQualityColor(i))
+		local desc = _G["ITEM_QUALITY"..i.."_DESC"]
+		quality_hexes[i] = hex
+		thresholds[i] = hex .. desc .. "|r"
+	end
+
+	_G.setfenv (1, _G)
+end
+
+addon = LibStub("AceAddon-3.0"):NewAddon(addon, "Ouro Loot",
+                "AceTimer-3.0", "AceComm-3.0", "AceConsole-3.0", "AceEvent-3.0")
+
+
+------ Locals
+local g_loot			= nil
+local g_restore_p		= nil
+local g_saved_tmp		= nil   -- restoring across a clear
+local g_wafer_thin		= nil   -- for prompting for additional rebroadcasters
+local g_today			= nil   -- "today" entry in g_loot
+local opts				= nil
+
+local pairs, ipairs, tinsert, tremove, tonumber = pairs, ipairs, table.insert, table.remove, tonumber
+
+local pprint, tabledump = addon.pprint, flib.tabledump
+
+-- En masse forward decls of symbols defined inside local blocks
+local _registerDBM -- break out into separate file
+local makedate, create_new_cache, _init
+
+-- Hypertext support, inspired by DBM broadcast pizza timers
+do
+	local hypertext_format_str = "|HOuroRaid:%s|h%s[%s]|r|h"
+
+	function addon.format_hypertext (code, text, color)
+		return hypertext_format_str:format (code,
+			type(color)=='number' and addon.quality_hexes[color] or color,
+			text)
+	end
+
+	DEFAULT_CHAT_FRAME:HookScript("OnHyperlinkClick", function(self, link, string, mousebutton)
+		local ltype, arg = strsplit(":",link)
+		if ltype ~= "OuroRaid" then return end
+		if arg == 'openloot' then
+			addon:BuildMainDisplay()
+		elseif arg == 'help' then
+			addon:BuildMainDisplay('help')
+		elseif arg == 'bcaston' then
+			if not addon.rebroadcast then
+				addon:Activate(nil,true)
+			end
+			addon:broadcast('bcast_responder')
+		elseif arg == 'waferthin' then   -- mint? it's wafer thin!
+			g_wafer_thin = true          -- fuck off, I'm full
+			addon:broadcast('bcast_denied')   -- remove once tested
+		end
+	end)
+
+	local old = ItemRefTooltip.SetHyperlink
+	function ItemRefTooltip:SetHyperlink (link, ...)
+		if link:match("^OuroRaid") then return end
+		return old (self, link, ...)
+	end
+end
+
+do
+	-- copied here because it's declared local to the calendar ui, thanks blizz ><
+	local CALENDAR_FULLDATE_MONTH_NAMES = {
+		FULLDATE_MONTH_JANUARY, FULLDATE_MONTH_FEBRUARY, FULLDATE_MONTH_MARCH,
+		FULLDATE_MONTH_APRIL, FULLDATE_MONTH_MAY, FULLDATE_MONTH_JUNE,
+		FULLDATE_MONTH_JULY, FULLDATE_MONTH_AUGUST, FULLDATE_MONTH_SEPTEMBER,
+		FULLDATE_MONTH_OCTOBER, FULLDATE_MONTH_NOVEMBER, FULLDATE_MONTH_DECEMBER,
+	}
+	-- returns "dd Month yyyy", mm, dd, yyyy
+	function makedate()
+		Calendar_LoadUI()
+		local _, M, D, Y = CalendarGetDate()
+		local text = ("%d %s %d"):format(D, CALENDAR_FULLDATE_MONTH_NAMES[M], Y)
+		return text, M, D, Y
+	end
+end
+
+-- Returns an instance name or abbreviation
+local function instance_tag()
+	local name, typeof, diffcode, diffstr, _, perbossheroic, isdynamic = GetInstanceInfo()
+	local t
+	name = addon.instance_abbrev[name] or name
+	if typeof == "none" then return name end
+	-- diffstr is "5 Player", "10 Player (Heroic)", etc.  ugh.
+	if diffcode == 1 then
+		t = ((GetNumRaidMembers()>0) and "10" or "5")
+	elseif diffcode == 2 then
+		t = ((GetNumRaidMembers()>0) and "25" or "5h")
+	elseif diffcode == 3 then
+		t = "10h"
+	elseif diffcode == 4 then
+		t = "25h"
+	end
+	-- dynamic difficulties always return normal "codes"
+	if isdynamic and perbossheroic == 1 then
+		t = t .. "h"
+	end
+	return name .. "(" .. t .. ")"
+end
+addon.instance_tag = instance_tag   -- grumble
+
+
+------ Expiring caches
+--[[
+foo = create_new_cache("myfoo",15[,cleanup]) -- ttl
+foo:add("blah")
+foo:test("blah")   -- returns true
+]]
+do
+	local caches = {}
+	local cleanup_group = AnimTimerFrame:CreateAnimationGroup()
+	cleanup_group:SetLooping("REPEAT")
+	cleanup_group:SetScript("OnLoop", function(cg)
+		addon.dprint('cache',"OnLoop firing")
+		local now = GetTime()
+		local alldone = true
+		-- this is ass-ugly
+		for _,c in ipairs(caches) do
+			while (#c > 0) and (now - c[1].t > c.ttl) do
+				addon.dprint('cache', c.name, "cache removing",c[1].t, c[1].m)
+				tremove(c,1)
+			end
+			alldone = alldone and (#c == 0)
+		end
+		if alldone then
+			addon.dprint('cache',"OnLoop finishing animation group")
+			cleanup_group:Finish()
+			for _,c in ipairs(caches) do
+				if c.func then c:func() end
+			end
+		end
+		addon.dprint('cache',"OnLoop done")
+	end)
+
+	local function _add (cache, x)
+		tinsert(cache, {t=GetTime(),m=x})
+		if not cleanup_group:IsPlaying() then
+			addon.dprint('cache', cache.name, "STARTING animation group")
+			cache.cleanup:SetDuration(2)  -- hmmm
+			cleanup_group:Play()
+		end
+	end
+	local function _test (cache, x)
+		for _,v in ipairs(cache) do
+			if v.m == x then return true end
+		end
+	end
+	function create_new_cache (name, ttl, on_alldone)
+		local c = {
+			ttl = ttl,
+			name = name,
+			add = _add,
+			test = _test,
+			cleanup = cleanup_group:CreateAnimation("Animation"),
+			func = on_alldone,
+		}
+		c.cleanup:SetOrder(1)
+		-- setting OnFinished for cleanup fires at the end of each inner loop,
+		-- with no 'requested' argument to distinguish cases.  thus, on_alldone.
+		tinsert (caches, c)
+		return c
+	end
+end
+
+
+------ Ace3 framework stuff
+function addon:OnInitialize()
+	-- VARIABLES_LOADED has fired by this point; test if we're doing something like
+	-- relogging during a raid and already have collected loot data
+	g_restore_p = OuroLootSV ~= nil
+	self.dprint('flow', "oninit sets restore as", g_restore_p)
+
+	if OuroLootSV_opts == nil then
+		OuroLootSV_opts = {}
+		self:ScheduleTimer(function(s)
+			s:Print(virgin, s.format_hypertext('help',"click here",ITEM_QUALITY_UNCOMMON))
+			virgin = nil
+		end,10,self)
+	end
+	opts = OuroLootSV_opts
+	for opt,default in pairs(option_defaults) do
+		if opts[opt] == nil then
+			opts[opt] = default
+		end
+	end
+	option_defaults = nil
+	-- transition/remove old options
+	opts["forum_use_itemid"] = nil
+	if opts["forum_format"] then
+		opts.forum["Custom..."] = opts["forum_format"]
+		opts["forum_format"] = nil
+	end
+	-- get item filter table if needed
+	if opts.itemfilter == nil then
+		opts.itemfilter = addon.default_itemfilter
+	end
+	addon.default_itemfilter = nil
+
+	self:RegisterChatCommand("ouroloot", "OnSlash")
+	-- maybe try to detect if this command is already in use...
+	if opts.register_slashloot then
+		SLASH_ACECONSOLE_OUROLOOT2 = "/loot"
+	end
+
+	self.history_all = self.history_all or OuroLootSV_hist or {}
+	local r = GetRealmName()
+	self.history_all[r] = self:_prep_new_history_category (self.history_all[r], r)
+	self.history = self.history_all[r]
+
+	_init(self)
+	self.OnInitialize = nil
+end
+
+function addon:OnEnable()
+	self:RegisterEvent "PLAYER_LOGOUT"
+	self:RegisterEvent "RAID_ROSTER_UPDATE"
+
+	-- Cribbed from Talented.  I like the way jerry thinks: the first argument
+	-- can be a format spec for the remainder of the arguments.  (The new
+	-- AceConsole:Printf isn't used because we can't specify a prefix without
+	-- jumping through ridonkulous hoops.)  The part about overriding :Print
+	-- with a version using prefix hyperlinks is my fault.
+	do
+		local AC = LibStub("AceConsole-3.0")
+		local chat_prefix = self.format_hypertext('openloot',"Ouro Loot",--[[legendary]]5)
+		function addon:Print (str, ...)
+			if type(str) == 'string' and str:find("%", nil, --[[plainmatch=]]true) then
+				return AC:Print (chat_prefix, str:format(...))
+			else
+				return AC:Print (chat_prefix, str, ...)
+			end
+		end
+	end
+
+	if opts.keybinding then
+		local btn = CreateFrame("Button", "OuroLootBindingOpen", nil, "SecureActionButtonTemplate")
+		btn:SetAttribute("type", "macro")
+		btn:SetAttribute("macrotext", "/ouroloot toggle")
+		if SetBindingClick(opts.keybinding_text, "OuroLootBindingOpen") then
+			SaveBindings(GetCurrentBindingSet())
+		else
+			self:Print("Error registering '%s' as a keybinding, check spelling!",
+				opts.keybinding_text)
+		end
+	end
+
+	if self.debug.flow then self:Print"is in control-flow debug mode." end
+end
+--function addon:OnDisable() end
+
+
+------ Event handlers
+function addon:_clear_SVs()
+	g_loot = {}  -- not saved, just fooling PLAYER_LOGOUT tests
+	OuroLootSV = nil
+	OuroLootSV_opts = nil
+	OuroLootSV_hist = nil
+end
+function addon:PLAYER_LOGOUT()
+	if (#g_loot > 0) or g_loot.saved
+	   or (g_loot.forum and g_loot.forum ~= "")
+	   or (g_loot.attend and g_loot.attend ~= "")
+	then
+		g_loot.autoshard = self.sharder
+		g_loot.threshold = self.threshold
+		--OuroLootSV = g_loot
+		--for i,e in ipairs(OuroLootSV) do
+		for i,e in ipairs(g_loot) do
+			e.cols = nil
+		end
+		OuroLootSV = g_loot
+	end
+	self.history.kind = nil
+	self.history.st = nil
+	self.history.byname = nil
+	OuroLootSV_hist = self.history_all
+end
+
+function addon:RAID_ROSTER_UPDATE (event)
+	if GetNumRaidMembers() > 0 then
+		local inside,whatkind = IsInInstance()
+		if inside and (whatkind == "pvp" or whatkind == "arena") then
+			return self.dprint('flow', "got RRU event but in pvp zone, bailing")
+		end
+		if event == "Activate" then
+			-- dispatched manually from Activate
+			self:RegisterEvent "CHAT_MSG_LOOT"
+			_registerDBM(self)
+		elseif event == "RAID_ROSTER_UPDATE" then
+			-- event registration from onload, joined a raid, maybe show popup
+			if opts.popup_on_join and not self.popped then
+				self.popped = StaticPopup_Show "OUROL_REMIND"
+				self.popped.data = self
+			end
+		end
+	else
+		self:UnregisterEvent "CHAT_MSG_LOOT"
+		self.popped = nil
+	end
+end
+
+-- helper for CHAT_MSG_LOOT handler
+do
+	-- Recent loot cache
+	addon.recent_loot = create_new_cache ('loot', comm_cleanup_ttl)
+
+	local GetItemInfo = GetItemInfo
+
+	-- 'from' and onwards only present if this is triggered by a broadcast
+	function addon:_do_loot (local_override, recipient, itemid, count, from, extratext)
+		local iname, ilink, iquality, _,_,_,_,_,_, itexture = GetItemInfo(itemid)
+		if not iname then return end   -- sigh
+		self.dprint('loot',">>_do_loot, R:", recipient, "I:", itemid, "C:", count, "frm:", from, "ex:", extratext)
+
+		local i
+		itemid = tonumber(ilink:match("item:(%d+)"))
+		if local_override or ((iquality >= self.threshold) and not opts.itemfilter[itemid]) then
+			if (self.rebroadcast and (not from)) and not local_override then
+				self:broadcast('loot', recipient, itemid, count)
+			end
+			if self.enabled or local_override then
+				local signature = recipient .. iname .. (count or "")
+				if self.recent_loot:test(signature) then
+					self.dprint('cache', "loot <",signature,"> already in cache, skipping")
+				else
+					self.recent_loot:add(signature)
+					i = self._addLootEntry{   -- There is some redundancy here...
+						kind		= 'loot',
+						person		= recipient,
+						person_class= select(2,UnitClass(recipient)),
+						quality		= iquality,
+						itemname	= iname,
+						id			= itemid,
+						itemlink	= ilink,
+						itexture	= itexture,
+						disposition	= (recipient == self.sharder) and 'shard' or nil,
+						count		= count,
+						bcast_from	= from,
+						extratext	= extratext,
+						is_heroic	= self:is_heroic_item(ilink),
+					}
+					self.dprint('loot', "added entry", i)
+					self:_addHistoryEntry(i)
+					if self.display then
+						self:redisplay()
+						--[[
+						local st = self.display:GetUserData("eoiST")
+						if st and st.frame:IsVisible() then
+							st:OuroLoot_Refresh()
+						end
+						]]
+					end
+				end
+			end
+		end
+		self.dprint('loot',"<<_do_loot out")
+		return i
+	end
+
+	function addon:CHAT_MSG_LOOT (event, ...)
+		if (not self.rebroadcast) and (not self.enabled) and (event ~= "manual") then return end
+
+		--[[
+			iname:		Hearthstone
+			iquality:	integer
+			ilink:		clickable formatted link
+			itemstring:	item:6948:....
+			itexture:	inventory icon texture
+		]]
+
+		if event == "CHAT_MSG_LOOT" then
+			local msg = ...
+			--ChatFrame2:AddMessage("original string:  >"..(msg:gsub("\124","\124\124")).."<")
+			local person, itemstring, remainder = msg:match(self.loot_pattern)
+			self.dprint('loot', "CHAT_MSG_LOOT, person is", person, ", itemstring is", itemstring, ", rest is", remainder)
+			if not person then return end    -- "So-and-So selected Greed", etc, not actual looting
+			local count = remainder and remainder:match(".*(x%d+)$")
+
+			-- Name might be colorized, remove the highlighting
+			local p = person:match("|c%x%x%x%x%x%x%x%x(%S+)")
+			person = p or person
+			person = (person == UNIT_YOU) and my_name or person
+
+			local id = tonumber((select(2, strsplit(":", itemstring))))
+
+			return self:_do_loot (false, person, id, count)
+
+		elseif event == "broadcast" then
+			return self:_do_loot(false, ...)
+
+		elseif event == "manual" then
+			local r,i,n = ...
+			return self:_do_loot(true, r,i,nil,nil,n)
+		end
+	end
+end
+
+
+------ Slash command handler
+-- Thought about breaking this up into a table-driven dispatcher.  But
+-- that would result in a pile of teensy functions, most of which would
+-- never be called.  Too much overhead.  (2.0:  Most of these removed now
+-- that GUI is in place.)
+function addon:OnSlash (txt) --, editbox)
+	txt = strtrim(txt:lower())
+	local cmd, arg = ""
+	do
+		local s,e = txt:find("^%a+")
+		if s then
+			cmd = txt:sub(s,e)
+			s = txt:find("%S", e+2)
+			if s then arg = txt:sub(s,-1) end
+		end
+	end
+
+	if cmd == "" then
+		if InCombatLockdown() then
+			return self:Print("Can't display window in combat.")
+		else
+			return self:BuildMainDisplay()
+		end
+
+	elseif cmd:find("^thre") then
+		self:SetThreshold(arg)
+
+	elseif cmd == "on" then                             self:Activate(arg)
+	elseif cmd == "off" then                            self:Deactivate()
+	elseif cmd == "broadcast" or cmd == "bcast" then    self:Activate(nil,true)
+
+	elseif cmd == "fake" then  -- maybe comment this out for real users
+		self:_mark_boss_kill (self._addLootEntry{
+			kind='boss',reason='kill',bosskill="Baron Steamroller",instance=instance_tag(),duration=0
+		})
+		self:CHAT_MSG_LOOT ('manual', my_name, 54797)
+		if self.display then
+			self:redisplay()
+		end
+		self:Print "Baron Steamroller has been slain.  Congratulations on your rug."
+
+	elseif cmd == "debug" then
+		if arg then
+			self.debug[arg] = not self.debug[arg]
+			_G.print(arg,self.debug[arg])
+			if self.debug[arg] then self.DEBUG_PRINT = true end
+		else
+			self.DEBUG_PRINT = not self.DEBUG_PRINT
+		end
+
+	elseif cmd == "save" and arg and arg:len() > 0 then
+		self:save_saveas(arg)
+	elseif cmd == "list" then
+		self:save_list()
+	elseif cmd == "restore" and arg and arg:len() > 0 then
+		self:save_restore(tonumber(arg))
+	elseif cmd == "delete" and arg and arg:len() > 0 then
+		self:save_delete(tonumber(arg))
+
+	elseif cmd == "help" then
+		self:BuildMainDisplay('help')
+	elseif cmd == "toggle" then
+		if self.display then
+			self.display:Hide()
+		else
+			return self:BuildMainDisplay()
+		end
+
+	else
+		if self:OpenMainDisplayToTab(cmd) then
+			return
+		end
+		self:Print("Unknown command '%s'. %s to see the help window.",
+			cmd, self.format_hypertext('help',"Click here",ITEM_QUALITY_UNCOMMON))
+	end
+end
+
+function addon:SetThreshold (arg, quiet_p)
+	local q = tonumber(arg)
+	if q then
+		q = math.floor(q+0.001)
+		if q<0 or q>6 then
+			return self:Print("Threshold must be 0-6.")
+		end
+	else
+		q = qualnames[arg]
+		if not q then
+			return self:Print("Unrecognized item quality argument.")
+		end
+	end
+	self.threshold = q
+	if not quiet_p then self:Print("Threshold now set to %s.", self.thresholds[q]) end
+end
+
+
+------ On/off
+function addon:Activate (opt_threshold, opt_bcast_only)
+	self:RegisterEvent "RAID_ROSTER_UPDATE"
+	self.popped = true
+	if GetNumRaidMembers() > 0 then
+		self:RAID_ROSTER_UPDATE("Activate")
+	elseif self.debug.notraid then
+		self:RegisterEvent "CHAT_MSG_LOOT"
+		_registerDBM(self)
+	elseif g_restore_p then
+		g_restore_p = nil
+		if #g_loot == 0 then return end -- only saved texts, not worth verbage
+		self:Print("Ouro Raid Loot restored previous data, but not in a raid",
+				"and 5-person mode not active.  |cffff0505NOT tracking loot|r;",
+				"use 'enable' to activate loot tracking, or 'clear' to erase",
+				"previous data, or 'help' to read about saved-texts commands.")
+		self.popped = nil  -- get the reminder if later joining a raid
+		return
+	end
+	self.rebroadcast = true  -- hardcode to true; this used to be more complicated
+	self.enabled = not opt_bcast_only
+	if opt_threshold then
+		self:SetThreshold (opt_threshold, --[[quiet_p=]]true)
+	end
+	self:Print("Ouro Raid Loot is %s.  Threshold currently %s.",
+		self.enabled and "tracking" or "only broadcasting",
+		self.thresholds[self.threshold])
+end
+
+-- Note:  running '/loot off' will also avoid the popup reminder when
+-- joining a raid, but will not change the saved option setting.
+function addon:Deactivate()
+	self.enabled = false
+	self.rebroadcast = false
+	self:UnregisterEvent "RAID_ROSTER_UPDATE"
+	self:UnregisterEvent "CHAT_MSG_LOOT"
+	self:Print("Ouro Raid Loot deactivated.")
+end
+
+function addon:Clear(verbose_p)
+	local repopup, st
+	if self.display then
+		-- in the new version, this is likely to always be the case
+		repopup = true
+		st = self.display:GetUserData("eoiST")
+		if not st then
+			self.dprint('flow', "Clear: display visible but eoiST not set??")
+		end
+		self.display:Hide()
+	end
+	g_restore_p = nil
+	OuroLootSV = nil
+	self:_reset_timestamps()
+	g_saved_tmp = g_loot.saved
+	if verbose_p then
+		if (g_saved_tmp and #g_saved_tmp>0) then
+			self:Print("Current loot data cleared, %d saved sets remaining.", #g_saved_tmp)
+		else
+			self:Print("Current loot data cleared.")
+		end
+	end
+	_init(self,st)
+	if repopup then
+		addon:BuildMainDisplay()
+	end
+end
+
+
+------ Behind the scenes routines
+-- Adds indices to traverse the tables in a nice sorted order.
+do
+	local byindex, temp = {}, {}
+	local function sort (src, dest)
+		for k in pairs(src) do
+			temp[#temp+1] = k
+		end
+		table.sort(temp)
+		table.wipe(dest)
+		for i = 1, #temp do
+			dest[i] = src[temp[i]]
+		end
+	end
+
+	function addon.sender_list.sort()
+		sort (addon.sender_list.active, byindex)
+		table.wipe(temp)
+		addon.sender_list.activeI = #byindex
+		sort (addon.sender_list.names, byindex)
+		table.wipe(temp)
+	end
+	addon.sender_list.namesI = byindex
+end
+
+-- Message sending.
+-- See OCR_funcs.tag at the end of this file for incoming message treatment.
+do
+	local function assemble(...)
+		local msg = ...
+		for i = 2, select('#',...) do
+			msg = msg .. '\a' .. (select(i,...) or "")
+		end
+		return msg
+	end
+
+	-- broadcast('tag', <stuff>)
+	function addon:broadcast(...)
+		local msg = assemble(...)
+		self.dprint('comm', "<broadcast>:", msg)
+		-- the "GUILD" here is just so that we can also pick up on it
+		self:SendCommMessage(self.ident, msg, self.debug.comm and "GUILD" or "RAID")
+	end
+	-- whispercast(<to>, 'tag', <stuff>)
+	function addon:whispercast(to,...)
+		local msg = assemble(...)
+		self.dprint('comm', "<whispercast>@", to, ":", msg)
+		self:SendCommMessage(self.identTg, msg, "WHISPER", to)
+	end
+end
+
+-- Generic helpers
+function addon._find_next_after (kind, index)
+	index = index + 1
+	while index <= #g_loot do
+		if g_loot[index].kind == kind then
+			return index, g_loot[index]
+		end
+		index = index + 1
+	end
+end
+
+-- Iterate through g_loot entries according to the KIND field.  Loop variables
+-- are g_loot indices and the corresponding entries (essentially ipairs + some
+-- conditionals).
+function addon:filtered_loot_iter (filter_kind)
+	return self._find_next_after, filter_kind, 0
+end
+
+do
+	local itt
+	local function create()
+		local tip, lefts = CreateFrame("GameTooltip"), {}
+		for i = 1, 2 do -- scanning idea here also snagged from Talented
+			local L,R = tip:CreateFontString(), tip:CreateFontString()
+			L:SetFontObject(GameFontNormal)
+			R:SetFontObject(GameFontNormal)
+			tip:AddFontStrings(L,R)
+			lefts[i] = L
+		end
+		tip.lefts = lefts
+		return tip
+	end
+	function addon:is_heroic_item(item)   -- returns true or *nil*
+		itt = itt or create()
+		itt:SetOwner(UIParent,"ANCHOR_NONE")
+		itt:ClearLines()
+		itt:SetHyperlink(item)
+		local t = itt.lefts[2]:GetText()
+		itt:Hide()
+		return (t == ITEM_HEROIC) or nil
+	end
+end
+
+-- Called when first loading up, and then also when a 'clear' is being
+-- performed.  If SV's are present then restore_p will be true.
+function _init (self, possible_st)
+	self.dprint('flow',"_init running")
+	self.loot_clean = nil
+	self.hist_clean = nil
+	if g_restore_p then
+		g_loot = OuroLootSV
+		self.popped = true
+		self.dprint('flow', "restoring", #g_loot, "entries")
+		self:ScheduleTimer("Activate", 8, g_loot.threshold)
+		-- FIXME printed could be too large if entries were deleted, how much do we care?
+		self.sharder = g_loot.autoshard
+	else
+		g_loot = { printed = {} }
+		g_loot.saved = g_saved_tmp; g_saved_tmp = nil	-- potentially restore across a clear
+	end
+
+	self.threshold = g_loot.threshold or self.threshold -- in the case of restoring but not tracking
+	self:gui_init(g_loot)
+
+	if g_restore_p then
+		self:zero_printed_fenceposts()                  -- g_loot.printed.* = previous/safe values
+	else
+		self:zero_printed_fenceposts(0)                 -- g_loot.printed.* = 0
+	end
+	if possible_st then
+		possible_st:SetData(g_loot)
+	end
+
+	self.status_text = ("v2r%d communicating as ident %s"):format(self.revision,self.ident)
+	self:RegisterComm(self.ident)
+	self:RegisterComm(self.identTg, "OnCommReceivedNocache")
+
+	if self.author_debug then
+		_G.OL = self
+		_G.Oloot = g_loot
+	end
+end
+
+-- Tie-ins with Deadly Boss Mods
+do
+	local candidates, location
+	local function fixup_durations (cache)
+		if candidates == nil then return end  -- this is called for *all* cache expirations, including non-boss
+		local boss, bossi
+		boss = candidates[1]
+		if #candidates == 1 then
+			-- (1) or (2)
+			boss.duration = boss.duration or 0
+			addon.dprint('loot', "only one candidate")
+		else
+			-- (3), should only be one 'cast entry and our local entry
+			if #candidates ~= 2 then
+				-- could get a bunch of 'cast entries on the heels of one another
+				-- before the local one ever fires, apparently... sigh
+				--addon:Print("<warning> s3 cache has %d entries, does that seem right to you?", #candidates)
+			end
+			if candidates[2].duration == nil then
+				--addon:Print("<warning> s3's second entry is not the local trigger, does that seem right to you?")
+			end
+			-- try and be generic anyhow
+			for i,c in ipairs(candidates) do
+				if c.duration then
+					boss = c
+					addon.dprint('loot', "fixup found candidate", i, "duration", c.duration)
+					break
+				end
+			end
+		end
+		bossi = addon._addLootEntry(boss)
+		addon.dprint('loot', "added entry", bossi)
+		if boss.reason == 'kill' then
+			addon:_mark_boss_kill (bossi)
+			if opts.chatty_on_kill then
+				addon:Print("Registered kill for '%s' in %s!", boss.bosskill, boss.instance)
+			end
+		end
+		candidates = nil
+	end
+	addon.recent_boss = create_new_cache ('boss', 10, fixup_durations)
+
+	-- Similar to _do_loot, but duration+ parms only present when locally generated.
+	local function _do_boss (self, reason, bossname, intag, duration, raiders)
+		self.dprint('loot',">>_do_boss, R:", reason, "B:", bossname, "T:", intag,
+		            "D:", duration, "RL:", (raiders and #raiders or 'nil'))
+		if self.rebroadcast and duration then
+			self:broadcast('boss', reason, bossname, intag)
+		end
+		-- This is only a loop to make jumping out of it easy, and still do cleanup below.
+		while self.enabled do
+			if reason == 'wipe' and opts.no_tracking_wipes then break end
+			bossname = (opts.snarky_boss and self.boss_abbrev[bossname] or bossname) or bossname
+			local not_from_local = duration == nil
+			local signature = bossname .. reason
+			if not_from_local and self.recent_boss:test(signature) then
+				self.dprint('cache', "boss <",signature,"> already in cache, skipping")
+			else
+				self.recent_boss:add(signature)
+				-- Possible scenarios:  (1) we don't see a boss event at all (e.g., we're
+				-- outside the instance) and so this only happens once as a non-local event,
+				-- (2) we see a local event first and all non-local events are filtered
+				-- by the cache, (3) we happen to get some non-local events before doing
+				-- our local event (not because of network weirdness but because our local
+				-- DBM might not trigger for a while).
+				local c = {
+					kind		= 'boss',
+					bosskill	= bossname,      -- minor misnomer, might not actually be a kill
+					reason		= reason,
+					instance	= intag,
+					duration	= duration,      -- these two deliberately may be nil
+					raiderlist	= raiders and table.concat(raiders, ", ")
+				}
+				candidates = candidates or {}
+				tinsert(candidates,c)
+				break
+			end
+		end
+		self.dprint('loot',"<<_do_boss out")
+	end
+	-- No wrapping layer for now
+	addon.on_boss_broadcast = _do_boss
+
+	function addon:_mark_boss_kill (index)
+		local e = g_loot[index]
+		if not e.bosskill then
+			return self:Print("Something horribly wrong;", index, "is not a boss entry!")
+		end
+		if e.reason ~= 'wipe' then
+			-- enh, bail
+			self.loot_clean = index-1
+		end
+		local attempts = 1
+		local first
+
+		local i,d = 1,g_loot[1]
+		while d ~= e do
+			if d.bosskill and
+			   d.bosskill == e.bosskill and
+			   d.reason == 'wipe'
+			then
+				first = first or i
+				attempts = attempts + 1
+				assert(tremove(g_loot,i)==d,"_mark_boss_kill screwed up data badly")
+			else
+				i = i + 1
+			end
+			d = g_loot[i]
+		end
+		e.reason = 'kill'
+		e.attempts = attempts
+		self.loot_clean = first or index-1
+	end
+
+	local GetRaidRosterInfo = GetRaidRosterInfo
+	function addon:DBMBossCallback (reason, mod, ...)
+		if (not self.rebroadcast) and (not self.enabled) then return end
+
+		local name
+		if mod.combatInfo and mod.combatInfo.name then
+			name = mod.combatInfo.name
+		elseif mod.id then
+			name = mod.id
+		else
+			name = "Unknown Boss"
+		end
+
+		local it = location or instance_tag()
+		location = nil
+
+		local duration = 0
+		if mod.combatInfo and mod.combatInfo.pull then
+			duration = math.floor (GetTime() - mod.combatInfo.pull)
+		end
+
+		-- attendance:  maybe put people in groups 6,7,8 into a "backup/standby"
+		-- list?  probably too specific to guild practices.
+		local raiders = {}
+		for i = 1, GetNumRaidMembers() do
+			tinsert(raiders, (GetRaidRosterInfo(i)))
+		end
+		table.sort(raiders)
+
+		return _do_boss (self, reason, name, it, duration, raiders)
+	end
+
+	local callback = function(...) addon:DBMBossCallback(...) end
+	function _registerDBM(self)
+		if DBM then
+			if not self.dbm_registered then
+				local rev = tonumber(DBM.Revision) or 0
+				if rev < 1503 then
+					self.status_text = "|cffff1010Deadly Boss Mods must be version 1.26 or newer to work with Ouro Loot.|r"
+					return
+				end
+				local r = DBM:RegisterCallback("kill", callback)
+						  DBM:RegisterCallback("wipe", callback)
+						  DBM:RegisterCallback("pull", function() location = instance_tag() end)
+				self.dbm_registered = r > 0
+			end
+		else
+			self.status_text = "|cffff1010Ouro Loot cannot find Deadly Boss Mods, loot will not be grouped by boss.|r"
+		end
+	end
+end  -- DBM tie-ins
+
+-- Adding entries to the loot record, and tracking the corresponding timestamp.
+do
+	-- This shouldn't be required.  /sadface
+	local loot_entry_mt = {
+		__index = function (e,key)
+			if key == 'cols' then
+				pprint('mt', e.kind)
+				--tabledump(e)  -- not actually that useful
+				addon:_fill_out_eoi_data(1)
+			end
+			return rawget(e,key)
+		end
+	}
+
+	-- Given a loot index, searches backwards for a timestamp.  Returns that
+	-- index and the time entry, or nil if it falls off the beginning.  Pass an
+	-- optional second index to search no earlier than it.
+	-- May also be able to make good use of this in forum-generation routine.
+	function addon:find_previous_time_entry(i,stop)
+		local stop = stop or 0
+		while i > stop do
+			if g_loot[i].kind == 'time' then
+				return i, g_loot[i]
+			end
+			i = i - 1
+		end
+	end
+
+	-- format_timestamp (["format_string"], Day, [Loot])
+	-- DAY is a loot entry with kind=='time', and controls the date printed.
+	-- LOOT may be any kind of entry in the g_loot table.  If present, it
+	--    overrides the hour and minute printed; if absent, those values are
+	--    taken from the DAY entry.
+	-- FORMAT_STRING may contain $x (x in Y/M/D/h/m) tokens.
+	local format_timestamp_values, point2dee = {}, "%.2d"
+	function addon:format_timestamp (fmt_opt, day_entry, time_entry_opt)
+		if not time_entry_opt then
+			if type(fmt_opt) == 'table' then        -- Two entries, default format
+				time_entry_opt, day_entry = day_entry, fmt_opt
+				fmt_opt = "$Y/$M/$D $h:$m"
+			--elseif type(fmt_opt) == "string" then   -- Day entry only, specified format
+			end
+		end
+		--format_timestamp_values.Y = point2dee:format (day_entry.startday.year % 100)
+		format_timestamp_values.Y = ("%.4d"):format (day_entry.startday.year)
+		format_timestamp_values.M = point2dee:format (day_entry.startday.month)
+		format_timestamp_values.D = point2dee:format (day_entry.startday.day)
+		format_timestamp_values.h = point2dee:format ((time_entry_opt or day_entry).hour)
+		format_timestamp_values.m = point2dee:format ((time_entry_opt or day_entry).minute)
+		return fmt_opt:gsub ('%$([YMDhm])', format_timestamp_values)
+	end
+
+	local done_todays_date
+	function addon:_reset_timestamps()
+		done_todays_date = nil
+	end
+	local function do_todays_date()
+		local text, M, D, Y = makedate()
+		local found,ts = #g_loot+1
+		repeat
+			found,ts = addon:find_previous_time_entry(found-1)
+			if found and ts.startday.text == text then
+				done_todays_date = true
+			end
+		until done_todays_date or (not found)
+		if done_todays_date then
+			g_today = ts
+		else
+			done_todays_date = true
+			g_today = g_loot[addon._addLootEntry{
+				kind = 'time',
+				startday = {
+					text = text, month = M, day = D, year = Y
+				}
+			}]
+		end
+		addon:_fill_out_eoi_data(1)
+	end
+
+	-- Adding anything original to g_loot goes through this routine.
+	function addon._addLootEntry (e)
+		setmetatable(e,loot_entry_mt)
+
+		if not done_todays_date then do_todays_date() end
+
+		local h, m = GetGameTime()
+		local localuptime = math.floor(GetTime())
+		e.hour = h
+		e.minute = m
+		e.stamp = localuptime
+		local index = #g_loot + 1
+		g_loot[index] = e
+		return index
+	end
+end
+
+
+------ Saved texts
+function addon:check_saved_table(silent_p)
+	local s = g_loot.saved
+	if s and (#s > 0) then return s end
+	g_loot.saved = nil
+	if not silent_p then self:Print("There are no saved loot texts.") end
+end
+
+function addon:save_list()
+	local s = self:check_saved_table(); if not s then return end;
+	for i,t in ipairs(s) do
+		self:Print("#%d   %s    %d entries     %s", i, t.date, t.count, t.name)
+	end
+end
+
+function addon:save_saveas(name)
+	g_loot.saved = g_loot.saved or {}
+	local n = #(g_loot.saved) + 1
+	local save = {
+		name = name,
+		date = makedate(),
+		count = #g_loot,
+		forum = g_loot.forum,
+		attend = g_loot.attend,
+	}
+	self:Print("Saving current loot texts to #%d '%s'", n, name)
+	g_loot.saved[n] = save
+	return self:save_list()
+end
+
+function addon:save_restore(num)
+	local s = self:check_saved_table(); if not s then return end;
+	if (not num) or (num > #s) then
+		return self:Print("Saved text number must be 1 - "..#s)
+	end
+	local save = s[num]
+	self:Print("Overwriting current loot data with saved text #%d '%s'", num, save.name)
+	self:Clear(--[[verbose_p=]]false)
+	-- Clear will already have displayed the window, and re-selected the first
+	-- tab.  Set these up for when the text tabs are clicked.
+	g_loot.forum = save.forum
+	g_loot.attend = save.attend
+end
+
+function addon:save_delete(num)
+	local s = self:check_saved_table(); if not s then return end;
+	if (not num) or (num > #s) then
+		return self:Print("Saved text number must be 1 - "..#s)
+	end
+	self:Print("Deleting saved text #"..num)
+	tremove(s,num)
+	return self:save_list()
+end
+
+
+------ Loot histories
+-- history_all = {
+--   ["Kilrogg"] = {
+--     ["realm"] = "Kilrogg",                 -- not saved
+--     ["st"] = { lib-st display table },     -- not saved
+--     ["byname"] = {                         -- not saved
+--       ["OtherPlayer"] = 2,
+--       ["Farmbuyer"] = 1,
+--     }
+--     [1] = {
+--       ["name"] = "Farmbuyer",
+--       [1] = { id = nnnnn, when = "formatted timestamp for displaying" }  -- most recent loot
+--       [2] = { ......., [count = "x3"]                                 }  -- previous loot
+--     },
+--     [2] = {
+--       ["name"] = "OtherPlayer",
+--       ......
+--     }, ......
+--   },
+--   ["OtherRealm"] = ......
+-- }
+do
+	-- Builds the map of names to array indices.
+	function addon:_build_history_names (opt_hist)
+		local hist = opt_hist or self.history
+		local m = {}
+		for i = 1, #hist do
+			m[hist[i].name] = i
+		end
+		hist.byname = m
+	end
+
+	-- Maps a name to an array index, creating new tables if needed.  Returns
+	function addon:get_loot_history (name)
+		local i
+		i = self.history.byname[name]
+		if not i then
+			i = #self.history + 1
+			self.history[i] = { name=name }
+			self.history.byname[name] = i
+		end
+		return self.history[i]
+	end
+
+	-- Prepares and returns table to be used as self.history.
+	function addon:_prep_new_history_category (prev_table, realmname)
+		local t = prev_table or {
+			--kind = 'realm',
+			realm = realmname,
+		}
+
+		--[[
+		t.cols = setmetatable({
+			{ value = realmname },
+		}, self.time_column1_used_mt)
+		]]
+
+		if not t.byname then
+			self:_build_history_names (t)
+		end
+
+		return t
+	end
+
+	function addon:_addHistoryEntry (lootindex)
+		local e = g_loot[lootindex]
+		local h = self:get_loot_history(e.person)
+		local n = {
+			id = e.id,
+			when = self:format_timestamp (g_today, e),
+			count = e.count,
+		}
+		h[#h+1] = n
+	end
+end
+
+
+------ Player communication
+do
+	local function adduser (name, status, active)
+		if status then addon.sender_list.names[name] = status end
+		if active then addon.sender_list.active[name] = active end
+	end
+
+	-- Incoming handler functions.  All take the sender name and the incoming
+	-- tag as the first two arguments.  All of these are active even when the
+	-- player is not tracking loot, so test for that when appropriate.
+	local OCR_funcs = {}
+
+	OCR_funcs.ping = function (sender)
+		pprint('comm', "incoming ping from", sender)
+		addon:whispercast (sender, 'pong', addon.revision, 
+			addon.enabled and "tracking" or (addon.rebroadcast and "broadcasting" or "disabled"))
+	end
+	OCR_funcs.pong = function (sender, _, rev, status)
+		local s = ("|cff00ff00%s|r v2r%s is |cff00ffff%s|r"):format(sender,rev,status)
+		addon:Print("Echo: ", s)
+		adduser (sender, s, status=="tracking" or status=="broadcasting" or nil)
+	end
+
+	OCR_funcs.loot = function (sender, _, recip, item, count, extratext)
+		addon.dprint('comm', "DOTloot, sender", sender, "recip", recip, "item", item, "count", count)
+		if not addon.enabled then return end
+		adduser (sender, nil, true)
+		addon:CHAT_MSG_LOOT ("broadcast", recip, item, count, sender, extratext)
+	end
+
+	OCR_funcs.boss = function (sender, _, reason, bossname, instancetag)
+		addon.dprint('comm', "DOTboss, sender", sender, "reason", reason, "name", bossname, "it", instancetag)
+		if not addon.enabled then return end
+		adduser (sender, nil, true)
+		addon:on_boss_broadcast (reason, bossname, instancetag)
+	end
+
+	OCR_funcs.bcast_req = function (sender)
+		if addon.debug.comm or ((not g_wafer_thin) and (not addon.rebroadcast))
+		then
+			addon:Print("%s has requested additional broadcasters! Choose %s to enable rebroadcasting, or %s to remain off and also ignore rebroadcast requests for as long as you're logged in. Or do nothing for now to see if other requests arrive.",
+				sender,
+				addon.format_hypertext('bcaston',"the red pill",'|cffff4040'),
+				addon.format_hypertext('waferthin',"the blue pill",'|cff0070dd'))
+		end
+		self.popped = true
+	end
+
+	OCR_funcs.bcast_responder = function (sender)
+		if addon.debug.comm or addon.requesting or
+		   ((not g_wafer_thin) and (not addon.rebroadcast))
+	   then
+			addon:Print(sender, "has answered the call and is now broadcasting loot.")
+		end
+	end
+	-- remove this tag once it's all tested
+	OCR_funcs.bcast_denied = function (sender)
+		if addon.requesting then addon:Print(sender, "declines futher broadcast requests.") end
+	end
+
+	-- Incoming message dispatcher
+	local function dotdotdot (sender, tag, ...)
+		local f = OCR_funcs[tag]
+		addon.dprint('comm', ":... processing",tag,"from",sender)
+		if f then return f(sender,tag,...) end
+		addon.dprint('comm', "unknown comm message",tag",from", sender)
+	end
+	-- Recent message cache
+	addon.recent_messages = create_new_cache ('comm', comm_cleanup_ttl)
+
+	function addon:OnCommReceived (prefix, msg, distribution, sender)
+		if prefix ~= self.ident then return end
+		if not self.debug.comm then
+			if distribution ~= "RAID" and distribution ~= "WHISPER" then return end
+			if sender == my_name then return end
+		end
+		self.dprint('comm', ":OCR from", sender, "message is", msg)
+
+		if self.recent_messages:test(msg) then
+			return self.dprint('cache', "message <",msg,"> already in cache, skipping")
+		end
+		self.recent_messages:add(msg)
+
+		-- Nothing is actually returned, just (ab)using tail calls.
+		return dotdotdot(sender,strsplit('\a',msg))
+	end
+
+	function addon:OnCommReceivedNocache (prefix, msg, distribution, sender)
+		if prefix ~= self.identTg then return end
+		if not self.debug.comm then
+			if distribution ~= "WHISPER" then return end
+			if sender == my_name then return end
+		end
+		self.dprint('comm', ":OCRN from", sender, "message is", msg)
+		return dotdotdot(sender,strsplit('\a',msg))
+	end
+end
+
+-- vim:noet
--- /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
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mleqdkp.lua	Sat Apr 16 06:03:29 2011 +0000
@@ -0,0 +1,350 @@
+if UnitName"player" ~= "Farmbuyer" then return end
+local addon = select(2,...)
+
+-- We keep some local state bundled up rather than trying to pass it around
+-- as paramters (which would have entailed creating a ton of closures).
+local state = {}
+local tag_lookup_handlers = {}
+local do_tag_lookup_handler
+
+
+--[[
+This is taken from CT_RaidTracker 1.7.32, reconstructing the output from
+code inspection.  No official format documents are available on the web
+without downloading and installing the EQDKP webserver package.  Bah.
+
+Case of tag names shouldn't matter, but is preserved here from CT_RaidTracker
+code for comparison of generated output.
+
+There is some waste adding newlines between elements here only to strip them
+out later, but it's worth the extra cycles for debugging and verification.
+
+$TIMESTAMP,$ENDTIME MM/DD/YY HH:mm:ss except we don't have seconds available
+$REALM              GetRealmName()
+$ZONE               raw name, not snarky
+$RAIDNOTE           arbitrary text for the raid event
+$PHAT_LEWTS         all accumulated loot entries
+]]
+local XML = ([====[
+<RaidInfo>
+<Version>1.4</Version>  {In live output, this is missing due to scoping bug in ct_raidtracker.lua:3471}
+<key>$TIMESTAMP</key>
+<realm>$REALM</realm>
+<start>$TIMESTAMP</start>   {Same as the key, apparently?}
+<end>$ENDTIME</end>    {Set by the "end the raid" command in the raid tracker, here just the final entry time}
+<zone>$ZONE</zone>   {may be optional.  first one on the list in case of multiple zones?}
+{<difficulty>$DIFFICULTY</difficulty>   {this scales badly in places like ICC.  may be optional?}}
+
+{<PlayerInfos>... {guh.}}
+
+<BossKills>
+  $BOSS_KILLS
+</BossKills>
+
+{<Wipes>  bleh}
+{<NextBoss>Baron Steamroller</NextBoss>  {only one "next boss" for the whole raid event?  huh?}}
+
+<note><![CDATA[$RAIDNOTE - Zone: $ZONE]]></note>
+
+{<Join>...</Join><Leave>...</Leave>   {specific timestamps per player. meh.}}
+
+<Loot>
+  $PHAT_LEWTS
+</Loot>
+</RaidInfo>]====]):gsub('%b{}', "")
+
+--[[
+See the loot markup, next block.
+]]
+local boss_kills_xml = ([====[
+  <key$N>
+    <name>$BOSS_NAME</name>
+    <time>$BOSS_TIME</time>
+    <attendees></attendees>   {this is actually empty in the working example...}
+    <difficulty>$DIFFICULTY</difficulty>
+  </key$N>
+]====]):gsub('%b{}', "")
+
+local function boss_kills_tag_lookup (tag)
+	if tag == 'N' then
+		return tostring(state.key)
+	elseif tag == 'BOSS_NAME' then
+		return state.entry.bosskill
+	elseif tag == 'BOSS_TIME' then
+		return do_tag_lookup_handler (state.index, state.entry, 'TIME')
+	else
+		return do_tag_lookup_handler (state.index, state.entry, tag) or 'NYI'
+	end
+end
+
+--[[
+$N                  1-based loop variable for key element
+$ITEMNAME           Without The Brackets
+$ITEMID             Not the ID, actually a full itemstring without the leading "item:"
+$ICON               Last component of texture path?
+$CLASS,$SUBCLASS    ItemType
+$COLOR              GetItemQualityColor, full 8-digit string
+$COUNT,$BOSS,$ZONE,
+  $PLAYER           all self-explanatory
+$COSTS              in DKP points... hmmm
+$ITEMNOTE           take the notes field for this one
+$TIME               another formatted timestamp
+]]
+local phat_lewt_xml = ([====[
+  <key$N>
+$LEWT_GRUNTWORK
+    <zone>$ZONE</zone>   {may be optional}
+    <difficulty>$DIFFICULTY</difficulty>   {this scales badly in places like ICC.  may be optional?}
+    <Note><![CDATA[$ITEMNOTE - Zone: $ZONE - Boss: $BOSS - $COSTS DKP]]></Note> {zone can be followed by difficulty}
+  </key$N>
+]====]):gsub('%b{}', "")
+
+local function phat_lewt_tag_lookup (tag)
+	if tag == 'N' then
+		return tostring(state.key)
+	elseif tag == 'COSTS'
+		then return '1'
+	else
+		return do_tag_lookup_handler (state.index, state.entry, tag) or 'NYI'
+	end
+end
+
+do
+	local gruntwork_tags = {
+		"ItemName", "ItemID", "Icon", "Class", "SubClass", "Color", "Count",
+		"Player", "Costs", "Boss", "Time",
+	}
+	for i,tag in ipairs(gruntwork_tags) do
+		gruntwork_tags[i] = ("    <%s>$%s</%s>"):format(tag,tag:upper(),tag)
+	end
+	phat_lewt_xml = phat_lewt_xml:gsub('$LEWT_GRUNTWORK', table.concat(gruntwork_tags,'\n'))
+end
+
+
+local function format_EQDKP_timestamp (day_entry, time_entry)
+	--assert(day_entry.kind == 'time', day_entry.kind .. " passed to MLEQDKP timestamp")
+	return addon:format_timestamp ("$M/$D/$Y $h:$m:00", day_entry, time_entry)
+end
+
+
+-- Look up tag strings for a particular item, given index and entry table.
+tag_lookup_handlers.ITEMNAME =
+	function (i, e)
+		return e.itemname
+	end
+
+tag_lookup_handlers.ITEMID =
+	function (i, e)
+		return e.itemlink:match("^|c%x+|H(item[%d:]+)|h%[")
+	end
+
+tag_lookup_handlers.ICON =
+	function (i, e)
+		local str = e.itexture
+		repeat
+			local s = str:find('\\')
+			if s then str = str:sub(s+1) end
+		until not s
+		return str
+	end
+
+tag_lookup_handlers.CLASS =
+	function (i, e)
+		return state.class
+	end
+
+tag_lookup_handlers.SUBCLASS =
+	function (i, e)
+		return state.subclass
+	end
+
+tag_lookup_handlers.COLOR =
+	function (i, e)
+		local q = select(4, GetItemQualityColor(e.quality))
+		return q:sub(3)  -- skip leading |c
+	end
+
+tag_lookup_handlers.COUNT =
+	function (i, e)
+		return e.count and e.count:sub(2) or "1"   -- skip the leading "x"
+	end
+
+-- maybe combine these next two
+tag_lookup_handlers.BOSS =
+	function (i, e)
+		while i > 0 and state.loot[i].kind ~= 'boss' do
+			i = i - 1
+		end
+		if i == 0 then return "No Boss Entry Found, Unknown Boss" end
+		return state.loot[i].bosskill
+	end
+
+tag_lookup_handlers.ZONE =
+	function (i, e)
+		while i > 0 and state.loot[i].kind ~= 'boss' do
+			i = i - 1
+		end
+		if i == 0 then return "No Boss Entry Found, Unknown Zone" end
+		return state.loot[i].instance
+	end
+
+tag_lookup_handlers.DIFFICULTY =
+	function (i, e)
+		local tag = tag_lookup_handlers.ZONE(i,e)
+		local N,h = tag:match("%((%d+)(h?)%)")
+		if not N then return "1" end -- maybe signal an error instead?
+		N = tonumber(N)
+		N = ( (N==10) and 1 or 2 ) + ( (h=='h') and 2 or 0 )
+		return tostring(N)
+	end
+
+tag_lookup_handlers.PLAYER =
+	function (i, e)
+		return state.player
+	end
+
+tag_lookup_handlers.ITEMNOTE =
+	function (i, e)
+		return state.itemnote
+	end
+
+tag_lookup_handlers.TIME =
+	function (i, e)
+		local ti,tl = addon:find_previous_time_entry(i)
+		return format_EQDKP_timestamp(tl,e)
+	end
+
+
+function do_tag_lookup_handler (index, entry, tag)
+	local h = tag_lookup_handlers[tag]
+	if h then
+		return h(index,entry)
+	else
+		error(("MLDKP tag lookup (index %d) on tag %s with no handler"):format(index,tag))
+	end
+end
+
+local function generator (ttype, loot, last_printed, generated, cache)
+	-- Because it's XML, generated text is "grown" by shoving more crap into
+	-- the middle instead of appending to the end.  Only easy way of doing that
+	-- here is regenerating it from scratch each time.
+	generated[ttype] = nil
+
+	local _
+	local text = XML
+	state.loot = loot
+
+	-- TIMESTAMPs
+	do
+		local f,l  -- first and last timestamps in the table
+		for i = 1, #loot do
+			if loot[i].kind == 'time' then
+				f = format_EQDKP_timestamp(loot[i])
+				break
+			end
+		end
+		_,l = addon:find_previous_time_entry(#loot)  -- latest timestamp
+		l = format_EQDKP_timestamp(l,loot[#loot])
+		text = text:gsub('$TIMESTAMP', f):gsub('$ENDTIME', l)
+	end
+
+	-- Loot
+	do
+		local all_lewts = {}
+		local lewt_template = phat_lewt_xml
+
+		state.key = 1
+		for i,e in addon:filtered_loot_iter('loot') do
+			state.index, state.entry = i, e
+			-- no sense doing repeated getiteminfo calls
+			state.class, state.subclass = select(6, GetItemInfo(e.id))
+
+			-- similar logic as text_tabs.lua:
+			-- assuming nobody names a toon "offspec" or "gvault"
+			local P, N
+			local disp = e.disposition or e.person
+			if disp == 'offspec' then
+				P,N = e.person, "offspec"
+			elseif disp == 'gvault' then
+				P,N = "guild vault", e.person
+			else
+				P,N = disp, ""
+			end
+			if e.extratext_byhand then
+				N = N .. " -- " .. e.extratext
+			end
+			state.player, state.itemnote = P, N
+
+			all_lewts[#all_lewts+1] = lewt_template:gsub('%$([%w_]+)',
+				phat_lewt_tag_lookup)
+			state.key = state.key + 1
+		end
+
+		text = text:gsub('$PHAT_LEWTS', table.concat(all_lewts, '\n'))
+	end
+
+	-- Bosses
+	do
+		local all_bosses = {}
+		local boss_template = boss_kills_xml
+
+		state.key = 1
+		for i,e in addon:filtered_loot_iter('boss') do
+			if e.reason == 'kill' then  -- oh, for a 'continue' statement...
+				state.index, state.entry = i, e
+				all_bosses[#all_bosses+1] = boss_template:gsub('%$([%w_]+)',
+					boss_kills_tag_lookup)
+				state.key = state.key + 1
+			end
+		end
+
+		text = text:gsub('$BOSS_KILLS', table.concat(all_bosses, '\n'))
+	end
+
+	-- In addition to doing the top-level zone, this will also catch any
+	-- leftover $ZONE tags.  There could be multiple places in the raid, so
+	-- we default to the first one we saw.
+	do
+		local iter = addon:filtered_loot_iter()  -- HACK
+		local first_boss = iter('boss',0)
+		local zone = first_boss and loot[first_boss].instance or "Unknown"
+		text = text:gsub('$ZONE', zone)
+	end
+
+	-- Misc
+	text = text:gsub('$REALM', (GetRealmName()))
+	--text = text:gsub('$DIFFICULTY', )
+	text = text:gsub('$RAIDNOTE', "")
+
+	cache[#cache+1] = "Formatted version (scroll down for unformatted):"
+	cache[#cache+1] = "==========================="
+	cache[#cache+1] = text
+	cache[#cache+1] = '\n'
+
+	cache[#cache+1] = "Unformatted version:"
+	cache[#cache+1] = "==========================="
+	text = text:gsub('>%s+<', "><")
+	cache[#cache+1] = text
+	cache[#cache+1] = '\n'
+
+	wipe(state)
+	return true
+end
+
+local function specials (_, editbox, container, mkbutton)
+	local hl = mkbutton("Highlight",
+		[[Highlight the unformatted copy for copy-and-pasting.]])
+	hl:SetFullWidth(true)
+	hl:SetCallback("OnClick", function(_hl)
+		local txt = editbox:GetText()
+		local _,start = txt:find("Unformatted version:\n=+\n")
+		local _,finish = txt:find("</RaidInfo>", start)
+		editbox.editBox:HighlightText(start,finish)
+		editbox.editBox:SetCursorPosition(start)
+	end)
+	container:AddChild(hl)
+end
+
+addon:register_text_generator ("mleqdkp", [[ML/EQ-DKP]], [[MLdkp 1.1 EQDKP format]], generator, specials)
+
+-- vim:noet
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/text_tabs.lua	Sat Apr 16 06:03:29 2011 +0000
@@ -0,0 +1,174 @@
+local addon = select(2,...)
+
+--[[ Generator:
+boolean FUNC (ttype, loot, last_printed, generated, cache)
+in      TTYPE:  the registered text type as passed to register_text_generator
+in      LOOT:  pointer to g_loot table
+in      LAST_PRINTED:  index into loot most recently formatted by this routine
+in      GENERATED.TTYPE:  (string) FIFO buffer for text created by this routine;
+           other parts of the GUI copy and nil out this string.  Do not change
+           this string, only examine it if needed.  If the generator is called
+           more than once between GUI updates, text will build up here.
+in/out  GENERATED.TTYPE_pos:  if non-nil, this is the saved cursor position in
+           the text window (so that it stays where the user last left it).
+           Move it if you're doing something strange with the displayed text.
+tmp     GENERATED.loc_TTYPE_*:  Use as needed.
+out     CACHE:  Empty output table.  Accumulate generated lines here, one entry
+           per visible line.  Do not terminate with a newline unless you want
+           an extra blank line there.
+
+Preconditions:
+  + LAST_PRINTED < #LOOT
+  + all "display-relevant" information for the main Loot tab has been filled
+    out (e.g., LOOT[i].cols[3] might have extra text, etc)
+  + LOOT.TTYPE is a non-nil string containing all text in the text box (and
+    if the user has edited the text box, this string will be updated).  Do not
+    change this, but like GENERATED.TTYPE it is available for examination.
+
+Return true if text was created, false if nothing was done.
+]]
+
+--[[ Optional special widgets:
+FUNC (ttype, editbox, container, mkbutton)
+    TTYPE:  see above
+    EDITBOX:  the MultiLineEditBox widget
+    CONTAINER:  widget container (already has 'Regenerate' button in it)
+    MKBUTTON:  function to create more AceGUI widgets, as follows:
+
+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")
+
+The 'display key' parameter will almost certainly be specified as nil for these functions.
+]]
+
+local forum_warned_heroic
+local warning_text
+do
+	local red = '|cffff0505'
+	local green = addon.quality_hexes[ITEM_QUALITY_UNCOMMON]
+	warning_text = ([[%sWARNING:|r  Heroic items sharing the same name as normal items often display incorrectly on forums that use the item name as the identifier.  Recommend you change the %sItem markup|r dropdown in the right-hand side to %s"[item] by ID"|r and regenerate this loot.]]):format(red, green, green)
+end
+
+local function forum (_, loot, last_printed, generated, cache)
+	local fmt = OuroLootSV_opts.forum[OuroLootSV_opts.forum_current] or ""
+	-- if it's capable of handling heroic items, consider them warned already
+	forum_warned_heroic = forum_warned_heroic or fmt:find'%$I'
+
+	for i = last_printed+1, #loot do
+		local e = loot[i]
+
+		if e.kind == 'loot' then
+			-- Assuming nobody names a toon "offspec" or "gvault"
+			-- 16Apr2011:  armory finds 20 Gvaults and 77 Offspecs... hulk smash.
+			local disp = e.disposition or e.person
+			if disp == 'offspec' then
+				disp = e.person .. " " .. 'offspec'
+			elseif disp == 'gvault' then
+				--disp = "guild vault (".. e.person .. ")"
+				disp = "guild vault"
+			end
+			if e.extratext_byhand then
+				disp = disp .. " -- " .. e.extratext
+			end
+			if e.is_heroic and not forum_warned_heroic then
+				forum_warned_heroic = true
+				addon:Print(warning_text)
+			end
+			local t = fmt:gsub('%$I', e.id)
+			             :gsub('%$N', e.itemname)
+			             :gsub('%$X', e.count or "")
+			             :gsub('%$T', disp)
+			cache[#cache+1] = t
+
+		elseif e.kind == 'boss' and e.reason == 'kill' then
+			-- first boss in an instance gets an instance tag, others get a blank line
+			if generated.last_instance == e.instance then
+				cache[#cache+1] = ""
+			else
+				cache[#cache+1] = "\n[b]" .. e.instance .. "[/b]"
+				generated.last_instance = e.instance
+			end
+			cache[#cache+1] = "[i]" .. e.bosskill .. "[/i]"
+
+		elseif e.kind == 'time' then
+			cache[#cache+1] = "[b]" .. e.startday.text .. "[/b]"
+
+		end
+	end
+	return true
+end
+
+local function forum_specials (_,_, container, mkbutton)
+	local map,current = {}
+	for label,format in pairs(OuroLootSV_opts.forum) do
+		table.insert(map,label)
+		if label == OuroLootSV_opts.forum_current then
+			current = #map
+		end
+	end
+
+	local dd, editbox
+	dd = mkbutton("Dropdown", nil, "",
+		[[Chose specific formatting of loot items.  See Help tab for more.  Regenerate to take effect.]])
+	dd:SetFullWidth(true)
+	dd:SetLabel("Item markup")
+	dd:SetList(map)
+	dd:SetValue(current)
+	dd:SetCallback("OnValueChanged", function(_dd,event,choice)
+		OuroLootSV_opts.forum_current = map[choice]
+		forum_warned_heroic = nil
+		editbox:SetDisabled(map[choice] ~= "Custom...")
+	end)
+	container:AddChild(dd)
+
+	editbox = mkbutton("EditBox", nil, OuroLootSV_opts.forum["Custom..."],
+		[[Format described in Help tab (Generated Text -> Forum Markup).]])
+	editbox:SetFullWidth(true)
+	editbox:SetLabel("Custom:")
+	editbox:SetCallback("OnEnterPressed", function(_e,event,value)
+		OuroLootSV_opts.forum["Custom..."] = value
+		_e.editbox:ClearFocus()
+	end)
+	editbox:SetDisabled(OuroLootSV_opts.forum_current ~= "Custom...")
+	container:AddChild(editbox)
+end
+
+addon:register_text_generator ("forum", [[Forum Markup]], [[BBcode ready for Ouroboros forums]], forum, forum_specials)
+
+
+local function att (_, loot, last_printed, _, cache)
+	for i = last_printed+1, #loot do
+		local e = loot[i]
+
+		if e.kind == 'boss' and e.reason == 'kill' then
+			cache[#cache+1] = ("\n%s -- %s\n%s"):format(e.instance, e.bosskill, e.raiderlist or '<none recorded>')
+
+		elseif e.kind == 'time' then
+			cache[#cache+1] = e.startday.text
+
+		end
+	end
+	return true
+end
+
+local function att_specials (_, editbox, container, mkbutton)
+	local w = mkbutton("Take Attendance",
+		[[Take attendance now (will continue to take attendance on each boss kill).]])
+	w:SetFullWidth(true)
+	w:SetCallback("OnClick", function(_w)
+		local raiders = {}
+		for i = 1, GetNumRaidMembers() do
+			table.insert(raiders, (GetRaidRosterInfo(i)))
+		end
+		table.sort(raiders)
+		local h, m = GetGameTime()
+		local additional = ("Attendance at %s:%s:\n%s"):format(h,m,table.concat(raiders, ", "))
+		editbox:SetText(editbox:GetText() .. '\n' .. additional)
+	end)
+	container:AddChild(w)
+end
+
+addon:register_text_generator ("attend", [[Attendance]], [[Attendance list for each kill]], att, att_specials)
+
+-- vim:noet
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/verbage.lua	Sat Apr 16 06:03:29 2011 +0000
@@ -0,0 +1,484 @@
+
+local todo = [[
+- broadcasted entries triggering auto-shard don't have "shard" text
+
+- [DONE,TEST,comm] releasing before DBM signals wipe results in outdoor location
+
+- implement ack, then fallback to recording if not ack'd
+
+- special treatment for recipes / BoE items?  default to guild vault?
+
+- rebroadcasting entire boss sections, entire days.  maybe only whisper
+to specific people rather than broadcast.
+
+- signpost a potential boss kill, pipeline loot until the cache clears
+
+- Being able to drag rows up and down the main loot grid would be awesome.  Coding
+that would be likely to drive me batshiat insane.
+]]
+
+local addon = select(2,...)
+
+addon.helptree = {
+	{
+		value = "about",
+		text = "About",
+	},
+	{
+		value = "basic",
+		text = "Basics",
+		children = {
+			{
+				value = "loot",
+				text = "Loot Entries",
+			},
+			{
+				value = "boss",
+				text = "Boss Entries",
+			},
+		},
+	},
+	{
+		value = "tracking",
+		text = "Tracking Loot",
+		children = {
+			{
+				value = "enabled",
+				text = "Full Tracking",
+			},
+			{
+				value = "bcast",
+				text = "Rebroadcasting",
+			},
+		},
+	},
+	{
+		value = "texts",
+		text = "Generated Texts",
+		children = {
+			{
+				value = "forum",
+				text = "Forum Markup",
+			},
+			{
+				value = "other",
+				text = "Other Texts",
+			},
+			{
+				value = "saved",
+				text = "Saved Texts",
+			},
+		},
+	},
+	{
+		value = "tips",
+		text = "Handy Tips",
+		children = {
+			{
+				value = "slashies",
+				text = "Slash Commands",
+			},
+		},
+	},
+	{
+		value = "todo",
+		text = "TODOs, Bugs, etc",
+		children = {
+			{
+				value = "gotchas",
+				text = "Gotchas",
+			},
+			{
+				value = "todolist",
+				text = "TODO/knownbugs",
+			},
+        },
+	},
+}
+
+-- Help text.  Formatting doesn't matter, but use a blank line to split
+-- paragraphs.  This file needs to be edited with a text editor that doesn't
+-- do anything stupid by placing extra spaces at the end of lines.
+do
+local replacement_colors = { ["+"]="|cff30adff", ["<"]="|cff00ff00", [">"]="|r" }
+local T={}
+T.about = [[
+Ouro Loot is the fault of Farmbuyer of Ouroboros on US-Kilrogg.  Bug reports,
+comments, and suggestions are welcome at the project page at curse.com or send
+them to <farmbuyer@gmail.com>.
+]]
+
+T.basic = [[
+The </ouroloot> (and </loot> by default) command opens this display.  The buttons
+on the right side control operation and are mostly self-explanatory.  Hovering over
+things will usually display some additional text in the gray line at the bottom.
+
+Each tab on the left side can additionally create extra contols in the lower-right
+section of the display.
+
+The first tab on the left side, <Loot>, is where everything goes to and comes
+from.  Looting events and Deadly Boss Mods notifications go to the <Loot> tab; the
+other tabs are all generated from the information in the <Loot> tab.
+]]
+
+T.basic_loot = [[
+A "loot row" in the first tab has three columns:  the item, the recipient, and any
+extra notes.  The recipient's class icon is displayed by their names, if class
+information is available at the time.
+
+<Mouse Hover>
+
+Hovering the mouse over the first column will display the item in a tooltip.
+
+Hovering over the second column will display a tooltip with the loot that person
+has received.  If they've won more than 10 items, the list is cut off with '...'
+at the end; to see the full list, use the right-click +Show only this player> option
+instead.
+
+<Right-Click>
+
+Right-clicking a loot row shows a dropdown menu.
+
+Right-clicking in the first or third columns will display options for special
+treatment of that loot entry (marking as offspec, etcetera).  Using any of those
+options will change the text in the third column (which will then affect the text
+in the generated tabs, such as forum markup).
+
+Right-clicking in the second column allows you to temporarily remove all other
+players from the loot display.  Use the reset button in the lower-right corner to
+restore the display to normal.  The menu also allows you to +reassign> loot from
+one player to another; if the new recipient is not in the raid group at the time,
+use the +Enter name...> option at the bottom of the list of names to type the
+name into a text box.  If your raid takes advantage of the new ability to trade
+soulbound items, you will need to reassign the item here for the generated text
+to be factually correct.
+
+See the help screen on "Boss Entries" for the +Insert new boss kill event> option.
+
+<Double-Click>
+
+Double-clicking a loot row in the third ("Notes") column allows you to edit that
+field directly.  The color of the text will still depend on any +Mark as ___>
+actions done to that loot row, whether automatically or by hand.
+]]
+
+T.basic_boss = [[
+Boss wipe/kill entries are entirely dependant on Deadly Boss Mods being enabled and
+up-to-date.  The typical sequence of events for our usual raids goes like this:
+
+We make four or five attempts on Baron Steamroller.  As DBM registers that combat
+ends, a <wipe> event is entered on the loot display along with the duration of the
+fight.  If the loot display is opened, the wipes will be visible with a light gray
+background.
+
+After reminding the dps classes to watch the threat meters, we manage to kill
+Steamroller.  When DBM registers the win, a <kill> event is entered on the display
+with a dark gray background.
+All previous <wipe>s are removed and collapsed into the <kill> event.  The final
+<kill> event shows the duration of the successful fight and the number of attempts
+needed (or "one-shot" if we manage to be competent).
+
+Sometimes this goes wrong, when DBM misses its own triggers.  If DBM does not catch
+the start of the boss fight, it can't register the end, so nothing at all is
+recorded.  If the fight was a win but DBM does not catch the victory conditions,
+then DBM will (after several seconds) decide that it was a wipe instead.  And
+sometimes useful loot will drop from trash mobs, which DBM knows nothing about.
+
+For all those reasons, right-clicking on a "boss row" will display options for
++Insert new boss kill event>, and for toggling a <wipe> into a <kill>.  We often
+insert bosses named "trash" to break up the display and correct the forum markup
+listing.
+]]
+
+T.tracking = [[
+The first button underneath +Main> in the right-hand column displays the current
+status of the addon.  If it is disabled, then no recording, rebroadcasting, or
+listening for rebroadcasts is performed.  Any loot already recorded will be restored
+across login sessions no matter the status.
+
+You can turn on tracking/broadcasting before joining a raid.  If you join a raid
+and the addon has not been turned on, then (by default) a popup dialog will ask for
+instructions.  (This can be turned off in the advanced <Options>.)
+
+The addon tries to be smart about logging on during a raid (due to a disconnect or
+relog).  If you log in, are already in a raid group, and loot has already been
+stored from tracking, it will re-enable itself automatically.  It will not (as of
+this writing) restore ancillary settings such as the tracking threshold.
+
+The intent of the addon design is that, after the end of a raid, all the generated
+markup text is done, optionally saved (see "Generated Texts - Saved Texts"), and
+then cleared from
+storage altogether.  As a result, if you login with restored loot information but
+are not in a raid, the addon will do nothing on its own -- but will assume that
+you've forgotten to finish those steps and will yammer about it in the chat window
+as a reminder.
+
+The +Threshold> drop-down has no connection at all with any current loot threshold
+set by you or a master looter.
+]]
+
+T.tracking_enabled = [[
+Full tracking records all loot events that fulfill these criteria:
+
+1)  The loot quality is equal to or better than what you have selected in the
++Threshold> drop-down.
+
+2)  The loot is not one of the few items hardcoded to not be tracked (badges,
+emblems, Stone Keeper Shards, etc).
+
+3)  <You can see the loot event.>  More precisely, you need to be close enough
+to the recipient of the loot to be able to see "So-And-So receives loot: [Stuff]"
+in your chat window, even if you have those actual loot messages turned off.
+
+It is (3) that causes complications.  A master looter can assign loot to anybody
+anywhere in a raid instance, but the range on detecting loot events is much
+smaller.  If your raid does not use master looting then you merely need to be
+close enough to the boss corpse, presuming that the winners will need to walk
+over to get their phat epix.
+
+If you do use master looter, then you have two options:  first, you can
+require players
+who might get loot to stay near the boss.  You would then also need to stay near
+the boss to detect the loot event.  (This can be less hassle if you are also
+the loot master.)  The downside is that other players moving on to fight to the
+next boss are doing so without the help of their teammates.
+
+The other option is to ask other players to also install Ouro Loot, and for
+them to turn on the "Rebroadcasting" feature.  Any loot events which they can
+see will be communicated to you.  Then it only becomes necessary for at least
+one person to be close enough to the loot recipient to see the item awarded,
+and you will record it no matter how far away you are -- even back in Dalaran.
+
+If you have Full Tracking enabled, then you are also automatically rebroadcasting.
+Having more than one player with Full Tracking turned on is probably a good
+idea, in case one of the trackers experiences a game crash or is suddenly kidnapped
+by robot ninja monkeys.
+]]
+
+T.tracking_bcast = [[
+The simplest method of operation is only rebroadcasting the loot events that you
+see, as you see them.  Nothing is recorded in your local copy of the addon.
+
+If you logout for any reason, the addon will not reactivate when you log back in.
+
+You can use </loot bcast> or </loot broadcast> to turn on rebroadcasting without
+opening the GUI.
+]]
+
+T.texts = [[
+The middle tabs are just large editboxes.  Their text is initially generated from
+the information stored on the main <Loot> tab, at the time you click on the tab.
+Not every bit of information that
+we want in the generated text is always available, or depends on things that the
+game itself can't know.  So you can edit the text in the tabs and your edits will
+be preserved.
+
+Each time you click one of the text tabs, every new entry on the <Loot> tab
+since the last time this tab was shown will be turned into text.
+
+Clicking the +Regenerate> button will throw away all the text on that tab, including
+any edits you've made, and recreate all of it from scratch.  If you've accidentally
+deleted the text from the editbox, or you've made manual changes to the <Loot> tab,
+you can use this button to start over.
+
+You can click in an editbox and use Control-A to select all text, then Control-C
+to copy it to the system clipboard for subsequent pasting into a web browser or
+whatever.  If you're on a Mac, you probably already know the equivalent keys.
+]]
+
+T.texts_forum = [[
+The <Forum Markup> tab creates text as used by the guild forums for Ouroboros
+of Kilrogg.  By default this is fairly standard BBcode.  The format of the
+individual loot items can be adjusted via the dropdown menu on the lower right
+of the tab.
+
+The [url] choice defaults to using Wowhead.  If you have the [item] extension
+for your BBcode installed, you can use either of those choices too.  The "by ID"
+variant is good for heroic ToC/ICC items that share names with nonheroic items,
+but is harder to read in the text tab.
+
+You can also specify a custom string.  Formatting is done with these replacements:
+
++$N>:  item name|r
+
++$I>:  (capital "eye", not "ell") numeric item ID|r
+
++$T>:  loot recipient and any additional notes|r
+
++$X>:  if more than one of the item was looted, this is the "x2", "x3", etc
+
+
+Pro tip #1:  if something has happened on the main <Loot> tab which cannot be
+changed directly but would generate incorrect text, you can click this tab to
+generate the text right away.  Then edit/move the text as needed.  When you
+close the display or click back on the <Loot> tab, your edited text will be
+preserved for later.
+
+Pro tip #2:  Barring things like pro tip #1, the author typically does not
+generate any text until the end of the raid.
+]]
+
+T.texts_other = [[
+So far the only other generated text is the <Attendance> tab, an alphabetized list
+on a per-boss basis.
+
+Other addons can register their own text tabs and corresponding generation
+functions.  If you want to be able to feed text into an offline program (for
+example, a spreadsheet or DKP tracker), then this may be of use to you.
+
+Ideas for more tabs?  Tell me!
+]]
+
+T.texts_saved = [[
+The contents of the <Forum Markup> and <Attendance> tabs can be saved, so that they
+will not be lost when you use the +Clear> button.
+
+Do any edits you want to the generated text tabs, then click the +Save Current As...>
+button on the right-hand side.  Enter a short descriptive reminder (for example,
+"thursday hardmodes") in the popup dialog.  The texts will remain in their tabs,
+but clearing loot information will not lose them now.
+
+All saved texts are listed on the right-hand side.  There is no technical limit to
+the number of saved texts, but the graphical display will begin to overflow after
+about half a dozen saved sets.  (And I don't care.)
+
+Clicking a saved text name lets you +Load> or +Delete> that saved set.  The primary
+<Loot> tab is not saved and restored by this process, only the generated texts.
+This also means you cannot +Regenerate> the texts.
+]]
+
+T.tips = [[
+Shift-clicking an item in the <Loot> display will paste it into an open chat editbox.
+
+The |cffff8000[Ouro Loot]|r "legendary item" link displayed at the start of all
+chat messages is a clickable link.  Clicking opens the main display.  An option
+on the <Options> tab will cause a message to be printed after a boss kill,
+mostly for lazy loot trackers who don't like typing slash commands to open windows.
+
+If you are broadcasting to somebody else who is tracking, you should probably be
+using the same threshold.  If yours is lower, then some of the loot you broadcast
+to him will be ignored.  If yours is higher, then you will not be sending information
+that he would have recorded.  The "correct" setting depends on what your guild wants
+to track.
+
+Ticking the "notraid" box in advanced debugging <Options>, before enabling tracking,
+will make the tracking work outside of a raid group.  Communication functions
+will behave a little strangely when doing this.  Be sure to check the threshold!
+You can also use <"/ouroloot debug notraid"> instead.
+
+Using the "Saved Texts" feature plus the +Clear> button is a great way of putting
+off pasting loot into your guild's website until a more convenient time.
+]]
+
+T.tips_slashies = [[
+The </ouroloot> command can take arguments to do things without going through
+the UI.  Parts given in *(angle brackets)* are required, parts in [square brackets]
+are optional:
+
++broadcast>/+bcast>:  turns on rebroadcasting|r
+
++on [T]>:  turns on full tracking, optionally setting threshold to T|r
+
++off>:  turns off everything|r
+
++thre[shold] T>:  sets tracking threshold to T|r
+
++list>:  prints saved text names and numbers|r
+
++save *(your set name)*>:  saves texts as "your set name"|r
+
++restore *(N)*>:  restores set number N|r
+
++delete *(N)*>:  deletes set number N|r
+
++help>:  opens the UI to the help tab|r
+
++toggle>:  opens or closes the UI (used mostly in automated wrappers)|r
+
+
+If you use the slash commands to enable tracking or set loot thresholds, you can
+give numbers or common names for the threshold.  For example, "0", "poor", "trash",
+"gray"/"grey" are all the same, "4", "epic", "purple" are the same, and so on.
+
+If you give an unrecognized argument to the </ouroloot> slash command, it will
+search the tab titles left to right for a title beginning with the same letters as
+the argument, and open the display to that tab.  For example, <"/loot a"> would
+open the <Attendance> tab, and <"/loot ad"> would open the <Advanced> tab.  If
+you had added a theoretical <EQDKP> tab, then <"/loot eq"> would be the fastest
+way to see it.
+]]
+
+T.todo = [[
+If you have ideas or complaints or bug reports, first check the Bugs subcategories
+to see if they're already being worked on.  Bug reports are especially helpful
+if you can include a screenshot (in whatever image format you find convenient).
+
+Click the "About" line on the left for contact information.
+]]
+
+T.todo_gotchas = [[
+<Things Which Might Surprise You> (and things I'm not sure I like in the
+current design):
+
+If you relog (or get disconnected) while in a raid group, behavior when you log
+back in can be surprising.  If you have already recorded loot (and therefore
+the loot list is restored), then OL assumes it's from the current raid and should
+reactivate automatically in full tracking mode.  If you were tracking but no
+loot had dropped yet (and therefore there was nothing *to* restore), then OL
+will pop up its reminder and ask again.  Either way, if you were only broadcasting
+then OL will *not* go back to only broadcasting.  This is probably a bug.
+
+The saved texts feature does exactly that: only saves the generated texts, not
+the full loot list.  Restoring will get you a blank first tab and whatever you
+previously had in the various generated text tabs.
+
+Using the right-click menu to change an item's treatment (shard, offspec, etc)
+does not broadcast that change to anyone else who is also tracking.  Changing
+the item and then selecting "rebroadcast this item" *does* include that extra
+info.  Doing that on the initial "mark as xxx" action is... tricky.
+
+The generated text tries to only list the name of the instance if it has not
+already been listed, or if it is different than the instance of the previous
+boss.  If you relog, the "last printed instance name" will be forgotten, and
+you'll see redundant raid instance names appearing in the text.
+
+After a boss wipe, multiple broadcasting players releasing spirit more than
+several seconds apart can cause spurious "wipe" entries (of zero duration) on
+the loot grid.  The surefire way to avoid this is to not release spirit until
+DBM announces the wipe, but the problem isn't serious enough to really worry
+about.  (Right-click the spurious entries and delete them.)
+]]
+
+T.todo_todolist = todo
+
+
+-- Fill out the table that will actually be used.  Join adjacent lines here so
+-- that they'll wrap properly.
+addon.helptext = {}
+for k,text in pairs(T) do
+	local funkykey = k:gsub('_','\001')  -- this is how TreeGroup makes unique keys
+	local wrapped = text
+	wrapped = wrapped:gsub ("[%+<>]", replacement_colors)
+	wrapped = wrapped:gsub ("([^\n])\n([^\n])", "%1 %2")
+	wrapped = wrapped:gsub ("|r\n\n", "|r\n")
+	wrapped = wrapped:gsub ("Ouroboros", "|cffa335ee<Ouroboros>|r")
+	wrapped = wrapped:gsub ("%*%(", "<") :gsub("%)%*", ">")
+	addon.helptext[funkykey] = wrapped
+end
+end -- do scope
+todo = nil
+
+
+-- Don't bother recording any of this loot:
+addon.default_itemfilter = {
+	[29434]		= true, -- Badge of Justice
+	[40752]		= true, -- Emblem of Heroism
+	[40753]		= true, -- Emblem of Valor
+	[45624]		= true, -- Emblem of Conquest
+	-- could probably remove the above now
+	[43228]		= true, -- Stone Keeper's Shard
+	[47241]		= true, -- Emblem of Triumph
+	[49426]		= true, -- Emblem of Frost
+}
+
+-- vim:noet