Files
ImprovedGuildWindow_OC/ImprovedGuildWindow_OC.lua
T
2026-05-11 21:36:56 -07:00

4369 lines
168 KiB
Lua

-- Improved Guild Window for WoW 1.12.1 / Turtle WoW
-- Author: Travis
local IGW = {}
IGW.VERSION = "3.1"
-- Global function for keybind (must be defined early)
function ImprovedGuildWindow_Toggle()
if IGW and IGW.ToggleWindow then
IGW:ToggleWindow()
end
end
local frame
local rosterData = {}
local displayedMembers = {}
local sortColumn = "name"
local sortAscending = true
local filterText = ""
local filterRank = -1
local showOffline = true
local currentTab = "details"
-- Configuration: Officer rank threshold
-- rankIndex: 0=Guild Master, 1-9=other ranks
-- Members with rankIndex <= this value are considered "officers"
-- Default: 2 (includes Guild Master and top 2 officer ranks)
local OFFICER_RANK_THRESHOLD = 2
-- Timezone lookup table
local TIMEZONES = {
PST = "PST (UTC-8)",
PDT = "PDT (UTC-7)",
MST = "MST (UTC-7)",
MDT = "MDT (UTC-6)",
CST = "CST (UTC-6)",
CDT = "CDT (UTC-5)",
EST = "EST (UTC-5)",
EDT = "EDT (UTC-4)",
GMT = "GMT (UTC+0)",
UTC = "UTC (UTC+0)",
CET = "CET (UTC+1)",
EET = "EET (UTC+2)",
MSK = "MSK (UTC+3)",
IST = "IST (UTC+5:30)",
JST = "JST (UTC+9)",
AEST = "AEST (UTC+10)",
NZST = "NZST (UTC+12)"
}
-- Dungeon details database
local DUNGEON_DETAILS = {
["Ragefire Chasm"] = {
location = "Orgrimmar",
faction = "Horde",
description = "A volcanic chasm beneath Orgrimmar inhabited by troggs and cultists.",
bosses = "Taragaman the Hungerer, Jergosh the Invoker",
notable = "Cloth, Fire resist gear"
},
["The Deadmines"] = {
location = "Westfall",
faction = "Alliance",
description = "An abandoned mine overrun by the Defias Brotherhood.",
bosses = "Edwin VanCleef, Sneed, Mr. Smite",
notable = "Cruel Barb, Smite's Hammer"
},
["Wailing Caverns"] = {
location = "Northern Barrens",
faction = "Neutral",
description = "Twisted caverns corrupted by druids of the Fang.",
bosses = "Lady Anacondra, Lord Pythas, Mutanus",
notable = "Armor of the Fang set, Glowing Lizardscale Cloak"
},
["Stockades"] = {
location = "Stormwind City",
faction = "Alliance",
description = "The prison of Stormwind, now controlled by rioting prisoners.",
bosses = "Bazil Thredd, Dextren Ward",
notable = "Prison variety loot"
},
["Blackfathom Deeps"] = {
location = "Ashenvale",
faction = "Neutral",
description = "Sunken temple ruins inhabited by naga and corrupted creatures.",
bosses = "Gelihast, Lady Sarevess, Aku'mai",
notable = "Blackfathom Mana Oil, Strike of the Hydra"
},
["Dragonmaw Retreat"] = {
location = "Wetlands (Turtle WoW)",
faction = "Neutral",
description = "Former stronghold of the Dragonmaw clan.",
bosses = "Various Dragonmaw leaders",
notable = "Custom Turtle WoW dungeon"
},
["SM-Graveyard"] = {
location = "Tirisfal Glades",
faction = "Neutral",
description = "Scarlet Monastery Graveyard wing, haunted by the undead.",
bosses = "Interrogator Vishas, Bloodmage Thalnos",
notable = "Scarlet gear, good XP"
},
["Gnomeregan"] = {
location = "Dun Morogh",
faction = "Alliance",
description = "The fallen capital of the gnomes, irradiated and overrun by troggs.",
bosses = "Mekgineer Thermaplugg, Crowd Pummeler 9-60",
notable = "Electrocutioner Leg, Triprunner Dungarees"
},
["Crescent Grove"] = {
location = "Feralas (Turtle WoW)",
faction = "Neutral",
description = "Ancient druidic grove with moonkin and satyr.",
bosses = "Various nature guardians",
notable = "Custom Turtle WoW dungeon"
},
["SM-Library"] = {
location = "Tirisfal Glades",
faction = "Neutral",
description = "Scarlet Monastery Library wing filled with zealots and undead.",
bosses = "Arcanist Doan, Houndmaster Loksey",
notable = "Illusionary Rod, Hypnotic Blade"
},
["Razorfen Kraul"] = {
location = "Southern Barrens",
faction = "Neutral",
description = "Twisting bramble-filled tunnels home to quilboar.",
bosses = "Charlga Razorflank, Agathelos",
notable = "Corpsemaker, Razorfen Spaulders"
},
["Stormwrought Ruins"] = {
location = "Azshara (Turtle WoW)",
faction = "Neutral",
description = "Ancient naga ruins crackling with arcane energy.",
bosses = "Various naga champions",
notable = "Custom Turtle WoW dungeon"
},
["SM-Armory"] = {
location = "Tirisfal Glades",
faction = "Neutral",
description = "Scarlet Monastery Armory, training ground for Scarlet warriors.",
bosses = "Herod the Bully",
notable = "Ravager, Scarlet Leggings"
},
["SM-Cathedral"] = {
location = "Tirisfal Glades",
faction = "Neutral",
description = "Scarlet Monastery Cathedral, seat of the Scarlet High Command.",
bosses = "High Inquisitor Whitemane, Scarlet Commander Mograine",
notable = "Whitemane's Chapeau, Mograine's Might"
},
["Uldaman"] = {
location = "Badlands",
faction = "Neutral",
description = "Ancient titan vault filled with troggs, dwarves, and stone constructs.",
bosses = "Archaedas, Ironaya, Galgann Firehammer",
notable = "The Rockpounder, Archaedic Stone"
},
["Razorfen Downs"] = {
location = "Southern Barrens",
faction = "Neutral",
description = "Death-knight controlled quilboar city serving the Scourge.",
bosses = "Amnennar the Coldbringer, Tuten'kash",
notable = "Shoulder of the Fallen Crusader, Ebon Vise"
},
["Gilneas City"] = {
location = "Gilneas (Turtle WoW)",
faction = "Neutral",
description = "Fallen city overrun by worgen and dark forces.",
bosses = "Various worgen alphas",
notable = "Custom Turtle WoW dungeon"
},
["Maraudon"] = {
location = "Desolace",
faction = "Neutral",
description = "Massive underground cavern complex with corrupted earth elementals.",
bosses = "Princess Theradras, Celebras, Noxxion",
notable = "Blackstone Ring, Nature Resist gear"
},
["Zul'Farrak"] = {
location = "Tanaris",
faction = "Neutral",
description = "Ancient troll city buried in the desert sands.",
bosses = "Chief Ukorz Sandscalp, Gahz'rilla",
notable = "Sul'thraze, Lifeforce Dirk"
},
["Sunken Temple"] = {
location = "Swamp of Sorrows",
faction = "Neutral",
description = "Temple of Atal'Hakkar, sunken ruin of troll worship.",
bosses = "Avatar of Hakkar, Shade of Eranikus",
notable = "Drakestone, Eranikus' Fang"
},
["Blackrock Depths"] = {
location = "Blackrock Mountain",
faction = "Neutral",
description = "Massive dark iron dwarf city inside Blackrock Mountain.",
bosses = "Emperor Dagran Thaurissan, General Angerforge",
notable = "Hand of Justice, Ironfoe"
},
["Hateforge Quarry"] = {
location = "Blackrock Mountain (Turtle WoW)",
faction = "Neutral",
description = "Dark iron mining operation with enslaved workers.",
bosses = "Various dark iron overseers",
notable = "Custom Turtle WoW dungeon"
},
["Dire Maul West"] = {
location = "Feralas",
faction = "Neutral",
description = "Western gardens of the fallen night elf city.",
bosses = "Tendris Warpwood, Immol'thar, Prince Tortheldrin",
notable = "Warpwood Binding, Dreadmist Belt"
},
["Dire Maul East"] = {
location = "Feralas",
faction = "Neutral",
description = "Eastern section overrun by demons and imps.",
bosses = "Alzzin the Wildshaper, Isalien",
notable = "Felhide Cap, Energized Chestplate"
},
["LBRS"] = {
location = "Blackrock Mountain",
faction = "Neutral",
description = "Lower Blackrock Spire, home to the Dark Horde.",
bosses = "General Drakkisath, War Master Voone",
notable = "Truestrike Shoulders, Dal'Rend's set"
},
["Dire Maul North"] = {
location = "Feralas",
faction = "Neutral",
description = "Northern wing featuring tribute runs and King Gordok.",
bosses = "King Gordok, Cho'Rush the Observer",
notable = "Tribute buffs, Ogre Tannin"
},
["Scholomance"] = {
location = "Western Plaguelands",
faction = "Neutral",
description = "Former school of necromancy, now Scourge stronghold.",
bosses = "Darkmaster Gandling, Ras Frostwhisper",
notable = "Deathbone set, Necromancy books"
},
["Stratholme"] = {
location = "Eastern Plaguelands",
faction = "Neutral",
description = "Plagued city divided between Scourge and Scarlet forces.",
bosses = "Baron Rivendare, Ramstein, Balnazzar",
notable = "Deathcharger's Reins, Runeblade of Baron Rivendare"
},
["UBRS"] = {
location = "Blackrock Mountain",
faction = "Neutral",
description = "Upper Blackrock Spire, lair of General Drakkisath.",
bosses = "General Drakkisath, Rend Blackhand",
notable = "Tier 0 upgrades, Dalrend's Sacred Charge"
},
["Stormwind Vault"] = {
location = "Stormwind City (Turtle WoW)",
faction = "Alliance",
description = "The royal treasury, infiltrated by thieves.",
bosses = "Various elite robbers",
notable = "Custom Turtle WoW dungeon"
},
["Karazhan Crypt"] = {
location = "Deadwind Pass (Turtle WoW)",
faction = "Neutral",
description = "Crypts beneath Karazhan filled with spirits and undead.",
bosses = "Various undead guardians",
notable = "Custom Turtle WoW dungeon"
}
}
-- Dungeon loading screen textures
local DUNGEON_TEXTURES = {
["Ragefire Chasm"] = "Interface\\Glues\\LoadingScreens\\LoadScreenRFC",
["The Deadmines"] = "Interface\\Glues\\LoadingScreens\\LoadScreenDeadmines",
["Wailing Caverns"] = "Interface\\Glues\\LoadingScreens\\LoadScreenWailingCaverns",
["Stockades"] = "Interface\\Glues\\LoadingScreens\\LoadScreenStockades",
["Blackfathom Deeps"] = "Interface\\Glues\\LoadingScreens\\LoadScreenBlackfathomDeeps",
["Gnomeregan"] = "Interface\\Glues\\LoadingScreens\\LoadScreenGnomeregan",
["Razorfen Kraul"] = "Interface\\Glues\\LoadingScreens\\LoadScreenRazorfenKraul",
["SM-Graveyard"] = "Interface\\Glues\\LoadingScreens\\LoadScreenScarletMonastery",
["SM-Library"] = "Interface\\Glues\\LoadingScreens\\LoadScreenScarletMonastery",
["SM-Armory"] = "Interface\\Glues\\LoadingScreens\\LoadScreenScarletMonastery",
["SM-Cathedral"] = "Interface\\Glues\\LoadingScreens\\LoadScreenScarletMonastery",
["Uldaman"] = "Interface\\Glues\\LoadingScreens\\LoadScreenUldaman",
["Razorfen Downs"] = "Interface\\Glues\\LoadingScreens\\LoadScreenRazorfenDowns",
["Maraudon"] = "Interface\\Glues\\LoadingScreens\\LoadScreenMaraudon",
["Zul'Farrak"] = "Interface\\Glues\\LoadingScreens\\LoadScreenZulFarrak",
["Sunken Temple"] = "Interface\\Glues\\LoadingScreens\\LoadScreenSunkenTemple",
["Blackrock Depths"] = "Interface\\Glues\\LoadingScreens\\LoadScreenBlackrockDepths",
["Dire Maul West"] = "Interface\\Glues\\LoadingScreens\\LoadScreenDireMaul",
["Dire Maul East"] = "Interface\\Glues\\LoadingScreens\\LoadScreenDireMaul",
["Dire Maul North"] = "Interface\\Glues\\LoadingScreens\\LoadScreenDireMaul",
["LBRS"] = "Interface\\Glues\\LoadingScreens\\LoadScreenBlackrockSpire",
["UBRS"] = "Interface\\Glues\\LoadingScreens\\LoadScreenBlackrockSpire",
["Scholomance"] = "Interface\\Glues\\LoadingScreens\\LoadScreenScholomance",
["Stratholme"] = "Interface\\Glues\\LoadingScreens\\LoadScreenStratholme"
}
-- Dungeon full display names
local DUNGEON_FULL_NAMES = {
["SM-Graveyard"] = "Scarlet Monastery - Graveyard",
["SM-Library"] = "Scarlet Monastery - Library",
["SM-Armory"] = "Scarlet Monastery - Armory",
["SM-Cathedral"] = "Scarlet Monastery - Cathedral",
["LBRS"] = "Lower Blackrock Spire",
["UBRS"] = "Upper Blackrock Spire"
}
-- Default loading screen for dungeons without custom backgrounds
local DEFAULT_LOADING_SCREEN = "Interface\\Glues\\LoadingScreens\\LoadScreenKalimdor"
-- Timezone to region mapping
local TIMEZONE_REGIONS = {
-- Americas
PST = "Americas",
PDT = "Americas",
MST = "Americas",
MDT = "Americas",
CST = "Americas",
CDT = "Americas",
EST = "Americas",
EDT = "Americas",
-- Oceania
AEST = "Oceania",
NZST = "Oceania",
-- Europe
GMT = "Europe",
UTC = "Europe",
CET = "Europe",
EET = "Europe",
-- Asia
MSK = "Asia",
IST = "Asia",
JST = "Asia"
}
-- Fixed background opacity (v1.2)
local IGW_BG_OPACITY = 0.95
-- Layout constants
local FILTER_ROW_TOP_Y = -50 -- where the first filter row starts
local FILTER_ROW_HEIGHT = 30 -- height of a single filter row
local FILTER_ROW_GAP = 5 -- spacing between filter rows
local FILTER_ROWS = 2 -- number of filter rows (second row reserved for future options)
local FILTER_EXTRA_SPACING = 0 -- extra space under filter rows (reserved for future options)
local HEADER_BASE_OFFSET_Y = 10 -- gap between filters and column headers
-- Row heights
local ROW_HEIGHT_DEFAULT = 20
local ROW_HEIGHT_DETAILS = 25 -- +5px only on Member Details tab
-- Visible rows
local VISIBLE_ROWS_GUILD = 13 -- Guild Members tab (show 1 fewer row)
local VISIBLE_ROWS_DETAILS = 11 -- Member Details tab
local function IGW_GetHeaderTopY()
return FILTER_ROW_TOP_Y
- (FILTER_ROW_HEIGHT * FILTER_ROWS)
- (FILTER_ROW_GAP * (FILTER_ROWS - 1))
- FILTER_EXTRA_SPACING
- HEADER_BASE_OFFSET_Y
end
-- Race detection from officer note (first character(s) followed by dash)
local function IGW_GetRace(officerNote)
if not officerNote or officerNote == "" then return "" end
-- Check for 2-character codes first (Ta-, He-, Go-, Hu-, NE-, Dw-, Un-)
local firstTwo = string.sub(officerNote, 1, 2)
local thirdChar = string.sub(officerNote, 3, 3)
if thirdChar == "-" then
if firstTwo == "Ta" then return "Tauren"
elseif firstTwo == "He" or firstTwo == "HE" then return "High Elf"
elseif firstTwo == "Go" then return "Goblin"
elseif firstTwo == "Hu" or firstTwo == "HU" then return "Human"
elseif firstTwo == "NE" then return "Night Elf"
elseif firstTwo == "Dw" or firstTwo == "DW" then return "Dwarf"
elseif firstTwo == "Un" or firstTwo == "UN" then return "Undead"
elseif firstTwo == "Gn" or firstTwo == "GN" then return "Gnome"
elseif firstTwo == "Or" or firstTwo == "OR" then return "Orc"
elseif firstTwo == "Tr" or firstTwo == "TR" then return "Troll"
end
end
-- Check for single char + dash (backwards compatibility)
local firstChar = string.sub(officerNote, 1, 1)
local secondChar = string.sub(officerNote, 2, 2)
if secondChar ~= "-" then
return "" -- No dash found, return empty
end
if firstChar == "N" then return "Night Elf"
elseif firstChar == "D" then return "Dwarf"
elseif firstChar == "H" then return "Human"
elseif firstChar == "T" then return "Troll"
elseif firstChar == "O" then return "Orc"
elseif firstChar == "G" then return "Gnome"
elseif firstChar == "U" then return "Undead"
else return ""
end
end
-- Faction based on race
local function IGW_GetFaction(race)
if race == "" then return "" end
if race == "Night Elf" or race == "Dwarf" or race == "Human" or race == "Gnome" or race == "High Elf" then
return "Alliance"
elseif race == "Troll" or race == "Tauren" or race == "Orc" or race == "Undead" or race == "Goblin" then
return "Horde"
else
return ""
end
end
-- Keybinding strings (shown in ESC > Key Bindings)
BINDING_HEADER_IMPROVEDGUILDWINDOW = "Improved Guild Window"
BINDING_NAME_IMPROVEDGUILDWINDOW_TOGGLE = "Toggle Window"
-- ESC-close support (WoW closes frames listed in UISpecialFrames before opening the game menu)
local function IGW_AddToSpecialFrames(frameName, toFront)
if not frameName or frameName == "" then return end
if not UISpecialFrames then UISpecialFrames = {} end
-- remove duplicates
for i = table.getn(UISpecialFrames), 1, -1 do
if UISpecialFrames[i] == frameName then
table.remove(UISpecialFrames, i)
end
end
if toFront then
table.insert(UISpecialFrames, 1, frameName)
else
table.insert(UISpecialFrames, frameName)
end
end
-- Class color lookup
local CLASS_COLORS = {
["Warrior"] = {r=0.78, g=0.61, b=0.43},
["Paladin"] = {r=0.96, g=0.55, b=0.73},
["Hunter"] = {r=0.67, g=0.83, b=0.45},
["Rogue"] = {r=1.0, g=0.96, b=0.41},
["Priest"] = {r=1.0, g=1.0, b=1.0},
["Shaman"] = {r=0.0, g=0.44, b=0.87},
["Mage"] = {r=0.41, g=0.8, b=0.94},
["Warlock"] = {r=0.58, g=0.51, b=0.79},
["Druid"] = {r=1.0, g=0.49, b=0.04}
}
-- Initialize saved variables
function IGW:OnLoad()
-- Note: At this point, SavedVariables are NOT yet loaded
-- We'll initialize them in OnPlayerLogin when they're available
self:RegisterEvents()
DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00Improved Guild Window loaded!|r")
DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00Set keybind in ESC > Key Bindings > Improved Guild Window|r")
DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00Or use /igw to open|r")
end
-- Initialize SavedVariables and create frame (called after PLAYER_LOGIN when SavedVariables are loaded)
function IGW:InitializeSavedVariables()
if not ImprovedGuildWindowDB then
ImprovedGuildWindowDB = {
position = {},
width = 650,
height = 500,
sortColumn = "name",
sortAscending = true,
showOffline = true,
opacity = 1.0,
bgColor = {r = 0.15, g = 0.15, b = 0.15} -- Default dark grey
}
end
-- Ensure bgColor exists (for existing saved variables)
if not ImprovedGuildWindowDB.bgColor then
ImprovedGuildWindowDB.bgColor = {r = 0.15, g = 0.15, b = 0.15}
end
-- Load saved opacity or use default
IGW_BG_OPACITY = ImprovedGuildWindowDB.opacity or 0.95
sortColumn = ImprovedGuildWindowDB.sortColumn or "name"
sortAscending = ImprovedGuildWindowDB.sortAscending
if sortAscending == nil then sortAscending = true end
-- If rememberSorting is off, reset to defaults
if ImprovedGuildWindowDB.rememberSorting == false then
sortColumn = "name"
sortAscending = true
end
showOffline = ImprovedGuildWindowDB.showOffline
if showOffline == nil then showOffline = true end
-- NOW create the main frame with SavedVariables loaded
self:CreateMainFrame()
end
-- Create the main window frame
function IGW:CreateMainFrame()
-- Ensure DB exists (should have been initialized by now)
if not ImprovedGuildWindowDB then
DEFAULT_CHAT_FRAME:AddMessage("|cFFFF0000ERROR: ImprovedGuildWindowDB not initialized!|r")
return
end
-- Don't create if already exists
if frame then
return
end
-- Main frame
frame = CreateFrame("Frame", "ImprovedGuildWindowFrame", UIParent)
IGW_AddToSpecialFrames("ImprovedGuildWindowFrame", false)
frame:SetWidth(ImprovedGuildWindowDB.width or 650)
frame:SetHeight(ImprovedGuildWindowDB.height or 500)
frame:SetFrameStrata("HIGH")
frame:SetFrameLevel(20)
frame:SetMovable(true)
frame:EnableMouse(true)
frame:SetClampedToScreen(true)
frame:Hide()
-- Background (border only, solid texture added separately)
frame:SetBackdrop({
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
edgeSize = 32,
insets = { left = 11, right = 12, top = 12, bottom = 11 }
})
frame:SetBackdropBorderColor(1, 1, 1, 1)
-- Create solid background texture (inside the border)
local bgTexture = frame:CreateTexture(nil, "BACKGROUND")
bgTexture:SetPoint("TOPLEFT", frame, "TOPLEFT", 11, -12)
bgTexture:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -12, 11)
-- Use saved color or default to dark grey
local bgColor = {r = 0.15, g = 0.15, b = 0.15}
if ImprovedGuildWindowDB and ImprovedGuildWindowDB.bgColor then
bgColor = ImprovedGuildWindowDB.bgColor
end
bgTexture:SetTexture(bgColor.r, bgColor.g, bgColor.b, IGW_BG_OPACITY)
frame.bgTexture = bgTexture
-- Title bar for dragging
local titleBar = CreateFrame("Frame", nil, frame)
titleBar:SetPoint("TOPLEFT", frame, "TOPLEFT", 12, -12)
titleBar:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -12, -12)
titleBar:SetHeight(30)
titleBar:SetFrameLevel(frame:GetFrameLevel() + 1)
titleBar:EnableMouse(true)
titleBar:RegisterForDrag("LeftButton")
titleBar:SetScript("OnDragStart", function()
frame:StartMoving()
end)
titleBar:SetScript("OnDragStop", function()
frame:StopMovingOrSizing()
IGW:SavePosition()
end)
-- Title text
local title = titleBar:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
title:SetPoint("CENTER", titleBar, "CENTER", 0, 0)
title:SetText(GetGuildInfo("player") or "Guild Window")
frame.titleText = title
-- Close button (with 5px padding)
local closeBtn = CreateFrame("Button", nil, frame, "UIPanelCloseButton")
closeBtn:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -10, -10)
closeBtn:SetFrameLevel(frame:GetFrameLevel() + 2)
closeBtn:SetScript("OnClick", function()
-- Save window states if remember is enabled
if ImprovedGuildWindowDB and ImprovedGuildWindowDB.rememberWindows then
ImprovedGuildWindowDB.windowStates = {
detailsOpen = IGW.detailsFrame and IGW.detailsFrame:IsVisible() or false,
infoOpen = IGW.infoFrame and IGW.infoFrame:IsVisible() or false
}
end
frame:Hide()
-- Close details window when main window closes
if IGW.detailsFrame then
IGW.detailsFrame:Hide()
end
if IGW.infoFrame then
IGW.infoFrame:Hide()
-- Un-highlight Guild Info button
frame.tab4:SetBackdropColor(0.2, 0.2, 0.2, 1)
frame.tab4Text:SetTextColor(0.7, 0.7, 0.7)
end
-- Close options window
if IGW.optionsFrame then
IGW.optionsFrame:Hide()
end
-- Close dungeon details dialog
if IGW.dungeonDialog then
IGW.dungeonDialog:Hide()
end
end)
-- Options button (standard button, anchored to left of close button)
local optionsBtn = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
optionsBtn:SetPoint("RIGHT", closeBtn, "LEFT", -2, 0) -- 2px gap from close button
optionsBtn:SetWidth(65)
optionsBtn:SetHeight(20)
optionsBtn:SetText("Options")
optionsBtn:SetFrameLevel(frame:GetFrameLevel() + 2)
optionsBtn:SetScript("OnClick", function()
IGW:ToggleOptionsWindow()
end)
-- Filter section
self:CreateFilterSection()
-- Column headers
self:CreateColumnHeaders()
-- Scroll frame for roster
self:CreateRosterScrollFrame()
-- Tabs
self:CreateTabs()
-- Member count - position above tabs, below roster
local memberCount = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
memberCount:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 20, 45)
memberCount:SetText("0 Online | 0 Total")
frame.memberCount = memberCount
-- Restore saved position or set default
if ImprovedGuildWindowDB.position and ImprovedGuildWindowDB.position.point then
frame:SetPoint(
ImprovedGuildWindowDB.position.point,
UIParent,
ImprovedGuildWindowDB.position.point,
ImprovedGuildWindowDB.position.x or 0,
ImprovedGuildWindowDB.position.y or 0
)
else
frame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
end
-- Initialize with Guild Members tab (details)
self:UpdateColumnHeaders("details")
end
-- Create filter section
function IGW:CreateFilterSection()
local filterFrame = CreateFrame("Frame", nil, frame)
filterFrame:SetPoint("TOPLEFT", frame, "TOPLEFT", 20, FILTER_ROW_TOP_Y)
filterFrame:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -20, FILTER_ROW_TOP_Y)
filterFrame:SetHeight(FILTER_ROW_HEIGHT)
-- Second filter row frame (Advanced Search row)
local filterFrame2 = CreateFrame("Frame", nil, frame)
filterFrame2:SetPoint("TOPLEFT", filterFrame, "BOTTOMLEFT", 0, -FILTER_ROW_GAP)
filterFrame2:SetPoint("TOPRIGHT", filterFrame, "BOTTOMRIGHT", 0, -FILTER_ROW_GAP)
filterFrame2:SetHeight(FILTER_ROW_HEIGHT)
-- Frame always visible, search boxes toggle
frame.filterFrame2 = filterFrame2
-- Advanced Search toggle button (on filterFrame2, left side)
local advSearchBtn = CreateFrame("Button", nil, filterFrame2, "UIPanelButtonTemplate")
advSearchBtn:SetPoint("LEFT", filterFrame2, "LEFT", 0, 0)
advSearchBtn:SetWidth(120)
advSearchBtn:SetHeight(22)
advSearchBtn:SetText("Advanced Search")
advSearchBtn:SetScript("OnClick", function()
if frame.advSearchNote:IsVisible() then
frame.advSearchNote:Hide()
frame.advSearchOfficerNote:Hide()
frame.advSearchNoteLabel:Hide()
frame.advSearchOfficerNoteLabel:Hide()
else
frame.advSearchNote:Show()
frame.advSearchOfficerNote:Show()
frame.advSearchNoteLabel:Show()
frame.advSearchOfficerNoteLabel:Show()
end
end)
frame.advSearchBtn = advSearchBtn
-- Advanced Search filters (name, note, officer note)
local filterWidth = 100
local filterGap = 15
-- Helper function to create search box
local function CreateAdvancedSearchBox(parent, label, xOffset, filterKey)
local labelText = parent:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
labelText:SetPoint("LEFT", parent, "LEFT", xOffset, 0)
labelText:SetText(label .. ":")
labelText:Hide() -- Hidden by default
local editBox = CreateFrame("EditBox", nil, parent)
editBox:SetPoint("LEFT", labelText, "RIGHT", 5, 0)
editBox:SetWidth(filterWidth)
editBox:SetHeight(22)
editBox:SetFontObject(GameFontNormal)
editBox:SetAutoFocus(false)
editBox:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true,
tileSize = 16,
edgeSize = 16,
insets = { left = 4, right = 4, top = 4, bottom = 4 }
})
editBox:SetBackdropColor(0, 0, 0, 0.8)
editBox:SetTextInsets(6, 6, 0, 0) -- Add left/right padding for text
editBox:SetScript("OnTextChanged", function()
local text = this:GetText()
if type(text) == "string" then
frame.advancedFilters[filterKey] = string.lower(text)
else
frame.advancedFilters[filterKey] = ""
end
IGW:UpdateRosterDisplay()
end)
editBox:SetScript("OnEscapePressed", function()
this:ClearFocus()
end)
editBox:Hide() -- Hidden by default
return editBox, labelText
end
-- Initialize advanced filters table
frame.advancedFilters = {
note = "",
officernote = "",
region = ""
}
-- Create search boxes (removed name field)
local xPos = 130 -- Start after button
local noteBox, noteLabel = CreateAdvancedSearchBox(filterFrame2, "Note", xPos, "note")
frame.advSearchNote = noteBox
frame.advSearchNoteLabel = noteLabel
xPos = xPos + 35 + filterWidth + filterGap
local officerNoteBox, officerNoteLabel = CreateAdvancedSearchBox(filterFrame2, "Officer Note", xPos, "officernote")
frame.advSearchOfficerNote = officerNoteBox
frame.advSearchOfficerNoteLabel = officerNoteLabel
xPos = xPos + 80 + filterWidth + filterGap
-- (Region dropdown moved to main filter bar)
-- Evenly space 4 filter elements across the filter bar
local filterBarWidth = 590 -- Match main window width
-- Search label and box (element 1)
local searchLabel = filterFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
searchLabel:SetPoint("LEFT", filterFrame, "LEFT", 0, 0)
searchLabel:SetText("Search:")
-- Search editbox
local searchBox = CreateFrame("EditBox", nil, filterFrame)
searchBox:SetPoint("LEFT", searchLabel, "RIGHT", 5, 0)
searchBox:SetWidth(120)
searchBox:SetHeight(22)
searchBox:SetFontObject(GameFontNormal)
searchBox:SetAutoFocus(false)
searchBox:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true,
tileSize = 16,
edgeSize = 16,
insets = { left = 4, right = 4, top = 4, bottom = 4 }
})
searchBox:SetBackdropColor(0, 0, 0, 0.8)
searchBox:SetTextInsets(6, 6, 0, 0) -- Add left/right padding for text
searchBox:SetScript("OnTextChanged", function()
local text = this:GetText()
if type(text) == "string" then
filterText = string.lower(text)
else
filterText = ""
end
IGW:UpdateRosterDisplay()
end)
searchBox:SetScript("OnEscapePressed", function()
this:ClearFocus()
end)
frame.searchBox = searchBox
-- Rank filter label (element 2 - positioned at 1/4 mark + 30px)
local rankLabel = filterFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
rankLabel:SetPoint("LEFT", filterFrame, "LEFT", filterBarWidth * 0.25 + 30, 0)
rankLabel:SetText("Rank:")
-- Rank dropdown
local rankDropdown = CreateFrame("Button", "IGW_RankDropdown", filterFrame, "UIDropDownMenuTemplate")
rankDropdown:SetPoint("LEFT", rankLabel, "RIGHT", -15, -2)
UIDropDownMenu_SetWidth(100, rankDropdown)
UIDropDownMenu_SetText("All Ranks", rankDropdown)
UIDropDownMenu_Initialize(rankDropdown, function()
local info = {}
info.text = "All Ranks"
info.value = -1
info.func = function()
filterRank = -1
UIDropDownMenu_SetText("All Ranks", rankDropdown)
IGW:UpdateRosterDisplay()
end
UIDropDownMenu_AddButton(info)
-- Add guild ranks
-- GuildControlGetRankName uses 1-based (1=GM, 2=next, etc)
-- GetGuildRosterInfo returns 0-based rankIndex (0=GM, 1=next, etc)
-- So we need to store value as i-1 to match GetGuildRosterInfo
for i = 1, GuildControlGetNumRanks() do
local rankName = GuildControlGetRankName(i)
local rankValue = i - 1 -- Convert to 0-based for matching
info = {}
info.text = rankName
info.value = rankValue
info.func = function()
filterRank = rankValue
UIDropDownMenu_SetText(rankName, rankDropdown)
IGW:UpdateRosterDisplay()
end
UIDropDownMenu_AddButton(info)
end
end)
-- Region dropdown (element 3 - positioned at 1/2 mark + 40px)
local regionLabel = filterFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
regionLabel:SetPoint("LEFT", filterFrame, "LEFT", filterBarWidth * 0.5 + 40, 2)
regionLabel:SetText("Region:")
local regionDropdown = CreateFrame("Frame", "IGW_RegionDropdown", filterFrame, "UIDropDownMenuTemplate")
regionDropdown:SetPoint("LEFT", regionLabel, "RIGHT", -15, -2)
UIDropDownMenu_SetWidth(90, regionDropdown)
UIDropDownMenu_SetText("All", regionDropdown)
frame.regionDropdown = regionDropdown
UIDropDownMenu_Initialize(regionDropdown, function()
local info = {}
-- All option
info.text = "All"
info.value = ""
info.func = function()
frame.advancedFilters.region = ""
UIDropDownMenu_SetText("All", regionDropdown)
IGW:UpdateRosterDisplay()
end
UIDropDownMenu_AddButton(info)
-- Americas
info = {}
info.text = "Americas"
info.value = "Americas"
info.func = function()
frame.advancedFilters.region = "Americas"
UIDropDownMenu_SetText("Americas", regionDropdown)
IGW:UpdateRosterDisplay()
end
UIDropDownMenu_AddButton(info)
-- Oceania
info = {}
info.text = "Oceania"
info.value = "Oceania"
info.func = function()
frame.advancedFilters.region = "Oceania"
UIDropDownMenu_SetText("Oceania", regionDropdown)
IGW:UpdateRosterDisplay()
end
UIDropDownMenu_AddButton(info)
-- Europe
info = {}
info.text = "Europe"
info.value = "Europe"
info.func = function()
frame.advancedFilters.region = "Europe"
UIDropDownMenu_SetText("Europe", regionDropdown)
IGW:UpdateRosterDisplay()
end
UIDropDownMenu_AddButton(info)
-- Asia
info = {}
info.text = "Asia"
info.value = "Asia"
info.func = function()
frame.advancedFilters.region = "Asia"
UIDropDownMenu_SetText("Asia", regionDropdown)
IGW:UpdateRosterDisplay()
end
UIDropDownMenu_AddButton(info)
end)
-- Refresh button (on advanced search row, aligned right)
local refreshBtn = CreateFrame("Button", nil, filterFrame2, "UIPanelButtonTemplate")
refreshBtn:SetPoint("RIGHT", filterFrame2, "RIGHT", 0, 0)
refreshBtn:SetWidth(100)
refreshBtn:SetHeight(22)
refreshBtn:SetText("Clear / Refresh")
refreshBtn:SetScript("OnClick", function()
-- Clear legacy search box
if frame.searchBox then
frame.searchBox:SetText("")
end
filterText = ""
-- Clear advanced search boxes
if frame.advSearchNote then
frame.advSearchNote:SetText("")
end
if frame.advSearchOfficerNote then
frame.advSearchOfficerNote:SetText("")
end
if frame.regionDropdown then
frame.advancedFilters.region = ""
UIDropDownMenu_SetText("All", frame.regionDropdown)
end
if frame.advancedFilters then
frame.advancedFilters.name = ""
frame.advancedFilters.note = ""
frame.advancedFilters.officernote = ""
end
-- Reset rank filter to "All Ranks"
filterRank = -1
if rankDropdown then
UIDropDownMenu_SetText("All Ranks", rankDropdown)
end
-- Reset sorting to default
sortColumn = "rank"
sortAscending = false
-- Update header arrows
if frame.headerButtons then
for _, btn in ipairs(frame.headerButtons) do
if btn.column == sortColumn then
btn.arrow:SetText(sortAscending and "^" or "v")
else
btn.arrow:SetText("")
end
end
end
-- Refresh guild roster
GuildRoster()
IGW:UpdateRosterDisplay()
end)
-- Show Offline checkbox (element 4 - positioned at 3/4 mark + 65px, aligned with search text)
local offlineCheck = CreateFrame("CheckButton", "IGW_OfflineCheck", filterFrame, "UICheckButtonTemplate")
offlineCheck:SetPoint("LEFT", filterFrame, "LEFT", filterBarWidth * 0.75 + 65, 0)
offlineCheck:SetWidth(24)
offlineCheck:SetHeight(24)
offlineCheck:SetChecked(showOffline)
local offlineLabel = offlineCheck:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
offlineLabel:SetPoint("LEFT", offlineCheck, "RIGHT", 0, 0)
offlineLabel:SetText("Show Offline")
offlineCheck:SetScript("OnClick", function()
showOffline = this:GetChecked() == 1
if ImprovedGuildWindowDB then
ImprovedGuildWindowDB.showOffline = showOffline
end
IGW:UpdateRosterDisplay()
end)
frame.offlineCheck = offlineCheck
end
-- Create column headers
function IGW:CreateColumnHeaders()
local headerFrame = CreateFrame("Frame", nil, frame)
local headerTopY = IGW_GetHeaderTopY()
headerFrame:SetPoint("TOPLEFT", frame, "TOPLEFT", 20, headerTopY)
headerFrame:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -40, headerTopY)
local headerTopY = IGW_GetHeaderTopY()
headerFrame:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -40, -90)
headerFrame:SetHeight(20)
frame.headerFrame = headerFrame
frame.headerButtons = {}
-- Initial headers will be set by UpdateColumnHeaders
end
-- Create roster scroll frame
function IGW:CreateRosterScrollFrame()
-- Create scroll frame with FauxScrollFrame
local scrollFrame = CreateFrame("ScrollFrame", "IGW_RosterScroll", frame, "FauxScrollFrameTemplate")
local headerTopY = IGW_GetHeaderTopY()
-- Scroll starts below the header row (20px header + ~5px breathing room)
scrollFrame:SetPoint("TOPLEFT", frame, "TOPLEFT", 20, headerTopY - 25)
scrollFrame:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -40, 50)
scrollFrame:SetFrameLevel(frame:GetFrameLevel() + 1)
-- Set up scroll behavior
scrollFrame:SetScript("OnVerticalScroll", function()
FauxScrollFrame_OnVerticalScroll(15, function() IGW:UpdateRosterDisplay() end)
end)
-- Create row frames as children of main frame
local rows = {}
for i = 1, 15 do
local row = CreateFrame("Button", nil, frame)
row:SetWidth(590)
row:SetHeight(ROW_HEIGHT_DEFAULT)
row:SetFrameLevel(frame:GetFrameLevel() + 2)
row:SetHighlightTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
-- Position relative to main frame, accounting for scroll frame position
row:SetPoint("TOPLEFT", frame, "TOPLEFT", 25, (headerTopY - 30) - (i-1) * ROW_HEIGHT_DEFAULT)
-- Name
local name = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
name:SetPoint("LEFT", row, "LEFT", 5, 0)
name:SetWidth(105)
name:SetJustifyH("LEFT")
row.name = name
-- Level
local level = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
level:SetPoint("LEFT", row, "LEFT", 115, 0)
level:SetWidth(35)
level:SetJustifyH("CENTER")
row.level = level
-- Class (reuse for rank in roster view)
local class = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
class:SetPoint("LEFT", row, "LEFT", 155, 0)
class:SetWidth(85)
class:SetJustifyH("LEFT")
row.class = class
-- Race
local race = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
race:SetPoint("LEFT", row, "LEFT", 245, 0)
race:SetWidth(75)
race:SetJustifyH("LEFT")
row.race = race
-- Faction
local faction = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
faction:SetPoint("LEFT", row, "LEFT", 325, 0)
faction:SetWidth(50)
faction:SetJustifyH("LEFT")
row.faction = faction
-- Rank (reuse for note in roster view) - shifted right
local rank = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
rank:SetPoint("LEFT", row, "LEFT", 380, 0)
rank:SetWidth(85)
rank:SetJustifyH("LEFT")
row.rank = rank
-- Note (reuse for officer note in roster view)
local note = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
note:SetPoint("LEFT", row, "LEFT", 355, 0)
note:SetWidth(105)
note:SetJustifyH("LEFT")
row.note = note
-- Officer Note (reuse for last online in roster view)
local officerNote = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
officerNote:SetPoint("LEFT", row, "LEFT", 465, 0)
officerNote:SetWidth(75)
officerNote:SetJustifyH("LEFT")
row.officerNote = officerNote
-- Click to show member details
row:SetScript("OnClick", function()
if row.memberIndex then
IGW:ShowMemberDetails(row.memberIndex)
end
end)
table.insert(rows, row)
end
frame.rosterRows = rows
frame.rosterScroll = scrollFrame
end
-- Create tabs at bottom of window
function IGW:CreateTabs()
local tabHeight = 25
-- All 4 tabs evenly distributed across bottom
local tabWidth = 120
local tabGap = 10
-- Calculate positions for 4 evenly spaced tabs
local windowWidth = 620 -- Main window width
local totalTabWidth = (tabWidth * 4)
local totalGapSpace = windowWidth - totalTabWidth
local gapSize = totalGapSpace / 5 -- 5 gaps: left, between tabs (3), right
-- Tab 4: Guild Info (far left, evenly spaced)
local tab4 = CreateFrame("Button", nil, frame)
tab4:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", gapSize, 15)
tab4:SetWidth(tabWidth)
tab4:SetHeight(tabHeight)
tab4:SetFrameLevel(frame:GetFrameLevel() + 1)
tab4:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true,
tileSize = 16,
edgeSize = 16,
insets = { left = 3, right = 3, top = 3, bottom = 3 }
})
tab4:SetBackdropColor(0.2, 0.2, 0.2, 1)
tab4:SetBackdropBorderColor(0.5, 0.5, 0.5, 1)
local tab4Text = tab4:CreateFontString(nil, "OVERLAY", "GameFontNormal")
tab4Text:SetPoint("CENTER", tab4, "CENTER", 0, 0)
tab4Text:SetText("|cFFFF0000<|r Guild Info") -- Red left arrow before text
tab4Text:SetTextColor(0.7, 0.7, 0.7)
tab4:SetScript("OnClick", function()
IGW:ToggleGuildInfoWindow()
end)
-- Tab 1: Guild Members
local tab1 = CreateFrame("Button", nil, frame)
tab1:SetPoint("LEFT", tab4, "RIGHT", gapSize, 0)
tab1:SetWidth(tabWidth)
tab1:SetHeight(tabHeight)
tab1:SetFrameLevel(frame:GetFrameLevel() + 1)
-- Background for tab
tab1:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true,
tileSize = 16,
edgeSize = 16,
insets = { left = 3, right = 3, top = 3, bottom = 3 }
})
tab1:SetBackdropColor(0.2, 0.2, 0.2, 1)
tab1:SetBackdropBorderColor(0.5, 0.5, 0.5, 1)
local tab1Text = tab1:CreateFontString(nil, "OVERLAY", "GameFontNormal")
tab1Text:SetPoint("CENTER", tab1, "CENTER", 0, 0)
tab1Text:SetText("Guild Members")
tab1:SetScript("OnClick", function()
IGW:SwitchTab("details")
end)
-- Tab 2: Notes & Rank
local tab2 = CreateFrame("Button", nil, frame)
tab2:SetPoint("LEFT", tab1, "RIGHT", gapSize, 0)
tab2:SetWidth(tabWidth)
tab2:SetHeight(tabHeight)
tab2:SetFrameLevel(frame:GetFrameLevel() + 1)
-- Background for tab
tab2:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true,
tileSize = 16,
edgeSize = 16,
insets = { left = 3, right = 3, top = 3, bottom = 3 }
})
tab2:SetBackdropColor(0.2, 0.2, 0.2, 1)
tab2:SetBackdropBorderColor(0.5, 0.5, 0.5, 1)
local tab2Text = tab2:CreateFontString(nil, "OVERLAY", "GameFontNormal")
tab2Text:SetPoint("CENTER", tab2, "CENTER", 0, 0)
tab2Text:SetText("Notes & Rank")
tab2:SetScript("OnClick", function()
IGW:SwitchTab("roster")
end)
-- Tab 3: Detailed View
local tab3 = CreateFrame("Button", nil, frame)
tab3:SetPoint("LEFT", tab2, "RIGHT", gapSize, 0)
tab3:SetWidth(tabWidth)
tab3:SetHeight(tabHeight)
tab3:SetFrameLevel(frame:GetFrameLevel() + 1)
-- Background for tab
tab3:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true,
tileSize = 16,
edgeSize = 16,
insets = { left = 3, right = 3, top = 3, bottom = 3 }
})
tab3:SetBackdropColor(0.2, 0.2, 0.2, 1)
tab3:SetBackdropBorderColor(0.5, 0.5, 0.5, 1)
local tab3Text = tab3:CreateFontString(nil, "OVERLAY", "GameFontNormal")
tab3Text:SetPoint("CENTER", tab3, "CENTER", 0, 0)
tab3Text:SetText("Detailed View")
tab3:SetScript("OnClick", function()
IGW:SwitchTab("detailed")
end)
frame.tab1 = tab1
frame.tab2 = tab2
frame.tab3 = tab3
frame.tab1Text = tab1Text
frame.tab2Text = tab2Text
frame.tab3Text = tab3Text
frame.tab4 = tab4
frame.tab4Text = tab4Text
end
-- Switch between tabs
function IGW:SwitchTab(tabName)
currentTab = tabName
if tabName == "details" then
-- Activate Guild Members tab (tab1)
frame.tab1:SetBackdropColor(0.5, 0.5, 0.5, 1)
frame.tab2:SetBackdropColor(0.2, 0.2, 0.2, 1)
frame.tab3:SetBackdropColor(0.2, 0.2, 0.2, 1)
frame.tab1Text:SetTextColor(1, 1, 1)
frame.tab2Text:SetTextColor(0.7, 0.7, 0.7)
frame.tab3Text:SetTextColor(0.7, 0.7, 0.7)
-- Set default sorting (only if not remembering)
if not ImprovedGuildWindowDB or ImprovedGuildWindowDB.rememberSorting == false then
sortColumn = "rank"
sortAscending = false -- Descending
end
-- Hide offline by default
showOffline = false
if frame.offlineCheck then
frame.offlineCheck:SetChecked(false)
end
-- Show details columns
IGW:UpdateColumnHeaders("details")
elseif tabName == "roster" then
-- Activate Notes & Rank tab (tab2)
frame.tab1:SetBackdropColor(0.2, 0.2, 0.2, 1)
frame.tab2:SetBackdropColor(0.5, 0.5, 0.5, 1)
frame.tab3:SetBackdropColor(0.2, 0.2, 0.2, 1)
frame.tab1Text:SetTextColor(0.7, 0.7, 0.7)
frame.tab2Text:SetTextColor(1, 1, 1)
frame.tab3Text:SetTextColor(0.7, 0.7, 0.7)
-- Set default sorting (only if not remembering)
if not ImprovedGuildWindowDB or ImprovedGuildWindowDB.rememberSorting == false then
sortColumn = "rank"
sortAscending = false -- Descending
end
-- Show offline on this tab (use saved preference)
showOffline = (ImprovedGuildWindowDB and ImprovedGuildWindowDB.showOffline ~= false)
if frame.offlineCheck then
frame.offlineCheck:SetChecked(showOffline)
end
-- Show roster columns
IGW:UpdateColumnHeaders("roster")
elseif tabName == "detailed" then
-- Activate Detailed View tab (tab3)
frame.tab1:SetBackdropColor(0.2, 0.2, 0.2, 1)
frame.tab2:SetBackdropColor(0.2, 0.2, 0.2, 1)
frame.tab3:SetBackdropColor(0.5, 0.5, 0.5, 1)
frame.tab1Text:SetTextColor(0.7, 0.7, 0.7)
frame.tab2Text:SetTextColor(0.7, 0.7, 0.7)
frame.tab3Text:SetTextColor(1, 1, 1)
-- Set default sorting (only if not remembering)
if not ImprovedGuildWindowDB or ImprovedGuildWindowDB.rememberSorting == false then
sortColumn = "name"
sortAscending = true
end
-- Show all members including offline
showOffline = true
if frame.offlineCheck then
frame.offlineCheck:SetChecked(true)
end
-- Show detailed view columns
IGW:UpdateColumnHeaders("detailed")
end
-- Update header arrows to show current sort state
if frame.headerButtons then
for _, btn in ipairs(frame.headerButtons) do
if btn.column == sortColumn then
btn.arrow:SetText(sortAscending and "^" or "v")
else
btn.arrow:SetText("")
end
end
end
IGW:UpdateRosterDisplay()
end
-- Update column headers based on tab
function IGW:UpdateColumnHeaders(tabName)
-- Hide all existing header buttons
if frame.headerButtons then
for _, btn in ipairs(frame.headerButtons) do
btn:Hide()
end
end
frame.headerButtons = {}
local headers
if tabName == "roster" then
headers = {
{text = "Name", width = 95, column = "name"},
{text = "Level", width = 55, column = "level"},
{text = "Rank", width = 80, column = "rank"},
{text = "Note", width = 140, column = "note"}, -- narrowed
{text = "Officer Note", width = 140, column = "officernote"}, -- narrowed
{text = "Last Online", width = 80, column = "lastonline"}
}
elseif tabName == "detailed" then
headers = {
{text = "Name", width = 140, column = "name"},
{text = "Level", width = 60, column = "level"},
{text = "Rank", width = 120, column = "rank"},
{text = "Date Joined", width = 110, column = "datejoined"},
{text = "Time Zone", width = 190, column = "timezone"}
}
else
headers = {
{text = "Name", width = 110, column = "name"},
{text = "Level", width = 60, column = "level"},
{text = "Class", width = 75, column = "class"},
{text = "Race", width = 75, column = "race"},
{text = "A/H", width = 50, column = "faction"},
{text = "Location", width = 145, column = "zone"},
{text = "Rank", width = 85, column = "rank"}
}
end
local headerFrame = frame.headerFrame
local xPos = 0
for _, header in ipairs(headers) do
local btn = CreateFrame("Button", nil, headerFrame)
btn:SetPoint("LEFT", headerFrame, "LEFT", xPos, 0)
btn:SetWidth(header.width)
btn:SetHeight(20)
local text = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
text:SetPoint("LEFT", btn, "LEFT", 5, 0)
text:SetPoint("RIGHT", btn, "RIGHT", -15, 0)
text:SetJustifyH("LEFT")
text:SetText(header.text)
-- Sort arrow
local arrow = btn:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
arrow:SetPoint("RIGHT", btn, "RIGHT", -5, 0)
arrow:SetJustifyH("RIGHT")
arrow:SetText("")
btn.arrow = arrow
btn.column = header.column
btn:SetScript("OnClick", function()
IGW:SortRoster(this.column)
end)
-- Highlight on hover
btn:SetScript("OnEnter", function()
text:SetTextColor(1, 1, 0)
end)
btn:SetScript("OnLeave", function()
text:SetTextColor(1, 1, 1)
end)
if header.column == sortColumn then
arrow:SetText(sortAscending and "^" or "v")
end
btn:Show()
xPos = xPos + header.width
table.insert(frame.headerButtons, btn)
end
-- Keep row columns aligned with header columns
self:ApplyRowLayout(tabName, headers)
end
-- Apply header-aligned column positions and widths to roster rows (keeps headers + data perfectly lined up)
function IGW:ApplyRowLayout(tabName, headers)
if not frame or not frame.rosterRows or not headers then return end
-- Build x positions for each header column
local xPos = 0
local colX = {}
local colW = {}
for _, h in ipairs(headers) do
colX[h.column] = xPos
colW[h.column] = h.width
xPos = xPos + h.width
end
for _, row in ipairs(frame.rosterRows) do
-- Ensure everything is left-justified for a clean grid
row.name:SetJustifyH("LEFT")
row.level:SetJustifyH("LEFT")
row.class:SetJustifyH("LEFT")
row.race:SetJustifyH("LEFT")
row.faction:SetJustifyH("LEFT")
row.rank:SetJustifyH("LEFT")
row.note:SetJustifyH("LEFT")
row.officerNote:SetJustifyH("LEFT")
if tabName == "roster" then
-- Member Details tab uses: name, level, (rank -> row.class), (note -> row.rank),
-- (officer note -> row.note), (last online -> row.officerNote)
row.name:ClearAllPoints()
row.name:SetPoint("LEFT", row, "LEFT", colX["name"] + 5, 0)
row.name:SetWidth(colW["name"] - 10)
row.level:ClearAllPoints()
row.level:SetPoint("LEFT", row, "LEFT", colX["level"] + 5, 0)
row.level:SetWidth(colW["level"] - 10)
row.class:ClearAllPoints()
row.class:SetPoint("LEFT", row, "LEFT", colX["rank"] + 5, 0)
row.class:SetWidth(colW["rank"] - 10)
row.rank:ClearAllPoints()
row.rank:SetPoint("LEFT", row, "LEFT", colX["note"] + 5, 0)
row.rank:SetWidth(colW["note"] - 10)
row.note:ClearAllPoints()
row.note:SetPoint("LEFT", row, "LEFT", colX["officernote"] + 5, 0)
row.note:SetWidth(colW["officernote"] - 10)
row.officerNote:ClearAllPoints()
row.officerNote:SetPoint("LEFT", row, "LEFT", colX["lastonline"] + 5, 0)
row.officerNote:SetWidth(colW["lastonline"] - 10)
elseif tabName == "detailed" then
-- Detailed View tab uses: name, level, (rank -> row.class), (datejoined -> row.rank),
-- (timezone -> row.note)
row.name:ClearAllPoints()
row.name:SetPoint("LEFT", row, "LEFT", colX["name"] + 5, 0)
row.name:SetWidth(colW["name"] - 10)
row.level:ClearAllPoints()
row.level:SetPoint("LEFT", row, "LEFT", colX["level"] + 5, 0)
row.level:SetWidth(colW["level"] - 10)
row.class:ClearAllPoints()
row.class:SetPoint("LEFT", row, "LEFT", colX["rank"] + 5, 0)
row.class:SetWidth(colW["rank"] - 10)
row.rank:ClearAllPoints()
row.rank:SetPoint("LEFT", row, "LEFT", colX["datejoined"] + 5, 0)
row.rank:SetWidth(colW["datejoined"] - 10)
row.note:ClearAllPoints()
row.note:SetPoint("LEFT", row, "LEFT", colX["timezone"] + 5, 0)
row.note:SetWidth(colW["timezone"] - 10)
-- Not used in this view
row.officerNote:SetText("")
row.race:SetText("")
row.faction:SetText("")
else
-- Guild Members tab uses: name, level, class, race, faction, (location -> row.rank), (rank -> row.note)
row.name:ClearAllPoints()
row.name:SetPoint("LEFT", row, "LEFT", colX["name"] + 5, 0)
row.name:SetWidth(colW["name"] - 10)
row.level:ClearAllPoints()
row.level:SetPoint("LEFT", row, "LEFT", colX["level"] + 5, 0)
row.level:SetWidth(colW["level"] - 10)
row.class:ClearAllPoints()
row.class:SetPoint("LEFT", row, "LEFT", colX["class"] + 5, 0)
row.class:SetWidth(colW["class"] - 10)
row.race:ClearAllPoints()
row.race:SetPoint("LEFT", row, "LEFT", colX["race"] + 5, 0)
row.race:SetWidth(colW["race"] - 10)
row.faction:ClearAllPoints()
row.faction:SetPoint("LEFT", row, "LEFT", colX["faction"] + 5, 0)
row.faction:SetWidth(colW["faction"] - 10)
row.rank:ClearAllPoints()
row.rank:SetPoint("LEFT", row, "LEFT", colX["zone"] + 5, 0)
row.rank:SetWidth(colW["zone"] - 10)
row.note:ClearAllPoints()
row.note:SetPoint("LEFT", row, "LEFT", colX["rank"] + 5, 0)
row.note:SetWidth(colW["rank"] - 10)
-- Not used in this view
row.officerNote:SetText("")
end
end
end
-- Show officer note edit dialog
function IGW:ShowOfficerNoteEdit(index, memberName)
-- Use default WoW dialog
SetGuildRosterSelection(index)
StaticPopup_Show("SET_GUILDOFFICERNOTE")
end
-- Show public note edit dialog
function IGW:ShowPublicNoteEdit(index, memberName)
-- Use default WoW dialog
SetGuildRosterSelection(index)
StaticPopup_Show("SET_GUILDPLAYERNOTE")
end
-- Show member details window
function IGW:ShowMemberDetails(index)
local member = rosterData[index]
if not member then return end
-- Create details frame if it doesn't exist
if not IGW.detailsFrame then
local df = CreateFrame("Frame", "IGW_DetailsFrame", UIParent)
IGW_AddToSpecialFrames("IGW_DetailsFrame", true)
df:SetWidth(250)
df:SetHeight(frame:GetHeight())
df:SetFrameStrata("HIGH")
df:SetFrameLevel(30)
-- Only allow moving if setting is enabled
df:SetMovable((ImprovedGuildWindowDB and ImprovedGuildWindowDB.allowMoveSideWindows) or false)
df:EnableMouse(true)
df:SetClampedToScreen(true)
df:Hide()
-- Background (border only, solid texture added separately)
df:SetBackdrop({
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
edgeSize = 32,
insets = { left = 11, right = 12, top = 12, bottom = 11 }
})
df:SetBackdropBorderColor(1, 1, 1, 1)
-- Create solid background texture (inside the border)
local dfBgTexture = df:CreateTexture(nil, "BACKGROUND")
dfBgTexture:SetPoint("TOPLEFT", df, "TOPLEFT", 11, -12)
dfBgTexture:SetPoint("BOTTOMRIGHT", df, "BOTTOMRIGHT", -12, 11)
-- Use saved color or default to dark grey
local bgColor = {r = 0.15, g = 0.15, b = 0.15}
if ImprovedGuildWindowDB and ImprovedGuildWindowDB.bgColor then
bgColor = ImprovedGuildWindowDB.bgColor
end
dfBgTexture:SetTexture(bgColor.r, bgColor.g, bgColor.b, IGW_BG_OPACITY)
df.bgTexture = dfBgTexture
-- Title bar
local titleBar = CreateFrame("Frame", nil, df)
titleBar:SetPoint("TOPLEFT", df, "TOPLEFT", 12, -12)
titleBar:SetPoint("TOPRIGHT", df, "TOPRIGHT", -12, -12)
titleBar:SetHeight(30)
titleBar:SetFrameLevel(df:GetFrameLevel() + 1)
titleBar:EnableMouse(true)
titleBar:RegisterForDrag("LeftButton")
titleBar:SetScript("OnDragStart", function()
if df:IsMovable() then
df:StartMoving()
end
end)
titleBar:SetScript("OnDragStop", function()
if df:IsMovable() then
df:StopMovingOrSizing()
end
end)
-- Title text
local title = titleBar:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
title:SetPoint("CENTER", titleBar, "CENTER", 0, 0)
title:SetText("Member Details")
df.title = title
-- Close button
local closeBtn = CreateFrame("Button", nil, df, "UIPanelCloseButton")
closeBtn:SetPoint("TOPRIGHT", df, "TOPRIGHT", -10, -10)
closeBtn:SetFrameLevel(df:GetFrameLevel() + 2)
closeBtn:SetScript("OnClick", function()
df:Hide()
end)
-- Content area
local yOffset = -50
local leftX = 20
-- Name
local nameLabel = df:CreateFontString(nil, "OVERLAY", "GameFontNormal")
nameLabel:SetPoint("TOPLEFT", df, "TOPLEFT", leftX, yOffset)
nameLabel:SetText("Name:")
local nameValue = df:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
nameValue:SetPoint("LEFT", nameLabel, "RIGHT", 10, 0)
nameValue:SetText("")
df.nameValue = nameValue
yOffset = yOffset - 20
-- Level & Class
local levelLabel = df:CreateFontString(nil, "OVERLAY", "GameFontNormal")
levelLabel:SetPoint("TOPLEFT", df, "TOPLEFT", leftX, yOffset)
levelLabel:SetText("Level:")
local levelValue = df:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
levelValue:SetPoint("LEFT", levelLabel, "RIGHT", 10, 0)
levelValue:SetText("")
df.levelValue = levelValue
local classValue = df:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
classValue:SetPoint("LEFT", levelValue, "RIGHT", 10, 0)
classValue:SetText("")
df.classValue = classValue
yOffset = yOffset - 20
-- Rank
local rankLabel = df:CreateFontString(nil, "OVERLAY", "GameFontNormal")
rankLabel:SetPoint("TOPLEFT", df, "TOPLEFT", leftX, yOffset)
rankLabel:SetText("Rank:")
local rankValue = df:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
rankValue:SetPoint("LEFT", rankLabel, "RIGHT", 10, 0)
rankValue:SetText("")
df.rankValue = rankValue
yOffset = yOffset - 20
-- Zone
local zoneLabel = df:CreateFontString(nil, "OVERLAY", "GameFontNormal")
zoneLabel:SetPoint("TOPLEFT", df, "TOPLEFT", leftX, yOffset)
zoneLabel:SetText("Zone:")
local zoneValue = df:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
zoneValue:SetPoint("LEFT", zoneLabel, "RIGHT", 10, 0)
zoneValue:SetWidth(150)
zoneValue:SetJustifyH("LEFT")
zoneValue:SetText("")
df.zoneValue = zoneValue
yOffset = yOffset - 20
-- Online status
local onlineLabel = df:CreateFontString(nil, "OVERLAY", "GameFontNormal")
onlineLabel:SetPoint("TOPLEFT", df, "TOPLEFT", leftX, yOffset)
onlineLabel:SetText("Status:")
local onlineValue = df:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
onlineValue:SetPoint("LEFT", onlineLabel, "RIGHT", 10, 0)
onlineValue:SetText("")
df.onlineValue = onlineValue
yOffset = yOffset - 20
-- Date Joined
local dateJoinedLabel = df:CreateFontString(nil, "OVERLAY", "GameFontNormal")
dateJoinedLabel:SetPoint("TOPLEFT", df, "TOPLEFT", leftX, yOffset)
dateJoinedLabel:SetText("Date Joined:")
local dateJoinedValue = df:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
dateJoinedValue:SetPoint("LEFT", dateJoinedLabel, "RIGHT", 10, 0)
dateJoinedValue:SetText("")
df.dateJoinedValue = dateJoinedValue
yOffset = yOffset - 20
-- Time Zone
local timeZoneLabel = df:CreateFontString(nil, "OVERLAY", "GameFontNormal")
timeZoneLabel:SetPoint("TOPLEFT", df, "TOPLEFT", leftX, yOffset)
timeZoneLabel:SetText("Time Zone:")
local timeZoneValue = df:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
timeZoneValue:SetPoint("LEFT", timeZoneLabel, "RIGHT", 10, 0)
timeZoneValue:SetText("")
df.timeZoneValue = timeZoneValue
-- Faction icon (top right, above divider)
local factionIcon = df:CreateTexture(nil, "ARTWORK")
factionIcon:SetWidth(48)
factionIcon:SetHeight(48)
factionIcon:SetPoint("TOPRIGHT", df, "TOPRIGHT", -12, -40)
factionIcon:Hide() -- Hidden by default
df.factionIcon = factionIcon
yOffset = yOffset - 30
-- Divider 1 (after status info)
local div1 = df:CreateTexture(nil, "ARTWORK")
div1:SetTexture("Interface\\DialogFrame\\UI-DialogBox-Divider")
div1:SetPoint("TOP", df, "TOP", 36, yOffset)
div1:SetWidth(300)
div1:SetHeight(16)
df.div1 = div1
yOffset = yOffset - 16
-- Public Note
local noteLabel = df:CreateFontString(nil, "OVERLAY", "GameFontNormal")
noteLabel:SetPoint("TOPLEFT", df, "TOPLEFT", leftX, yOffset)
noteLabel:SetText("Public Note:")
df.noteLabel = noteLabel
-- Make heading clickable
local noteLabelBtn = CreateFrame("Button", nil, df)
noteLabelBtn:SetPoint("TOPLEFT", noteLabel, "TOPLEFT", -2, 2)
noteLabelBtn:SetPoint("BOTTOMRIGHT", noteLabel, "BOTTOMRIGHT", 2, -2)
noteLabelBtn:SetFrameLevel(df:GetFrameLevel() + 1)
noteLabelBtn:SetScript("OnClick", function()
if df.memberIndex and (not CanEditPublicNote or CanEditPublicNote()) then
IGW:ShowPublicNoteEdit(df.memberIndex, df.memberName)
end
end)
yOffset = yOffset - 18
local noteValue = df:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
noteValue:SetPoint("TOPLEFT", df, "TOPLEFT", leftX, yOffset)
noteValue:SetWidth(210)
noteValue:SetJustifyH("LEFT")
noteValue:SetText("")
df.noteValue = noteValue
local noteValueBtn = CreateFrame("Button", nil, df)
noteValueBtn:SetPoint("TOPLEFT", df, "TOPLEFT", leftX - 2, yOffset + 2)
noteValueBtn:SetWidth(214)
noteValueBtn:SetHeight(40)
noteValueBtn:SetFrameLevel(df:GetFrameLevel() + 1)
noteValueBtn:SetScript("OnClick", function()
if df.memberIndex and (not CanEditPublicNote or CanEditPublicNote()) then
IGW:ShowPublicNoteEdit(df.memberIndex, df.memberName)
end
end)
yOffset = yOffset - 48
-- Officer Note (clickable label)
local officerNoteLabel = df:CreateFontString(nil, "OVERLAY", "GameFontNormal")
officerNoteLabel:SetPoint("TOPLEFT", df, "TOPLEFT", leftX, yOffset)
officerNoteLabel:SetText("Officer Note:")
df.officerNoteLabel = officerNoteLabel
-- Make label clickable
local officerNoteLabelBtn = CreateFrame("Button", nil, df)
officerNoteLabelBtn:SetPoint("TOPLEFT", officerNoteLabel, "TOPLEFT", -2, 2)
officerNoteLabelBtn:SetPoint("BOTTOMRIGHT", officerNoteLabel, "BOTTOMRIGHT", 2, -2)
officerNoteLabelBtn:SetFrameLevel(df:GetFrameLevel() + 1)
officerNoteLabelBtn:SetScript("OnClick", function()
if df.memberIndex and CanEditOfficerNote() then
IGW:ShowOfficerNoteEdit(df.memberIndex, df.memberName)
end
end)
yOffset = yOffset - 18
-- Officer Note Value (clickable)
local officerNoteValue = df:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
officerNoteValue:SetPoint("TOPLEFT", df, "TOPLEFT", leftX, yOffset)
officerNoteValue:SetWidth(210)
officerNoteValue:SetJustifyH("LEFT")
officerNoteValue:SetText("")
df.officerNoteValue = officerNoteValue
local officerNoteValueBtn = CreateFrame("Button", nil, df)
officerNoteValueBtn:SetPoint("TOPLEFT", df, "TOPLEFT", leftX - 2, yOffset + 2)
officerNoteValueBtn:SetWidth(214)
officerNoteValueBtn:SetHeight(40)
officerNoteValueBtn:SetFrameLevel(df:GetFrameLevel() + 1)
officerNoteValueBtn:SetScript("OnClick", function()
if df.memberIndex and CanEditOfficerNote() then
IGW:ShowOfficerNoteEdit(df.memberIndex, df.memberName)
end
end)
yOffset = yOffset - 48
-- Divider 2 (after officer notes)
local div2 = df:CreateTexture(nil, "ARTWORK")
div2:SetTexture("Interface\\DialogFrame\\UI-DialogBox-Divider")
div2:SetPoint("TOP", df, "TOP", 36, yOffset)
div2:SetWidth(300)
div2:SetHeight(16)
df.div2 = div2
yOffset = yOffset - 16
-- Whisper + Invite buttons (even spacing)
local buttonYGap = 8
local buttonGap = 10
local whisperWidth = 80
local inviteWidth = 120
local buttonHeight = 22
local whisperBtn = CreateFrame("Button", nil, df, "UIPanelButtonTemplate")
whisperBtn:SetPoint("TOPLEFT", df, "TOPLEFT", leftX, yOffset)
whisperBtn:SetWidth(whisperWidth)
whisperBtn:SetHeight(buttonHeight)
whisperBtn:SetFrameLevel(df:GetFrameLevel() + 1)
whisperBtn:SetText("Whisper")
whisperBtn:SetScript("OnClick", function()
if df.memberName then
ChatFrame_OpenChat("/w " .. df.memberName .. " ", DEFAULT_CHAT_FRAME)
end
end)
df.whisperBtn = whisperBtn
local inviteBtn = CreateFrame("Button", nil, df, "UIPanelButtonTemplate")
inviteBtn:SetPoint("LEFT", whisperBtn, "RIGHT", buttonGap, 0)
inviteBtn:SetWidth(inviteWidth)
inviteBtn:SetHeight(buttonHeight)
inviteBtn:SetFrameLevel(df:GetFrameLevel() + 1)
inviteBtn:SetText("Invite to Group")
inviteBtn:SetScript("OnClick", function()
if df.memberName then
InviteByName(df.memberName)
end
end)
df.inviteBtn = inviteBtn
yOffset = yOffset - (buttonHeight + buttonYGap)
-- Divider 3 (after buttons)
local div3 = df:CreateTexture(nil, "ARTWORK")
div3:SetTexture("Interface\\DialogFrame\\UI-DialogBox-Divider")
div3:SetPoint("TOP", df, "TOP", 36, yOffset)
div3:SetWidth(300)
div3:SetHeight(16)
df.div3 = div3
yOffset = yOffset - 16
-- Alts section
local altsLabel = df:CreateFontString(nil, "OVERLAY", "GameFontNormal")
altsLabel:SetPoint("TOPLEFT", df, "TOPLEFT", leftX, yOffset)
altsLabel:SetText("Other Characters:")
df.altsLabel = altsLabel
yOffset = yOffset - 18
local altsValue = df:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
altsValue:SetPoint("TOPLEFT", df, "TOPLEFT", leftX, yOffset)
altsValue:SetWidth(210)
altsValue:SetJustifyH("LEFT")
altsValue:SetText("—")
df.altsValue = altsValue
IGW.detailsFrame = df
end
-- Populate with member data
local df = IGW.detailsFrame
df.memberIndex = index
df.memberName = member.name
-- Set values
df.nameValue:SetText(member.name or "Unknown")
-- Get race from officer note
local race = IGW_GetRace(member.officernote or "")
-- Get class color
local color = CLASS_COLORS[member.class] or {r=1, g=1, b=1}
local classColorCode = string.format("|cFF%02x%02x%02x", color.r * 255, color.g * 255, color.b * 255)
-- Build level display: "Level Race Class" with class colored
local levelText = tostring(member.level or "?")
if race ~= "" then
levelText = levelText .. " " .. race
end
levelText = levelText .. " " .. classColorCode .. (member.class or "") .. "|r"
df.levelValue:SetText(levelText)
-- Clear the separate class field (now included in levelValue)
df.classValue:SetText("")
df.rankValue:SetText(member.rank or "")
df.zoneValue:SetText(member.zone or "Unknown")
-- Online status
if member.online then
df.onlineValue:SetTextColor(0, 1, 0)
df.onlineValue:SetText("Online")
else
df.onlineValue:SetTextColor(0.5, 0.5, 0.5)
df.onlineValue:SetText("Offline")
end
-- Date Joined and Time Zone (extract from officer note)
-- Format: RaceCode-MMDDYYTTT or just MMDDYYTTT (if no race code) or just TT (timezone only)
-- Example: O-030125PST or 030125PST or O-030125 (no timezone) or O-PST (just timezone)
local dateJoined = "—"
local timeZone = "—"
local officerNote = member.officernote or ""
-- First try to find 6 digits + 3 letters (date with timezone)
local _, _, dateStr, tzStr = string.find(officerNote, "(%d%d%d%d%d%d)(%a%a%a)")
-- If no timezone found with date, try to find just 6 digits (date only)
if not dateStr then
_, _, dateStr = string.find(officerNote, "(%d%d%d%d%d%d)")
end
-- If still no timezone found, try to find just 3 letters (timezone only)
if not tzStr then
-- Look for 3 consecutive letters that match a known timezone
for tz, _ in pairs(TIMEZONES) do
if string.find(officerNote, tz) then
tzStr = tz
break
end
end
end
if dateStr then
-- Parse MMDDYY
local month = string.sub(dateStr, 1, 2)
local day = string.sub(dateStr, 3, 4)
local year = string.sub(dateStr, 5, 6)
-- Format as MM/DD/YY
dateJoined = month .. "/" .. day .. "/" .. year
end
-- Look up timezone full name if we have one
if tzStr then
timeZone = TIMEZONES[tzStr] or tzStr
end
df.dateJoinedValue:SetText(dateJoined)
df.timeZoneValue:SetText(timeZone)
-- Faction icon
local faction = IGW_GetFaction(race)
if faction == "Alliance" then
df.factionIcon:SetTexture("Interface\\TargetingFrame\\UI-PVP-Alliance")
df.factionIcon:Show()
elseif faction == "Horde" then
df.factionIcon:SetTexture("Interface\\TargetingFrame\\UI-PVP-Horde")
df.factionIcon:Show()
else
df.factionIcon:Hide() -- No faction data
end
df.noteValue:SetText(member.note or "")
-- Officer note (only if can view)
if CanViewOfficerNote() then
df.officerNoteLabel:Show()
df.officerNoteValue:Show()
df.officerNoteValue:SetText(member.officernote or "")
else
-- Hide officer note section when user can't view
df.officerNoteLabel:Hide()
df.officerNoteValue:Hide()
end
-- Find alts: bidirectional detection
-- 1. Find characters with "alt of [current player]"
-- 2. If current player is an alt, find the main and all other alts
local alts = {}
local searchName = string.lower(member.name or "")
local mainCharacter = nil
-- Helper function to check for exact name match with word boundaries
local function hasExactNameMatch(note, name)
if string.find(note, "^" .. name .. "[%s%p]") or -- Start of string
string.find(note, "[%s%p]" .. name .. "$") or -- End of string
string.find(note, "[%s%p]" .. name .. "[%s%p]") or -- Middle
note == name then -- Exact match only
return true
end
return false
end
-- Check if current character is an alt (has "alt of X" in their note)
if member.note then
local currentNote = string.lower(member.note)
if string.find(currentNote, "alt") then
-- Extract main character name from note
for _, guildMember in ipairs(rosterData) do
if guildMember.name ~= member.name then
local testName = string.lower(guildMember.name)
if hasExactNameMatch(currentNote, testName) then
mainCharacter = guildMember.name
break
end
end
end
end
end
-- If we found a main character, search for all alts of that main
if mainCharacter then
local mainNameLower = string.lower(mainCharacter)
-- Add the main character to the list
table.insert(alts, mainCharacter)
-- Find all other alts of this main
for _, guildMember in ipairs(rosterData) do
if guildMember.name ~= member.name and guildMember.note then
local note = string.lower(guildMember.note)
if string.find(note, "alt") and hasExactNameMatch(note, mainNameLower) then
table.insert(alts, guildMember.name)
end
end
end
else
-- Current character is the main, find their alts
for _, guildMember in ipairs(rosterData) do
if guildMember.name ~= member.name and guildMember.note then
local note = string.lower(guildMember.note)
if string.find(note, "alt") and hasExactNameMatch(note, searchName) then
table.insert(alts, guildMember.name)
end
end
end
end
-- Display alts
if table.getn(alts) > 0 then
df.altsLabel:Show()
df.altsValue:Show()
df.div3:Show()
df.altsValue:SetText(table.concat(alts, ", "))
else
-- Hide alts section when no alts found
df.altsLabel:Hide()
df.altsValue:Hide()
df.div3:Hide()
end
-- Position to the right of main window with 5px gap, same top alignment
df:ClearAllPoints()
df:SetPoint("TOPLEFT", frame, "TOPRIGHT", 5, 0)
df:SetHeight(frame:GetHeight())
df:Show()
end
-- Update opacity of windows
function IGW:UpdateOpacity()
-- Background opacity is fixed in v1.2
local opacity = IGW_BG_OPACITY
if frame then
frame:SetBackdropColor(0, 0, 0, opacity)
end
if IGW.detailsFrame then
IGW.detailsFrame:SetBackdropColor(0, 0, 0, opacity)
end
end
-- Save window position
function IGW:SavePosition()
if not ImprovedGuildWindowDB then return end
local point, _, _, x, y = frame:GetPoint()
ImprovedGuildWindowDB.position = {
point = point,
x = x,
y = y
}
end
-- Register events
function IGW:RegisterEvents()
local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("PLAYER_LOGIN")
eventFrame:RegisterEvent("PLAYER_GUILD_UPDATE")
eventFrame:RegisterEvent("GUILD_ROSTER_UPDATE")
eventFrame:SetScript("OnEvent", function()
if event == "PLAYER_LOGIN" then
IGW:OnPlayerLogin()
elseif event == "PLAYER_GUILD_UPDATE" or event == "GUILD_ROSTER_UPDATE" then
IGW:UpdateGuildData()
end
end)
end
-- Handle player login
function IGW:OnPlayerLogin()
-- Initialize SavedVariables and create main frame now that SavedVariables are loaded
self:InitializeSavedVariables()
if IsInGuild() then
GuildRoster()
end
end
-- Update guild data
function IGW:UpdateGuildData()
rosterData = {}
local numMembers = GetNumGuildMembers(true)
-- Update window title with guild name
if frame and frame.titleText then
local guildName = GetGuildInfo("player")
if guildName then
frame.titleText:SetText(guildName)
end
end
for i = 1, numMembers do
local name, rank, rankIndex, level, class, zone, note, officernote, online, status, classFileName, achievementPoints, achievementRank, isMobile, canSoR, repStanding, guid = GetGuildRosterInfo(i)
-- Get last online time (years, months, days, hours)
local yearsOffline, monthsOffline, daysOffline, hoursOffline = GetGuildRosterLastOnline(i)
table.insert(rosterData, {
name = name,
rank = rank,
rankIndex = rankIndex,
level = level,
class = class,
zone = zone,
note = note,
officernote = officernote,
online = online,
status = status,
yearsOffline = yearsOffline,
monthsOffline = monthsOffline,
daysOffline = daysOffline,
hoursOffline = hoursOffline
})
end
self:UpdateRosterDisplay()
if IGW.infoFrame and IGW.infoFrame:IsVisible() then
IGW:UpdateGuildInfoWindow()
end
end
-- Sort roster
function IGW:SortRoster(column)
if sortColumn == column then
sortAscending = not sortAscending
else
sortColumn = column
-- Level defaults to descending (high to low), others ascending
sortAscending = (column ~= "level")
end
if ImprovedGuildWindowDB and ImprovedGuildWindowDB.rememberSorting ~= false then
ImprovedGuildWindowDB.sortColumn = sortColumn
ImprovedGuildWindowDB.sortAscending = sortAscending
end
-- Update header arrows
if frame.headerButtons then
for _, btn in ipairs(frame.headerButtons) do
if btn.column == sortColumn then
btn.arrow:SetText(sortAscending and "^" or "v")
else
btn.arrow:SetText("")
end
end
end
self:UpdateRosterDisplay()
end
-- Filter and display roster
function IGW:UpdateRosterDisplay()
if not frame or not frame:IsVisible() then return end
if not frame.rosterRows or not frame.rosterScroll then return end
displayedMembers = {}
-- Filter members
for i, member in ipairs(rosterData) do
local show = true
-- Text filter (legacy search box)
if filterText ~= "" then
local searchIn = string.lower(member.name or "") ..
string.lower(member.class or "") ..
string.lower(member.rank or "") ..
string.lower(member.note or "") ..
string.lower(member.officernote or "")
if not string.find(searchIn, filterText, 1, true) then
show = false
end
end
-- Advanced filters (individual column searches)
if show and frame.advancedFilters then
if show and frame.advancedFilters.note ~= "" then
local noteSearch = string.lower(member.note or "")
if not string.find(noteSearch, frame.advancedFilters.note, 1, true) then
show = false
end
end
if show and frame.advancedFilters.officernote ~= "" then
local officerNoteSearch = string.lower(member.officernote or "")
if not string.find(officerNoteSearch, frame.advancedFilters.officernote, 1, true) then
show = false
end
end
-- Region filter (based on timezone)
if show and frame.advancedFilters.region ~= "" then
local officerNote = member.officernote or ""
local _, _, tzStr = string.find(officerNote, "%d%d%d%d%d%d(%a%a%a)")
local memberRegion = tzStr and TIMEZONE_REGIONS[tzStr] or nil
if memberRegion ~= frame.advancedFilters.region then
show = false
end
end
end
-- Offline filter
if show and not showOffline then
if not member.online then
show = false
end
end
-- Rank filter
if show and filterRank >= 0 then
if member.rankIndex ~= filterRank then
show = false
end
end
if show then
table.insert(displayedMembers, {
index = i,
data = member
})
end
end
-- Sort members
table.sort(displayedMembers, function(a, b)
local aVal, bVal
if sortColumn == "name" then
aVal = a.data.name or ""
bVal = b.data.name or ""
elseif sortColumn == "level" then
aVal = a.data.level or 0
bVal = b.data.level or 0
elseif sortColumn == "class" then
aVal = a.data.class or ""
bVal = b.data.class or ""
elseif sortColumn == "rank" then
aVal = a.data.rankIndex or 0
bVal = b.data.rankIndex or 0
elseif sortColumn == "note" then
aVal = a.data.note or ""
bVal = b.data.note or ""
elseif sortColumn == "officernote" then
aVal = a.data.officernote or ""
bVal = b.data.officernote or ""
elseif sortColumn == "lastonline" or sortColumn == "online" then
-- Sort by time offline (online first, then by how long offline)
if a.data.online and b.data.online then
aVal = 0
bVal = 0
elseif a.data.online then
aVal = 0 -- Online = 0 (most recent)
bVal = 999999 -- Offline = large number
elseif b.data.online then
aVal = 999999
bVal = 0
else
-- Both offline, calculate time in hours
local aHours = (a.data.yearsOffline or 0) * 8760 +
(a.data.monthsOffline or 0) * 730 +
(a.data.daysOffline or 0) * 24 +
(a.data.hoursOffline or 0)
local bHours = (b.data.yearsOffline or 0) * 8760 +
(b.data.monthsOffline or 0) * 730 +
(b.data.daysOffline or 0) * 24 +
(b.data.hoursOffline or 0)
aVal = aHours
bVal = bHours
end
elseif sortColumn == "zone" then
aVal = a.data.zone or ""
bVal = b.data.zone or ""
elseif sortColumn == "race" then
local aRace = IGW_GetRace(a.data.officernote or "")
local bRace = IGW_GetRace(b.data.officernote or "")
aVal = aRace
bVal = bRace
elseif sortColumn == "faction" then
local aRace = IGW_GetRace(a.data.officernote or "")
local bRace = IGW_GetRace(b.data.officernote or "")
local aFaction = IGW_GetFaction(aRace)
local bFaction = IGW_GetFaction(bRace)
aVal = aFaction
bVal = bFaction
elseif sortColumn == "datejoined" then
-- Extract date directly from officer note (no main fallback)
local _, _, aDateStr = string.find(a.data.officernote or "", "(%d%d%d%d%d%d)")
local _, _, bDateStr = string.find(b.data.officernote or "", "(%d%d%d%d%d%d)")
-- Convert MMDDYY to YYMMDD for proper sorting (newest first when descending)
if aDateStr then
local aMonth = string.sub(aDateStr, 1, 2)
local aDay = string.sub(aDateStr, 3, 4)
local aYear = string.sub(aDateStr, 5, 6)
aVal = aYear .. aMonth .. aDay
else
aVal = "999999" -- No date = sort to end
end
if bDateStr then
local bMonth = string.sub(bDateStr, 1, 2)
local bDay = string.sub(bDateStr, 3, 4)
local bYear = string.sub(bDateStr, 5, 6)
bVal = bYear .. bMonth .. bDay
else
bVal = "999999" -- No date = sort to end
end
elseif sortColumn == "timezone" then
-- Extract timezone from officer note (can be with or without date)
local _, _, aTzStr = string.find(a.data.officernote or "", "%d%d%d%d%d%d(%a%a%a)")
local _, _, bTzStr = string.find(b.data.officernote or "", "%d%d%d%d%d%d(%a%a%a)")
-- If not found with date, try standalone timezone
if not aTzStr then
local aOfficerNote = a.data.officernote or ""
for tz, _ in pairs(TIMEZONES) do
if string.find(aOfficerNote, tz) then
aTzStr = tz
break
end
end
end
if not bTzStr then
local bOfficerNote = b.data.officernote or ""
for tz, _ in pairs(TIMEZONES) do
if string.find(bOfficerNote, tz) then
bTzStr = tz
break
end
end
end
aVal = aTzStr or "zzz" -- No timezone = sort to end
bVal = bTzStr or "zzz"
end
if sortAscending then
return aVal < bVal
else
return aVal > bVal
end
end)
-- Update scroll frame
local numDisplayed = table.getn(displayedMembers)
-- Update FauxScrollFrame
local rowHeight = (currentTab == "roster" or currentTab == "detailed") and ROW_HEIGHT_DETAILS or ROW_HEIGHT_DEFAULT
local maxRows = (currentTab == "roster" or currentTab == "detailed") and VISIBLE_ROWS_DETAILS or VISIBLE_ROWS_GUILD
-- Ensure we don't have more scroll range than needed
-- If we have fewer items than visible rows, no scrolling needed
local scrollItems = numDisplayed
if numDisplayed <= maxRows then
scrollItems = maxRows -- Set to maxRows to disable scrolling
end
FauxScrollFrame_Update(frame.rosterScroll, scrollItems, maxRows, rowHeight)
local offset = FauxScrollFrame_GetOffset(frame.rosterScroll)
-- Safety clamp: ensure offset doesn't exceed valid range
local maxOffset = math.max(0, numDisplayed - maxRows)
if offset > maxOffset then
offset = maxOffset
end
local headerTopY = IGW_GetHeaderTopY()
-- Display rows with offset
for i = 1, 14 do
local row = frame.rosterRows[i]
local maxRows = (currentTab == "roster" or currentTab == "detailed") and VISIBLE_ROWS_DETAILS or VISIBLE_ROWS_GUILD
if i > maxRows then
row:Hide()
end
-- Only show fewer rows on Notes & Rank or Detailed View tabs
if (currentTab == "roster" or currentTab == "detailed") and i > VISIBLE_ROWS_DETAILS then
row:Hide()
end
row:SetHeight(rowHeight)
row:ClearAllPoints()
row:SetPoint("TOPLEFT", frame, "TOPLEFT", 25, (headerTopY - 30) - (i-1) * rowHeight)
local dataIndex = offset + i
if dataIndex <= numDisplayed and not ((currentTab == "roster" or currentTab == "detailed") and i > VISIBLE_ROWS_DETAILS) then
local member = displayedMembers[dataIndex].data
local index = displayedMembers[dataIndex].index
-- Debug first few rows
if currentTab == "roster" then
-- Roster view columns: Name, Level, Rank, Note, Officer Note, Last Online
-- Hide Race and Faction (not used in this view)
row.race:SetText("")
row.faction:SetText("")
-- Name (without offline tag)
row.name:SetText(member.name or "")
if member.online then
row.name:SetTextColor(1, 1, 1)
else
row.name:SetTextColor(0.5, 0.5, 0.5)
end
-- Level
row.level:SetText(member.level)
row.level:SetTextColor(1, 1, 1)
-- Rank (using class field)
row.class:SetText(member.rank or "")
row.class:SetTextColor(1, 1, 1)
-- Note (using rank field)
row.rank:SetText(member.note or "")
row.rank:SetTextColor(1, 1, 1)
-- Officer Note (using note field)
if CanViewOfficerNote() then
row.note:SetText(member.officernote or "")
else
row.note:SetText("")
end
row.note:SetTextColor(1, 1, 1)
-- Last Online (using officerNote field)
if member.online then
row.officerNote:SetText("Online")
row.officerNote:SetTextColor(0, 1, 0)
else
-- Format last online time
local lastOnlineText = ""
local isOldOffline = false
if member.yearsOffline and member.yearsOffline > 0 then
lastOnlineText = member.yearsOffline .. "y ago"
isOldOffline = true
elseif member.monthsOffline and member.monthsOffline >= 2 then
lastOnlineText = member.monthsOffline .. "m ago"
isOldOffline = true
elseif member.monthsOffline and member.monthsOffline > 0 then
lastOnlineText = member.monthsOffline .. "m ago"
elseif member.daysOffline and member.daysOffline > 0 then
lastOnlineText = member.daysOffline .. "d ago"
elseif member.hoursOffline and member.hoursOffline > 0 then
lastOnlineText = member.hoursOffline .. "h ago"
else
lastOnlineText = "Recently"
end
row.officerNote:SetText(lastOnlineText)
if isOldOffline then
row.officerNote:SetTextColor(1, 0, 0) -- Red for 2+ months
else
row.officerNote:SetTextColor(0.5, 0.5, 0.5) -- Gray
end
end
elseif currentTab == "detailed" then
-- Detailed View columns: Name, Level, Rank, Date Joined, Time Zone
-- Name (without offline tag)
row.name:SetText(member.name or "")
if member.online then
row.name:SetTextColor(1, 1, 1)
else
row.name:SetTextColor(0.5, 0.5, 0.5)
end
-- Level
row.level:SetText(member.level)
row.level:SetTextColor(1, 1, 1)
-- Rank (using class field)
row.class:SetText(member.rank or "")
row.class:SetTextColor(1, 1, 1)
-- Extract Date Joined and Time Zone from officer note (timezone can be standalone)
local dateJoined = "—"
local timeZone = "—"
local officerNote = member.officernote or ""
local _, _, dateStr, tzStr = string.find(officerNote, "(%d%d%d%d%d%d)(%a%a%a)")
if not dateStr then
_, _, dateStr = string.find(officerNote, "(%d%d%d%d%d%d)")
end
-- If still no timezone found, try to find just 3 letters (timezone only)
if not tzStr then
for tz, _ in pairs(TIMEZONES) do
if string.find(officerNote, tz) then
tzStr = tz
break
end
end
end
if dateStr then
local month = string.sub(dateStr, 1, 2)
local day = string.sub(dateStr, 3, 4)
local year = string.sub(dateStr, 5, 6)
dateJoined = month .. "/" .. day .. "/" .. year
end
if tzStr then
timeZone = TIMEZONES[tzStr] or tzStr
end
-- Date Joined (using rank field)
row.rank:SetText(dateJoined)
row.rank:SetTextColor(1, 1, 1)
-- Time Zone (using note field)
row.note:SetText(timeZone)
row.note:SetTextColor(1, 1, 1)
-- Hide race and faction fields
row.race:SetText("")
row.faction:SetText("")
row.officerNote:SetText("")
else
-- Guild Members view columns: Name, Level, Class, Location, Rank
-- Name
local nameText = member.name
row.name:SetText(nameText)
if member.online then
row.name:SetTextColor(1, 1, 1)
else
row.name:SetTextColor(0.5, 0.5, 0.5)
end
-- Level
row.level:SetText(member.level)
row.level:SetTextColor(1, 1, 1)
-- Class with color
local color = CLASS_COLORS[member.class] or {r=1, g=1, b=1}
row.class:SetTextColor(color.r, color.g, color.b)
row.class:SetText(member.class or "")
-- Race
local race = IGW_GetRace(member.officernote or "")
row.race:SetText(race)
row.race:SetTextColor(1, 1, 1)
-- Faction
local faction = IGW_GetFaction(race)
if faction == "Alliance" then
row.faction:SetTextColor(0.2, 0.5, 1) -- Blue
row.faction:SetText("A")
elseif faction == "Horde" then
row.faction:SetTextColor(1, 0.2, 0.2) -- Red
row.faction:SetText("H")
else
row.faction:SetText("") -- Blank for unknown
end
-- Location (use rank field for display)
if member.online then
row.rank:SetText(member.zone or "Unknown")
row.rank:SetTextColor(1, 1, 1)
else
row.rank:SetText("Offline")
row.rank:SetTextColor(0.5, 0.5, 0.5)
end
-- Rank (use note field for display)
row.note:SetText(member.rank or "")
row.note:SetTextColor(1, 1, 1)
-- Hide officer note field (not used in this view)
row.officerNote:SetText("")
end
row.memberIndex = index
row.memberName = member.name
row:Show()
else
row:Hide()
end
end
-- Update member count with online and total
local onlineCount = 0
local totalMembers = table.getn(rosterData)
for _, m in ipairs(rosterData) do
if m.online then
onlineCount = onlineCount + 1
end
end
if frame.memberCount then
frame.memberCount:SetText(string.format("%d Online | %d Total", onlineCount, totalMembers))
end
end
-- Toggle window visibility
function IGW:ToggleWindow()
if frame:IsVisible() then
frame:Hide()
-- Save window states if remember is enabled
if ImprovedGuildWindowDB and ImprovedGuildWindowDB.rememberWindows then
ImprovedGuildWindowDB.windowStates = {
detailsOpen = IGW.detailsFrame and IGW.detailsFrame:IsVisible() or false,
infoOpen = IGW.infoFrame and IGW.infoFrame:IsVisible() or false
}
end
-- Close details window when main window closes
if IGW.detailsFrame then
IGW.detailsFrame:Hide()
end
-- Close guild info window when main window closes
if IGW.infoFrame then
IGW.infoFrame:Hide()
end
-- Close dungeon details dialog when main window closes
if IGW.dungeonDialog then
IGW.dungeonDialog:Hide()
end
else
if IsInGuild() then
GuildRoster()
-- Use saved default tab or fall back to "details"
local defaultTab = "details"
if ImprovedGuildWindowDB and ImprovedGuildWindowDB.defaultTab then
defaultTab = ImprovedGuildWindowDB.defaultTab
end
self:SwitchTab(defaultTab)
frame:Show()
self:UpdateRosterDisplay()
-- Restore remembered windows if enabled
if ImprovedGuildWindowDB and ImprovedGuildWindowDB.rememberWindows and ImprovedGuildWindowDB.windowStates then
if ImprovedGuildWindowDB.windowStates.detailsOpen and IGW.detailsFrame then
IGW.detailsFrame:Show()
end
if ImprovedGuildWindowDB.windowStates.infoOpen and IGW.infoFrame then
IGW:ToggleGuildInfoWindow()
end
end
else
DEFAULT_CHAT_FRAME:AddMessage("|cFFFF0000You are not in a guild.|r")
end
end
end
-- Update Guild Info window contents
function IGW:UpdateGuildInfoWindow()
if not IGW.infoFrame then return end
local gf = IGW.infoFrame
-- Guild name
local guildName = GetGuildInfo("player") or "Guild"
if gf.guildNameText then
gf.guildNameText:SetText(guildName)
end
-- Total members (use rosterData if populated, otherwise query API)
local total = 0
-- Count guild members (excluding alts) and alts separately
local totalMembers = 0
local totalAlts = 0
if rosterData and table.getn(rosterData) > 0 then
for _, member in ipairs(rosterData) do
local rank = string.lower(member.rank or "")
if string.find(rank, "alt") then
totalAlts = totalAlts + 1
else
totalMembers = totalMembers + 1
end
end
else
totalMembers = GetNumGuildMembers(true) or 0
end
if gf.totalMembersText then
gf.totalMembersText:SetText("Guild Members: " .. totalMembers)
end
if gf.totalAltsText then
gf.totalAltsText:SetText("Guild Member Alts: " .. totalAlts)
end
-- MOTD
local motd = ""
if GetGuildRosterMOTD then
motd = GetGuildRosterMOTD() or ""
end
if motd == "" then motd = "—" end
if gf.motdValue then
gf.motdValue:SetText(motd)
end
-- Guild Information (public guild info text)
local infoText = ""
if GetGuildInfoText then
infoText = GetGuildInfoText() or ""
end
if infoText == "" then infoText = "—" end
if gf.infoValue then
gf.infoValue:SetText(infoText)
end
-- Class distribution (from rosterData) - Bar Graph
-- Clear existing bars
if gf.classBars then
for _, bar in ipairs(gf.classBars) do
bar.frame:Hide()
end
gf.classBars = {}
end
local counts = {}
local maxCount = 0
for _, m in ipairs(rosterData or {}) do
if m and m.class then
counts[m.class] = (counts[m.class] or 0) + 1
if counts[m.class] > maxCount then
maxCount = counts[m.class]
end
end
end
local entries = {}
for cls, c in pairs(counts) do
table.insert(entries, {cls=cls, c=c})
end
table.sort(entries, function(a,b)
if a.c == b.c then
return a.cls < b.cls
end
return a.c > b.c
end)
-- Create bars
local barHeight = 12
local barGap = 2
local maxBarWidth = 180
local yOffset = 0
for i, e in ipairs(entries) do
if i > 10 then break end -- Limit to top 10 classes
local barFrame = CreateFrame("Frame", nil, gf.classBarsContainer)
barFrame:SetPoint("TOPLEFT", gf.classBarsContainer, "TOPLEFT", 0, yOffset)
barFrame:SetHeight(barHeight)
-- Calculate bar width based on count
local barWidth = maxCount > 0 and (e.c / maxCount) * maxBarWidth or 0
barFrame:SetWidth(barWidth)
-- Bar background with class color
local color = CLASS_COLORS[e.cls] or {r=0.5, g=0.5, b=0.5}
local barBg = barFrame:CreateTexture(nil, "BACKGROUND")
barBg:SetAllPoints(barFrame)
barBg:SetTexture("Interface\\TargetingFrame\\UI-StatusBar")
barBg:SetVertexColor(color.r, color.g, color.b, 0.8)
-- Label (class name and count)
local label = barFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
label:SetPoint("LEFT", barFrame, "LEFT", 3, 0)
label:SetText(string.format("%s: %d", e.cls, e.c))
label:SetTextColor(1, 1, 1)
table.insert(gf.classBars, {frame=barFrame, bg=barBg, label=label})
yOffset = yOffset - (barHeight + barGap)
end
-- Level 60 Class distribution - Bar Graph
-- Clear existing bars
if gf.class60Bars then
for _, bar in ipairs(gf.class60Bars) do
bar.frame:Hide()
end
gf.class60Bars = {}
end
local counts60 = {}
local maxCount60 = 0
for _, m in ipairs(rosterData or {}) do
if m and m.class and m.level == 60 then
counts60[m.class] = (counts60[m.class] or 0) + 1
if counts60[m.class] > maxCount60 then
maxCount60 = counts60[m.class]
end
end
end
local entries60 = {}
for cls, c in pairs(counts60) do
table.insert(entries60, {cls=cls, c=c})
end
table.sort(entries60, function(a,b)
if a.c == b.c then
return a.cls < b.cls
end
return a.c > b.c
end)
-- Create level 60 bars
local barHeight60 = 12
local barGap60 = 2
local maxBarWidth60 = 180
local yOffset60 = 0
for i, e in ipairs(entries60) do
if i > 10 then break end -- Limit to top 10 classes
local barFrame = CreateFrame("Frame", nil, gf.class60BarsContainer)
barFrame:SetPoint("TOPLEFT", gf.class60BarsContainer, "TOPLEFT", 0, yOffset60)
barFrame:SetHeight(barHeight60)
-- Calculate bar width based on count
local barWidth = maxCount60 > 0 and (e.c / maxCount60) * maxBarWidth60 or 0
barFrame:SetWidth(barWidth)
-- Bar background with class color
local color = CLASS_COLORS[e.cls] or {r=0.5, g=0.5, b=0.5}
local barBg = barFrame:CreateTexture(nil, "BACKGROUND")
barBg:SetAllPoints(barFrame)
barBg:SetTexture("Interface\\TargetingFrame\\UI-StatusBar")
barBg:SetVertexColor(color.r, color.g, color.b, 0.8)
-- Label (class name and count)
local label = barFrame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
label:SetPoint("LEFT", barFrame, "LEFT", 3, 0)
label:SetText(string.format("%s: %d", e.cls, e.c))
label:SetTextColor(1, 1, 1)
table.insert(gf.class60Bars, {frame=barFrame, bg=barBg, label=label})
yOffset60 = yOffset60 - (barHeight60 + barGap60)
end
-- Officers Online (uses OFFICER_RANK_THRESHOLD configuration)
local onlineOfficers = {}
for _, m in ipairs(rosterData or {}) do
if m and m.online and m.rankIndex ~= nil then
-- rankIndex: 0=Guild Master, 1-9=other ranks
if m.rankIndex <= OFFICER_RANK_THRESHOLD then
table.insert(onlineOfficers, m.name)
end
end
end
local officersText = ""
if table.getn(onlineOfficers) > 0 then
table.sort(onlineOfficers)
officersText = table.concat(onlineOfficers, "\n")
else
officersText = "No officers online"
end
if gf.officersValue then
gf.officersValue:SetText(officersText)
end
end
-- Update Page 3 - Crafters
function IGW:UpdateCraftersPage()
if not IGW.infoFrame then return end
local gf = IGW.infoFrame
local content = gf.craftersContent
if not content then return end
-- Clear existing content
local children = {content:GetChildren()}
for _, child in ipairs(children) do
child:Hide()
child:SetParent(nil)
end
-- Parse crafters from public notes (300+ skill level only)
local crafters = {}
for _, m in ipairs(rosterData or {}) do
if m and m.name and m.note then
local noteUpper = string.upper(m.note)
-- Profession search patterns with abbreviations (crafting professions only)
local professions = {
{name="Alchemy", patterns={"ALCHEMY", "ALCH", "ALC"}},
{name="Blacksmithing", patterns={"BLACKSMITHING", "BLACKSMITH", "SMITH", "BS", "B%.S%."}},
{name="Enchanting", patterns={"ENCHANTING", "ENCHANT", "ENCH", "ENC"}},
{name="Engineering", patterns={"ENGINEERING", "ENGINEER", "ENG", "ENGI"}},
{name="Jewelcrafting", patterns={"JEWELCRAFTING", "JEWEL", "JC", "J%.C%."}},
{name="Leatherworking", patterns={"LEATHERWORKING", "LEATHER", "LW", "L%.W%."}},
{name="Tailoring", patterns={"TAILORING", "TAILOR", "TAIL", "TLR"}}
}
for _, prof in ipairs(professions) do
for _, pattern in ipairs(prof.patterns) do
local skill = nil
-- Try pattern: ProfessionName 300 or ProfessionName: 300
local _, _, skill1 = string.find(noteUpper, pattern .. "%s*:?%s*(%d+)")
-- Try pattern: 300 ProfessionName
local _, _, skill2 = string.find(noteUpper, "(%d+)%s*" .. pattern)
skill = skill1 or skill2
if skill then
local skillNum = tonumber(skill)
-- Only include online players with 300+ skill
if skillNum and skillNum >= 300 and m.online then
-- Check if we already added this profession for this player
local alreadyAdded = false
for _, existing in ipairs(crafters) do
if existing.name == m.name and existing.prof == prof.name then
alreadyAdded = true
break
end
end
if not alreadyAdded then
table.insert(crafters, {
name = m.name,
prof = prof.name,
skill = skillNum,
online = true
})
end
break -- Found this profession, skip other patterns for it
end
end
end
end
end
end
-- Group by profession
local professionGroups = {}
for _, crafter in ipairs(crafters) do
if not professionGroups[crafter.prof] then
professionGroups[crafter.prof] = {}
end
table.insert(professionGroups[crafter.prof], crafter)
end
-- Sort each profession group alphabetically by name
for prof, group in pairs(professionGroups) do
table.sort(group, function(a, b)
return a.name < b.name
end)
end
-- Sort professions alphabetically
local sortedProfs = {}
for prof, _ in pairs(professionGroups) do
table.insert(sortedProfs, prof)
end
table.sort(sortedProfs)
-- Define all 7 crafting professions in order
local allProfessions = {
"Alchemy", "Blacksmithing", "Enchanting",
"Engineering", "Jewelcrafting", "Leatherworking",
"Tailoring"
}
-- Display all professions with fixed spacing
local yOffset = 0
local sectionHeight = 50 -- Height per profession section (title + 2 rows + gap)
for _, prof in ipairs(allProfessions) do
local group = professionGroups[prof] or {} -- Use empty table if no crafters
-- Profession subtitle (centered)
local subtitle = content:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
subtitle:SetPoint("TOP", content, "TOP", 0, yOffset)
subtitle:SetWidth(210)
subtitle:SetJustifyH("CENTER")
subtitle:SetTextColor(1, 1, 1) -- White color
subtitle:SetText(prof)
yOffset = yOffset - 18
-- Display up to 6 crafters in 2 lines (3 per line, clickable)
for line = 1, 2 do
local startIdx = (line - 1) * 3 + 1
local lineMembers = {}
for i = startIdx, math.min(startIdx + 2, table.getn(group)) do
table.insert(lineMembers, group[i])
end
if table.getn(lineMembers) > 0 then
-- Create container for this line
local lineContainer = CreateFrame("Frame", nil, content)
lineContainer:SetPoint("TOP", content, "TOP", 0, yOffset)
lineContainer:SetWidth(210)
lineContainer:SetHeight(14)
-- Calculate actual text widths using a temporary FontString
local tempText = lineContainer:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
local totalWidth = 0
local buttonWidths = {}
for _, member in ipairs(lineMembers) do
tempText:SetText(member.name)
local nameWidth = tempText:GetStringWidth() + 1 -- Reduced from +2 to +1
table.insert(buttonWidths, nameWidth)
totalWidth = totalWidth + nameWidth
end
tempText:Hide() -- Hide temporary text
-- Add comma widths (", " spacing)
if table.getn(lineMembers) > 1 then
tempText:SetText(", ")
local commaWidth = tempText:GetStringWidth()
totalWidth = totalWidth + ((table.getn(lineMembers) - 1) * commaWidth)
end
-- Starting position (centered)
local xOffset = -(totalWidth / 2)
-- Create clickable buttons for each name
for idx, member in ipairs(lineMembers) do
-- Capture member name in local variable to avoid closure issues
local memberName = member.name
local nameBtn = CreateFrame("Button", nil, lineContainer)
nameBtn:SetPoint("LEFT", lineContainer, "LEFT", xOffset + 105, 0)
nameBtn:SetWidth(buttonWidths[idx])
nameBtn:SetHeight(14)
local nameText = nameBtn:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
nameText:SetAllPoints(nameBtn)
nameText:SetJustifyH("LEFT")
nameText:SetText(string.format("|cFF00FF00%s|r", memberName))
nameBtn:SetScript("OnClick", function()
ChatFrameEditBox:SetText("/w " .. memberName .. " ")
ChatFrameEditBox:Show()
ChatFrameEditBox:SetFocus()
end)
nameBtn:SetScript("OnEnter", function()
nameText:SetText(string.format("|cFFFFFF00%s|r", memberName)) -- Yellow on hover
end)
nameBtn:SetScript("OnLeave", function()
nameText:SetText(string.format("|cFF00FF00%s|r", memberName)) -- Green normally
end)
xOffset = xOffset + buttonWidths[idx]
-- Add comma after name (except last one)
if idx < table.getn(lineMembers) then
local comma = lineContainer:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
comma:SetPoint("LEFT", lineContainer, "LEFT", xOffset + 105, 0)
comma:SetText("|cFF00FF00, |r")
tempText:SetText(", ")
xOffset = xOffset + tempText:GetStringWidth()
end
end
end
yOffset = yOffset - 14
end
-- Gap after each profession
yOffset = yOffset - 6
end
end
-- Show dungeon details dialog
function IGW:ShowDungeonDetailsDialog(dungeon)
if not dungeon then return end
-- Create dialog if it doesn't exist
if not IGW.dungeonDialog then
local dialog = CreateFrame("Frame", "IGW_DungeonDetailsDialog", UIParent)
dialog:SetWidth(350)
dialog:SetHeight(220)
dialog:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
dialog:SetFrameStrata("DIALOG")
dialog:SetBackdrop({
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
tile = true,
tileSize = 32,
edgeSize = 32,
insets = { left = 11, right = 12, top = 12, bottom = 11 }
})
dialog:SetBackdropColor(0, 0, 0, 1)
dialog:EnableMouse(true)
dialog:SetMovable(true)
dialog:RegisterForDrag("LeftButton")
dialog:SetScript("OnDragStart", function() this:StartMoving() end)
dialog:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
-- Title
local title = dialog:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
title:SetPoint("TOP", dialog, "TOP", 0, -20)
dialog.title = title
-- Level range
local levelRange = dialog:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
levelRange:SetPoint("TOP", title, "BOTTOM", 0, -10)
dialog.levelRange = levelRange
-- Location
local location = dialog:CreateFontString(nil, "OVERLAY", "GameFontNormal")
location:SetPoint("TOP", levelRange, "BOTTOM", 0, -8)
dialog.location = location
-- Faction
local faction = dialog:CreateFontString(nil, "OVERLAY", "GameFontNormal")
faction:SetPoint("TOP", location, "BOTTOM", 0, -5)
dialog.faction = faction
-- Divider
local divider = dialog:CreateTexture(nil, "ARTWORK")
divider:SetHeight(1)
divider:SetWidth(310)
divider:SetPoint("TOP", faction, "BOTTOM", 0, -10)
divider:SetTexture(1, 1, 1, 0.2)
-- Description
local description = dialog:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
description:SetPoint("TOP", divider, "BOTTOM", 0, -10)
description:SetWidth(310)
description:SetJustifyH("LEFT")
description:SetSpacing(3)
dialog.description = description
-- Close button
local closeBtn = CreateFrame("Button", nil, dialog, "UIPanelCloseButton")
closeBtn:SetPoint("TOPRIGHT", dialog, "TOPRIGHT", -5, -5)
dialog:Hide()
IGW.dungeonDialog = dialog
end
local dialog = IGW.dungeonDialog
-- Update dialog content
dialog.title:SetText(dungeon.name)
dialog.levelRange:SetText(string.format("Level %d-%d", dungeon.minLevel, dungeon.maxLevel))
dialog.location:SetText("Location: " .. (dungeon.location or "Unknown"))
-- Faction coloring
local factionText = "Faction: "
if dungeon.faction == "Alliance" then
dialog.faction:SetText(factionText .. "|cFF0080FFAlliance|r")
elseif dungeon.faction == "Horde" then
dialog.faction:SetText(factionText .. "|cFFFF0000Horde|r")
else
dialog.faction:SetText(factionText .. "Both")
end
dialog.description:SetText(dungeon.description or "No description available.")
dialog:Show()
end
-- Update Page 4 - Suggested Dungeons
function IGW:UpdateDungeonsPage()
local gf = IGW.infoFrame
if not gf or not gf.dungeonsContent then return end
local content = gf.dungeonsContent
-- Clear existing dungeon entries
local children = {content:GetChildren()}
for _, child in ipairs(children) do
child:Hide()
end
-- Get online members' levels
local onlineLevels = {}
for _, m in ipairs(rosterData or {}) do
if m and m.online and m.level then
table.insert(onlineLevels, m.level)
end
end
if table.getn(onlineLevels) == 0 then
local noData = content:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
noData:SetPoint("TOP", content, "TOP", 0, -20)
noData:SetWidth(210)
noData:SetJustifyH("CENTER")
noData:SetText("No members online")
noData:SetTextColor(0.7, 0.7, 0.7)
return
end
-- Calculate average level of online members
local totalLevel = 0
for _, lvl in ipairs(onlineLevels) do
totalLevel = totalLevel + lvl
end
local avgLevel = totalLevel / table.getn(onlineLevels)
-- Dungeon database with level ranges (Turtle WoW)
local dungeons = {
{name = "Ragefire Chasm", minLevel = 13, maxLevel = 18, location = "Orgrimmar", faction = "Horde", description = "A lava-filled cavern beneath Orgrimmar, home to Trogg invaders and fire cultists."},
{name = "The Deadmines", minLevel = 17, maxLevel = 24, location = "Westfall", faction = "Alliance", description = "A network of tunnels beneath Westfall controlled by the Defias Brotherhood."},
{name = "Wailing Caverns", minLevel = 17, maxLevel = 24, location = "The Barrens", faction = "Both", description = "A corrupted underground oasis in the Barrens with deviate creatures."},
{name = "Stockades", minLevel = 22, maxLevel = 30, location = "Stormwind", faction = "Alliance", description = "A high-security prison in Stormwind where dangerous criminals are held."},
{name = "Blackfathom Deeps", minLevel = 22, maxLevel = 31, location = "Ashenvale", faction = "Both", description = "A former temple now overrun by the Twilight's Hammer cult."},
{name = "Dragonmaw Retreat", minLevel = 25, maxLevel = 34, location = "Wetlands", faction = "Both", description = "A custom Turtle WoW dungeon featuring Dragonmaw orcs in the Wetlands."},
{name = "SM-Graveyard", minLevel = 27, maxLevel = 36, location = "Tirisfal Glades", faction = "Both", description = "Scarlet Monastery: Graveyard - The burial grounds corrupted by the undead."},
{name = "Gnomeregan", minLevel = 29, maxLevel = 38, location = "Dun Morogh", faction = "Both", description = "The irradiated gnomish city, now overrun by troggs and malfunctioning machines."},
{name = "Crescent Grove", minLevel = 32, maxLevel = 38, location = "Feralas", faction = "Both", description = "A custom Turtle WoW dungeon in Feralas with ancient night elf ruins."},
{name = "SM-Library", minLevel = 32, maxLevel = 39, location = "Tirisfal Glades", faction = "Both", description = "Scarlet Monastery: Library - Repository of Scarlet Crusade knowledge."},
{name = "Razorfen Kraul", minLevel = 32, maxLevel = 42, location = "The Barrens", faction = "Both", description = "An ancient quilboar stronghold with death speaker cultists."},
{name = "Stormwrought Ruins", minLevel = 35, maxLevel = 41, location = "Azshara", faction = "Both", description = "A custom Turtle WoW dungeon featuring elementals and ancient magic."},
{name = "SM-Armory", minLevel = 40, maxLevel = 45, location = "Tirisfal Glades", faction = "Both", description = "Scarlet Monastery: Armory - Training grounds of the Scarlet Crusade."},
{name = "SM-Cathedral", minLevel = 40, maxLevel = 45, location = "Tirisfal Glades", faction = "Both", description = "Scarlet Monastery: Cathedral - The heart of Scarlet Crusade power."},
{name = "Uldaman", minLevel = 40, maxLevel = 51, location = "Badlands", faction = "Both", description = "An ancient titan vault filled with troggs and earthen constructs."},
{name = "Razorfen Downs", minLevel = 42, maxLevel = 44, location = "The Barrens", faction = "Both", description = "A quilboar burial ground corrupted by the Scourge."},
{name = "Gilneas City", minLevel = 43, maxLevel = 49, location = "Gilneas", faction = "Both", description = "A custom Turtle WoW dungeon in the cursed ruins of Gilneas."},
{name = "Maraudon", minLevel = 45, maxLevel = 55, location = "Desolace", faction = "Both", description = "A vast cavern complex beneath Desolace with centaur and elemental forces."},
{name = "Zul'Farrak", minLevel = 46, maxLevel = 56, location = "Tanaris", faction = "Both", description = "An ancient troll city in Tanaris filled with sandfury trolls."},
{name = "Sunken Temple", minLevel = 50, maxLevel = 60, location = "Swamp of Sorrows", faction = "Both", description = "Temple of Atal'Hakkar - An ancient troll temple dedicated to the blood god."},
{name = "Blackrock Depths", minLevel = 52, maxLevel = 60, location = "Blackrock Mountain", faction = "Both", description = "A massive Dark Iron dwarf city within Blackrock Mountain."},
{name = "Hateforge Quarry", minLevel = 52, maxLevel = 60, location = "Searing Gorge", faction = "Both", description = "A custom Turtle WoW dungeon with Dark Iron operations."},
{name = "Dire Maul West", minLevel = 55, maxLevel = 60, location = "Feralas", faction = "Both", description = "Ancient elven ruins overrun by ogres - Western wing."},
{name = "Dire Maul East", minLevel = 55, maxLevel = 60, location = "Feralas", faction = "Both", description = "Ancient elven ruins overrun by ogres - Eastern wing."},
{name = "LBRS", minLevel = 55, maxLevel = 60, location = "Blackrock Mountain", faction = "Both", description = "Lower Blackrock Spire - Orc stronghold within Blackrock Mountain."},
{name = "Dire Maul North", minLevel = 58, maxLevel = 60, location = "Feralas", faction = "Both", description = "Ancient elven ruins - Northern wing with the Tribute Run."},
{name = "Scholomance", minLevel = 58, maxLevel = 60, location = "Western Plaguelands", faction = "Both", description = "A school of necromancy run by the Scourge."},
{name = "Stratholme", minLevel = 58, maxLevel = 60, location = "Eastern Plaguelands", faction = "Both", description = "A city destroyed by the Scourge with Living and Undead sides."},
{name = "UBRS", minLevel = 55, maxLevel = 60, location = "Blackrock Mountain", faction = "Both", description = "Upper Blackrock Spire - Stronghold of the Blackrock orcs."},
{name = "Stormwind Vault", minLevel = 60, maxLevel = 60, location = "Stormwind", faction = "Alliance", description = "A custom Turtle WoW dungeon beneath Stormwind."},
{name = "Karazhan Crypt", minLevel = 60, maxLevel = 60, location = "Deadwind Pass", faction = "Both", description = "A custom Turtle WoW dungeon in the crypts beneath Karazhan."}
}
-- Score each dungeon based on how well it fits the online levels
for _, dungeon in ipairs(dungeons) do
local score = 0
for _, lvl in ipairs(onlineLevels) do
if lvl >= dungeon.minLevel and lvl <= dungeon.maxLevel then
score = score + 1
end
end
dungeon.score = score
dungeon.coverage = table.getn(onlineLevels) > 0 and (score / table.getn(onlineLevels)) or 0
end
-- Sort by minimum level ascending
table.sort(dungeons, function(a, b)
return a.minLevel < b.minLevel
end)
-- Display up to 22 dungeons with at least 4 players
local yOffset = -10
local displayed = 0
-- Get current player's level
local playerLevel = UnitLevel("player")
for i, dungeon in ipairs(dungeons) do
if displayed >= 22 then break end
if dungeon.score >= 4 then -- Only show dungeons with at least 4 matching members
-- Create clickable button
local dungeonBtn = CreateFrame("Button", nil, content)
dungeonBtn:SetPoint("TOP", content, "TOP", 0, yOffset)
dungeonBtn:SetWidth(210)
dungeonBtn:SetHeight(16)
-- Create text for the button
local btnText = dungeonBtn:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
btnText:SetPoint("LEFT", dungeonBtn, "LEFT", 0, 0)
btnText:SetWidth(210)
btnText:SetJustifyH("LEFT")
dungeonBtn:SetFontString(btnText)
-- Color based on player level vs dungeon range
local colorCode = "|cFFFFFFFF" -- White (default)
if playerLevel >= dungeon.minLevel and playerLevel <= dungeon.maxLevel then
colorCode = "|cFF00FF00" -- Green (perfect fit)
elseif playerLevel >= dungeon.minLevel - 3 and playerLevel <= dungeon.maxLevel + 3 then
colorCode = "|cFFFFFF00" -- Yellow (close fit)
elseif playerLevel < dungeon.minLevel then
colorCode = "|cFFFF4040" -- Red (too low)
else
colorCode = "|cFF808080" -- Gray (too high)
end
-- Format player count text
local playerText = dungeon.score == 1 and "1 Player" or string.format("%d Players", dungeon.score)
btnText:SetText(string.format("%s%s|r (%d-%d) - |cFF888888%s|r",
colorCode, dungeon.name, dungeon.minLevel, dungeon.maxLevel, playerText))
-- Highlight on hover
local highlight = dungeonBtn:CreateTexture(nil, "BACKGROUND")
highlight:SetAllPoints()
highlight:SetTexture(1, 1, 1, 0.1)
dungeonBtn:SetHighlightTexture(highlight)
-- Store dungeon info directly on the button
dungeonBtn.dungeonName = dungeon.name
dungeonBtn.minLevel = dungeon.minLevel
dungeonBtn.maxLevel = dungeon.maxLevel
dungeonBtn.score = dungeon.score
-- Click handler - show dungeon details dialog
dungeonBtn:SetScript("OnClick", function()
IGW:ShowDungeonDetails(this.dungeonName, this.minLevel, this.maxLevel, this.score)
end)
yOffset = yOffset - 16
displayed = displayed + 1
end
end
if displayed == 0 then
local noSuggestions = content:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
noSuggestions:SetPoint("TOP", content, "TOP", 0, -20)
noSuggestions:SetWidth(210)
noSuggestions:SetJustifyH("CENTER")
noSuggestions:SetText("No suitable dungeons found")
noSuggestions:SetTextColor(0.7, 0.7, 0.7)
end
end
-- Show dungeon details dialog
function IGW:ShowDungeonDetails(dungeonName, minLevel, maxLevel, score)
if not dungeonName or not DUNGEON_DETAILS[dungeonName] then return end
local details = DUNGEON_DETAILS[dungeonName]
-- Create or reuse dialog
if not IGW.dungeonDialog then
local dialog = CreateFrame("Frame", "IGW_DungeonDialog", UIParent)
dialog:SetWidth(350)
dialog:SetHeight(220)
dialog:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
dialog:SetFrameStrata("DIALOG")
dialog:SetBackdrop({
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
tile = true,
tileSize = 32,
edgeSize = 32,
insets = { left = 11, right = 12, top = 12, bottom = 11 }
})
dialog:SetBackdropColor(0, 0, 0, 1)
dialog:EnableMouse(true)
dialog:SetMovable(true)
dialog:RegisterForDrag("LeftButton")
dialog:SetScript("OnDragStart", function() this:StartMoving() end)
dialog:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
-- Background loading screen texture (scaled UP by 75% = 175% total)
local bgTexture = dialog:CreateTexture(nil, "BACKGROUND")
bgTexture:SetPoint("TOPLEFT", dialog, "TOPLEFT", 11, -12)
bgTexture:SetPoint("BOTTOMRIGHT", dialog, "BOTTOMRIGHT", -12, 11)
-- Crop the texture to show center portion at 175% zoom
-- To show center at 1.75x zoom, we need to show the middle 57.1% of the texture (1/1.75 = 0.571)
local cropAmount = (1 - (1 / 1.75)) / 2 -- 0.214 on each side
bgTexture:SetTexCoord(cropAmount, 1 - cropAmount, cropAmount, 1 - cropAmount)
dialog.bgTexture = bgTexture
-- Dark overlay for readability (35% opacity)
local overlay = dialog:CreateTexture(nil, "BORDER")
overlay:SetPoint("TOPLEFT", dialog, "TOPLEFT", 11, -12)
overlay:SetPoint("BOTTOMRIGHT", dialog, "BOTTOMRIGHT", -12, 11)
overlay:SetTexture(0, 0, 0, 0.35)
-- Title
local title = dialog:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
title:SetPoint("TOP", dialog, "TOP", 0, -20)
title:SetWidth(310)
dialog.title = title
-- Location
local location = dialog:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
location:SetPoint("TOP", title, "BOTTOM", 0, -8)
location:SetWidth(310)
location:SetTextColor(0.8, 0.8, 0.8)
dialog.location = location
-- Level range
local levelRange = dialog:CreateFontString(nil, "OVERLAY", "GameFontNormal")
levelRange:SetPoint("TOP", location, "BOTTOM", 0, -10)
levelRange:SetWidth(310)
dialog.levelRange = levelRange
-- Player count
local playerCount = dialog:CreateFontString(nil, "OVERLAY", "GameFontNormal")
playerCount:SetPoint("TOP", levelRange, "BOTTOM", 0, -5)
playerCount:SetWidth(310)
playerCount:SetTextColor(0.5, 1.0, 0.5)
dialog.playerCount = playerCount
-- Description
local desc = dialog:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
desc:SetPoint("TOP", playerCount, "BOTTOM", 0, -15)
desc:SetWidth(310)
desc:SetJustifyH("CENTER")
desc:SetTextColor(1, 1, 1)
dialog.desc = desc
-- Bosses label
local bossesLabel = dialog:CreateFontString(nil, "OVERLAY", "GameFontNormal")
bossesLabel:SetPoint("TOP", desc, "BOTTOM", 0, -15)
bossesLabel:SetWidth(310)
bossesLabel:SetText("Bosses:")
bossesLabel:SetTextColor(1, 0.82, 0)
-- Bosses
local bosses = dialog:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
bosses:SetPoint("TOP", bossesLabel, "BOTTOM", 0, -5)
bosses:SetWidth(310)
bosses:SetJustifyH("CENTER")
bosses:SetTextColor(0.9, 0.9, 0.9)
dialog.bosses = bosses
-- Close button
local closeBtn = CreateFrame("Button", nil, dialog, "UIPanelCloseButton")
closeBtn:SetPoint("TOPRIGHT", dialog, "TOPRIGHT", -5, -5)
-- Make ESC key close the dialog
table.insert(UISpecialFrames, "IGW_DungeonDialog")
dialog:Hide()
IGW.dungeonDialog = dialog
end
local dialog = IGW.dungeonDialog
-- Update loading screen background
-- First clear any existing texture, then set new one
local texture = DUNGEON_TEXTURES[dungeonName] or DEFAULT_LOADING_SCREEN
dialog.bgTexture:SetTexture(texture)
-- Get full display name if available
local displayName = DUNGEON_FULL_NAMES[dungeonName] or dungeonName
-- Update dialog content
dialog.title:SetText(displayName)
dialog.location:SetText(details.location .. " (" .. details.faction .. ")")
dialog.levelRange:SetText("Levels: " .. minLevel .. "-" .. maxLevel)
local playerText = score == 1 and "1 player online" or score .. " players online"
dialog.playerCount:SetText(playerText)
dialog.desc:SetText(details.description)
dialog.bosses:SetText(details.bosses)
dialog:Show()
end
-- Toggle Guild Info window (left-side companion window; empty for now)
function IGW:ToggleGuildInfoWindow()
if not frame then return end
-- Create if needed
if not IGW.infoFrame then
local gf = CreateFrame("Frame", "IGW_GuildInfoFrame", UIParent)
IGW_AddToSpecialFrames("IGW_GuildInfoFrame", true)
gf:SetWidth(250)
gf:SetHeight(frame:GetHeight())
gf:SetFrameStrata("HIGH")
gf:SetFrameLevel(30)
-- Only allow moving if setting is enabled
gf:SetMovable((ImprovedGuildWindowDB and ImprovedGuildWindowDB.allowMoveSideWindows) or false)
gf:EnableMouse(true)
gf:SetClampedToScreen(true)
gf:Hide()
-- Background (border only, solid texture added separately)
gf:SetBackdrop({
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
edgeSize = 32,
insets = { left = 11, right = 12, top = 12, bottom = 11 }
})
gf:SetBackdropBorderColor(1, 1, 1, 1)
-- Create solid background texture (inside the border)
local gfBgTexture = gf:CreateTexture(nil, "BACKGROUND")
gfBgTexture:SetPoint("TOPLEFT", gf, "TOPLEFT", 11, -12)
gfBgTexture:SetPoint("BOTTOMRIGHT", gf, "BOTTOMRIGHT", -12, 11)
-- Use saved color or default to dark grey
local bgColor = {r = 0.15, g = 0.15, b = 0.15}
if ImprovedGuildWindowDB and ImprovedGuildWindowDB.bgColor then
bgColor = ImprovedGuildWindowDB.bgColor
end
gfBgTexture:SetTexture(bgColor.r, bgColor.g, bgColor.b, IGW_BG_OPACITY)
gf.bgTexture = gfBgTexture
-- Title bar
local titleBar = CreateFrame("Frame", nil, gf)
titleBar:SetPoint("TOPLEFT", gf, "TOPLEFT", 12, -12)
titleBar:SetPoint("TOPRIGHT", gf, "TOPRIGHT", -12, -12)
titleBar:SetHeight(30)
titleBar:SetFrameLevel(gf:GetFrameLevel() + 1)
titleBar:EnableMouse(true)
titleBar:RegisterForDrag("LeftButton")
titleBar:SetScript("OnDragStart", function()
if gf:IsMovable() then
gf:StartMoving()
end
end)
titleBar:SetScript("OnDragStop", function()
if gf:IsMovable() then
gf:StopMovingOrSizing()
end
end)
local title = titleBar:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
title:SetPoint("CENTER", titleBar, "CENTER", 0, 0)
title:SetText("Guild Info")
gf.title = title
local closeBtn = CreateFrame("Button", nil, gf, "UIPanelCloseButton")
closeBtn:SetPoint("TOPRIGHT", gf, "TOPRIGHT", -10, -10)
closeBtn:SetFrameLevel(gf:GetFrameLevel() + 2)
closeBtn:SetScript("OnClick", function() gf:Hide() end)
-- Content area
local content = CreateFrame("Frame", nil, gf)
content:SetPoint("TOPLEFT", gf, "TOPLEFT", 20, -50)
content:SetPoint("BOTTOMRIGHT", gf, "BOTTOMRIGHT", -20, 20)
gf.content = content
-- Guild name (large, centered)
local guildNameText = content:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
guildNameText:SetPoint("TOP", content, "TOP", 0, 0)
guildNameText:SetJustifyH("CENTER")
guildNameText:SetText("Guild")
gf.guildNameText = guildNameText
-- Guild Members (excluding alts)
local totalMembersText = content:CreateFontString(nil, "OVERLAY", "GameFontNormal")
totalMembersText:SetPoint("TOP", guildNameText, "BOTTOM", 0, -10)
totalMembersText:SetJustifyH("CENTER")
totalMembersText:SetText("Guild Members: 0")
gf.totalMembersText = totalMembersText
-- Guild Member Alts (members with "alt" in rank)
local totalAltsText = content:CreateFontString(nil, "OVERLAY", "GameFontNormal")
totalAltsText:SetPoint("TOP", totalMembersText, "BOTTOM", 0, -5)
totalAltsText:SetJustifyH("CENTER")
totalAltsText:SetText("Guild Member Alts: 0")
gf.totalAltsText = totalAltsText
-- Divider 1 (after member counts)
local div1 = content:CreateTexture(nil, "ARTWORK")
div1:SetTexture("Interface\\DialogFrame\\UI-DialogBox-Divider")
div1:SetPoint("TOP", totalAltsText, "BOTTOM", 36, -12)
div1:SetWidth(300)
div1:SetHeight(16)
-- MOTD
local motdLabel = content:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
motdLabel:SetPoint("TOP", div1, "BOTTOM", -36, -10)
motdLabel:SetJustifyH("CENTER")
motdLabel:SetText("Message of the Day")
gf.motdLabel = motdLabel
local motdValue = content:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
motdValue:SetPoint("TOP", motdLabel, "BOTTOM", 0, -6)
motdValue:SetWidth(210)
motdValue:SetJustifyH("CENTER")
motdValue:SetText("—")
gf.motdValue = motdValue
-- Divider 2 (after MOTD)
local div2 = content:CreateTexture(nil, "ARTWORK")
div2:SetTexture("Interface\\DialogFrame\\UI-DialogBox-Divider")
div2:SetPoint("TOP", motdValue, "BOTTOM", 36, -12)
div2:SetWidth(300)
div2:SetHeight(16)
-- Guild Information
local infoLabel = content:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
infoLabel:SetPoint("TOP", div2, "BOTTOM", -36, -10)
infoLabel:SetJustifyH("CENTER")
infoLabel:SetText("Guild Information")
gf.infoLabel = infoLabel
local infoValue = content:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
infoValue:SetPoint("TOP", infoLabel, "BOTTOM", 0, -6)
infoValue:SetWidth(210)
infoValue:SetJustifyH("CENTER")
infoValue:SetText("—")
gf.infoValue = infoValue
-- Page 2 content frame (hidden by default)
local content2 = CreateFrame("Frame", nil, gf)
content2:SetPoint("TOPLEFT", gf, "TOPLEFT", 20, -50)
content2:SetPoint("BOTTOMRIGHT", gf, "BOTTOMRIGHT", -20, 50)
content2:Hide()
gf.content2 = content2
-- Page 2 - Class Distribution
local classLabel = content2:CreateFontString(nil, "OVERLAY", "GameFontNormal")
classLabel:SetPoint("TOP", content2, "TOP", 0, 0)
classLabel:SetJustifyH("CENTER")
classLabel:SetText("Class Distribution")
classLabel:SetTextColor(1, 1, 1)
gf.classLabel = classLabel
-- Container for class bars (reduced height)
local classBarsContainer = CreateFrame("Frame", nil, content2)
classBarsContainer:SetPoint("TOP", classLabel, "BOTTOM", 0, -4)
classBarsContainer:SetWidth(210)
classBarsContainer:SetHeight(120)
gf.classBarsContainer = classBarsContainer
-- Store bars for updating
gf.classBars = {}
-- Divider before Level 60 Class Distribution
local page2Div1 = content2:CreateTexture(nil, "ARTWORK")
page2Div1:SetTexture("Interface\\DialogFrame\\UI-DialogBox-Divider")
page2Div1:SetPoint("TOP", classBarsContainer, "BOTTOM", 36, -8)
page2Div1:SetWidth(300)
page2Div1:SetHeight(16)
-- Level 60 Class Distribution
local class60Label = content2:CreateFontString(nil, "OVERLAY", "GameFontNormal")
class60Label:SetPoint("TOP", page2Div1, "BOTTOM", -36, -4)
class60Label:SetJustifyH("CENTER")
class60Label:SetText("Level 60 Class Distribution")
class60Label:SetTextColor(1, 1, 1)
gf.class60Label = class60Label
-- Container for level 60 class bars
local class60BarsContainer = CreateFrame("Frame", nil, content2)
class60BarsContainer:SetPoint("TOP", class60Label, "BOTTOM", 0, -4)
class60BarsContainer:SetWidth(210)
class60BarsContainer:SetHeight(120)
gf.class60BarsContainer = class60BarsContainer
-- Store bars for updating
gf.class60Bars = {}
-- Divider before Officers
local page2Div2 = content2:CreateTexture(nil, "ARTWORK")
page2Div2:SetTexture("Interface\\DialogFrame\\UI-DialogBox-Divider")
page2Div2:SetPoint("TOP", class60BarsContainer, "BOTTOM", 36, -8)
page2Div2:SetWidth(300)
page2Div2:SetHeight(16)
-- Officers Online
local officersLabel = content2:CreateFontString(nil, "OVERLAY", "GameFontNormal")
officersLabel:SetPoint("TOP", page2Div2, "BOTTOM", -36, -4)
officersLabel:SetJustifyH("CENTER")
officersLabel:SetText("Officers Online")
officersLabel:SetTextColor(1, 1, 1)
gf.officersLabel = officersLabel
local officersValue = content2:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
officersValue:SetPoint("TOP", officersLabel, "BOTTOM", 0, -4)
officersValue:SetWidth(210)
officersValue:SetJustifyH("CENTER")
officersValue:SetText("—")
gf.officersValue = officersValue
-- Page 3 content frame (hidden by default)
local content3 = CreateFrame("Frame", nil, gf)
content3:SetPoint("TOPLEFT", gf, "TOPLEFT", 20, -50)
content3:SetPoint("BOTTOMRIGHT", gf, "BOTTOMRIGHT", -20, 50)
content3:Hide()
gf.content3 = content3
-- Page 3 - Max Level Crafters Online
local craftersTitle = content3:CreateFontString(nil, "OVERLAY", "GameFontNormal")
craftersTitle:SetPoint("TOP", content3, "TOP", 0, 0)
craftersTitle:SetJustifyH("CENTER")
craftersTitle:SetText("Max Level Crafters Online")
gf.craftersTitle = craftersTitle
-- Instruction text (green)
local instructionText = content3:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
instructionText:SetPoint("TOP", craftersTitle, "BOTTOM", 0, -6)
instructionText:SetJustifyH("CENTER")
instructionText:SetTextColor(0, 1, 0) -- Green
instructionText:SetText("Click Player to Whisper")
-- Fixed content area for crafters (not scrollable, centered)
local craftersContent = CreateFrame("Frame", nil, content3)
craftersContent:SetPoint("TOP", instructionText, "BOTTOM", 0, -6)
craftersContent:SetWidth(210)
craftersContent:SetPoint("BOTTOM", content3, "BOTTOM", 0, 50)
gf.craftersContent = craftersContent
-- Page 4 content frame (hidden by default)
local content4 = CreateFrame("Frame", nil, gf)
content4:SetPoint("TOPLEFT", gf, "TOPLEFT", 20, -50)
content4:SetPoint("BOTTOMRIGHT", gf, "BOTTOMRIGHT", -20, 50)
content4:Hide()
gf.content4 = content4
-- Page 4 - Suggested Dungeons
local dungeonsTitle = content4:CreateFontString(nil, "OVERLAY", "GameFontNormal")
dungeonsTitle:SetPoint("TOP", content4, "TOP", 0, 0)
dungeonsTitle:SetJustifyH("CENTER")
dungeonsTitle:SetText("Suggested Dungeons")
dungeonsTitle:SetTextColor(1, 1, 1)
gf.dungeonsTitle = dungeonsTitle
local dungeonsInstruction = content4:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
dungeonsInstruction:SetPoint("TOP", dungeonsTitle, "BOTTOM", 0, -4)
dungeonsInstruction:SetWidth(210)
dungeonsInstruction:SetJustifyH("CENTER")
dungeonsInstruction:SetText("Based on online guild members' levels")
dungeonsInstruction:SetTextColor(0.8, 0.8, 0.8)
gf.dungeonsInstruction = dungeonsInstruction
-- Container for dungeons (no scroll, shows all matching)
local dungeonsContent = CreateFrame("Frame", nil, content4)
dungeonsContent:SetPoint("TOPLEFT", dungeonsInstruction, "BOTTOMLEFT", 0, -8)
dungeonsContent:SetPoint("TOPRIGHT", dungeonsInstruction, "BOTTOMRIGHT", 0, -8)
dungeonsContent:SetPoint("BOTTOM", content4, "BOTTOM", 0, 50)
gf.dungeonsContent = dungeonsContent
-- Pagination buttons at bottom (match tab button styling)
local buttonHeight = 25 -- Match tab button height
local prevBtn = CreateFrame("Button", nil, gf, "UIPanelButtonTemplate")
prevBtn:SetPoint("BOTTOMLEFT", gf, "BOTTOMLEFT", 15, 15)
prevBtn:SetWidth(30)
prevBtn:SetHeight(buttonHeight)
prevBtn:SetText("<")
prevBtn:SetScript("OnClick", function()
if gf.currentPage == 2 then
gf.currentPage = 1
gf.content:Show()
gf.content2:Hide()
gf.content3:Hide()
gf.content4:Hide()
gf.pageIndicator:SetText("Page 1 / 4")
elseif gf.currentPage == 3 then
gf.currentPage = 2
gf.content:Hide()
gf.content2:Show()
gf.content3:Hide()
gf.content4:Hide()
gf.pageIndicator:SetText("Page 2 / 4")
elseif gf.currentPage == 4 then
gf.currentPage = 3
gf.content:Hide()
gf.content2:Hide()
gf.content3:Show()
gf.content4:Hide()
IGW:UpdateCraftersPage()
gf.pageIndicator:SetText("Page 3 / 4")
end
end)
gf.prevBtn = prevBtn
local nextBtn = CreateFrame("Button", nil, gf, "UIPanelButtonTemplate")
nextBtn:SetPoint("BOTTOMRIGHT", gf, "BOTTOMRIGHT", -15, 15)
nextBtn:SetWidth(30)
nextBtn:SetHeight(buttonHeight)
nextBtn:SetText(">")
nextBtn:SetScript("OnClick", function()
if gf.currentPage == 1 then
gf.currentPage = 2
gf.content:Hide()
gf.content2:Show()
gf.content3:Hide()
gf.content4:Hide()
gf.pageIndicator:SetText("Page 2 / 4")
elseif gf.currentPage == 2 then
gf.currentPage = 3
gf.content:Hide()
gf.content2:Hide()
gf.content3:Show()
gf.content4:Hide()
IGW:UpdateCraftersPage()
gf.pageIndicator:SetText("Page 3 / 4")
elseif gf.currentPage == 3 then
gf.currentPage = 4
gf.content:Hide()
gf.content2:Hide()
gf.content3:Hide()
gf.content4:Show()
IGW:UpdateDungeonsPage()
gf.pageIndicator:SetText("Page 4 / 4")
end
end)
gf.nextBtn = nextBtn
-- Page indicator (vertically centered with buttons)
local pageIndicator = gf:CreateFontString(nil, "OVERLAY", "GameFontNormal")
pageIndicator:SetPoint("BOTTOM", gf, "BOTTOM", 0, 15 + (buttonHeight / 2))
pageIndicator:SetText("Page 1 / 4")
gf.pageIndicator = pageIndicator
-- Initialize to page 1
gf.currentPage = 1
IGW.infoFrame = gf
end
local gf = IGW.infoFrame
if gf:IsVisible() then
gf:Hide()
-- Un-highlight tab4 button
frame.tab4:SetBackdropColor(0.2, 0.2, 0.2, 1)
frame.tab4Text:SetTextColor(0.7, 0.7, 0.7)
else
gf:ClearAllPoints()
gf:SetPoint("TOPRIGHT", frame, "TOPLEFT", -5, 0)
gf:SetHeight(frame:GetHeight())
-- Highlight tab4 button
frame.tab4:SetBackdropColor(0.5, 0.5, 0.5, 1)
frame.tab4Text:SetTextColor(1, 1, 1)
-- Update texture with saved color and opacity
if gf.bgTexture then
local bgColor = {r = 0.15, g = 0.15, b = 0.15}
if ImprovedGuildWindowDB and ImprovedGuildWindowDB.bgColor then
bgColor = ImprovedGuildWindowDB.bgColor
end
gf.bgTexture:SetTexture(bgColor.r, bgColor.g, bgColor.b, IGW_BG_OPACITY)
end
IGW:UpdateGuildInfoWindow()
gf:Show()
end
end
-- Toggle Options window
function IGW:ToggleOptionsWindow()
if not frame then return end
-- Create if needed
if not IGW.optionsFrame then
local of = CreateFrame("Frame", "IGW_OptionsFrame", UIParent)
of:SetWidth(650) -- Same width as main window
of:SetHeight(500) -- Same height as main window
of:SetFrameStrata("DIALOG")
of:SetFrameLevel(50)
of:SetMovable(true)
of:EnableMouse(true)
of:SetClampedToScreen(true)
of:Hide()
-- Add to special frames for ESC key support
IGW_AddToSpecialFrames("IGW_OptionsFrame", true)
-- Border first
of:SetBackdrop({
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
edgeSize = 32,
insets = { left = 11, right = 12, top = 12, bottom = 11 }
})
of:SetBackdropBorderColor(1, 1, 1, 1)
-- Create solid background texture (inside the border)
local bgTexture = of:CreateTexture(nil, "BACKGROUND")
bgTexture:SetPoint("TOPLEFT", of, "TOPLEFT", 11, -12)
bgTexture:SetPoint("BOTTOMRIGHT", of, "BOTTOMRIGHT", -12, 11)
-- Use saved color or default to dark grey, always 90% opaque for options
local bgColor = {r = 0.15, g = 0.15, b = 0.15}
if ImprovedGuildWindowDB and ImprovedGuildWindowDB.bgColor then
bgColor = ImprovedGuildWindowDB.bgColor
end
bgTexture:SetTexture(bgColor.r, bgColor.g, bgColor.b, 0.9)
of.bgTexture = bgTexture
-- Title bar
local titleBar = CreateFrame("Frame", nil, of)
titleBar:SetPoint("TOPLEFT", of, "TOPLEFT", 12, -12)
titleBar:SetPoint("TOPRIGHT", of, "TOPRIGHT", -12, -12)
titleBar:SetHeight(30)
titleBar:SetFrameLevel(of:GetFrameLevel() + 1)
titleBar:EnableMouse(true)
titleBar:RegisterForDrag("LeftButton")
titleBar:SetScript("OnDragStart", function() of:StartMoving() end)
titleBar:SetScript("OnDragStop", function() of:StopMovingOrSizing() end)
local title = titleBar:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
title:SetPoint("CENTER", titleBar, "CENTER", 0, 0)
title:SetText("Improved Guild Window - Options")
local closeBtn = CreateFrame("Button", nil, of, "UIPanelCloseButton")
closeBtn:SetPoint("TOPRIGHT", of, "TOPRIGHT", -10, -10)
closeBtn:SetFrameLevel(of:GetFrameLevel() + 2)
closeBtn:SetScript("OnClick", function() of:Hide() end)
-- Content area
local content = CreateFrame("Frame", nil, of)
content:SetPoint("TOPLEFT", of, "TOPLEFT", 20, -50)
content:SetPoint("BOTTOMRIGHT", of, "BOTTOMRIGHT", -20, 20)
of.content = content
local yOffset = 0
-- Visual Settings Section
local visualHeader = content:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
visualHeader:SetPoint("TOPLEFT", content, "TOPLEFT", 0, yOffset)
visualHeader:SetText("Visual Settings")
yOffset = yOffset - 25
-- Opacity Slider
local opacityLabel = content:CreateFontString(nil, "OVERLAY", "GameFontNormal")
opacityLabel:SetPoint("TOPLEFT", content, "TOPLEFT", 10, yOffset)
opacityLabel:SetText("Window Opacity:")
local opacitySlider = CreateFrame("Slider", "IGW_OpacitySlider", content, "OptionsSliderTemplate")
opacitySlider:SetPoint("TOPLEFT", opacityLabel, "BOTTOMLEFT", 0, -10)
opacitySlider:SetWidth(300)
opacitySlider:SetHeight(15)
opacitySlider:SetMinMaxValues(0.3, 1.0)
opacitySlider:SetValueStep(0.05)
opacitySlider:SetValue(IGW_BG_OPACITY)
getglobal("IGW_OpacitySliderLow"):SetText("30%")
getglobal("IGW_OpacitySliderHigh"):SetText("100%")
getglobal("IGW_OpacitySliderText"):SetText(string.format("%.0f%%", IGW_BG_OPACITY * 100))
opacitySlider:SetScript("OnValueChanged", function()
local value = this:GetValue()
getglobal("IGW_OpacitySliderText"):SetText(string.format("%.0f%%", value * 100))
-- Will be applied on save
end)
of.opacitySlider = opacitySlider
-- Color options label (to the right of slider, moved up)
local colorLabel = content:CreateFontString(nil, "OVERLAY", "GameFontNormal")
colorLabel:SetPoint("LEFT", opacitySlider, "RIGHT", 40, 30)
colorLabel:SetText("Background Color:")
-- Define 8 color options
local colorOptions = {
{r = 0.15, g = 0.15, b = 0.15, name = "Dark Grey"}, -- Default
{r = 0.05, g = 0.05, b = 0.05, name = "Black"},
{r = 0.1, g = 0.1, b = 0.2, name = "Dark Blue"},
{r = 0.2, g = 0.15, b = 0.1, name = "Dark Brown"},
{r = 0.15, g = 0.2, b = 0.15, name = "Dark Green"},
{r = 0.2, g = 0.1, b = 0.15, name = "Dark Purple"},
{r = 0.2, g = 0.15, b = 0.15, name = "Dark Red"},
{r = 0.1, g = 0.15, b = 0.15, name = "Dark Cyan"}
}
-- Current selected color (default to first option)
of.selectedColor = 1
-- Create clickable color boxes
local colorBoxes = {}
for i, color in ipairs(colorOptions) do
local box = CreateFrame("Button", nil, content)
box:SetPoint("LEFT", colorLabel, "BOTTOMLEFT", (i-1) * 32, -30)
box:SetWidth(30)
box:SetHeight(30)
-- Store color info on box
box.colorName = color.name
box.colorIndex = i
-- Border first
box:SetBackdrop({
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
edgeSize = 12,
insets = { left = 2, right = 2, top = 2, bottom = 2 }
})
box:SetBackdropBorderColor(0.5, 0.5, 0.5, 1)
-- Background color (inset to stay within border)
local bgTex = box:CreateTexture(nil, "BACKGROUND")
bgTex:SetPoint("TOPLEFT", box, "TOPLEFT", 4, -4)
bgTex:SetPoint("BOTTOMRIGHT", box, "BOTTOMRIGHT", -4, 4)
bgTex:SetTexture(color.r, color.g, color.b, 1)
box.bgTex = bgTex
-- Selection indicator (white border when selected)
box.selected = false
box:SetScript("OnClick", function()
-- Deselect all
for j, b in ipairs(colorBoxes) do
b:SetBackdropBorderColor(0.5, 0.5, 0.5, 1)
b.selected = false
end
-- Select this one
this:SetBackdropBorderColor(1, 1, 1, 1)
this.selected = true
of.selectedColor = this.colorIndex
end)
box:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_TOP")
GameTooltip:SetText(this.colorName)
GameTooltip:Show()
end)
box:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
colorBoxes[i] = box
end
-- Preselect the saved color or default to first
local savedColor = {r = 0.15, g = 0.15, b = 0.15}
if ImprovedGuildWindowDB and ImprovedGuildWindowDB.bgColor then
savedColor = ImprovedGuildWindowDB.bgColor
end
local selectedIndex = 1
for i, color in ipairs(colorOptions) do
-- Compare with small tolerance for floating point
local rMatch = math.abs(color.r - savedColor.r) < 0.01
local gMatch = math.abs(color.g - savedColor.g) < 0.01
local bMatch = math.abs(color.b - savedColor.b) < 0.01
if rMatch and gMatch and bMatch then
selectedIndex = i
break
end
end
colorBoxes[selectedIndex]:SetBackdropBorderColor(1, 1, 1, 1)
colorBoxes[selectedIndex].selected = true
of.selectedColor = selectedIndex
of.colorOptions = colorOptions
of.colorBoxes = colorBoxes
yOffset = yOffset - 65
-- Divider
local divider1 = content:CreateTexture(nil, "ARTWORK")
divider1:SetTexture("Interface\\DialogFrame\\UI-DialogBox-Divider")
divider1:SetPoint("TOP", content, "TOP", 103, yOffset)
divider1:SetWidth(830)
divider1:SetHeight(16)
yOffset = yOffset - 30
-- Window Behavior Section
local behaviorHeader = content:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
behaviorHeader:SetPoint("TOPLEFT", content, "TOPLEFT", 0, yOffset)
behaviorHeader:SetText("Window Behavior")
yOffset = yOffset - 25
-- Remember Open Windows Checkbox
local rememberWindowsCheck = CreateFrame("CheckButton", nil, content, "UICheckButtonTemplate")
rememberWindowsCheck:SetPoint("TOPLEFT", content, "TOPLEFT", 10, yOffset)
rememberWindowsCheck:SetWidth(24)
rememberWindowsCheck:SetHeight(24)
rememberWindowsCheck:SetChecked((ImprovedGuildWindowDB and ImprovedGuildWindowDB.rememberWindows) or false)
local rememberWindowsLabel = content:CreateFontString(nil, "OVERLAY", "GameFontNormal")
rememberWindowsLabel:SetPoint("LEFT", rememberWindowsCheck, "RIGHT", 5, 0)
rememberWindowsLabel:SetText("Remember which windows are open")
of.rememberWindowsCheck = rememberWindowsCheck
yOffset = yOffset - 25
-- Allow Moving Side Windows Checkbox
local allowMoveSideCheck = CreateFrame("CheckButton", nil, content, "UICheckButtonTemplate")
allowMoveSideCheck:SetPoint("TOPLEFT", content, "TOPLEFT", 10, yOffset)
allowMoveSideCheck:SetWidth(24)
allowMoveSideCheck:SetHeight(24)
allowMoveSideCheck:SetChecked((ImprovedGuildWindowDB and ImprovedGuildWindowDB.allowMoveSideWindows) or false)
local allowMoveSideLabel = content:CreateFontString(nil, "OVERLAY", "GameFontNormal")
allowMoveSideLabel:SetPoint("LEFT", allowMoveSideCheck, "RIGHT", 5, 0)
allowMoveSideLabel:SetText("Allow moving side windows (Member Details / Guild Info)")
of.allowMoveSideCheck = allowMoveSideCheck
yOffset = yOffset - 25
-- Show Offline by Default Checkbox
local showOfflineDefaultCheck = CreateFrame("CheckButton", nil, content, "UICheckButtonTemplate")
showOfflineDefaultCheck:SetPoint("TOPLEFT", content, "TOPLEFT", 10, yOffset)
showOfflineDefaultCheck:SetWidth(24)
showOfflineDefaultCheck:SetHeight(24)
showOfflineDefaultCheck:SetChecked((ImprovedGuildWindowDB and ImprovedGuildWindowDB.showOffline ~= false))
local showOfflineDefaultLabel = content:CreateFontString(nil, "OVERLAY", "GameFontNormal")
showOfflineDefaultLabel:SetPoint("LEFT", showOfflineDefaultCheck, "RIGHT", 5, 0)
showOfflineDefaultLabel:SetText("Show offline members by default (Member Details tab)")
of.showOfflineDefaultCheck = showOfflineDefaultCheck
yOffset = yOffset - 25
-- Remember Sorting Checkbox
local rememberSortingCheck = CreateFrame("CheckButton", nil, content, "UICheckButtonTemplate")
rememberSortingCheck:SetPoint("TOPLEFT", content, "TOPLEFT", 10, yOffset)
rememberSortingCheck:SetWidth(24)
rememberSortingCheck:SetHeight(24)
rememberSortingCheck:SetChecked((ImprovedGuildWindowDB and ImprovedGuildWindowDB.rememberSorting ~= false))
local rememberSortingLabel = content:CreateFontString(nil, "OVERLAY", "GameFontNormal")
rememberSortingLabel:SetPoint("LEFT", rememberSortingCheck, "RIGHT", 5, 0)
rememberSortingLabel:SetText("Remember sorting (column and direction)")
of.rememberSortingCheck = rememberSortingCheck
yOffset = yOffset - 30
-- Divider
local divider2 = content:CreateTexture(nil, "ARTWORK")
divider2:SetTexture("Interface\\DialogFrame\\UI-DialogBox-Divider")
divider2:SetPoint("TOP", content, "TOP", 103, yOffset)
divider2:SetWidth(830)
divider2:SetHeight(16)
yOffset = yOffset - 25
-- Default View Section
local defaultViewHeader = content:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
defaultViewHeader:SetPoint("TOPLEFT", content, "TOPLEFT", 0, yOffset)
defaultViewHeader:SetText("Default View")
yOffset = yOffset - 25
-- Default Tab Label
local defaultTabLabel = content:CreateFontString(nil, "OVERLAY", "GameFontNormal")
defaultTabLabel:SetPoint("TOPLEFT", content, "TOPLEFT", 10, yOffset)
defaultTabLabel:SetText("Default tab on open:")
yOffset = yOffset - 25
-- Radio Buttons for Default Tab
local guildMembersRadio = CreateFrame("CheckButton", nil, content, "UIRadioButtonTemplate")
guildMembersRadio:SetPoint("TOPLEFT", content, "TOPLEFT", 20, yOffset)
guildMembersRadio:SetWidth(20)
guildMembersRadio:SetHeight(20)
local guildMembersLabel = content:CreateFontString(nil, "OVERLAY", "GameFontNormal")
guildMembersLabel:SetPoint("LEFT", guildMembersRadio, "RIGHT", 5, 0)
guildMembersLabel:SetText("Guild Members (online only)")
yOffset = yOffset - 25
local memberDetailsRadio = CreateFrame("CheckButton", nil, content, "UIRadioButtonTemplate")
memberDetailsRadio:SetPoint("TOPLEFT", content, "TOPLEFT", 20, yOffset)
memberDetailsRadio:SetWidth(20)
memberDetailsRadio:SetHeight(20)
local memberDetailsLabel = content:CreateFontString(nil, "OVERLAY", "GameFontNormal")
memberDetailsLabel:SetPoint("LEFT", memberDetailsRadio, "RIGHT", 5, 0)
memberDetailsLabel:SetText("Notes & Rank (all members)")
-- Set initial radio button states
local defaultTab = (ImprovedGuildWindowDB and ImprovedGuildWindowDB.defaultTab) or "details"
if defaultTab == "details" then
guildMembersRadio:SetChecked(true)
memberDetailsRadio:SetChecked(false)
elseif defaultTab == "detailed" then
-- If set to detailed view, default to guild members for now (no radio option yet)
guildMembersRadio:SetChecked(true)
memberDetailsRadio:SetChecked(false)
else
guildMembersRadio:SetChecked(false)
memberDetailsRadio:SetChecked(true)
end
-- Radio button scripts
guildMembersRadio:SetScript("OnClick", function()
guildMembersRadio:SetChecked(true)
memberDetailsRadio:SetChecked(false)
end)
memberDetailsRadio:SetScript("OnClick", function()
guildMembersRadio:SetChecked(false)
memberDetailsRadio:SetChecked(true)
end)
of.guildMembersRadio = guildMembersRadio
of.memberDetailsRadio = memberDetailsRadio
-- Author Label
local authorLabel = content:CreateFontString(nil, "OVERLAY", "GameFontWhite")
authorLabel:SetPoint("BOTTOM", of, "BOTTOM", 0, 50)
authorLabel:SetText("Created by: Olzon")
-- Save Button
local saveBtn = CreateFrame("Button", nil, of, "UIPanelButtonTemplate")
saveBtn:SetPoint("BOTTOM", of, "BOTTOM", 0, 15)
saveBtn:SetWidth(100)
saveBtn:SetHeight(25)
saveBtn:SetText("Save")
saveBtn:SetScript("OnClick", function()
IGW:SaveOptions()
of:Hide()
end)
IGW.optionsFrame = of
end
local of = IGW.optionsFrame
if of:IsVisible() then
of:Hide()
else
-- Update background color before showing
if of.bgTexture then
local bgColor = {r = 0.15, g = 0.15, b = 0.15}
if ImprovedGuildWindowDB and ImprovedGuildWindowDB.bgColor then
bgColor = ImprovedGuildWindowDB.bgColor
end
of.bgTexture:SetTexture(bgColor.r, bgColor.g, bgColor.b, 0.9)
end
-- Anchor top of options window to top of main window
of:ClearAllPoints()
of:SetPoint("TOP", frame, "TOP", 0, 0)
of:Show()
end
end
-- Save Options from settings window
function IGW:SaveOptions()
local of = IGW.optionsFrame
if not of then return end
-- Initialize DB if it doesn't exist
if not ImprovedGuildWindowDB then
ImprovedGuildWindowDB = {}
end
-- Save opacity
local opacityValue = of.opacitySlider:GetValue()
IGW_BG_OPACITY = opacityValue
ImprovedGuildWindowDB.opacity = opacityValue
-- Apply opacity and color to all windows immediately
local selectedColor = of.colorOptions and of.colorOptions[of.selectedColor or 1]
if selectedColor then
if frame and frame.bgTexture then
frame.bgTexture:SetTexture(selectedColor.r, selectedColor.g, selectedColor.b, opacityValue)
end
if IGW.detailsFrame and IGW.detailsFrame.bgTexture then
IGW.detailsFrame.bgTexture:SetTexture(selectedColor.r, selectedColor.g, selectedColor.b, opacityValue)
end
if IGW.infoFrame and IGW.infoFrame.bgTexture then
IGW.infoFrame.bgTexture:SetTexture(selectedColor.r, selectedColor.g, selectedColor.b, opacityValue)
end
-- Apply to options window (always 90% opaque)
if of and of.bgTexture then
of.bgTexture:SetTexture(selectedColor.r, selectedColor.g, selectedColor.b, 0.9)
end
-- Save background color
ImprovedGuildWindowDB.bgColor = {r = selectedColor.r, g = selectedColor.g, b = selectedColor.b}
end
-- Save remember windows setting
ImprovedGuildWindowDB.rememberWindows = of.rememberWindowsCheck:GetChecked() == 1
-- Save allow moving side windows setting
ImprovedGuildWindowDB.allowMoveSideWindows = of.allowMoveSideCheck:GetChecked() == 1
-- Apply movable state to side windows immediately
if IGW.detailsFrame then
IGW.detailsFrame:SetMovable(ImprovedGuildWindowDB.allowMoveSideWindows)
end
if IGW.infoFrame then
IGW.infoFrame:SetMovable(ImprovedGuildWindowDB.allowMoveSideWindows)
end
-- Save show offline default setting and apply immediately
ImprovedGuildWindowDB.showOffline = of.showOfflineDefaultCheck:GetChecked() == 1
showOffline = ImprovedGuildWindowDB.showOffline
if frame and frame.offlineCheck then
frame.offlineCheck:SetChecked(showOffline)
end
-- Save remember sorting setting
ImprovedGuildWindowDB.rememberSorting = of.rememberSortingCheck:GetChecked() == 1
-- Save which windows are currently open (if remember is enabled)
if ImprovedGuildWindowDB.rememberWindows then
ImprovedGuildWindowDB.windowStates = {
detailsOpen = IGW.detailsFrame and IGW.detailsFrame:IsVisible() or false,
infoOpen = IGW.infoFrame and IGW.infoFrame:IsVisible() or false
}
end
-- Save default tab
if of.guildMembersRadio:GetChecked() then
ImprovedGuildWindowDB.defaultTab = "details"
else
ImprovedGuildWindowDB.defaultTab = "roster"
end
DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00Options saved!|r")
end
-- Slash command
SLASH_IMPROVEDGUILDWINDOW1 = "/igw"
SLASH_IMPROVEDGUILDWINDOW2 = "/improvedguild"
SlashCmdList["IMPROVEDGUILDWINDOW"] = function(msg)
msg = string.lower(msg or "")
if msg == "" or msg == "show" or msg == "toggle" then
IGW:ToggleWindow()
elseif msg == "hide" then
frame:Hide()
elseif msg == "debug" then
DEFAULT_CHAT_FRAME:AddMessage("=== IGW Debug Info ===")
DEFAULT_CHAT_FRAME:AddMessage("Total roster data: " .. table.getn(rosterData))
DEFAULT_CHAT_FRAME:AddMessage("Current filterRank: " .. filterRank)
DEFAULT_CHAT_FRAME:AddMessage("Num ranks: " .. GuildControlGetNumRanks())
-- Count members by rank
local rankCounts = {}
for i = 0, 9 do rankCounts[i] = 0 end
for i, m in ipairs(rosterData) do
if m.rankIndex then
rankCounts[m.rankIndex] = (rankCounts[m.rankIndex] or 0) + 1
end
end
DEFAULT_CHAT_FRAME:AddMessage("Members per rank:")
for i = 1, GuildControlGetNumRanks() do
local rankName = GuildControlGetRankName(i)
local rankIdx = i - 1
DEFAULT_CHAT_FRAME:AddMessage(string.format(" %s (idx %d): %d members",
rankName, rankIdx, rankCounts[rankIdx] or 0))
end
DEFAULT_CHAT_FRAME:AddMessage("First 5 members:")
for i = 1, math.min(5, table.getn(rosterData)) do
local m = rosterData[i]
DEFAULT_CHAT_FRAME:AddMessage(string.format(" %s: rank=%s, rankIndex=%s",
m.name or "nil", m.rank or "nil", tostring(m.rankIndex or "nil")))
end
else
DEFAULT_CHAT_FRAME:AddMessage("|cFF00FF00Improved Guild Window Commands:|r")
DEFAULT_CHAT_FRAME:AddMessage("/igw - Toggle window")
DEFAULT_CHAT_FRAME:AddMessage("/igw show - Show window")
DEFAULT_CHAT_FRAME:AddMessage("/igw hide - Hide window")
DEFAULT_CHAT_FRAME:AddMessage("/igw debug - Show debug info")
end
end
-- Global entry point for other addon files (e.g. calendar switching back)
function IGW_ToggleWindow()
IGW:ToggleWindow()
end
-- Initialize addon
IGW:OnLoad()
-- Keybind note: Users can set Shift+O in Key Bindings menu under "Improved Guild Window"