Quantcast

Alts : new built-in WIP

urnati [04-12-26 - 00:07]
Alts : new built-in WIP
Filename
Titan/Titan.lua
Titan/TitanUtils.lua
Titan/_ATitanDoc.lua
Titan/_TitanIDE.lua
TitanAlts/Artwork/Alliance.blp
TitanAlts/Artwork/Alts.blp
TitanAlts/Artwork/Horde.blp
TitanAlts/Artwork/Neutral.blp
TitanAlts/Artwork/TitanPanelPushpinIn.tga
TitanAlts/Artwork/TitanPanelPushpinOut.tga
TitanAlts/TitanAlts.lua
TitanAlts/TitanAlts.toc
diff --git a/Titan/Titan.lua b/Titan/Titan.lua
index 763b8d3..00df403 100644
--- a/Titan/Titan.lua
+++ b/Titan/Titan.lua
@@ -222,8 +222,11 @@ local function RegisterForEvents()
 	_G[TITAN_PANEL_CONTROL]:RegisterEvent("ZONE_CHANGED_NEW_AREA");

 	-- 2026 Apr Add more info for Alts
-	_G[TITAN_PANEL_CONTROL]:RegisterEvent("PLAYER_MONEY");
-	_G[TITAN_PANEL_CONTROL]:RegisterEvent("PLAYER_EQUIPMENT_CHANGED");
+	_G[TITAN_PANEL_CONTROL]:RegisterEvent("PLAYER_MONEY")
+	_G[TITAN_PANEL_CONTROL]:RegisterEvent("PLAYER_EQUIPMENT_CHANGED")
+	_G[TITAN_PANEL_CONTROL]:RegisterEvent("TIME_PLAYED_MSG")
+	_G[TITAN_PANEL_CONTROL]:RegisterEvent("PLAYER_LEVEL_UP")
+	_G[TITAN_PANEL_CONTROL]:RegisterEvent("PLAYER_XP_UPDATE")
 end

 --------------------------------------------------------------
@@ -262,6 +265,81 @@ local function RegisterAddonCompartment()
 	end
 end

+--****** overload the 'time played' text to Chat - if XP requested the API call
+local requesting
+---@diagnostic disable: duplicate-set-field
+
+-- Save orignal output to Chat
+-- somewhere in 11.* (The World Within) this changed
+local orig_ChatFrame_DisplayTimePlayed = function(...) end
+
+-- Do not output Chat messages when using RequestTimePlayed
+function TitanPanelBarButton:RequestTimePlayed()
+	requesting = true
+	RequestTimePlayed()
+end
+
+if Titan_Global.switch.chat_class then
+	orig_ChatFrame_DisplayTimePlayed = ChatFrameUtil.DisplayTimePlayed
+
+	ChatFrameUtil.DisplayTimePlayed = function(...) --TimePlayed(...)
+		if requesting then
+			-- XP requested time played, do not spam Chat
+			requesting = false
+		else
+			-- XP did not request time played so output
+			---@diagnostic disable-next-line: need-check-nil
+			orig_ChatFrame_DisplayTimePlayed(...)
+		end
+	end
+else
+	orig_ChatFrame_DisplayTimePlayed = ChatFrame_DisplayTimePlayed
+
+	ChatFrameUtil.DisplayTimePlayed = function(...) --TimePlayed(...)
+		if requesting then
+			-- XP requested time played, do not spam Chat
+			requesting = false
+		else
+			-- XP did not request time played so output
+			---@diagnostic disable-next-line: need-check-nil
+			orig_ChatFrame_DisplayTimePlayed(...)
+		end
+	end
+end
+--****** overload end
+
+local function SetToonZoneInfo()
+	local toon_info = TitanSettings.Players[TitanSettings.Player].Info ---@class CharInfo
+	toon_info.zoneText = GetZoneText()
+	toon_info.subZoneText = GetSubZoneText() or ""
+end
+
+local function GetElapsed(time_ago)
+	return (_G.time() - time_ago)
+end
+
+--local Collect info for time played
+local function SetToonPlayedInfo(action, total, level)
+	local toon_info = TitanSettings.Players[TitanSettings.Player].Info ---@class CharInfo
+
+	if action == 'init' then
+		TitanPanelBarButton:RequestTimePlayed() -- TIME_PLAYED_MSG
+		toon_info.played_start = _G.time()
+	elseif action == 'update' then -- via event TIME_PLAYED_MSG
+		toon_info.played_total = total + GetElapsed(toon_info.played_start)
+		toon_info.played_this_level = level + GetElapsed(toon_info.played_start)
+	elseif action == 'logout' then --
+		toon_info.played_total = toon_info.played_total + GetElapsed(toon_info.played_start)
+		toon_info.played_this_level = toon_info.played_this_level + GetElapsed(toon_info.played_start)
+	elseif action == 'level' then
+		toon_info.played_start = _G.time()
+		toon_info.played_total = toon_info.played_total + GetElapsed(toon_info.played_start)
+		toon_info.played_this_level = GetElapsed(toon_info.played_start)
+	else
+		-- !?
+	end
+end
+
 local function SetToonInfo(toon)
 	-- New Dec 2025 Collect some toon info for profile display
 	-- Unlikely to change on reload but...
@@ -302,23 +380,30 @@ local function SetToonInfo(toon)
 	toon_info.gold_toon = GetMoney() -- NO Warband

 	local avgItemLevel, avgItemLevelEquipped, avgItemLevelPvp = GetAverageItemLevel()
-	toon_info.itemLevelAve = avgItemLevel -- using ony equp change event, this may not be accurate...
+	toon_info.itemLevelAve = avgItemLevel -- using only equp change event, this may not be accurate...
 	toon_info.itemLevelEquipped = avgItemLevelEquipped -- this is the one we are tracking
 	toon_info.itemLevelPvp = avgItemLevelPvp
+
+	SetToonPlayedInfo('init')
+
+	toon_info.unit_xp = UnitXP("player")
+	toon_info.unit_xp_max = UnitXPMax("player")
 end

 local function SetToonLogout(toon)
 	-- New Dec 2025 Collect some toon info for profile display
 	-- Unlikely to change on reload but...
 	local toon_info = TitanSettings.Players[toon].Info
-	local unit = "player"

-	toon_info.zoneText = GetZoneText()
-	toon_info.subZoneText = GetSubZoneText() or ""
+	toon_info.gold_toon = GetMoney()

 	local now = _G.time()
 	toon_info.logout = now
 	toon_info.logoutStr = TitanUtils_GetDateText(now, true)
+
+	-- Zone and subzone set via events
+
+	SetToonPlayedInfo('logout')
 end

 local function SetPluginsAndConfig()
@@ -548,6 +633,7 @@ function TitanPanelBarButton:PLAYER_ENTERING_WORLD(arg1, arg2, arg3, arg4)

 	Titan_Debug.Out('titan', 'p_e_w', "Titan init processing done")
 end
+
 ---Titan Handle CVAR_UPDATE React to user changed WoW options.
 function TitanPanelBarButton:CVAR_UPDATE(cvarname, cvarvalue)
 	if cvarname == "USE_UISCALE"
@@ -587,16 +673,19 @@ end
 ---Titan Handle ZONE_CHANGED_INDOORS Hide Titan top bars if user requested to hide Top bar(s) in BG or arena
 function TitanPanelBarButton:ZONE_CHANGED()
 	TitanPanelBarButton_DisplayBarsWanted("ZONE_CHANGED")
+	SetToonZoneInfo()
 end

 ---Titan Handle ZONE_CHANGED_INDOORS Hide Titan top bars if user requested to hide Top bar(s) in BG or arena
 function TitanPanelBarButton:ZONE_CHANGED_INDOORS()
 	TitanPanelBarButton_DisplayBarsWanted("ZONE_CHANGED_INDOORS")
+	SetToonZoneInfo()
 end

 ---Titan Handle ZONE_CHANGED_INDOORS Hide Titan top bars if user requested to hide Top bar(s) in BG or arena
 function TitanPanelBarButton:ZONE_CHANGED_NEW_AREA()
 	TitanPanelBarButton_DisplayBarsWanted("ZONE_CHANGED_NEW_AREA")
+	SetToonZoneInfo()
 end

 ---Titan Handle PET_BATTLE_CLOSE Hide Titan bars during pet battle.
