-- Improved Guild Window for WoW 1.12.1 / Turtle WoW -- Author: Travis local IGW = {} IGW.VERSION = "3.3" -- 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 officerRankCount = 3, -- Default: Guild Master + top 2 officer ranks (threshold = 2) showRaceFaction = true } 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 -- Ensure officerRankCount exists (for existing saved variables) if not ImprovedGuildWindowDB.officerRankCount then ImprovedGuildWindowDB.officerRankCount = 3 end -- Ensure showRaceFaction exists (for existing saved variables) if ImprovedGuildWindowDB.showRaceFaction == nil then ImprovedGuildWindowDB.showRaceFaction = true end -- Apply saved officer rank count: rankCount includes the GM (rank 0), -- so threshold = rankCount - 1 (e.g. 3 ranks → threshold 2 → indices 0,1,2) OFFICER_RANK_THRESHOLD = ImprovedGuildWindowDB.officerRankCount - 1 -- 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 local showRF = not (ImprovedGuildWindowDB and ImprovedGuildWindowDB.showRaceFaction == false) if showRF then 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"} } else headers = { {text = "Name", width = 175, column = "name"}, {text = "Level", width = 60, column = "level"}, {text = "Class", width = 75, column = "class"}, {text = "Location", width = 210, column = "zone"}, {text = "Rank", width = 80, column = "rank"} } end 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, rank local showRF = not (ImprovedGuildWindowDB and ImprovedGuildWindowDB.showRaceFaction == false) 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) if showRF then 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) else row.race:SetText("") row.faction:SetText("") end 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 & Faction (only if setting is enabled) local showRF = not (ImprovedGuildWindowDB and ImprovedGuildWindowDB.showRaceFaction == false) if showRF then local race = IGW_GetRace(member.officernote or "") row.race:SetText(race) row.race:SetTextColor(1, 1, 1) 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 else row.race:SetText("") row.faction:SetText("") 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(580) -- Increased to accommodate officer rank setting 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 - 20 -- 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 - 35 -- # of Officer Ranks in Guild Section local officerRankHeader = content:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge") officerRankHeader:SetPoint("TOPLEFT", content, "TOPLEFT", 0, yOffset) officerRankHeader:SetText("# of Officer Ranks in Guild") yOffset = yOffset - 28 local officerRankLabel = content:CreateFontString(nil, "OVERLAY", "GameFontNormal") officerRankLabel:SetPoint("TOPLEFT", content, "TOPLEFT", 10, yOffset) officerRankLabel:SetText("Officer ranks (including Guild Master):") local officerRankDropdown = CreateFrame("Button", "IGW_OfficerRankDropdown", content, "UIDropDownMenuTemplate") officerRankDropdown:SetPoint("LEFT", officerRankLabel, "RIGHT", -10, -2) UIDropDownMenu_SetWidth(60, officerRankDropdown) local savedOfficerRankCount = (ImprovedGuildWindowDB and ImprovedGuildWindowDB.officerRankCount) or 3 UIDropDownMenu_SetText(savedOfficerRankCount, officerRankDropdown) UIDropDownMenu_Initialize(officerRankDropdown, function() local counts = {2, 3, 4} for i = 1, table.getn(counts) do local info = {} info.text = counts[i] info.value = counts[i] info.func = function() local val = this.value UIDropDownMenu_SetText(val, officerRankDropdown) of.officerRankCount = val end UIDropDownMenu_AddButton(info) end end) -- Store current value on the frame for SaveOptions to read of.officerRankCount = savedOfficerRankCount of.officerRankDropdown = officerRankDropdown yOffset = yOffset - 35 -- Show Race/Faction columns checkbox local showRaceFactionCheck = CreateFrame("CheckButton", "IGW_ShowRaceFactionCheck", content, "UICheckButtonTemplate") showRaceFactionCheck:SetPoint("TOPLEFT", content, "TOPLEFT", 10, yOffset) showRaceFactionCheck:SetWidth(24) showRaceFactionCheck:SetHeight(24) local savedShowRF = (ImprovedGuildWindowDB and ImprovedGuildWindowDB.showRaceFaction ~= false) showRaceFactionCheck:SetChecked(savedShowRF and 1 or 0) local showRaceFactionLabel = content:CreateFontString(nil, "OVERLAY", "GameFontNormal") showRaceFactionLabel:SetPoint("LEFT", showRaceFactionCheck, "RIGHT", 5, 0) showRaceFactionLabel:SetText("Show Race & Faction columns (Guild Members tab)") of.showRaceFactionCheck = showRaceFactionCheck 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 - 15 -- 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 officer rank count and apply immediately if of.officerRankCount then ImprovedGuildWindowDB.officerRankCount = of.officerRankCount OFFICER_RANK_THRESHOLD = of.officerRankCount - 1 end -- Save show race/faction setting and refresh immediately if of.showRaceFactionCheck then ImprovedGuildWindowDB.showRaceFaction = of.showRaceFactionCheck:GetChecked() == 1 -- Rebuild column headers and repopulate so change takes effect without reload IGW:UpdateColumnHeaders(currentTab) IGW:UpdateRosterDisplay() end -- 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"