@@ -632,14 +721,14 @@ function TitanPanelBarButton:PLAYER_REGEN_DISABLED()
 	TitanPanelBarButton_DisplayBarsWanted("PLAYER_REGEN_DISABLED")
 end

----Titan Handle ZONE_CHANGED_INDOORS Hide Titan top bars if user requested to hide Top bar(s) in BG or arena
+---Titan Store in profile
 function TitanPanelBarButton:PLAYER_MONEY()
 	local toon_info = TitanSettings.Players[TitanSettings.Player].Info
 	-- 2026 Mar Add more info for Alts
 	toon_info.gold_toon = GetMoney() -- NO Warband
 end

----Titan Handle ZONE_CHANGED_INDOORS Hide Titan top bars if user requested to hide Top bar(s) in BG or arena
+---Titan Store in profile
 function TitanPanelBarButton:PLAYER_EQUIPMENT_CHANGED()
 	local toon_info = TitanSettings.Players[TitanSettings.Player].Info
 	-- 2026 Mar Add more info for Alts
@@ -653,6 +742,26 @@ function TitanPanelBarButton:PLAYER_EQUIPMENT_CHANGED()

 end

+---Titan Handle TIME_PLAYED_MSG Total and this level for /played
+function TitanPanelBarButton:TIME_PLAYED_MSG(a1, a2, ...)
+	local toon_info = TitanSettings.Players[TitanSettings.Player].Info ---@class CharInfo
+	SetToonPlayedInfo('update', a1, a2)
+end
+
+---Titan Handle TIME_PLAYED_MSG Total and this level for /played
+function TitanPanelBarButton:PLAYER_LEVEL_UP(...)
+	local toon_info = TitanSettings.Players[TitanSettings.Player].Info ---@class CharInfo
+	SetToonPlayedInfo('level')
+end
+
+---Titan Store XP and Max in profile
+function TitanPanelBarButton:PLAYER_XP_UPDATE(...)
+	local toon_info = TitanSettings.Players[TitanSettings.Player].Info ---@class CharInfo
+	toon_info.unit_xp = UnitXP("player")
+	toon_info.unit_xp_max = UnitXPMax("player")
+end
+
+
 if Titan_Global.switch.can_edit_ui then
 	-- Do not need to adjust frames
 else
diff --git a/Titan/TitanUtils.lua b/Titan/TitanUtils.lua
index 2544029..f5f70c5 100644
--- a/Titan/TitanUtils.lua
+++ b/Titan/TitanUtils.lua
@@ -1678,7 +1678,7 @@ end
 ---@param profile string A player name to look up
 ---@param attrib string Specific data to look up
 ---@param create boolean If true and no data table, create an empty table
----@return string result is_custom | found | not_found | created
+---@return string result is_custom | found | not_found or created
 ---@return table? data nil or table of data found/created
 function TitanUtils_GetProfileInfo(profile, attrib, create)
 	local _, server, is_custom = TitanUtils_ParseName(profile)
diff --git a/Titan/_ATitanDoc.lua b/Titan/_ATitanDoc.lua
index 7220510..45a5866 100644
--- a/Titan/_ATitanDoc.lua
+++ b/Titan/_ATitanDoc.lua
@@ -51,6 +51,13 @@ Contains ...
 --]===]
 Each file has a terse description of its contents.

+--[===[ Var
+...
+--]===]
+These are critical tables and info used within Titan.
+Unless specifically marked otherwise, treat these as "---Titan".
+
+
 ---API
 These are routines Titan will keep stable.
 Changes to these varibles and routines will be broadcast to developers via Discord at a minimum.
@@ -58,12 +65,6 @@ Changes to these varibles and routines will be broadcast to developers via Disco
 ---Titan
 These are global routines Titan uses. These may change at any time per Titan needs and design.

---[===[ Var
-...
---]===]
-These are critical tables and info used within Titan.
-Unless specifically marked otherwise, treat these as "---Titan".
-
 ---local
 These are local routines that may change at any time.
 --]======]
@@ -146,6 +147,43 @@ We don’t recommend using an online source to convert one image format to anoth
 They have a tendency to add additional code or info to the image.
 --]======]

+--[======[ Titan Panel Profiles
+Titan has always run on profiles. In release 9.* this became more explicit to the user with Sync.
+Custom profiles have been available for some time before 9.* but are more usable with Sync and Sync All.
+
+On PEW, Titan determines the profile to use - See code flow below.
+This sets Titan the way the user wants; showing the selected bars and plugins.
+
+Also in 9.* character data such as gold and mail (Post) were moved into the profile.
+This reduced the data built-in plugins needed to store on each character.
+The moving of data changed Titan saved vars and processing.
+
+Every character has had a section in saved variables since release 1.0:
+TitanSettings.Players.<name>@<server>
+
+Under this .Info table was added in 9.* to hold character information.
+Some built-in plugins added a .<plugin> at the same level as .Info; such as .Gold for Gold
+The table holds data needed for the plugin such as a show / hide character option.
+
+The Saved Variables section below shows a sample saved variable structure.
+
+Consequences of this change are:
+1) Titan must ensure a .Info exists for each character profile.
+This is done in TitanVariables before Titan decides the profile.
+.Info can be an empty table for a character not logged into yet but it must exist.
+Dealing with non existent values before using is easier than seeing if a table exists before every reference.
+
+2) Plugins with their own section must ensure the entry exists for each character profile.
+This should be done as part of the OnShow to ensure Titan tables are whole.
+
+3) Titan and Plugins that need .Info must NOT assume values exist.
+This allows information to be built over time as the user logs into characters.
+Plugins should set default values as the table entry is created.
+
+Note that character profiles do not include custom profiles. One can not log into custom profiles.
+
+--]======]
+
 --[======[ Titan Addon code flow

 First step: ======= Starting WoW
@@ -353,59 +391,75 @@ TitanSettings = {
 	["Players"] = {
 		["Embic@Staghelm"] = {
 			["Panel"] = {
-				-- Holds all the Titan settings for this character
+				-- Holds all the Titan level settings for this character
+				}
+			["Info"] = {
+				-- Holds various information for this character, used by Titan and some built-in plugins
+				-- example :
+				["levelText"] = "80",
+				["itemLevelPvp"] = 137.75,
+				["class"] = "Demon Hunter",
+				["raceName"] = "NightElf",
+				["race"] = "Night Elf",
+				["played_start"] = 1775660178,
+				["factionName"] = "Alliance",
+				["level"] = 80,
+				["server"] = "Staghelm",
+				["played_total"] = 805746,
+				["gold_toon"] = 1086716295,
+				["itemLevelEquipped"] = 137.75,
+				["logout"] = 1775660193,
+				["itemLevelAve"] = 137.75,
+				["raceId"] = 4,
+				["faction"] = "Alliance",
+				["logoutStr"] = "2026-04-08 10:56",
+				["name"] = "Stormblade",
+				["className"] = "DEMONHUNTER",
+				["subZoneText"] = "The Bazaar",
+				["played_this_level"] = 128790,
+				["classId"] = 12,
+				["zoneText"] = "Silvermoon City",
+				}
+			[<plugin>] = {
+				-- Holds various information for this character, specific to THIS plugin
 				}
 			["BarVars"] = {
 				-- Holds all the Titan bar settings for this character
 				}
 			["Plugins"] = {
-				-- Each registered plugin will be here
-					["Starter"] = {
-						["notes"] = "Adds bag and free slot information to Titan Panel.\n",
-						["menuTextFunction"] = nil,
-						["id"] = "Starter",
-						["menuText"] = "Bag",
-						["iconWidth"] = 16,
-						["savedVariables"] = {
-							["ShowColoredText"] = 1,
-							["CustomLabel3Text"] = "",
-							["ShowIcon"] = 1,
-							["OpenBags"] = false,
-							["CustomLabel3TextShow"] = false,
-							["CustomLabelTextShow"] = false,
-							["CustomLabel4Text"] = "",
-							["CustomLabel2Text"] = "",
-							["OpenBagsClassic"] = "new_install",
-							["ShowLabelText"] = 1,
-							["CustomLabel4TextShow"] = false,
-							["CountProfBagSlots"] = false,
-							["ShowUsedSlots"] = 1,
-							["DisplayOnRightSide"] = false,
-							["ShowDetailedInfo"] = false,
-							["CustomLabel2TextShow"] = false,
-							["CustomLabelText"] = "",
-						},
-						["controlVariables"] = {
-							["DisplayOnRightSide"] = true,
-							["ShowColoredText"] = true,
-							["ShowIcon"] = true,
-							["ShowLabelText"] = true,
-						},
-						["version"] = "1.0.0",
-						["category"] = "Information",
-						["buttonTextFunction"] = nil ,
-						["tooltipTextFunction"] = nil ,
-						["icon"] = "Interface\\AddOns\\TitanPlugin\\Artwork\\TitanStarter",
-						["tooltipTitle"] = "Bags Info",
-					},
-				}
+				-- Each registered plugin saved vars will be here
+				-- Below is Location as an example
+				["Location"] = {
+					["CoordsLoc"] = "Bottom",
+					["ShowZoneText"] = 1,
+					["NumLabelsSeen"] = 2,
+					["ShowRealmText"] = false,
+					["ShowIcon"] = 1,
+					["ShowCoordsText"] = true,
+					["ShowSubZoneText"] = false,
+					["CustomLabelText"] = "",
+					["CustomLabel3TextShow"] = false,
+					["CustomLabelTextShow"] = false,
+					["ShowCoordsOnMap"] = true,
+					["CustomLabel4Text"] = "",
+					["ShowCursorOnMap"] = true,
+					["CoordsFormat"] = "(%.d, %.d)",
+					["ShowLabelText"] = 1,
+					["DisplayOnRightSide"] = false,
+					["ShowColoredText"] = 1,
+					["UpdateWorldmap"] = false,
+					["CustomLabel2Text"] = "",
+					["CustomLabel2TextShow"] = false,
+					["CoordsLabel"] = true,
+					["CustomLabel3Text"] = "",
+					["CustomLabel4TextShow"] = false,
+				},
 			["Adjust"] = {
 				-- Holds offsets for frames the user may adjust - Retail and Classic have different list of frames
 				}
 			["Register"] = {
-				-- Holds data as each plugin and LDB is attempted to be registered.
-				-- There may be helpful debug data here under your plugin name if the plugin is not shown as expected.
-				-- Titan > Configuration > Attempts shows some of this data, including errors.
+				-- Exists but empty - used to holds data as each plugin and LDB is attempted to be registered.
+				-- Titan > Configuration > Attempts shows some data, including errors.
 				}
 ...
 TitanAll = {
diff --git a/Titan/_TitanIDE.lua b/Titan/_TitanIDE.lua
index 2aaa335..ec4149a 100644
--- a/Titan/_TitanIDE.lua
+++ b/Titan/_TitanIDE.lua
@@ -198,7 +198,7 @@ C_Bank = {} -- 11.0.0 New Warbank - Hopefully WoW API extension will catch up so
 ---@field AddContextMenu function Titan ONLY to create root menu

 --====== New Dec 2025 Collect info on each toon for Profile Config
--- Also used by plugins Gold and Post
+-- Also used by plugins such as Gold and Post
 ---@class CharInfo
 ---@field name string
 ---@field server string
@@ -216,6 +216,11 @@ C_Bank = {} -- 11.0.0 New Warbank - Hopefully WoW API extension will catch up so
 ---@field itemLevelAve number
 ---@field itemLevelEquipped number
 ---@field itemLevelPvp number
+---@field played_start number
+---@field played_total number
+---@field played_this_level number
+---@field unit_xp number
+---@field unit_xp_max number

 --====== Profile output from Utils
 ---@class Get_Profile_Result
diff --git a/TitanAlts/Artwork/Alliance.blp b/TitanAlts/Artwork/Alliance.blp
new file mode 100755
index 0000000..b5508fc
Binary files /dev/null and b/TitanAlts/Artwork/Alliance.blp differ
diff --git a/TitanAlts/Artwork/Alts.blp b/TitanAlts/Artwork/Alts.blp
new file mode 100755
index 0000000..ceb493d
Binary files /dev/null and b/TitanAlts/Artwork/Alts.blp differ
diff --git a/TitanAlts/Artwork/Horde.blp b/TitanAlts/Artwork/Horde.blp
new file mode 100755
index 0000000..570ca67
Binary files /dev/null and b/TitanAlts/Artwork/Horde.blp differ
diff --git a/TitanAlts/Artwork/Neutral.blp b/TitanAlts/Artwork/Neutral.blp
new file mode 100755
index 0000000..8ccbffd
Binary files /dev/null and b/TitanAlts/Artwork/Neutral.blp differ
diff --git a/TitanAlts/Artwork/TitanPanelPushpinIn.tga b/TitanAlts/Artwork/TitanPanelPushpinIn.tga
new file mode 100755
index 0000000..406ee98
Binary files /dev/null and b/TitanAlts/Artwork/TitanPanelPushpinIn.tga differ
diff --git a/TitanAlts/Artwork/TitanPanelPushpinOut.tga b/TitanAlts/Artwork/TitanPanelPushpinOut.tga
new file mode 100755
index 0000000..d3c892b
Binary files /dev/null and b/TitanAlts/Artwork/TitanPanelPushpinOut.tga differ
diff --git a/TitanAlts/TitanAlts.lua b/TitanAlts/TitanAlts.lua
new file mode 100755
index 0000000..12fc2be
--- /dev/null
+++ b/TitanAlts/TitanAlts.lua
@@ -0,0 +1,1219 @@
+---@diagnostic disable: duplicate-set-field
+--[[
+-- **************************************************************************
+-- * TitanUI.lua
+-- *
+-- * By: The Titan Panel Development Team
+-- **************************************************************************
+--]]
+-- ******************************** Constants *******************************
+local add_on = ...
+local _G = _G --getfenv(0);
+local L = LibStub("AceLocale-3.0"):GetLocale(TITAN_ID, true)
+
+local QTip = LibStub("LibQTip-1.0")
+local iconProvider, cellPrototype, baseCellPrototype = QTip:CreateCellProvider(QTip.LabelProvider)
+-- Required for Create - just call the base init
+function cellPrototype:InitializeCell()
+	baseCellPrototype.InitializeCell(self);
+end
+
+-- Required for Create - override the base Setup to use an icon
+function cellPrototype:SetupCell(tooltip, value, justification, font)
+	local _, height = baseCellPrototype.SetupCell(self, tooltip, format("|T%s:0|t", tostring(value)), "CENTER");
+	return baseCellPrototype.SetupCell(self, tooltip,
+		format("|T%s:%2$d:%2$d:0:0:64:64:4:60:4:60|t", tostring(value), height), "CENTER");
+end
+
+local UNK = TitanUtils_GetGrayText("-")
+--]]
+
+local AceConfigDialog = LibStub("AceConfigDialog-3.0")
+
+local artwork_path = "Interface\\AddOns\\TitanAlts\\Artwork\\"
+local TITAN_PLUGIN = "Alts"
+local TITLE = "Alts"
+local TITAN_BUTTON = "TitanPanel" .. TITAN_PLUGIN .. "Button"
+local VERSION = C_AddOns.GetAddOnMetadata(add_on, "Version")
+local MAX_COLS = 10
+
+local Alts = {} -- namespace for Alts routines as needed
+
+-- ******************************** IDE *******************************
+---@class AltInfo -- To store desired info for display
+---@field name_titan string
+---@field server string
+---@field class string
+---@field className string
+---@field classId number
+---@field faction string
+---@field factionName string
+---@field level number
+---@field levelText string
+---@field race string
+---@field raceName string
+---@field raceId number
+---@field gold_toon string
+---@field itemLevelAve number
+---@field itemLevelEquipped number
+---@field itemLevelPvp number
+---@field zoneText string
+---@field subZoneText string
+---@field logout string
+---@field xp_now number -- UnitXP("player") and UnitXPMax("player"); GetXPExhaustion()
+---@field xp_max number
+---@field xp_per string
+---@field xp_rest number -- ?? GetRestState() for XP multiplier
+---@field prof_1 string -- GetProfessions() then GetProfessionInfo(index)
+---@field prof_2 string
+---@field played_total number -- RequestTimePlayed() >> TIME_PLAYED_MSG
+---@field sync_titan string -- may implement
+---@field sync_global boolean -- may implement
+
+-- ******************************** Variables *******************************
+local trace = false -- true / false    Make true when debug output is needed.
+local alts_tt = {}  -- Holds alt data for tooltip display; gen once except for logged in toon
+local alts_tt_sort_col = "" -- one sort to rule them all...
+local alts_tt_sort_ascend = true -- default on click; click again to flip
+local tt_frame = {} -- tooltip on the QTip :)
+
+Titan_Debug.alts = {}
+Titan_Debug.alts.tool_tips = false
+
+-- ******************************** Routines *******************************
+
+--Helper routines
+
+local function CloseQTooltip(self, force)
+	if self.qtip:Acquire("TitanAlts_Tooltip", MAX_COLS) then --(not self.qtooltip) then
+		if (force) then
+			self.qtooltip.locked = false;
+		end
+		if (self.qtooltip.locked) then
+			-- leave tooltip alone
+		else
+			self.qtooltip:SetScale(1);
+			self.qtip:Release(self.qtooltip);
+			--        table.wipe(alts_tooltip);
+		end
+	else
+		return;
+	end
+end
+
+local function GetTimeParts(seconds_value)
+	local s = seconds_value
+	local years = 0
+	local days = 0
+	local hours = 0
+	local minutes = 0
+	local seconds = 0
+	if not s or (s < 0) then
+		seconds = -1
+	else
+		years = floor(s / 365 / 24 / 60 / 60); s = mod(s,  365 * 24 * 60 * 60);
+		days = floor(s / 24 / 60 / 60); s = mod(s, 24 * 60 * 60);
+		hours = floor(s / 60 / 60); s = mod(s, 60 * 60);
+		minutes = floor(s / 60); s = mod(s, 60);
+		seconds = s;
+	end
+
+	return years, days, hours, minutes, seconds
+end
+
+local function GetAbbrTimeText(seconds_value)
+	local timeText = "";
+	local years, days, hours, minutes, seconds = GetTimeParts(seconds_value)
+	if seconds == nil
+	or seconds == 0
+	then
+		timeText = UNK
+	else
+		if (years > 0) then
+			timeText = timeText .. format("%d" .. "y" .. " ", years);
+		end
+		if (days > 0) then
+			timeText = timeText .. format("%d" .. L["TITAN_PANEL_DAYS_ABBR"] .. " ", days);
+		end
+		if (hours > 0) then
+			timeText = timeText .. format("%d" .. L["TITAN_PANEL_HOURS_ABBR"] .. " ", hours);
+		elseif (hours == 0 and minutes > 0) then
+			timeText = timeText .. format("%d" .. L["TITAN_PANEL_HOURS_ABBR"] .. " ", hours);
+		end
+--[[
+		if (days ~= 0 or hours ~= 0 or minutes ~= 0) then
+			timeText = timeText .. format("%d" .. L["TITAN_PANEL_MINUTES_ABBR"] .. " ", minutes);
+		end
+--		timeText = timeText .. format("%d" .. L["TITAN_PANEL_SECONDS_ABBR"], seconds)
+--]]
+	end
+	return timeText;
+end
+
+---local Helper to set both the parent and the position of Tooltip for the plugin tooltip.
+---@param parent table Reference to the frame to attach the tooltip to
+---@param anchorPoint string Tooltip anchor location (side or corner) to use
+---@param relativeToFrame string name name of the frame, usually the plugin), to attach the tooltip to
+---@param relativePoint string Parent anchor location (side or corner) to use
+---@param xOffset number X offset
+---@param yOffset number Y offset
+---@param frame table Tooltip frame
+---@param custom boolean If custom / not tooltip frame
+local function SetOwnerPosition(parent, anchorPoint, relativeToFrame, relativePoint, xOffset, yOffset, frame, custom)
+	-- Changes for 9.1.5 Removed the background template from the Tooltip
+	-- Making changes to it difficult and possibly changing the tooltip globally.
+
+	if custom then
+		-- do NOT set owner - it clears the contents!
+	else
+		frame:SetOwner(parent, "ANCHOR_NONE")
+	end
+
+	frame:SetPoint(anchorPoint, relativeToFrame, relativePoint, xOffset, yOffset);
+
+	-- set font size for the Game Tooltip
+	if TitanPanelGetVar("DisableTooltipFont") then
+		-- use UI scale
+	else
+		if TitanTooltipScaleSet < 1 then
+			TitanTooltipOrigScale = frame:GetScale();
+			TitanTooltipScaleSet = TitanTooltipScaleSet + 1;
+		end
+		frame:SetScale(TitanPanelGetVar("TooltipFont"));
+	end
+
+	local dbg_msg = "_SetOwner _pos"
+		.. " '" .. tostring(frame:GetName()) .. "'"
+		.. " " .. tostring(frame:IsShown()) .. ""
+		.. " @ '" .. tostring(relativeToFrame) .. "'"
+		.. " " .. tostring(_G[relativeToFrame]:IsShown()) .. ""
+	Titan_Debug.Out('alts', 'tool_tips', dbg_msg)
+	dbg_msg = ">>_pos"
+		.. " " .. tostring(anchorPoint) .. ""
+		.. " " .. tostring(relativePoint) .. ""
+		.. " w" .. tostring(format("%0.1f", frame:GetWidth())) .. ""
+		.. " h" .. tostring(format("%0.1f", frame:GetHeight())) .. ""
+	Titan_Debug.Out('alts', 'tool_tips', dbg_msg)
+end
+
+---local Helper to set the screen position of the tooltip frame
+---@param self table Plugin frame
+---@param id string Plugin id name
+---@param frame table Tooltip frame to use
+---@param custom? boolean If custom / not tooltip frame
+local function SetPanelTooltip(self, id, frame, custom)
+	local is_custom = custom or false
+	local button = _G[id]
+
+	if button then
+		-- Adjust the Y offset as needed
+		local top = self:GetTop()
+		local hgt = frame:GetHeight()
+		local lft = self:GetLeft()
+
+		local rel_y = top - hgt
+		local pt = ""
+		local rel_pt = ""
+		if rel_y > 0 then
+			pt = "TOP";
+			rel_pt = "BOTTOM";
+		else
+			-- too close to bottom of screen
+			pt = "BOTTOM";
+			rel_pt = "TOP";
+		end
+		local rel_x = lft + hgt
+		if (rel_x < GetScreenWidth()) then
+			-- menu will fit
+			pt = pt .. "LEFT";
+			rel_pt = rel_pt .. "LEFT";
+		else
+			pt = pt .. "RIGHT";
+			rel_pt = rel_pt .. "RIGHT";
+		end
+
+		SetOwnerPosition(button, pt, button:GetName(), rel_pt, 0, 0, frame, is_custom)
+	end
+end
+
+local function ClassColors(class, str)
+	local res = str
+	local use = TitanGetVar(TITAN_PLUGIN, "use_class_colors")
+	if use then
+		--	local colors = (CUSTOM_CLASS_COLORS or RAID_CLASS_COLORS)[class]
+		local colors = RAID_CLASS_COLORS[class]
+		if colors then
+			local r = string.format("%02x", colors.r * 255)
+			local g = string.format("%02x", colors.g * 255)
+			local b = string.format("%02x", colors.b * 255)
+			local coloredText = "|cff" .. r .. g .. b .. str .. "|r"
+			res = coloredText
+		end
+	else
+		-- untouched
+	end
+	return res
+end
+
+local function SetVal(value)
+	local res = ""
+	if value then
+		res = value
+	else
+		res = UNK
+	end
+	return res
+end
+
+local function Get_gold(money)
+	local res = ""
+	if money == nil
+		or money == 0 then
+		res = UNK
+	else
+		res = TitanUtils_CashToString(money, ",", ".", true, false, true, true)
+	end
+
+	return res
+end
+
+local function Get_ilvl(level)
+	local res = ""
+	if level == nil
+		or level == 0 then
+		res = UNK
+	else
+		res = string.format("%.0f", level)
+	end
+
+	return res
+end
+
+local function Get_lvl(level)
+	local res = ""
+	if level == nil
+		or level == 0 then
+		res = UNK
+	else
+		res = string.format("%.0f", level)
+	end
+
+	return res
+end
+
+local function GetSync(toon)
+	local profile = TitanVariables_GetProfile(toon)
+	local res = ""
+	local global = false
+
+	-- Create string after Profile name in header
+	if profile.ptype == Titan_Global.profile.GLOBAL then
+		res = profile.cname
+		global = true
+	elseif profile.ptype == Titan_Global.profile.SYNC then
+		res = profile.cname
+	elseif profile.ptype == Titan_Global.profile.TOON then
+		res = Titan_Global.profile.NONE
+	else
+		res = UNK
+	end
+
+	return res, global
+end
+
+local function Cell_tt_show(self, text)
+	--    HideDetail();
+	local tt = tt_frame
+	tt:SetClampedToScreen(true)
+	tt:SetOwner(self, "ANCHOR_RIGHT", 10, -10)
+	SetPanelTooltip(self, self, tt, false)
+	tt:SetFrameLevel(self:GetFrameLevel() + 1)
+	tt:SetText(text)
+
+	-- for keeping tooltip up if cursor never goes over tooltip
+	-- via OnUpdate script
+	tt.parent_frame = self
+	tt:Show()
+end
+
+local function Cell_tt_hide(self, text, parent)
+	local tt = tt_frame
+	tt:Hide()
+end
+
+local function SortDB(self, tt_key)
+
+	if alts_tt_sort_col == tt_key then
+		alts_tt_sort_ascend = not alts_tt_sort_ascend -- flip the sort
+	else
+		alts_tt_sort_ascend = true
+	end
+	alts_tt_sort_col = tt_key
+
+	local sorter = nil
+	if alts_tt_sort_ascend then
+		sorter = function(a, b)
+			return a[tt_key] < b[tt_key]
+		end;
+	else
+		sorter = function (a, b)
+			return a[tt_key] > b[tt_key]
+		end;
+	end
+
+	for idx = 1, #alts_tt do
+		table.sort(alts_tt, sorter)
+	end
+
+	Alts.GenTooltip(_G[TITAN_BUTTON])
+end
+
+-- PLAYER_EQUIPMENT_CHANGED
+-- Grab the button text to display
+local function GetButtonText(id)
+	local avgItemLevel, avgItemLevelEquipped, avgItemLevelPvp = GetAverageItemLevel()
+	local strA, strB = TITLE, ""
+	local ave = format("%.2f", avgItemLevel)
+	local eq = format("%.0f", avgItemLevelEquipped)
+	local pvp = format("%.2f", avgItemLevelPvp)
+	return strA, strB, "ilvl : ", eq
+end
+
+-- Create the tooltip string
+local function GetTooltipText()
+	local res = ""
+	local rtn = "\n"
+	local tab = "\t"
+
+	local header = "Name" .. tab
+		.. "Realm" .. tab
+		.. "Class" .. tab
+		.. "Zone" .. tab
+		.. "XP" --.. tab
+		.. rtn
+	--		.. TitanUtils_GetHighlightText(GetRealmName()) .. rtn
+	print("alts"
+		.. " " .. tostring(header) .. ""
+	)
+	local now = _G.time()
+
+	local resets = "Resets in Server Time" .. rtn
+	local week_reset = C_DateAndTime.GetSecondsUntilWeeklyReset()
+	local weekly = TitanUtils_GetNormalText("Weekly :") .. tab
+		.. TitanUtils_GetHighlightText(TitanUtils_GetDateText(week_reset + now, true)) .. rtn
+
+	local day_reset = C_DateAndTime.GetSecondsUntilDailyReset()
+	local daily = TitanUtils_GetNormalText("Daily :") .. tab
+		.. TitanUtils_GetHighlightText(TitanUtils_GetDateText(day_reset + now, true)) .. rtn
+
+	local hints = ""
+	--		.. TitanUtils_GetGreenText("Left Click: Reloads the User Interface") .. rtn
+	--		.. TitanUtils_GetGreenText("Right Click: For Shortcuts and Debug Tools") .. rtn
+
+	res = res
+		.. header
+		.. rtn
+		.. resets .. daily .. weekly
+		.. rtn
+		.. hints
+
+	return res
+end
+
+-- Routine per column...
+local headerFont = {}
+local header_justify = "CENTER"
+
+local function GenFaction(self, row, col, action, tt_info, toon_info)
+	local next = nil
+	if action == 'header' then
+		row, next = self.qtooltip:SetCell(row,
+			col,
+			artwork_path .. "TitanPanelPushpin" .. (self.qtooltip.locked and "In" or "Out"), -- value
+			iconProvider                                                               --,
+		)
+		self.qtooltip:SetCellScript(row, col, "OnEnter", Cell_tt_show, (self.qtooltip.locked and UNLOCK or LOCK) );
+		self.qtooltip:SetCellScript(row, col, "OnLeave", Cell_tt_hide);
+		--    self.qtooltip:SetCellScript(row, col, "OnMouseDown", OnLockClick);
+	elseif action == 'row' then
+		if toon_info.faction == "Alliance" then
+			row, next = self.qtooltip:SetCell(row, col, artwork_path .. "Alliance",	iconProvider)
+		elseif toon_info.faction == "Horde" then
+			row, next = self.qtooltip:SetCell(row, col, artwork_path .. "Horde", iconProvider)
+		else
+			-- just in case. Technically a faction must be chosen now but there could be OLD toons out there.
+			row, next = self.qtooltip:SetCell(row, col, "")
+		end
+	elseif action == 'total' then
+		row, next = self.qtooltip:SetCell(row, col, "")
+	else
+		-- !?
+	end
+
+	return row, next
+end
+
+local total_toons = 0
+local total_shown = 0
+local function GenToonName(self, row, col, action, tt_info, toon_info)
+	local next = nil
+	if action == 'header' then
+		row, next = self.qtooltip:SetCell(row, col, tt_info.title, headerFont, header_justify)
+		self.qtooltip:SetCellScript(row, col, "OnMouseDown", SortDB, tt_info.tt_key)
+		total_shown = 0
+	elseif action == 'row' then
+		row, next = self.qtooltip:SetCell(row, col, ClassColors(toon_info.className, toon_info.name_titan))
+		total_shown = total_shown + 1
+	elseif action == 'total' then
+		row, next = self.qtooltip:SetCell(row, col, tostring(total_shown).." / "..tostring(total_toons))
+	else
+		-- !?
+	end
+
+	return row, next
+end
+
+local function GenLevel(self, row, col, action, tt_info, toon_info)
+	local next = nil
+	if action == 'header' then
+		row, next = self.qtooltip:SetCell(row, col, tt_info.title, headerFont, header_justify);
+		self.qtooltip:SetCellScript(row, col, "OnMouseDown", SortDB, tt_info.tt_key)
+	elseif action == 'row' then
+			row, next = self.qtooltip:SetCell(row, col, SetVal(toon_info.levelText), "RIGHT")
+	elseif action == 'total' then
+		row, next = self.qtooltip:SetCell(row, col, "")
+	else
+		-- !?
+	end
+
+	return row, next
+end
+
+local function GenILevel(self, row, col, action, tt_info, toon_info)
+	local next = nil
+	if action == 'header' then
+		row, next = self.qtooltip:SetCell(row, col, tt_info.title, headerFont, header_justify);
+		self.qtooltip:SetCellScript(row, col, "OnMouseDown", SortDB, tt_info.tt_key)
+	elseif action == 'row' then
+		local str = ""
+		local tt_str = ""
+		if toon_info.itemLevelEquipped == nil
+			or toon_info.itemLevelEquipped == 0 then
+			str = UNK
+		else
+			str = string.format("%.0f", toon_info.itemLevelEquipped)
+			tt_str = "Equipped : "..PVP.." "..string.format("%.0f", toon_info.itemLevelPvp)
+		end
+		row, next = self.qtooltip:SetCell(row, col, str, "RIGHT")
+		if tt_str == "" then
+			-- nothing to show
+		else
+			self.qtooltip:SetCellScript(row, col, "OnEnter", Cell_tt_show, tt_str);
+			self.qtooltip:SetCellScript(row, col, "OnLeave", Cell_tt_hide);
+		end
+	elseif action == 'total' then
+		row, next = self.qtooltip:SetCell(row, col, "")
+	else
+		-- !?
+	end
+
+	return row, next
+end
+
+local function GenZone(self, row, col, action, tt_info, toon_info)
+	local next = nil
+	if action == 'header' then
+		row, next = self.qtooltip:SetCell(row, col, tt_info.title, headerFont, header_justify);
+	elseif action == 'row' then
+		row, next = self.qtooltip:SetCell(row, col, SetVal(toon_info.zoneText))
+		self.qtooltip:SetCellScript(row, col, "OnEnter", Cell_tt_show, (toon_info.subZoneText or "") );
+		self.qtooltip:SetCellScript(row, col, "OnLeave", Cell_tt_hide);
+	elseif action == 'total' then
+		row, next = self.qtooltip:SetCell(row, col, "")
+	else
+		-- !?
+	end
+
+	return row, next
+end
+
+local function GenLogout(self, row, col, action, tt_info, toon_info)
+	local next = nil
+	if action == 'header' then
+		row, next = self.qtooltip:SetCell(row, col, tt_info.title, headerFont, header_justify);
+	elseif action == 'row' then
+		local str = toon_info.logout
+		row, next = self.qtooltip:SetCell(row, col, str)
+	elseif action == 'total' then
+		row, next = self.qtooltip:SetCell(row, col, "")
+	else
+		-- !?
+	end
+
+	return row, next
+end
+
+local total_gold = 0
+local function GenMoney(self, row, col, action, tt_info, toon_info)
+	local next = nil
+	local str = ""
+	if action == 'header' then
+		row, next = self.qtooltip:SetCell(row, col, tt_info.title, headerFont, header_justify)
+		self.qtooltip:SetCellScript(row, col, "OnMouseDown", SortDB, tt_info.tt_key)
+		total_gold = 0
+	elseif action == 'row' then
+		str = ""
+		if toon_info.gold_toon == nil
+			or toon_info.gold_toon == 0 then
+			str = UNK
+		else
+			str = TitanUtils_CashToString(toon_info.gold_toon, ",", ".", true, false, true, true)
+			total_gold = total_gold + toon_info.gold_toon
+		end
+		row, next = self.qtooltip:SetCell(row, col, str, "RIGHT")
+	elseif action == 'total' then
+		str = TitanUtils_CashToString(total_gold, ",", ".", true, false, true, true)
+		row, next = self.qtooltip:SetCell(row, col, str)
+	else
+		-- !?
+	end
+
+	return row, next
+end
+
+local total_played = 0
+local function GenPlayed(self, row, col, action, tt_info, toon_info)
+	local next = nil
+	local str = ""
+	if action == 'header' then
+		row, next = self.qtooltip:SetCell(row, col, tt_info.title, headerFont, header_justify)
+		self.qtooltip:SetCellScript(row, col, "OnMouseDown", SortDB, tt_info.tt_key)
+		total_played = 0
+	elseif action == 'row' then
+		total_played = total_played + (toon_info.played_total or 0)
+		-- total time played
+		str = ""
+		if toon_info.played_total == nil
+			or toon_info.played_total == 0 then
+			str = UNK
+		else
+			str = GetAbbrTimeText(toon_info.played_total)
+		end
+		row, next = self.qtooltip:SetCell(row, col, str, "RIGHT")
+	elseif action == 'total' then
+		str = GetAbbrTimeText(total_played)
+		row, next = self.qtooltip:SetCell(row, col, str)
+	else
+		-- !?
+	end
+
+	return row, next
+end
+
+local function GenSync(self, row, col, action, tt_info, toon_info)
+	local next = nil
+	local str = ""
+	if action == 'header' then
+		row, next = self.qtooltip:SetCell(row, col, tt_info.title, headerFont, header_justify)
+	elseif action == 'row' then
+		-- total time played
+		str = ""
+		if toon_info.sync_titan == nil
+			or toon_info.sync_titan == 0 then
+			str = UNK
+		else
+			str = toon_info.sync_titan
+		end
+		row, next = self.qtooltip:SetCell(row, col, str)
+	elseif action == 'total' then
+		row, next = self.qtooltip:SetCell(row, col, "")
+	else
+		-- !?
+	end
+
+	return row, next
+end
+
+local max_level = 0
+local function GenXP(self, row, col, action, tt_info, toon_info)
+	local next = nil
+	local str = ""
+	if action == 'header' then
+		row, next = self.qtooltip:SetCell(row, col, tt_info.title, headerFont, header_justify)
+	elseif action == 'row' then
+		str = ""
+		local tt_str = ""
+		if toon_info.xp_now == nil
+			or toon_info.xp_max == nil
+			or toon_info.xp_now == 0
+			or toon_info.xp_max == 0 then
+			str = UNK
+			tt_str = "Log in"
+		elseif toon_info.level_toon == max_level then
+			str = UNK
+			tt_str = "Max level"
+		else
+			local per = (toon_info.xp_now / toon_info.xp_max) * 100
+			str = string.format("%d%%", per)
+			tt_str = toon_info.xp_now.. " / ".. toon_info.xp_max
+		end
+		row, next = self.qtooltip:SetCell(row, col, str)
+		self.qtooltip:SetCellScript(row, col, "OnEnter", Cell_tt_show, tt_str);
+		self.qtooltip:SetCellScript(row, col, "OnLeave", Cell_tt_hide);
+	elseif action == 'total' then
+		row, next = self.qtooltip:SetCell(row, col, "")
+	else
+		-- !?
+	end
+
+	return row, next
+end
+
+--[[
+This scheme may be overkill but is a data driven approach when columns are
+across header, rows, and total of the tooltip.
+
+tt_cols : holds the col name in the order they will be displayed
+tt_data : index are names in tt_col; holds info to fill a cell
+- config : if true place in config for Show / Hide
+- title : column title
+- tt_key : column field; used in sort if enabled; also saved var name for show / hide if config is true
+Used to allow loops and min 'hard coding' - only need to change in two places tt_data and OnLoad
+- cell_func expects (plugin(Alts), row, col, action, toon name, toon table/info) for SetCell
+returning row, next_col/nil
+A routine is needed for each col because each col has unique processing needs.
+
+In the GenTooltip there will be 3 loops : header, row, and total
+- cell_func must handle each action to ensure data aligns
+- Qtip will handle the size and spacing
+
+--]]
+
+local tt_cols = { -- set order of the cols, user can show / hide most
+	"faction",
+	"toon_name",
+	"level",
+	"item_level",
+	"zone",
+	"logout",
+	"money",
+	"played",
+	"profile",
+	"unit_xp",
+}
+local tt_data = { -- index MUST include all tt_cols values !!!
+	["faction"] = {
+		config = false,
+		tt_key = "faction",
+		sortable = false,
+		title = "",
+		cell_func = GenFaction,
+	},
+	["toon_name"] = {
+		config = false,
+		tt_key = "name_titan",
+		title = NAME,
+		cell_func = GenToonName,
+	},
+	["level"] = {
+		config = true,
+		tt_key = "level_toon",
+		title = LEVEL_ABBR,
+		cell_func = GenLevel,
+	},
+	["item_level"] = {
+		config = true,
+		tt_key = "itemLevelEquipped",
+		title = ITEM_LEVEL_ABBR,
+		cell_func = GenILevel,
+	},
+	["zone"] = {
+		config = true,
+		tt_key = "zone",
+		title = ZONE,
+		cell_func = GenZone,
+	},
+	["logout"] = {
+		config = true,
+		tt_key = "logout",
+		title = L["TITAN_PANEL_MENU_PROFILE_LOGOUT"],
+		cell_func = GenLogout,
+	},
+	["money"] = {
+		config = true,
+		tt_key = "gold_toon",
+		title = L["TITAN_GOLD_MENU_TEXT"],
+		cell_func = GenMoney,
+	},
+	["played"] = {
+		config = true,
+		tt_key = "played_total",
+		title = PLAYED,
+		cell_func = GenPlayed,
+	},
+	["profile"] = {
+		config = true,
+		tt_key = "sync_titan",
+		title = L["TITAN_PANEL_MENU_PROFILE_SYNC"],
+		cell_func = GenSync,
+	},
+	["unit_xp"] = {
+		config = true,
+		tt_key = "xp_now",
+		title = L["TITAN_XP_MENU_TEXT"],
+		cell_func = GenXP,
+	},
+}
+
+---Generate or wipe the tooltip DB
+---@param action string create | wipe
+local function GenDB(self, action)
+	self.qtooltip.locked = false;
+
+	wipe(alts_tt)
+	if action == 'wipe' then
+		-- already cleared, get out
+	else
+		for idx, pdata in TitanUtils_PlayerIter() do
+			local result = ""
+			local str = ""
+			local toon_info ---@class CharInfo
+			local alt_info = {} ---@class AltInfo
+
+			result, toon_info = TitanUtils_GetProfileInfo(idx, "Info", false)
+			if result == "is_custom" then
+				-- skip, can not log in
+			elseif toon_info then -- found BUT could be empty...
+				-- Need to copy so Titan Settings are not changed
+				alt_info[tt_data["faction"].tt_key] = toon_info.faction
+				alt_info[tt_data["toon_name"].tt_key] = idx
+				alt_info.className = toon_info.className --ClassColors(toon_info.className, toon_info.class)
+				alt_info[tt_data["level"].tt_key] = toon_info.level
+				alt_info.levelText = Get_lvl(toon_info.level)
+				alt_info[tt_data["zone"].tt_key] = toon_info.zoneText
+				alt_info.subZoneText = toon_info.subZoneText
+				alt_info[tt_data["logout"].tt_key] = TitanUtils_GetDateText(toon_info.logout, false)
+				alt_info[tt_data["money"].tt_key] = toon_info.gold_toon
+				alt_info[tt_data["item_level"].tt_key] = toon_info.itemLevelEquipped
+				alt_info.itemLevelPvp = toon_info.itemLevelPvp
+
+				alt_info[tt_data["played"].tt_key] = toon_info.played_total
+
+				alt_info[tt_data["profile"].tt_key], alt_info.sync_global = GetSync(idx)
+
+				alt_info[tt_data["unit_xp"].tt_key] = toon_info.unit_xp
+				alt_info.xp_max = toon_info.unit_xp_max
+				alt_info.xp_per = ""
+
+				-- Get Alts plugin info;
+				-- this will create .Alts table on a character profile, if it does not exist
+				local res, plugin_info = TitanUtils_GetProfileInfo(idx, "Alts", true)
+				if res == 'created' then
+					plugin_info.show = true -- default
+				else
+					-- use the value
+				end
+				local show_me = false
+				if plugin_info then -- sometimes IDE is a pain... :)
+					show_me = plugin_info.show
+				else
+					-- should never get here
+				end
+				alt_info.show = show_me
+
+
+				table.insert(alts_tt, alt_info)
+			end
+		end
+
+		-- Sort by name initially
+		-- cobbled from SortDB :)
+		local tt_key = "name_titan"
+		alts_tt_sort_col = tt_key
+		local sorter = function(a, b)
+			return a[tt_key] < b[tt_key]
+		end;
+
+		for idx = 1, #alts_tt do
+			table.sort(alts_tt, sorter)
+		end
+	end
+end
+
+function Alts.GenTooltip(self)
+	self.qtooltip:Clear()
+	self.qtooltip:SetScale(TitanPanelGetVar("Scale"));
+	self.qtooltip:SmartAnchorTo(self);
+	self.qtooltip.parent = self;
+
+	headerFont = self.qtooltip:GetHeaderFont()
+	headerFont:SetTextColor(NORMAL_FONT_COLOR:GetRGB())
+
+	-- This may not be ideal but it does collect the current toon...
+	GenDB(self, 'create')
+	local column
+	local row
+
+	-- To allow configurable cols, we need to check at each point we build a line
+	-- to keep data in the right cols.
+	-- Declare here to use local vars
+	local function CheckCol(col_name, action, toon_info)
+		local ok = false
+		if tt_data[col_name].config then
+			if TitanGetVar(TITAN_PLUGIN, tt_data[col_name].tt_key) then
+				ok = true
+			else
+				-- user does not want to show
+			end
+		else
+			ok = true -- required
+		end
+		if ok then
+			row, column = tt_data[col_name].cell_func(self, row, column, action, tt_data[col_name], toon_info)
+		else
+			-- skip this col
+		end
+	end
+
+	-- NOTE: The various QTip cell functions advance 'column' on success
+	-- NOTE: The various QTip row functions advance 'row' on success
+
+	-- Create header
+	row, column = self.qtooltip:AddHeader()
+	for idx = 1, #tt_cols do
+		CheckCol(tt_cols[idx], 'header', nil)
+	end
+
+	row, column = self.qtooltip:AddSeparator();
+
+	total_toons = 0
+	for idx = 1, #alts_tt do
+		local toon_info ---@class AltInfo
+		toon_info = alts_tt[idx]
+		local result, this_toon = TitanUtils_GetProfileInfo(toon_info.name_titan, "Alts", false)
+
+		if toon_info == nil then
+			-- IDE sanity check, should be filled by GenDB
+		elseif this_toon then
+			if this_toon.show == true then
+				row, column = self.qtooltip:AddLine()
+
+				for col_idx = 1, #tt_cols do
+					CheckCol(tt_cols[col_idx], 'row', toon_info)
+				end
+			else
+				-- user deselected
+			end
+			total_toons = total_toons + 1 -- count toward total
+		else
+			-- !?
+		end
+	end
+
+	row, column = self.qtooltip:AddSeparator()
+	row, column = self.qtooltip:AddLine()
+
+	local str = " "
+	for idx = 1, #tt_cols do
+		CheckCol(tt_cols[idx], 'total', nil)
+	end
+
+	self.qtooltip:UpdateScrolling(512);
+	self.qtooltip:Show();
+
+	if (self.qtooltip.locked) then
+		self.qtooltip:SetAutoHideDelay(nil);
+	else
+		self.qtooltip:SetAutoHideDelay(0.5, self)
+	end
+end
+
+local scroll_hgt = math.floor(GetScreenHeight() * .6) -- virtual height in pixels
+local function GeneratorFunction(owner, rootDescription)
+	local id = TITAN_PLUGIN
+	local root = rootDescription -- menu widget to start with
+
+	CloseQTooltip(_G[TITAN_BUTTON], true)
+
+	local opts_show = Titan_Menu.AddButton(root, SHOW)
+	do -- next level options
+		for idx = 1, #tt_cols do
+			local col_name = tt_cols[idx]
+
+			if tt_data[col_name].config then
+				Titan_Menu.AddSelector(opts_show, id, tt_data[col_name].title, tt_data[col_name].tt_key)
+			else
+				-- not user selectable to hide
+			end
+		end
+		Titan_Menu.AddDivider(opts_show)
+		Titan_Menu.AddSelector(opts_show, id, "Use Class Colors", "use_class_colors")
+	end
+
+	--		for idx, pdata in TitanUtils_PlayerIter() do
+	local opts_show_toons = Titan_Menu.AddButton(root, L["TITAN_PANEL_MENU_PROFILE_CHARS"])
+	do -- next level options
+		for idx, pdata in TitanUtils_PlayerIter() do
+			local result, toon_info = TitanUtils_GetProfileInfo(idx, "Alts", false)
+			if result == "is_custom" then
+				-- skip, can not log in
+			elseif toon_info then
+				Titan_Menu.AddSelectorGeneric(opts_show_toons, idx,
+					function(data)
+						return data.toon.show
+					end,
+					function(data)
+						data.toon.show = not data.toon.show
+					end,
+					{ toon = toon_info }
+				)
+			else
+				-- not user selectable to hide
+			end
+		end
+	end
+	Titan_Menu.SetScroll(opts_show_toons, scroll_hgt) -- in case menu height is larger than screen / window
+
+end
+
+local function OnEnter(self)
+	if self.qtip:IsAcquired("TitanAlts_Tooltip") then
+		-- tooltip is active
+	else
+		self.qtooltip = self.qtip:Acquire("TitanAlts_Tooltip", MAX_COLS);
+		Alts.GenTooltip(self)
+	end
+end
+
+local function OnLeave(self, force)
+	CloseQTooltip(self, force)
+end
+
+-- Create the .registry for Titan so it can register and place the plugin
+
+local function OnLoad(self)
+	local notes = ""
+		.. "Relevant info on toon and alts.\n"
+	--		.."- xxx.\n"
+	self.registry = {
+		id = TITAN_PLUGIN,
+		category = "Built-ins",
+		version = VERSION,
+		menuText = TITLE,
+		--		menuTextFunction = CreateMenu,
+		menuContextFunction = GeneratorFunction, -- NEW scheme
+		buttonTextFunction = GetButtonText,
+		tooltipTitle = TITLE,
+		--		tooltipTextFunction = GetTooltipText,
+		icon = artwork_path .. "Alts",
+		iconWidth = 16,
+		notes = notes,
+		controlVariables = {
+			ShowIcon = true,
+			ShowLabelText = true,
+			--			ShowColoredText = true,
+			DisplayOnRightSide = true,
+		},
+		savedVariables = {
+			ShowIcon = 1,
+			ShowLabelText = 1,
+			--			ShowColoredText = 1,
+			DisplayOnRightSide = false,
+			-- show / hide cols
+			--tt_key doubles as saved var
+			level_toon = true,
+			itemLevelEquipped = true,
+			zone = false,
+			logout = false,
+			gold_toon = true,
+			played_total = false,
+			profile = false,
+			xp_now = true,
+			-- end show / hide cols
+			use_class_colors = true,
+		}
+	}
+
+	self.qtip = QTip
+	self.qtipIconProvider = iconProvider
+	self.qtooltip = {}
+	self.qtooltip.locked = false
+
+end
+
+-- Parse and react to registered events
+local function OnEvent(self, event, a1, a2, ...)
+	if (event == "PLAYER_EQUIPMENT_CHANGED") then
+		TitanPanelButton_UpdateButton(TITAN_PLUGIN)
+	end
+
+	Titan_Debug.Out('gold', 'events', event)
+end
+
+-- Handle mouse clicks
+local function OnClick(self, button)
+	if trace then
+		TitanPluginDebug(TITAN_PLUGIN, "Titan Alts click"
+			.. " " .. tostring(button) .. ""
+		)
+	end
+	if (button == "LeftButton") then
+		--		C_UI.Reload() --ReloadUI()
+	end
+end
+
+local function OnShow(self)
+	self:RegisterEvent("PLAYER_EQUIPMENT_CHANGED")
+--	GenDB(self, 'create')
+	TitanPanelButton_UpdateButton(TITAN_PLUGIN)
+
+	max_level = GetMaxLevelForPlayerExpansion()
+end
+
+local function OnHide(self)
+	self:UnregisterEvent("PLAYER_EQUIPMENT_CHANGED")
+	GenDB(self, 'wipe')
+end
+
+-- ====== Create needed frames
+local function Create_Frames()
+	if _G[TITAN_BUTTON] then
+		return -- if already created
+	end
+
+	if trace then
+		TitanPluginDebug(TITAN_PLUGIN, "TS frames"
+			.. " '" .. tostring(TITAN_BUTTON) .. "'"
+		)
+	end
+
+	-- general container frame
+	local f = CreateFrame("Frame", nil, UIParent)
+	local window = CreateFrame("Button", TITAN_BUTTON, f, "TitanPanelComboTemplate")
+	window:SetFrameStrata("FULLSCREEN")
+	-- Using SetScript to set "OnLoad" does not work
+	--
+	-- This routine sets the guts of the plugin - the .registry
+	OnLoad(window);
+
+	window:SetScript("OnShow", function(self)
+		OnShow(self)
+		-- This routine ensures the plugin is put where the user requested it.
+		-- Titan saves the bar the plugin was on. It does not save the relative order.
+		TitanPanelButton_OnShow(self);
+	end)
+	window:SetScript("OnHide", function(self)
+		OnHide(self)
+	end)
+	window:SetScript("OnEvent", function(self, event, ...)
+		OnEvent(self, event, ...)
+	end)
+	window:SetScript("OnClick", function(self, button)
+		-- Typically this routine handles actions on left click
+		OnClick(self, button);
+		-- Typically this routine handles the menu creation on right click
+		TitanPanelButton_OnClick(self, button);
+	end)
+
+	window:SetScript("OnEnter", function(self)
+		OnEnter(self)
+	end)
+	window:SetScript("OnLeave", function(self)
+		--OnLeave(self)
+	end)
+
+	-- Create tooltip frame for this plugin
+	-- OnUpdate starts as soon as OnShow is done...
+	tt_frame = CreateFrame("GameTooltip", "TitanRepairTooltip", UIParent, "GameTooltipTemplate")
+	local tt_init_timeout = .5
+
+	tt_frame:SetScript("OnShow", function(self)
+		local time_out = TitanPanelGetVar("TooltipTimeout")
+		local dbg_msg = "OnShow"
+			.. " timeout: " .. tostring(time_out) .. ""
+			.. " USING:  " .. tostring(tt_init_timeout) .. ""
+			.. " isCounting: " .. tostring(self.isCounting) .. ""
+			.. " timer: " .. tostring(self.frameTimer) .. ""
+			.. " plugin: " .. tostring(self.registry_id) .. ""
+			.. " plugin_frame: " .. tostring(self.plugin_frame_str) .. ""
+		Titan_Debug.Out('alts', 'tool_tips', dbg_msg)
+
+		-- OnShow will start the OnUpdate.
+		-- If user enters plugin, the tooltip will show
+		-- BUT if the user never enters the tooltip, it will keep showing because
+		-- the OnLeave did not kick the timer.
+		TitanUtils_StartFrameCounting(self, tt_init_timeout)
+	end)
+	tt_frame:SetScript("OnEnter", function(self)
+		local time_out = TitanPanelGetVar("TooltipTimeout")
+
+		local dbg_msg = "OnEnter"
+			.. " timeout: " .. tostring(time_out) .. ""
+			.. " isCounting: " .. tostring(self.isCounting) .. ""
+			.. " timer: " .. tostring(self.frameTimer) .. ""
+		Titan_Debug.Out('alts', 'tool_tips', dbg_msg)
+
+		TitanUtils_StopFrameCounting(self)
+	end)
+	tt_frame:SetScript("OnLeave", function(self)
+		local time_out = TitanPanelGetVar("TooltipTimeout")
+
+		local dbg_msg = "OnLeave"
+			.. " timeout: " .. tostring(time_out) .. ""
+			.. " isCounting: " .. tostring(self.isCounting) .. ""
+			.. " timer: " .. tostring(self.frameTimer) .. ""
+		Titan_Debug.Out('alts', 'tool_tips', dbg_msg)
+
+		if time_out < 0.1 then
+			tt_frame:Hide() -- hide right away
+		else
+			TitanUtils_StartFrameCounting(self, time_out)
+		end
+	end)
+
+	local debug_over = false
+	local debug_over_new = false
+	tt_frame:SetScript("OnUpdate", function(self, elapsed)
+		local time_out = TitanPanelGetVar("TooltipTimeout")
+
+		--[[ -- Be VERY careful enabling this debug :)
+	local dbg_msg = "TT OnUpdate"
+		.. " timeout: " .. tostring(time_out) .. ""
+		.. " isCounting: " .. tostring(self.isCounting) .. ""
+		.. " timer: " .. tostring(self.frameTimer) .. ""
+	--Titan_Debug.Out('alts', 'tool_tips', dbg_msg)
+	if self.isCounting == nil then
+		Titan_Debug.Out('alts', 'tool_tips', dbg_msg)
+	elseif self.frameTimer <= 0.01 then
+		Titan_Debug.Out('alts', 'tool_tips', dbg_msg)
+	else
+	end
+	--]]
+
+		-- Compromise to keep tooltip open if the user stays over plugin
+		-- and does not mouse over tooltip frame (OnEnter)
+		local is_over = self.parent_frame:IsMouseOver()
+		if is_over then
+			TitanUtils_StopFrameCounting(self)
+			debug_over_new = true
+		else
+			TitanUtils_CheckFrameCounting(self, elapsed);
+			debug_over = false
+		end
+
+		--[[
+	if debug_over ~= debug_over_new then
+		debug_over = debug_over_new
+		local dbg_msg = "OnUpdate"
+			.. " over: " .. tostring(is_over) .. ""
+			.. " timeout: " .. tostring(time_out) .. ""
+			.. " isCounting: " .. tostring(self.isCounting) .. ""
+			.. " timer: " .. tostring(self.frameTimer) .. ""
+		Titan_Debug.Out('alts', 'tool_tips', dbg_msg)
+	else
+	end
+	--]]
+	end)
+end
+
+Create_Frames() -- do the work
diff --git a/TitanAlts/TitanAlts.toc b/TitanAlts/TitanAlts.toc
new file mode 100644
index 0000000..a9538dc
--- /dev/null
+++ b/TitanAlts/TitanAlts.toc
@@ -0,0 +1,12 @@
+## Interface: 120001, 110205, 50502, 20505, 11508
+## Title: Titan Panel [|cffeda55fAlts|r] |cff00aa009.1.5|r
+## Author: Titan Panel Dev Team
+## Version: 9.1.5
+## Notes: Adds Reload and select functions in a plugin
+## Author: Titan Panel Development Team
+## IconTexture: Interface\AddOns\TitanAlts\Artwork\Alts
+## SavedVariables:
+## Dependencies: Titan
+## DefaultState: Enabled
+
+TitanAlts.lua