├── .gitignore ├── DifficultBulletinBoard.lua ├── DifficultBulletinBoard.toc ├── DifficultBulletinBoardDefaults.lua ├── DifficultBulletinBoardMainFrame.lua ├── DifficultBulletinBoardMainFrame.xml ├── DifficultBulletinBoardMessageProcessor.lua ├── DifficultBulletinBoardMinimapButton.xml ├── DifficultBulletinBoardOptionFrame.lua ├── DifficultBulletinBoardOptionFrame.xml ├── DifficultBulletinBoardPlayerScanner.lua ├── DifficultBulletinBoardVars.lua ├── DifficultBulletinBoardVersionCheck.lua ├── README.md ├── icons ├── UI-ChatIM-SizeGrabber-Down.tga ├── UI-ChatIM-SizeGrabber-Highlight.tga ├── UI-ChatIM-SizeGrabber-Up.tga ├── bell.tga ├── check_sign.tga ├── close.tga ├── close_light.tga ├── down.tga ├── druid_class_icon.tga ├── gradient_down.tga ├── hunter_class_icon.tga ├── icon.tga ├── mage_class_icon.tga ├── paladin_class_icon.tga ├── priest_class_icon.tga ├── rogue_class_icon.tga ├── shaman_class_icon.tga ├── warlock_class_icon.tga └── warrior_class_icon.tga └── images ├── grouplogs.png ├── groups.png ├── hardcore.png └── professions.png /.gitignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /DifficultBulletinBoard.lua: -------------------------------------------------------------------------------- 1 | -- DifficultBulletinBoard.lua 2 | -- Main addon file for Difficult Bulletin Board 3 | -- Handles event hooks, core functionality, and message filtering 4 | 5 | DifficultBulletinBoard = DifficultBulletinBoard or {} 6 | DifficultBulletinBoardVars = DifficultBulletinBoardVars or {} 7 | DifficultBulletinBoardDefaults = DifficultBulletinBoardDefaults or {} 8 | DifficultBulletinBoardOptionFrame = DifficultBulletinBoardOptionFrame or {} 9 | 10 | local lastFilteredMessages = {} 11 | local FILTER_LOG_TIMEOUT = 5 -- seconds 12 | 13 | local string_gfind = string.gmatch or string.gfind 14 | 15 | -- Fallback for string.match in Lua 5.0 (WoW Vanilla) 16 | if not string.match then 17 | function string.match(s, pattern) 18 | local _, _, c1, c2 = string.find(s, pattern) 19 | return c1, c2 20 | end 21 | end 22 | 23 | local mainFrame = DifficultBulletinBoardMainFrame 24 | local optionFrame = DifficultBulletinBoardOptionFrame 25 | local blacklistFrame = DifficultBulletinBoardBlacklistFrame 26 | 27 | -- Flag to track if the last processed message was matched 28 | local lastMessageWasMatched = false 29 | 30 | -- Track previous messages to handle filtering 31 | local previousMessages = {} 32 | 33 | 34 | 35 | -- Split input string into lowercase words for tag matching 36 | function DifficultBulletinBoard.SplitIntoLowerWords(input) 37 | local tags = {} 38 | 39 | -- iterate over words (separated by spaces) and insert them into the tags table 40 | for tag in string_gfind(input, "%S+") do 41 | table.insert(tags, string.lower(tag)) 42 | end 43 | 44 | return tags 45 | end 46 | 47 | -- Toggle the options frame visibility 48 | -- Modify the DifficultBulletinBoard_ToggleOptionFrame function to close the blacklist frame 49 | function DifficultBulletinBoard_ToggleOptionFrame() 50 | if optionFrame then 51 | if optionFrame:IsShown() then 52 | if DifficultBulletinBoardVars.optionFrameSound == "true" then 53 | PlaySound("igSpellBookClose"); 54 | end 55 | -- Hide all dropdowns before hiding the frame 56 | DifficultBulletinBoardOptionFrame.HideAllDropdownMenus() 57 | optionFrame:Hide() 58 | -- Also hide the blacklist frame when closing the option frame 59 | if blacklistFrame and blacklistFrame:IsShown() then 60 | blacklistFrame:Hide() 61 | end 62 | else 63 | if DifficultBulletinBoardVars.optionFrameSound == "true" then 64 | PlaySound("igSpellBookOpen"); 65 | end 66 | optionFrame:Show() 67 | mainFrame:Hide() 68 | end 69 | end 70 | end 71 | 72 | -- Toggle the main bulletin board frame 73 | -- Modify the DifficultBulletinBoard_ToggleMainFrame function to close the blacklist frame 74 | function DifficultBulletinBoard_ToggleMainFrame() 75 | if mainFrame then 76 | if mainFrame:IsShown() then 77 | if DifficultBulletinBoardVars.mainFrameSound == "true" then 78 | PlaySound("igQuestLogOpen"); 79 | end 80 | mainFrame:Hide() 81 | else 82 | if DifficultBulletinBoardVars.mainFrameSound == "true" then 83 | PlaySound("igQuestLogClose"); 84 | end 85 | -- Hide any open dropdowns when showing main frame 86 | if DifficultBulletinBoardOptionFrame.HideAllDropdownMenus then 87 | DifficultBulletinBoardOptionFrame.HideAllDropdownMenus() 88 | end 89 | -- Recalculate scroll ranges before showing frame to avoid flicker 90 | if DifficultBulletinBoardMainFrame.RefreshAllScrollRanges then 91 | DifficultBulletinBoardMainFrame.RefreshAllScrollRanges() 92 | end 93 | mainFrame:Show() 94 | optionFrame:Hide() 95 | -- Also hide the blacklist frame when opening the main frame 96 | if blacklistFrame and blacklistFrame:IsShown() then 97 | blacklistFrame:Hide() 98 | end 99 | end 100 | end 101 | end 102 | 103 | -- Start minimap button dragging when shift+click 104 | function DifficultBulletinBoard_DragMinimapStart() 105 | local button = DifficultBulletinBoard_MinimapButtonFrame 106 | 107 | if (IsShiftKeyDown()) and button then 108 | button:StartMoving() 109 | end 110 | end 111 | 112 | -- Stop minimap button dragging and save position 113 | function DifficultBulletinBoard_DragMinimapStop() 114 | local button = DifficultBulletinBoard_MinimapButtonFrame 115 | 116 | if button then 117 | button:StopMovingOrSizing() 118 | 119 | local x, y = button:GetCenter() 120 | button.db = button.db or {} 121 | button.db.posX = x 122 | button.db.posY = y 123 | end 124 | end 125 | 126 | -- Register slash commands 127 | SLASH_DIFFICULTBB1 = "/dbb" 128 | SlashCmdList["DIFFICULTBB"] = function(msg) 129 | -- Parse the first word as command and the rest as argument 130 | local command, arg = string.match(msg, "^(%S*)%s*(.*)$") 131 | if command == "expire" then 132 | local secs = tonumber(arg) 133 | if secs then 134 | -- Call expiration function on main frame 135 | DifficultBulletinBoard.ExpireMessages(secs) 136 | -- Remember manual expiration setting to reapply on new messages 137 | DifficultBulletinBoardVars.manualExpireSeconds = secs 138 | DEFAULT_CHAT_FRAME:AddMessage("[DBB] Expired messages older than " .. secs .. " seconds.") 139 | else 140 | DEFAULT_CHAT_FRAME:AddMessage("[DBB] Usage: /dbb expire ") 141 | end 142 | else 143 | -- Toggle main bulletin board frame 144 | DifficultBulletinBoard_ToggleMainFrame() 145 | end 146 | end 147 | 148 | 149 | 150 | -- Initialize addon when loaded 151 | local function initializeAddon(event, arg1) 152 | if event == "ADDON_LOADED" and arg1 == "DifficultBulletinBoard" then 153 | DifficultBulletinBoardVars.LoadSavedVariables() 154 | 155 | -- Create option frame first 156 | if DifficultBulletinBoardOptionFrame and DifficultBulletinBoardOptionFrame.InitializeOptionFrame then 157 | DifficultBulletinBoardOptionFrame.InitializeOptionFrame() 158 | else 159 | DEFAULT_CHAT_FRAME:AddMessage("|cFFFF0000[DBB Error]|r Failed to initialize option frame - module not loaded properly.") 160 | end 161 | 162 | -- Create main frame afterwards 163 | if DifficultBulletinBoardMainFrame and DifficultBulletinBoardMainFrame.InitializeMainFrame then 164 | DifficultBulletinBoardMainFrame.InitializeMainFrame() 165 | else 166 | DEFAULT_CHAT_FRAME:AddMessage("|cFFFF0000[DBB Error]|r Failed to initialize main frame - module not loaded properly.") 167 | end 168 | 169 | -- Install ChatFrame_OnEvent hook (complete replacement) 170 | if not DifficultBulletinBoard.hookInstalled then 171 | DifficultBulletinBoard.originalChatFrameOnEvent = ChatFrame_OnEvent 172 | ChatFrame_OnEvent = DifficultBulletinBoard.hookedChatFrameOnEvent 173 | DifficultBulletinBoard.hookInstalled = true 174 | end 175 | end 176 | end 177 | 178 | -- Advanced function to check if a message contains a keyword as a whole word 179 | local function messageContainsKeyword(message, keyword) 180 | -- Bail out on empty inputs 181 | if not message or not keyword or keyword == "" then 182 | return false 183 | end 184 | 185 | -- Convert both strings to lowercase for case-insensitive matching 186 | local lowerMessage = string.lower(message) 187 | local lowerKeyword = string.lower(keyword) 188 | 189 | -- Add spaces at beginning and end of message to help find boundaries 190 | lowerMessage = " " .. lowerMessage .. " " 191 | 192 | -- If keyword already contains punctuation, use more flexible matching for it 193 | if string.find(lowerKeyword, "[.,!?;:\"']") then 194 | -- 1. Exact match with spaces (standard case) 195 | if string.find(lowerMessage, " " .. lowerKeyword .. " ", 1, true) then 196 | return true 197 | end 198 | 199 | -- 2. At beginning of message 200 | if string.find(lowerMessage, "^ " .. lowerKeyword, 1, true) then 201 | return true 202 | end 203 | 204 | -- 3. At end of message 205 | if string.find(lowerMessage, " " .. lowerKeyword .. " $", 1, true) then 206 | return true 207 | end 208 | 209 | -- 4. With additional punctuation after (e.g. "members," followed by another punctuation) 210 | if string.find(lowerMessage, " " .. lowerKeyword .. "[.,!?;:\"']", 1, false) then 211 | return true 212 | end 213 | 214 | -- If we got here, no match found for the custom punctuated keyword 215 | return false 216 | end 217 | 218 | -- PATTERN MATCHING FOR REGULAR WORDS (without user-added punctuation) 219 | 220 | -- 1. Standard word match with spaces (most common case) 221 | if string.find(lowerMessage, " " .. lowerKeyword .. " ", 1, true) then 222 | return true 223 | end 224 | 225 | -- 2. Word followed by punctuation (catches "members," in text) 226 | local punctuation = "[.,!?;:\"'%-%)]" 227 | if string.find(lowerMessage, " " .. lowerKeyword .. punctuation, 1, false) then 228 | return true 229 | end 230 | 231 | -- 3. Word at start of message 232 | if string.find(lowerMessage, "^ " .. lowerKeyword .. "[ " .. punctuation .. "]", 1, false) then 233 | return true 234 | end 235 | 236 | -- 4. Word at end of message with possible punctuation 237 | if string.find(lowerMessage, " " .. lowerKeyword .. " $", 1, false) then 238 | return true 239 | end 240 | 241 | -- 5. Word with opening punctuation before it (less common) 242 | if string.find(lowerMessage, "[\"%('%-] *" .. lowerKeyword .. " ", 1, false) then 243 | return true 244 | end 245 | 246 | -- No whole word match found 247 | return false 248 | end 249 | 250 | -- Keyword management helper with punctuation support 251 | function DifficultBulletinBoard_AddKeywordToBlacklist(keyword) 252 | -- Skip empty keywords 253 | if not keyword or keyword == "" then 254 | return 255 | end 256 | 257 | -- Ensure the blacklist variable exists 258 | if not DifficultBulletinBoardSavedVariables.keywordBlacklist then 259 | DifficultBulletinBoardSavedVariables.keywordBlacklist = "" 260 | end 261 | 262 | -- Trim spaces from the keyword 263 | keyword = string.gsub(keyword, "^%s*(.-)%s*$", "%1") 264 | 265 | -- Check if keyword is already in the blacklist (exact match) 266 | local currentBlacklist = DifficultBulletinBoardSavedVariables.keywordBlacklist 267 | for existingKeyword in string.gmatch(currentBlacklist, "[^,]+") do 268 | existingKeyword = string.gsub(existingKeyword, "^%s*(.-)%s*$", "%1") -- Trim spaces 269 | if existingKeyword == keyword then 270 | DEFAULT_CHAT_FRAME:AddMessage("|cFFFFCC00[DBB]|r Keyword '" .. keyword .. "' is already in the blacklist.") 271 | return 272 | end 273 | end 274 | 275 | -- Add the keyword to the blacklist with a comma separator 276 | if currentBlacklist == "" then 277 | DifficultBulletinBoardSavedVariables.keywordBlacklist = keyword 278 | else 279 | DifficultBulletinBoardSavedVariables.keywordBlacklist = currentBlacklist .. "," .. keyword 280 | end 281 | 282 | -- Update UI elements with the new blacklist 283 | if DifficultBulletinBoard_SyncKeywordBlacklist then 284 | DifficultBulletinBoard_SyncKeywordBlacklist(DifficultBulletinBoardSavedVariables.keywordBlacklist) 285 | end 286 | 287 | DEFAULT_CHAT_FRAME:AddMessage("|cFFFFCC00[DBB]|r Added keyword '" .. keyword .. "' to blacklist.") 288 | end 289 | 290 | -- Replacement for ChatFrame_OnEvent that implements message filtering 291 | function DifficultBulletinBoard.hookedChatFrameOnEvent(event) 292 | local name = arg2 or "empty_name" 293 | local message = arg1 or "empty_message" 294 | 295 | -- This caused the CHAT_MSG_SYSTEM eventhandler to not work. 296 | -- CHAT_MSG_SYSTEM messages do not contain a name (arg2) and therefore never pass this check. 297 | --if not arg1 or not arg2 or arg2 == "" or arg2 == UnitName("player") or not arg9 then 298 | -- DifficultBulletinBoard.originalChatFrameOnEvent(event) 299 | -- return 300 | --end 301 | 302 | -- Create a unique identifier for this message 303 | local messageKey = name .. ":" .. message 304 | 305 | -- Check if we've recently logged this exact filtered message 306 | if lastFilteredMessages[messageKey] and lastFilteredMessages[messageKey] + FILTER_LOG_TIMEOUT > GetTime() then 307 | -- We've already logged this message recently, skip logging again 308 | if previousMessages[name] and previousMessages[name][3] then 309 | return -- Skip this message as it was marked for filtering 310 | end 311 | end 312 | 313 | -- Check if message contains any blacklisted keywords 314 | if event == "CHAT_MSG_CHANNEL" and DifficultBulletinBoardSavedVariables.keywordBlacklist and 315 | DifficultBulletinBoardSavedVariables.keywordBlacklist ~= "" then 316 | 317 | local keywordList = DifficultBulletinBoardSavedVariables.keywordBlacklist 318 | local hasMatchingKeyword = false 319 | local matchedKeyword = "" 320 | 321 | -- Split keywords by commas and check each one 322 | for keyword in string_gfind(keywordList, "[^,]+") do 323 | -- Trim whitespace 324 | keyword = string.gsub(keyword, "^%s*(.-)%s*$", "%1") 325 | 326 | -- Skip empty keywords 327 | if keyword ~= "" then 328 | if messageContainsKeyword(message, keyword) then 329 | matchedKeyword = keyword 330 | hasMatchingKeyword = true 331 | break 332 | end 333 | end 334 | end 335 | 336 | if hasMatchingKeyword then 337 | -- Only log if we haven't logged this message recently 338 | if not lastFilteredMessages[messageKey] or lastFilteredMessages[messageKey] + FILTER_LOG_TIMEOUT <= GetTime() then 339 | lastFilteredMessages[messageKey] = GetTime() 340 | end 341 | 342 | -- Mark message as filtered in the previous messages tracker 343 | if previousMessages[name] then 344 | previousMessages[name][3] = true 345 | else 346 | previousMessages[name] = {message, GetTime(), true, arg9} 347 | end 348 | 349 | return -- Skip this message as it contains a blacklisted keyword 350 | end 351 | end 352 | 353 | -- Only process chat channel messages 354 | if event == "CHAT_MSG_CHANNEL" then 355 | -- Check if we've seen this message before 356 | if not previousMessages[name] or previousMessages[name][1] ~= message or 357 | previousMessages[name][2] + 30 < GetTime() then 358 | 359 | -- Store the message with: [1]=content, [2]=timestamp, [3]=shouldFilter 360 | previousMessages[name] = {message, GetTime(), false, arg9} 361 | 362 | -- Process with our addon's message handler 363 | lastMessageWasMatched = DifficultBulletinBoard.OnChatMessage(message, name, arg9) 364 | 365 | -- If matched and filtering enabled, mark for filtering 366 | if lastMessageWasMatched and DifficultBulletinBoardVars.filterMatchedMessages == "true" then 367 | -- Only log if we haven't logged this message recently 368 | if not lastFilteredMessages[messageKey] or lastFilteredMessages[messageKey] + FILTER_LOG_TIMEOUT <= GetTime() then 369 | lastFilteredMessages[messageKey] = GetTime() 370 | end 371 | previousMessages[name][3] = true 372 | return -- Skip this message entirely 373 | end 374 | else 375 | -- This is a repeat message we've seen recently 376 | if previousMessages[name][3] then 377 | -- It was marked for filtering 378 | return -- Skip this message 379 | end 380 | end 381 | end 382 | 383 | if event == "CHAT_MSG_SYSTEM" then 384 | lastMessageWasMatched = DifficultBulletinBoard.OnSystemMessage(message) 385 | 386 | if lastMessageWasMatched and DifficultBulletinBoardVars.filterMatchedMessages == "true" then 387 | -- Only log if we haven't logged this message recently 388 | if not lastFilteredMessages[messageKey] or lastFilteredMessages[messageKey] + FILTER_LOG_TIMEOUT <= GetTime() then 389 | lastFilteredMessages[messageKey] = GetTime() 390 | end 391 | return -- Skip this message 392 | end 393 | end 394 | 395 | if event == "CHAT_MSG_HARDCORE" then 396 | lastMessageWasMatched = DifficultBulletinBoard.OnChatMessage(message, name, "HC") 397 | 398 | if lastMessageWasMatched and DifficultBulletinBoardVars.filterMatchedMessages == "true" then 399 | -- Only log if we haven't logged this message recently 400 | if not lastFilteredMessages[messageKey] or lastFilteredMessages[messageKey] + FILTER_LOG_TIMEOUT <= GetTime() then 401 | lastFilteredMessages[messageKey] = GetTime() 402 | end 403 | return -- Skip this message 404 | end 405 | end 406 | 407 | -- Call the original handler for non-filtered messages 408 | DifficultBulletinBoard.originalChatFrameOnEvent(event) 409 | end 410 | 411 | -- Event handler for registered events 412 | local function handleEvent() 413 | if event == "ADDON_LOADED" then 414 | initializeAddon(event, arg1) 415 | end 416 | 417 | -- The message filtering is now handled by the hookedChatFrameOnEvent function 418 | -- so we just need to handle the ADDON_LOADED event here 419 | end 420 | 421 | -- Initialize with the current server time 422 | local lastUpdateTime = GetTime() 423 | local lastCleanupTime = GetTime() 424 | 425 | -- OnUpdate handler for regular tasks 426 | local function OnUpdate() 427 | local currentTime = GetTime() 428 | local deltaTime = currentTime - lastUpdateTime 429 | 430 | -- Update only if at least 1 second has passed 431 | if deltaTime >= 1 then 432 | -- Update the lastUpdateTime 433 | lastUpdateTime = currentTime 434 | 435 | DifficultBulletinBoardMainFrame.UpdateServerTime() 436 | 437 | if DifficultBulletinBoardVars.timeFormat == "elapsed" then 438 | DifficultBulletinBoardMainFrame.UpdateElapsedTimes() 439 | end 440 | 441 | -- Auto-expiration of messages based on user setting (0 = disabled) 442 | local expireSecs = tonumber(DifficultBulletinBoardVars.messageExpirationTime) 443 | if expireSecs and expireSecs > 0 then 444 | DifficultBulletinBoard.ExpireMessages(expireSecs) 445 | end 446 | 447 | -- Clean up old message entries every 5 minutes 448 | if currentTime - lastCleanupTime > 300 then 449 | lastCleanupTime = currentTime 450 | local tempMessages = {} 451 | for sender, data in pairs(previousMessages) do 452 | if data[2] + 60 > GetTime() then 453 | tempMessages[sender] = data 454 | end 455 | end 456 | previousMessages = tempMessages 457 | end 458 | end 459 | end 460 | 461 | -- DifficultBulletinBoard Frame Linking System 462 | -- Links blacklist and option frames to move together 463 | DifficultBulletinBoard.FrameLinker = DifficultBulletinBoard.FrameLinker or {} 464 | local FrameLinker = DifficultBulletinBoard.FrameLinker 465 | 466 | -- Configuration - spacing between linked frames 467 | FrameLinker.FRAME_OFFSET_X = 1 -- Horizontal offset 468 | FrameLinker.FRAME_OFFSET_Y = 0 -- Vertical offset 469 | 470 | -- Create update frame for monitoring frame movement 471 | local linkUpdateFrame = CreateFrame("Frame") 472 | linkUpdateFrame:Hide() 473 | 474 | -- Track frame movement and update linked frame position 475 | linkUpdateFrame:SetScript("OnUpdate", function() 476 | local blacklistFrame = DifficultBulletinBoardBlacklistFrame 477 | local optionFrame = DifficultBulletinBoardOptionFrame 478 | 479 | -- Skip if either frame doesn't exist yet 480 | if not blacklistFrame or not optionFrame then 481 | return 482 | end 483 | 484 | -- Check if either frame is being moved 485 | if blacklistFrame.isMoving and optionFrame:IsShown() then 486 | -- Blacklist is moving, update option frame position 487 | optionFrame:ClearAllPoints() 488 | optionFrame:SetPoint("TOPRIGHT", blacklistFrame, "TOPLEFT", 489 | -FrameLinker.FRAME_OFFSET_X, FrameLinker.FRAME_OFFSET_Y) 490 | elseif optionFrame.isMoving and blacklistFrame:IsShown() then 491 | -- Option is moving, update blacklist frame position 492 | blacklistFrame:ClearAllPoints() 493 | blacklistFrame:SetPoint("TOPLEFT", optionFrame, "TOPRIGHT", 494 | FrameLinker.FRAME_OFFSET_X, FrameLinker.FRAME_OFFSET_Y) 495 | end 496 | end) 497 | 498 | -- Store original toggle function 499 | local originalToggleBlacklist = DifficultBulletinBoard_ToggleBlacklistFrame 500 | 501 | -- Override blacklist toggle function with robust error handling 502 | DifficultBulletinBoard_ToggleBlacklistFrame = function() 503 | -- Always hide the blacklist frame, never show it since we've removed the UI elements to open it 504 | if DifficultBulletinBoardBlacklistFrame and DifficultBulletinBoardBlacklistFrame:IsShown() then 505 | DifficultBulletinBoardBlacklistFrame:Hide() 506 | end 507 | end 508 | 509 | -- Start tracking frame movement 510 | linkUpdateFrame:Show() 511 | 512 | -- Register events and set up script handlers 513 | mainFrame:RegisterEvent("ADDON_LOADED") 514 | mainFrame:RegisterEvent("CHAT_MSG_CHANNEL") 515 | mainFrame:RegisterEvent("CHAT_MSG_HARDCORE") 516 | mainFrame:RegisterEvent("CHAT_MSG_SYSTEM") 517 | mainFrame:SetScript("OnEvent", handleEvent) 518 | mainFrame:SetScript("OnUpdate", OnUpdate) 519 | 520 | --make frames closable by pressing ESC 521 | UIPanelWindows["DifficultBulletinBoardMainFrame"] = { 522 | area = "center", 523 | pushable = 0, 524 | whileDead = true, 525 | } 526 | 527 | UIPanelWindows["DifficultBulletinBoardOptionFrame"] = { 528 | area = "center", 529 | pushable = 0, 530 | whileDead = true, 531 | } 532 | 533 | tinsert(UISpecialFrames, "DifficultBulletinBoardMainFrame") 534 | tinsert(UISpecialFrames, "DifficultBulletinBoardOptionFrame") -------------------------------------------------------------------------------- /DifficultBulletinBoard.toc: -------------------------------------------------------------------------------- 1 | ## Interface: 11200 2 | ## Title: Difficult Bulletin Board 3 | ## Notes: Bulletin Board for Turtle WoW 4 | ## SavedVariables: DifficultBulletinBoardSavedVariables 5 | ## Author: Difficult 6 | 7 | icon.tga 8 | resize.tga 9 | DifficultBulletinBoardDefaults.lua 10 | DifficultBulletinBoardVars.lua 11 | DifficultBulletinBoardVersionCheck.lua 12 | DifficultBulletinBoardMinimapButton.xml 13 | DifficultBulletinBoardOptionFrame.xml 14 | DifficultBulletinBoardMainFrame.xml 15 | DifficultBulletinBoardOptionFrame.lua 16 | DifficultBulletinBoardMessageProcessor.lua 17 | DifficultBulletinBoardMainFrame.lua 18 | DifficultBulletinBoardPlayerScanner.lua 19 | DifficultBulletinBoard.lua -------------------------------------------------------------------------------- /DifficultBulletinBoardDefaults.lua: -------------------------------------------------------------------------------- 1 | DifficultBulletinBoardDefaults = DifficultBulletinBoardDefaults or {} 2 | 3 | DifficultBulletinBoardDefaults.version = 3 4 | 5 | DifficultBulletinBoardDefaults.defaultFontSize = 11 6 | 7 | DifficultBulletinBoardDefaults.defaultServerTimePosition = "top-left" 8 | 9 | DifficultBulletinBoardDefaults.defaultTimeFormat = "fixed" 10 | 11 | DifficultBulletinBoardDefaults.defaultMainFrameSound = "true" 12 | 13 | DifficultBulletinBoardDefaults.defaultOptionFrameSound = "true" 14 | 15 | DifficultBulletinBoardDefaults.defaultFilterMatchedMessages = "false" 16 | 17 | DifficultBulletinBoardDefaults.defaultHardcoreOnly = "false" 18 | 19 | DifficultBulletinBoardDefaults.defaultMessageExpirationTime = "300" 20 | 21 | DifficultBulletinBoardDefaults.defaultNumberOfGroupPlaceholders = 3 22 | 23 | DifficultBulletinBoardDefaults.defaultGroupTopics = { 24 | { name = "Custom Topic", selected = false, tags = {} }, 25 | { name = "Upper Karazhan Halls", selected = true, tags = { "kara40", "ukh"} }, 26 | { name = "Naxxramas", selected = true, tags = { "naxxramas", "naxx" } }, 27 | { name = "Temple of Ahn'Qiraj", selected = true, tags = { "ahn'qiraj", "ahnqiraj", "aq40", "aq" } }, 28 | { name = "Emerald Sanctum", selected = true, tags = { "emerald", "sanctum", "es" } }, 29 | { name = "Blackwing Lair", selected = true, tags = { "blackwing", "bwl" } }, 30 | { name = "Lower Karazhan Halls", selected = true, tags = { "karazhan", "kara", "kara10" } }, 31 | { name = "Onyxia's Lair", selected = true, tags = { "onyxia", "ony" } }, 32 | { name = "Molten Core", selected = true, tags = { "molten", "mc" } }, 33 | { name = "Ruins of Ahn'Qiraj", selected = true, tags = { "ruins", "ahn'qiraj", "ahnqiraj", "aq20", "aq" } }, 34 | { name = "Zul'Gurub", selected = true, tags = { "zul'gurub", "zulgurub", "zg" } }, 35 | { name = "Stormwind Vault", selected = true, tags = { "vault", "swvault" } }, 36 | { name = "Caverns of Time: Black Morass", selected = true, tags = { "cot", "morass", "cavern", "cot:bm", "bm" } }, 37 | { name = "Karazhan Crypt", selected = true, tags = { "crypt", "kara", "karazhan" } }, 38 | { name = "Upper Blackrock Spire", selected = true, tags = { "ubrs", "blackrock", "upper", "spire" } }, 39 | { name = "Lower Blackrock Spire", selected = true, tags = { "lbrs", "blackrock", "lower", "spire" } }, 40 | { name = "Stratholme", selected = true, tags = { "strat", "stratholme" } }, 41 | { name = "Scholomance", selected = true, tags = { "scholo", "scholomance" } }, 42 | { name = "Dire Maul", selected = true, tags = { "dire", "maul", "dm", "dm:e", "dm:w", "dm:n", "dmw", "dmn", "dme" } }, 43 | { name = "Blackrock Depths", selected = true, tags = { "brd", "blackrock", "depths", "emp", "lava" } }, 44 | { name = "Hateforge Quarry", selected = true, tags = { "hateforge", "quarry", "hq", "hfq" } }, 45 | { name = "The Sunken Temple", selected = true, tags = { "st", "sunken", "temple" } }, 46 | { name = "Zul'Farrak", selected = true, tags = { "zf", "zul'farrak", "zulfarrak", "farrak" } }, 47 | { name = "Maraudon", selected = true, tags = { "mara", "maraudon" } }, 48 | { name = "Gilneas City", selected = true, tags = { "gilneas" } }, 49 | { name = "Uldaman", selected = true, tags = { "uldaman" } }, 50 | { name = "Razorfen Downs", selected = true, tags = { "razorfen", "downs", "rfd" } }, 51 | { name = "Scarlet Monastery", selected = true, tags = { "scarlet", "monastery", "sm", "armory", "cathedral", "cath", "library", "lib", "graveyard" } }, 52 | { name = "The Crescent Grove", selected = true, tags = { "crescent", "grove" } }, 53 | { name = "Razorfen Kraul", selected = true, tags = { "razorfen", "kraul" } }, 54 | { name = "Gnomeregan", selected = true, tags = { "gnomeregan", "gnomer" } }, 55 | { name = "The Stockade", selected = true, tags = { "stockade", "stockades", "stock", "stocks" } }, 56 | { name = "Blackfathom Deeps", selected = true, tags = { "bfd", "blackfathom" } }, 57 | { name = "Shadowfang Keep", selected = true, tags = { "sfk", "shadowfang" } }, 58 | { name = "The Deadmines", selected = true, tags = { "vc", "dm", "deadmine", "deadmines" } }, 59 | { name = "Wailing Caverns", selected = true, tags = { "wc", "wailing", "caverns" } }, 60 | { name = "Ragefire Chasm", selected = true, tags = { "rfc", "ragefire", "chasm" } } 61 | } 62 | 63 | DifficultBulletinBoardDefaults.defaultNumberOfProfessionPlaceholders = 5 64 | 65 | DifficultBulletinBoardDefaults.defaultProfessionTopics = { 66 | { name = "Alchemy", selected = true, tags = { "alchemist", "alchemy", "alch" } }, 67 | { name = "Blacksmithing", selected = true, tags = { "blacksmithing", "blacksmith", "bs" } }, 68 | { name = "Enchanting", selected = true, tags = { "enchanting", "enchanter", "enchant", "ench" } }, 69 | { name = "Engineering", selected = true, tags = { "engineering", "engineer", "eng" } }, 70 | { name = "Herbalism", selected = true, tags = { "herbalism", "herbalist", "herb" } }, 71 | { name = "Leatherworking", selected = true, tags = { "leatherworking", "leatherworker", "lw" } }, 72 | { name = "Mining", selected = true, tags = { "mining", "miner" } }, 73 | { name = "Tailoring", selected = true, tags = { "tailoring", "tailor" } }, 74 | { name = "Jewelcrafting", selected = true, tags = { "jewelcrafting", "jeweler", "jewel", "jc" } }, 75 | { name = "Cooking", selected = true, tags = { "cooking", "cook" } } 76 | } 77 | 78 | DifficultBulletinBoardDefaults.defaultNumberOfHardcorePlaceholders = 15 79 | 80 | DifficultBulletinBoardDefaults.defaultHardcoreTopics = { 81 | { name = "Deaths", selected = true, tags = { "tragedy"} }, 82 | { name = "Level Ups", selected = true, tags = { "reached", "inferno" } } 83 | } 84 | 85 | function DifficultBulletinBoardDefaults.deepCopy(original) 86 | local copy = {} 87 | for key, value in pairs(original) do 88 | if type(value) == "table" then 89 | copy[key] = DifficultBulletinBoardDefaults.deepCopy(value) 90 | else 91 | copy[key] = value 92 | end 93 | end 94 | 95 | return copy 96 | end -------------------------------------------------------------------------------- /DifficultBulletinBoardMainFrame.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 133 | 134 | 135 | 164 | 165 | 166 | 195 | 196 | 197 | 226 | 227 | 228 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | if not DifficultBulletinBoardMainFrame.isMoving then 316 | DifficultBulletinBoardMainFrame:StartMoving(); 317 | DifficultBulletinBoardMainFrame.isMoving = true; 318 | end 319 | 320 | 321 | if DifficultBulletinBoardMainFrame.isMoving then 322 | DifficultBulletinBoardMainFrame:StopMovingOrSizing(); 323 | DifficultBulletinBoardMainFrame.isMoving = false; 324 | end 325 | 326 | 327 | 328 | -------------------------------------------------------------------------------- /DifficultBulletinBoardMessageProcessor.lua: -------------------------------------------------------------------------------- 1 | -- DifficultBulletinBoardMessageProcessor.lua 2 | -- Message processing, filtering, and data management for Difficult Bulletin Board 3 | -- Handles chat message analysis, entry management, tooltips, and filtering 4 | 5 | -- Ensure global namespaces exist 6 | DifficultBulletinBoard = DifficultBulletinBoard or {} 7 | DifficultBulletinBoardVars = DifficultBulletinBoardVars or {} 8 | DifficultBulletinBoardMainFrame = DifficultBulletinBoardMainFrame or {} 9 | DifficultBulletinBoardMessageProcessor = DifficultBulletinBoardMessageProcessor or {} 10 | 11 | -- Local reference for string.gfind compatibility 12 | local string_gfind = string.gmatch or string.gfind 13 | 14 | -- Forward declarations for variables that will be set by the main frame 15 | local groupTopicPlaceholders 16 | local groupsLogsPlaceholders 17 | local professionTopicPlaceholders 18 | local hardcoreTopicPlaceholders 19 | local currentGroupsLogsFilter 20 | local MAX_GROUPS_LOGS_ENTRIES 21 | local RepackEntries 22 | local ReflowTopicEntries 23 | 24 | -- Initialize function to receive references from main frame 25 | function DifficultBulletinBoardMessageProcessor.Initialize(placeholders, filter, maxEntries, helpers) 26 | groupTopicPlaceholders = placeholders.groupTopicPlaceholders 27 | groupsLogsPlaceholders = placeholders.groupsLogsPlaceholders 28 | professionTopicPlaceholders = placeholders.professionTopicPlaceholders 29 | hardcoreTopicPlaceholders = placeholders.hardcoreTopicPlaceholders 30 | currentGroupsLogsFilter = filter 31 | MAX_GROUPS_LOGS_ENTRIES = maxEntries 32 | 33 | -- Store helper functions if provided 34 | if helpers then 35 | RepackEntries = helpers.RepackEntries 36 | ReflowTopicEntries = helpers.ReflowTopicEntries 37 | end 38 | end 39 | 40 | -- Apply filter to Groups Logs entries 41 | function DifficultBulletinBoardMessageProcessor.ApplyGroupsLogsFilter(searchText) 42 | -- Only filter if we have entries to filter 43 | if groupsLogsPlaceholders and groupsLogsPlaceholders["Group Logs"] then 44 | -- Process each entry 45 | for _, entry in ipairs(groupsLogsPlaceholders["Group Logs"]) do 46 | -- Check if this is a valid entry with name and message 47 | if entry and entry.nameButton and entry.messageFontString then 48 | local name = entry.nameButton:GetText() or "" 49 | local message = entry.messageFontString:GetText() or "" 50 | 51 | -- Skip placeholders 52 | if name == "-" or message == "-" then 53 | -- Do nothing for placeholder entries 54 | else 55 | -- If no search text, show everything 56 | if searchText == "" then 57 | entry.nameButton:SetAlpha(1.0) 58 | entry.messageFontString:SetAlpha(1.0) 59 | entry.timeFontString:SetAlpha(1.0) 60 | if entry.icon then entry.icon:SetAlpha(1.0) end 61 | else 62 | -- Prepare terms for comma-separated search 63 | local terms = {} 64 | for term in string_gfind(searchText, "[^,]+") do 65 | term = string.gsub(term, "^%s*(.-)%s*$", "%1") -- Trim whitespace 66 | table.insert(terms, term) 67 | end 68 | 69 | -- Convert entry text to lowercase 70 | local lowerName = string.lower(name) 71 | local lowerMessage = string.lower(message) 72 | 73 | -- Check all search terms 74 | local matches = false 75 | for _, term in ipairs(terms) do 76 | if term ~= "" and 77 | (string.find(lowerName, term, 1, true) or 78 | string.find(lowerMessage, term, 1, true)) then 79 | matches = true 80 | break 81 | end 82 | end 83 | 84 | -- Set visibility based on match result 85 | if matches then 86 | entry.nameButton:SetAlpha(1.0) 87 | entry.messageFontString:SetAlpha(1.0) 88 | entry.timeFontString:SetAlpha(1.0) 89 | if entry.icon then entry.icon:SetAlpha(1.0) end 90 | else 91 | entry.nameButton:SetAlpha(0.25) 92 | entry.messageFontString:SetAlpha(0.25) 93 | entry.timeFontString:SetAlpha(0.25) 94 | if entry.icon then entry.icon:SetAlpha(0.25) end 95 | end 96 | end 97 | end 98 | end 99 | end 100 | end 101 | end 102 | 103 | -- Function to reduce noise in messages and making matching easier 104 | local function replaceSymbolsWithSpace(inputString) 105 | return string.gsub(inputString, "[,/!%?.]", " ") 106 | end 107 | 108 | local function topicPlaceholdersContainsCharacterName(topicPlaceholders, topicName, characterName) 109 | local topicData = topicPlaceholders[topicName] 110 | if not topicData then 111 | return false, nil 112 | end 113 | 114 | for index, row in ipairs(topicData) do 115 | local nameColumn = row.nameButton 116 | 117 | if nameColumn:GetText() == characterName then 118 | return true, index 119 | end 120 | end 121 | 122 | return false, nil 123 | end 124 | 125 | local function getClassIconFromClassName(class) 126 | if class == "Druid" then 127 | return "Interface\\AddOns\\DifficultBulletinBoard\\icons\\druid_class_icon" 128 | elseif class == "Hunter" then 129 | return "Interface\\AddOns\\DifficultBulletinBoard\\icons\\hunter_class_icon" 130 | elseif class == "Mage" then 131 | return "Interface\\AddOns\\DifficultBulletinBoard\\icons\\mage_class_icon" 132 | elseif class == "Paladin" then 133 | return "Interface\\AddOns\\DifficultBulletinBoard\\icons\\paladin_class_icon" 134 | elseif class == "Priest" then 135 | return "Interface\\AddOns\\DifficultBulletinBoard\\icons\\priest_class_icon" 136 | elseif class == "Rogue" then 137 | return "Interface\\AddOns\\DifficultBulletinBoard\\icons\\rogue_class_icon" 138 | elseif class == "Shaman" then 139 | return "Interface\\AddOns\\DifficultBulletinBoard\\icons\\shaman_class_icon" 140 | elseif class == "Warlock" then 141 | return "Interface\\AddOns\\DifficultBulletinBoard\\icons\\warlock_class_icon" 142 | elseif class == "Warrior" then 143 | return "Interface\\AddOns\\DifficultBulletinBoard\\icons\\warrior_class_icon" 144 | else 145 | return nil 146 | end 147 | end 148 | 149 | -- Updates the specified placeholder for a topic with new name, message, and timestamp, 150 | -- then moves the updated entry to the top of the list, shifting other entries down. 151 | local function UpdateTopicEntryAndPromoteToTop(topicPlaceholders, topic, numberOfPlaceholders, channelName, name, message, index) 152 | local topicData = topicPlaceholders[topic] 153 | 154 | local timestamp 155 | if DifficultBulletinBoardVars.timeFormat == "elapsed" then 156 | timestamp = "00:00" 157 | else 158 | timestamp = date("%H:%M:%S") 159 | end 160 | 161 | -- Shift all entries down from index 1 to the updated entry's position 162 | for i = index, 2, -1 do 163 | topicData[i].nameButton:SetText(topicData[i - 1].nameButton:GetText()) 164 | topicData[i].messageFontString:SetText(topicData[i - 1].messageFontString:GetText()) 165 | topicData[i].timeFontString:SetText(topicData[i - 1].timeFontString:GetText()) 166 | topicData[i].creationTimestamp = topicData[i - 1].creationTimestamp 167 | topicData[i].icon:SetTexture(topicData[i - 1].icon:GetTexture()) 168 | end 169 | 170 | -- Place the updated entry's data at the top 171 | topicData[1].nameButton:SetText(name) 172 | -- Add this line to update hit rect for updated name 173 | topicData[1].nameButton:SetHitRectInsets(0, -45, 0, 0) 174 | 175 | topicData[1].messageFontString:SetText("[" .. channelName .. "] " .. message or "No Message") 176 | topicData[1].timeFontString:SetText(timestamp) 177 | topicData[1].creationTimestamp = date("%H:%M:%S") 178 | local class = DifficultBulletinBoardVars.GetPlayerClassFromDatabase(name) 179 | topicData[1].icon:SetTexture(getClassIconFromClassName(class)) 180 | 181 | -- Tooltip handlers are now dynamic and don't need manual updates 182 | 183 | -- Apply filter if this is a Groups Logs entry 184 | if topic == "Group Logs" and topicPlaceholders == groupsLogsPlaceholders then 185 | local currentFilter = "" 186 | if DifficultBulletinBoardMainFrame and DifficultBulletinBoardMainFrame.GetCurrentGroupsLogsFilter then 187 | currentFilter = DifficultBulletinBoardMainFrame.GetCurrentGroupsLogsFilter() 188 | end 189 | DifficultBulletinBoardMessageProcessor.ApplyGroupsLogsFilter(currentFilter) 190 | end 191 | end 192 | 193 | -- Helper function to convert HH:MM:SS to seconds 194 | local function timeToSeconds(timeString) 195 | --idk how else to do it :/ just loop over and return the first match 196 | for hours, minutes, seconds in string_gfind(timeString, "(%d+):(%d+):(%d+)") do 197 | return (tonumber(hours) * 3600) + (tonumber(minutes) * 60) + tonumber(seconds) 198 | end 199 | end 200 | 201 | -- Helper function to format seconds into MM:SS 202 | local function secondsToMMSS(totalSeconds) 203 | local minutes = math.floor(totalSeconds / 60) 204 | local seconds = totalSeconds - math.floor(totalSeconds / 60) * 60 205 | 206 | -- Return "99:59" if minutes exceed 99 207 | if minutes > 99 then 208 | return "99:59" 209 | end 210 | 211 | return string.format("%02d:%02d", minutes, seconds) 212 | end 213 | 214 | -- Calculate delta in MM:SS format 215 | local function calculateDelta(creationTimestamp, currentTime) 216 | local creationInSeconds = timeToSeconds(creationTimestamp) 217 | local currentInSeconds = timeToSeconds(currentTime) 218 | local deltaSeconds = currentInSeconds - creationInSeconds 219 | 220 | -- Handle negative delta (e.g., crossing midnight) 221 | if deltaSeconds < 0 then 222 | -- Add a day's worth of seconds 223 | deltaSeconds = deltaSeconds + 86400 224 | end 225 | 226 | return secondsToMMSS(deltaSeconds) 227 | end 228 | 229 | -- Function to add a new entry to the given topic with and shift other entries down 230 | local function AddNewTopicEntryAndShiftOthers(topicPlaceholders, topic, numberOfPlaceholders, channelName, name, message) 231 | local topicData = topicPlaceholders[topic] 232 | if not topicData or not topicData[1] then 233 | return 234 | end 235 | 236 | local timestamp 237 | if DifficultBulletinBoardVars.timeFormat == "elapsed" then 238 | timestamp = "00:00" 239 | else 240 | timestamp = date("%H:%M:%S") 241 | end 242 | 243 | for i = numberOfPlaceholders, 2, -1 do 244 | -- Copy the data from the previous placeholder to the current one 245 | local currentFontString = topicData[i] 246 | local previousFontString = topicData[i - 1] 247 | 248 | if not currentFontString or not previousFontString then 249 | break 250 | end 251 | 252 | -- Update the current placeholder with the previous placeholder's data safely 253 | if currentFontString.nameButton and type(currentFontString.nameButton.SetText) == "function" and 254 | previousFontString.nameButton and type(previousFontString.nameButton.GetText) == "function" then 255 | currentFontString.nameButton:SetText(previousFontString.nameButton:GetText()) 256 | end 257 | 258 | if currentFontString.messageFontString and type(currentFontString.messageFontString.SetText) == "function" and 259 | previousFontString.messageFontString and type(previousFontString.messageFontString.GetText) == "function" then 260 | currentFontString.messageFontString:SetText(previousFontString.messageFontString:GetText()) 261 | end 262 | 263 | if currentFontString.timeFontString and type(currentFontString.timeFontString.SetText) == "function" and 264 | previousFontString.timeFontString and type(previousFontString.timeFontString.GetText) == "function" then 265 | currentFontString.timeFontString:SetText(previousFontString.timeFontString:GetText()) 266 | end 267 | 268 | currentFontString.creationTimestamp = previousFontString.creationTimestamp 269 | 270 | if currentFontString.icon and type(currentFontString.icon.SetTexture) == "function" and 271 | previousFontString.icon and type(previousFontString.icon.GetTexture) == "function" then 272 | currentFontString.icon:SetTexture(previousFontString.icon:GetTexture()) 273 | end 274 | end 275 | 276 | -- Update the first placeholder with the new data safely 277 | local firstFontString = topicData[1] 278 | if firstFontString.nameButton and type(firstFontString.nameButton.SetText) == "function" then 279 | firstFontString.nameButton:SetText(name) 280 | -- Add this line to update hit rect for the new name 281 | firstFontString.nameButton:SetHitRectInsets(0, -45, 0, 0) 282 | end 283 | 284 | if firstFontString.messageFontString and type(firstFontString.messageFontString.SetText) == "function" then 285 | firstFontString.messageFontString:SetText("[" .. channelName .. "] " .. message) 286 | end 287 | 288 | if firstFontString.timeFontString and type(firstFontString.timeFontString.SetText) == "function" then 289 | firstFontString.timeFontString:SetText(timestamp) 290 | end 291 | 292 | firstFontString.creationTimestamp = date("%H:%M:%S") 293 | local class = DifficultBulletinBoardVars.GetPlayerClassFromDatabase(name) 294 | 295 | if firstFontString.icon and type(firstFontString.icon.SetTexture) == "function" then 296 | firstFontString.icon:SetTexture(getClassIconFromClassName(class)) 297 | end 298 | 299 | -- Show a RaidWarning for enabled notifications. dont show it for the Group Logs 300 | if topic ~= "Group Logs" and DifficultBulletinBoard.notificationList[topic] == true then 301 | RaidWarningFrame:AddMessage("DBB Notification: " .. message) 302 | end 303 | 304 | -- Apply filter if this is a Groups Logs entry 305 | if topic == "Group Logs" and topicPlaceholders == groupsLogsPlaceholders then 306 | local currentFilter = "" 307 | if DifficultBulletinBoardMainFrame and DifficultBulletinBoardMainFrame.GetCurrentGroupsLogsFilter then 308 | currentFilter = DifficultBulletinBoardMainFrame.GetCurrentGroupsLogsFilter() 309 | end 310 | DifficultBulletinBoardMessageProcessor.ApplyGroupsLogsFilter(currentFilter) 311 | end 312 | end 313 | 314 | -- Function to update the first placeholder for a given topic with new message, and time and shift other placeholders down 315 | local function AddNewSystemTopicEntryAndShiftOthers(topicPlaceholders, topic, message) 316 | local topicData = topicPlaceholders[topic] 317 | if not topicData or not topicData[1] then 318 | return 319 | end 320 | 321 | local timestamp 322 | if DifficultBulletinBoardVars.timeFormat == "elapsed" then 323 | timestamp = "00:00" 324 | else 325 | timestamp = date("%H:%M:%S") 326 | end 327 | 328 | local index = 0 329 | for i, _ in ipairs(topicData) do index = i end 330 | for i = index, 2, -1 do 331 | -- Copy the data from the previous placeholder to the current one 332 | local currentFontString = topicData[i] 333 | local previousFontString = topicData[i - 1] 334 | 335 | if not currentFontString or not previousFontString then 336 | break 337 | end 338 | 339 | -- Update the current placeholder with the previous placeholder's data safely 340 | if currentFontString.messageFontString and type(currentFontString.messageFontString.SetText) == "function" and 341 | previousFontString.messageFontString and type(previousFontString.messageFontString.GetText) == "function" then 342 | currentFontString.messageFontString:SetText(previousFontString.messageFontString:GetText()) 343 | end 344 | 345 | if currentFontString.timeFontString and type(currentFontString.timeFontString.SetText) == "function" and 346 | previousFontString.timeFontString and type(previousFontString.timeFontString.GetText) == "function" then 347 | currentFontString.timeFontString:SetText(previousFontString.timeFontString:GetText()) 348 | end 349 | 350 | currentFontString.creationTimestamp = previousFontString.creationTimestamp 351 | end 352 | 353 | -- Update the first placeholder with the new data safely 354 | local firstFontString = topicData[1] 355 | if firstFontString.messageFontString and type(firstFontString.messageFontString.SetText) == "function" then 356 | firstFontString.messageFontString:SetText(message) 357 | end 358 | 359 | if firstFontString.timeFontString and type(firstFontString.timeFontString.SetText) == "function" then 360 | firstFontString.timeFontString:SetText(timestamp) 361 | end 362 | 363 | firstFontString.creationTimestamp = date("%H:%M:%S") 364 | 365 | -- Tooltip handlers are now dynamic and don't need manual updates 366 | end 367 | 368 | -- Function to check if a character name already exists in the Groups Logs 369 | local function groupsLogsContainsCharacterName(characterName) 370 | if not groupsLogsPlaceholders["Group Logs"] then 371 | return false, nil 372 | end 373 | 374 | local entries = groupsLogsPlaceholders["Group Logs"] 375 | for index, entry in ipairs(entries) do 376 | if entry.nameButton and entry.nameButton:GetText() == characterName then 377 | return true, index 378 | end 379 | end 380 | 381 | return false, nil 382 | end 383 | 384 | -- Searches the passed topicList for the passed words. If a match is found the topicPlaceholders will be updated 385 | local function analyzeChatMessage(channelName, characterName, chatMessage, words, topicList, topicPlaceholders, numberOfPlaceholders) 386 | local isAnyMatch = false 387 | local isGroupMatch = false 388 | 389 | for _, topic in ipairs(topicList) do 390 | if topic.selected then 391 | local matchFound = false -- Flag to control breaking out of nested loops 392 | 393 | for _, tag in ipairs(topic.tags) do 394 | for _, word in ipairs(words) do 395 | if word == string.lower(tag) then 396 | local found, index = topicPlaceholdersContainsCharacterName(topicPlaceholders, topic.name, characterName) 397 | if found then 398 | UpdateTopicEntryAndPromoteToTop(topicPlaceholders, topic.name, numberOfPlaceholders, channelName, characterName, chatMessage, index) 399 | else 400 | AddNewTopicEntryAndShiftOthers(topicPlaceholders, topic.name, numberOfPlaceholders, channelName, characterName, chatMessage) 401 | end 402 | 403 | matchFound = true -- Set the flag to true to break out of loops 404 | isAnyMatch = true -- Set the outer function result 405 | 406 | -- If this is a group topic, mark it 407 | if topicList == DifficultBulletinBoardVars.allGroupTopics then 408 | isGroupMatch = true 409 | end 410 | 411 | break 412 | end 413 | end 414 | 415 | if matchFound then break end 416 | end 417 | 418 | --if matchFound then break end --removed because it would skip remaining topics 419 | end 420 | end 421 | 422 | return isGroupMatch == true and isGroupMatch or isAnyMatch 423 | end 424 | 425 | -- Searches the passed topicList for the passed words. If a match is found the topicPlaceholders will be updated 426 | local function analyzeSystemMessage(chatMessage, words, topicList, topicPlaceholders) 427 | local isAnyMatch = false 428 | 429 | for _, topic in ipairs(topicList) do 430 | if topic.selected then 431 | local matchFound = false -- Flag to control breaking out of nested loops 432 | 433 | for _, tag in ipairs(topic.tags) do 434 | for _, word in ipairs(words) do 435 | if word == string.lower(tag) then 436 | AddNewSystemTopicEntryAndShiftOthers(topicPlaceholders, topic.name, chatMessage) 437 | 438 | matchFound = true -- Set the flag to true to break out of loops 439 | isAnyMatch = true -- Set the outer function result 440 | break 441 | end 442 | end 443 | 444 | if matchFound then break end 445 | end 446 | 447 | if matchFound then break end 448 | end 449 | end 450 | 451 | return isAnyMatch 452 | end 453 | 454 | -- Process chat messages and add matched content to the appropriate sections 455 | function DifficultBulletinBoard.OnChatMessage(arg1, arg2, arg9) 456 | local chatMessage = arg1 457 | local characterName = arg2 458 | local channelName = arg9 459 | 460 | if characterName == "" or characterName == nil then 461 | return 462 | end 463 | 464 | local stringWithoutNoise = replaceSymbolsWithSpace(chatMessage) 465 | 466 | local words = DifficultBulletinBoard.SplitIntoLowerWords(stringWithoutNoise) 467 | 468 | if DifficultBulletinBoardVars.hardcoreOnly == "true" and channelName ~= "HC" then 469 | local found = false 470 | for _, w in ipairs(words) do 471 | if w == "hc" or w == "hardcore" or w == "inferno" then 472 | found = true 473 | break 474 | end 475 | end 476 | 477 | -- bail out if nothing matched 478 | if not found then 479 | return 480 | end 481 | end 482 | 483 | -- Process group topics and check if it's a group-related message 484 | local isGroupMessage = analyzeChatMessage(channelName, characterName, chatMessage, words, 485 | DifficultBulletinBoardVars.allGroupTopics, 486 | groupTopicPlaceholders, 487 | DifficultBulletinBoardVars.numberOfGroupPlaceholders) 488 | 489 | -- After updating group entries, reflow the Groups tab 490 | if isGroupMessage then DifficultBulletinBoardMainFrame.ReflowGroupsTab() end 491 | 492 | -- If it's a group message, add it to the Groups Logs with duplicate checking 493 | if isGroupMessage then 494 | local found, index = groupsLogsContainsCharacterName(characterName) 495 | if found then 496 | -- Update the existing entry and move it to the top 497 | local entries = groupsLogsPlaceholders["Group Logs"] 498 | if not entries then 499 | return isGroupMessage or isProfessionMessage 500 | end 501 | 502 | -- Get timestamp 503 | local timestamp 504 | if DifficultBulletinBoardVars.timeFormat == "elapsed" then 505 | timestamp = "00:00" 506 | else 507 | timestamp = date("%H:%M:%S") 508 | end 509 | 510 | -- Shift entries down from index to top 511 | for i = index, 2, -1 do 512 | if not entries[i] or not entries[i-1] then 513 | break 514 | end 515 | 516 | if entries[i].nameButton and type(entries[i].nameButton.SetText) == "function" and 517 | entries[i-1].nameButton and type(entries[i-1].nameButton.GetText) == "function" then 518 | entries[i].nameButton:SetText(entries[i-1].nameButton:GetText()) 519 | end 520 | 521 | if entries[i].messageFontString and type(entries[i].messageFontString.SetText) == "function" and 522 | entries[i-1].messageFontString and type(entries[i-1].messageFontString.GetText) == "function" then 523 | entries[i].messageFontString:SetText(entries[i-1].messageFontString:GetText()) 524 | end 525 | 526 | if entries[i].timeFontString and type(entries[i].timeFontString.SetText) == "function" and 527 | entries[i-1].timeFontString and type(entries[i-1].timeFontString.GetText) == "function" then 528 | entries[i].timeFontString:SetText(entries[i-1].timeFontString:GetText()) 529 | end 530 | 531 | entries[i].creationTimestamp = entries[i-1].creationTimestamp 532 | 533 | if entries[i].icon and type(entries[i].icon.SetTexture) == "function" and 534 | entries[i-1].icon and type(entries[i-1].icon.GetTexture) == "function" then 535 | entries[i].icon:SetTexture(entries[i-1].icon:GetTexture()) 536 | end 537 | end 538 | 539 | -- Update top entry 540 | if entries[1] and entries[1].nameButton and type(entries[1].nameButton.SetText) == "function" then 541 | entries[1].nameButton:SetText(characterName) 542 | end 543 | 544 | if entries[1] and entries[1].messageFontString and type(entries[1].messageFontString.SetText) == "function" then 545 | entries[1].messageFontString:SetText("[" .. channelName .. "] " .. chatMessage) 546 | end 547 | 548 | if entries[1] and entries[1].timeFontString and type(entries[1].timeFontString.SetText) == "function" then 549 | entries[1].timeFontString:SetText(timestamp) 550 | end 551 | 552 | entries[1].creationTimestamp = date("%H:%M:%S") 553 | 554 | local class = DifficultBulletinBoardVars.GetPlayerClassFromDatabase(characterName) 555 | if entries[1] and entries[1].icon and type(entries[1].icon.SetTexture) == "function" then 556 | entries[1].icon:SetTexture(getClassIconFromClassName(class)) 557 | end 558 | 559 | -- reflow Group Logs placeholders first 560 | local glEntries = groupsLogsPlaceholders["Group Logs"] 561 | if glEntries then 562 | DifficultBulletinBoardMainFrame.RepackEntries(glEntries) 563 | DifficultBulletinBoardMainFrame.ReflowTopicEntries(glEntries) 564 | -- Reflow the Groups tab to adjust to updated entries 565 | DifficultBulletinBoardMainFrame.ReflowGroupsTab() 566 | end 567 | 568 | -- Apply the current filter AFTER repack/reflow to ensure it's not overridden 569 | local currentFilter = "" 570 | if DifficultBulletinBoardMainFrame and DifficultBulletinBoardMainFrame.GetCurrentGroupsLogsFilter then 571 | currentFilter = DifficultBulletinBoardMainFrame.GetCurrentGroupsLogsFilter() 572 | end 573 | DifficultBulletinBoardMessageProcessor.ApplyGroupsLogsFilter(currentFilter) 574 | else 575 | -- Add as new entry 576 | AddNewTopicEntryAndShiftOthers(groupsLogsPlaceholders, "Group Logs", MAX_GROUPS_LOGS_ENTRIES, channelName, characterName, chatMessage) 577 | -- reflow after adding new entry 578 | local glEntries = groupsLogsPlaceholders["Group Logs"] 579 | if glEntries then 580 | DifficultBulletinBoardMainFrame.RepackEntries(glEntries) 581 | DifficultBulletinBoardMainFrame.ReflowTopicEntries(glEntries) 582 | -- Reflow the Groups tab after adding new entry 583 | DifficultBulletinBoardMainFrame.ReflowGroupsTab() 584 | end 585 | 586 | -- Apply the current filter AFTER repack/reflow to ensure it's not overridden 587 | local currentFilter = "" 588 | if DifficultBulletinBoardMainFrame and DifficultBulletinBoardMainFrame.GetCurrentGroupsLogsFilter then 589 | currentFilter = DifficultBulletinBoardMainFrame.GetCurrentGroupsLogsFilter() 590 | end 591 | DifficultBulletinBoardMessageProcessor.ApplyGroupsLogsFilter(currentFilter) 592 | end 593 | end 594 | 595 | -- Reapply manual expiration setting to keep old entries hidden 596 | if DifficultBulletinBoardVars.manualExpireSeconds then 597 | DifficultBulletinBoard.ExpireMessages(DifficultBulletinBoardVars.manualExpireSeconds) 598 | end 599 | 600 | -- Process profession topics as usual 601 | local isProfessionMessage = analyzeChatMessage(channelName, characterName, chatMessage, words, 602 | DifficultBulletinBoardVars.allProfessionTopics, 603 | professionTopicPlaceholders, 604 | DifficultBulletinBoardVars.numberOfProfessionPlaceholders) 605 | 606 | -- After updating profession entries, reflow the Professions tab 607 | if isProfessionMessage then DifficultBulletinBoardMainFrame.ReflowProfessionsTab() end 608 | 609 | -- Return true if any match was found 610 | return isGroupMessage or isProfessionMessage 611 | end 612 | 613 | function DifficultBulletinBoard.OnSystemMessage(arg1) 614 | local systemMessage = arg1 615 | 616 | local stringWithoutNoise = replaceSymbolsWithSpace(systemMessage) 617 | 618 | local words = DifficultBulletinBoard.SplitIntoLowerWords(stringWithoutNoise) 619 | 620 | local isMatched = analyzeSystemMessage(systemMessage, words, DifficultBulletinBoardVars.allHardcoreTopics, hardcoreTopicPlaceholders) 621 | 622 | -- After updating hardcore entries, reflow the Hardcore tab 623 | if isMatched then DifficultBulletinBoardMainFrame.ReflowHardcoreTab() end 624 | 625 | return isMatched 626 | end 627 | 628 | -- Dynamic tooltip system that finds the correct message based on visual position 629 | -- This solves the issue where repositioned entries show wrong tooltips 630 | function DifficultBulletinBoardMainFrame.ShowDynamicTooltip(frame) 631 | if not frame then 632 | return 633 | end 634 | 635 | -- Get frame position for position-based matching 636 | local frameX, frameY = frame:GetLeft(), frame:GetTop() 637 | 638 | -- Find message by matching VISIBLE position instead of frame object 639 | -- This works because reflow functions reposition frames but don't change the frame objects 640 | local message = nil 641 | local bestMatch = nil 642 | local POSITION_TOLERANCE = 2.0 -- Allow 2 pixel tolerance for position matching 643 | 644 | -- Search all placeholder tables for frames at the same visual position 645 | local allPlaceholderTables = { 646 | {name = "Groups", table = groupTopicPlaceholders}, 647 | {name = "GroupsLogs", table = groupsLogsPlaceholders}, 648 | {name = "Professions", table = professionTopicPlaceholders}, 649 | {name = "Hardcore", table = hardcoreTopicPlaceholders} 650 | } 651 | 652 | for _, placeholderInfo in ipairs(allPlaceholderTables) do 653 | local placeholderTable = placeholderInfo.table 654 | for topicName, entries in pairs(placeholderTable) do 655 | for entryIndex, entry in ipairs(entries) do 656 | -- Check if this entry has a visible message frame 657 | if entry.messageFrame and entry.messageFrame:IsVisible() then 658 | local entryX, entryY = entry.messageFrame:GetLeft(), entry.messageFrame:GetTop() 659 | 660 | -- Check if positions match within tolerance 661 | if entryX and entryY and frameX and frameY then 662 | local deltaX = math.abs(entryX - frameX) 663 | local deltaY = math.abs(entryY - frameY) 664 | 665 | if deltaX <= POSITION_TOLERANCE and deltaY <= POSITION_TOLERANCE then 666 | local msg = entry.messageFontString and entry.messageFontString:GetText() or "" 667 | 668 | -- Prefer entries with actual content over placeholders 669 | if msg and msg ~= "-" and msg ~= "" then 670 | message = msg 671 | bestMatch = entry 672 | break 673 | elseif not bestMatch then 674 | -- Keep this as a fallback if no better match is found 675 | bestMatch = entry 676 | end 677 | end 678 | end 679 | end 680 | end 681 | if message and message ~= "-" and message ~= "" then break end 682 | end 683 | if message and message ~= "-" and message ~= "" then break end 684 | end 685 | 686 | -- Show tooltip if we found a valid message 687 | if message and message ~= "-" and message ~= "" then 688 | DifficultBulletinBoardMainFrame.ShowMessageTooltip(frame, message) 689 | end 690 | end 691 | 692 | -- Robust tooltip helper function to handle common tooltip issues in Vanilla WoW 693 | function DifficultBulletinBoardMainFrame.ShowMessageTooltip(frame, message) 694 | if not frame then 695 | return 696 | end 697 | 698 | if not message or message == "-" or message == "" then 699 | return 700 | end 701 | 702 | -- Check if GameTooltip exists 703 | if not GameTooltip then 704 | return 705 | end 706 | 707 | -- Ensure GameTooltip is properly reset before use 708 | GameTooltip:Hide() 709 | GameTooltip:ClearLines() 710 | 711 | -- Set owner and anchor with error checking 712 | if GameTooltip.SetOwner then 713 | GameTooltip:SetOwner(frame, "ANCHOR_CURSOR") 714 | else 715 | return -- GameTooltip not available 716 | end 717 | 718 | -- Set the text with word wrapping enabled 719 | GameTooltip:SetText(message, 1, 1, 1, 1, true) 720 | 721 | -- Ensure proper font and sizing for better readability using user's font size setting + 2 722 | if GameTooltipTextLeft1 and GameTooltipTextLeft1.SetFont then 723 | local tooltipFontSize = (DifficultBulletinBoardVars.fontSize or 12) + 2 724 | GameTooltipTextLeft1:SetFont("Fonts\\ARIALN.TTF", tooltipFontSize, "") 725 | end 726 | 727 | -- Set tooltip border color to match header color (light blue-white) 728 | if GameTooltip.SetBackdropBorderColor then 729 | GameTooltip:SetBackdropBorderColor(0.9, 0.9, 1.0, 1.0) 730 | end 731 | 732 | -- Force the tooltip to appear on top of other UI elements 733 | GameTooltip:SetFrameStrata("TOOLTIP") 734 | 735 | -- Show the tooltip 736 | GameTooltip:Show() 737 | end 738 | 739 | -- Safe tooltip hide function 740 | function DifficultBulletinBoardMainFrame.HideMessageTooltip(frame) 741 | if GameTooltip and GameTooltip.IsOwned and GameTooltip:IsOwned(frame) then 742 | GameTooltip:Hide() 743 | elseif GameTooltip and GameTooltip.Hide then 744 | -- Fallback for cases where IsOwned might not work 745 | GameTooltip:Hide() 746 | end 747 | end 748 | 749 | -- Helper to reflow entries and remove blank gaps (supports optional nameButton/icon) 750 | function DifficultBulletinBoardMainFrame.RepackEntries(entries) 751 | -- Collect non-expired entries data 752 | local validData = {} 753 | for _, entry in ipairs(entries) do 754 | if entry.creationTimestamp then 755 | local item = { 756 | message = entry.messageFontString and entry.messageFontString:GetText() or "", 757 | timeText = entry.timeFontString and entry.timeFontString:GetText() or "", 758 | timestamp = entry.creationTimestamp 759 | } 760 | if entry.nameButton then 761 | item.name = entry.nameButton:GetText() 762 | end 763 | if entry.icon then 764 | item.iconTexture = entry.icon:GetTexture() 765 | end 766 | table.insert(validData, item) 767 | end 768 | end 769 | local validCount = table.getn(validData) 770 | for i, entry in ipairs(entries) do 771 | if i <= validCount then 772 | local data = validData[i] 773 | if entry.nameButton and data.name then 774 | entry.nameButton:SetText(data.name) 775 | entry.nameButton:Show() 776 | end 777 | if entry.messageFontString then 778 | entry.messageFontString:SetText(data.message) 779 | entry.messageFontString:Show() 780 | end 781 | if entry.timeFontString then 782 | entry.timeFontString:SetText(data.timeText) 783 | entry.timeFontString:Show() 784 | end 785 | entry.creationTimestamp = data.timestamp 786 | if entry.icon then 787 | if data.iconTexture then 788 | entry.icon:SetTexture(data.iconTexture) 789 | entry.icon:Show() 790 | else 791 | entry.icon:Hide() 792 | end 793 | end 794 | else 795 | if entry.nameButton then 796 | entry.nameButton:SetText("-") 797 | entry.nameButton:Show() 798 | end 799 | if entry.messageFontString then 800 | entry.messageFontString:SetText("-") 801 | entry.messageFontString:Show() 802 | end 803 | if entry.timeFontString then 804 | entry.timeFontString:SetText("-") 805 | entry.timeFontString:Show() 806 | end 807 | entry.creationTimestamp = nil 808 | if entry.icon then entry.icon:Hide() end 809 | end 810 | 811 | -- Tooltip handlers are now dynamic and don't need updating after data movement 812 | end 813 | end 814 | 815 | -- Helper to reflow visible placeholders top-to-bottom 816 | function DifficultBulletinBoardMessageProcessor.ReflowTopicEntries(entries) 817 | local ROW_HEIGHT = 18 818 | -- Determine the top Y based on first placeholder 819 | local initialY = entries[1] and entries[1].baseY or 0 820 | local baseX = entries[1] and entries[1].baseX or 0 821 | local cf = entries[1] and entries[1].contentFrame 822 | for i, entry in ipairs(entries) do 823 | if entry.creationTimestamp and entry.nameButton and cf then 824 | -- Reposition row using common initialY 825 | entry.nameButton:ClearAllPoints() 826 | entry.nameButton:SetPoint("TOPLEFT", cf, "TOPLEFT", baseX, initialY - (i-1)*ROW_HEIGHT) 827 | -- Reposition icon alongside 828 | if entry.icon then 829 | entry.icon:ClearAllPoints() 830 | -- Anchor icon to the left of the name button for consistent alignment 831 | entry.icon:SetPoint("RIGHT", entry.nameButton, "LEFT", 3, 0) 832 | entry.icon:Show() 833 | end 834 | -- Show text columns 835 | if entry.messageFontString then entry.messageFontString:Show() end 836 | if entry.timeFontString then entry.timeFontString:Show() end 837 | entry.nameButton:Show() 838 | else 839 | -- Hide unused rows 840 | if entry.nameButton then entry.nameButton:Hide() end 841 | if entry.icon then entry.icon:Hide() end 842 | if entry.messageFontString then entry.messageFontString:Hide() end 843 | if entry.timeFontString then entry.timeFontString:Hide() end 844 | end 845 | end 846 | end 847 | 848 | -- Modified to also update times in the Groups Logs tab 849 | function DifficultBulletinBoardMessageProcessor.UpdateElapsedTimes() 850 | for topicName, entries in pairs(groupTopicPlaceholders) do 851 | for _, entry in ipairs(entries) do 852 | if entry and entry.timeFontString and entry.creationTimestamp then 853 | local currentTimeStr = date("%H:%M:%S") 854 | local ageSeconds = timeToSeconds(currentTimeStr) - timeToSeconds(entry.creationTimestamp) 855 | if ageSeconds < 0 then ageSeconds = ageSeconds + 86400 end 856 | local expirationTime = tonumber(DifficultBulletinBoardVars.messageExpirationTime) 857 | if expirationTime and expirationTime > 0 and ageSeconds >= expirationTime then 858 | entry.nameButton:SetText("-") 859 | entry.messageFontString:SetText("-") 860 | entry.timeFontString:SetText("-") 861 | entry.creationTimestamp = nil 862 | if entry.icon then entry.icon:SetTexture(nil) end 863 | else 864 | local delta = calculateDelta(entry.creationTimestamp, currentTimeStr) 865 | entry.timeFontString:SetText(delta) 866 | end 867 | end 868 | end 869 | end 870 | 871 | -- Update and expire Groups Logs times 872 | if groupsLogsPlaceholders["Group Logs"] then 873 | for _, entry in ipairs(groupsLogsPlaceholders["Group Logs"]) do 874 | if entry and entry.timeFontString and entry.creationTimestamp then 875 | local currentTimeStr = date("%H:%M:%S") 876 | local ageSeconds = timeToSeconds(currentTimeStr) - timeToSeconds(entry.creationTimestamp) 877 | if ageSeconds < 0 then ageSeconds = ageSeconds + 86400 end 878 | local expirationTime = tonumber(DifficultBulletinBoardVars.messageExpirationTime) 879 | if expirationTime and expirationTime > 0 and ageSeconds >= expirationTime then 880 | entry.nameButton:SetText("-") 881 | entry.messageFontString:SetText("-") 882 | entry.timeFontString:SetText("-") 883 | entry.creationTimestamp = nil 884 | if entry.icon then entry.icon:SetTexture(nil) end 885 | else 886 | local delta = calculateDelta(entry.creationTimestamp, currentTimeStr) 887 | entry.timeFontString:SetText(delta) 888 | end 889 | end 890 | end 891 | end 892 | 893 | for topicName, entries in pairs(professionTopicPlaceholders) do 894 | for _, entry in ipairs(entries) do 895 | if entry and entry.timeFontString and entry.creationTimestamp then 896 | local currentTimeStr = date("%H:%M:%S") 897 | local ageSeconds = timeToSeconds(currentTimeStr) - timeToSeconds(entry.creationTimestamp) 898 | if ageSeconds < 0 then ageSeconds = ageSeconds + 86400 end 899 | local expirationTime = tonumber(DifficultBulletinBoardVars.messageExpirationTime) 900 | if expirationTime and expirationTime > 0 and ageSeconds >= expirationTime then 901 | entry.nameButton:SetText("-") 902 | entry.messageFontString:SetText("-") 903 | entry.timeFontString:SetText("-") 904 | entry.creationTimestamp = nil 905 | if entry.icon then entry.icon:SetTexture(nil) end 906 | else 907 | local delta = calculateDelta(entry.creationTimestamp, currentTimeStr) 908 | entry.timeFontString:SetText(delta) 909 | end 910 | end 911 | end 912 | end 913 | 914 | for topicName, entries in pairs(hardcoreTopicPlaceholders) do 915 | for _, entry in ipairs(entries) do 916 | if entry and entry.timeFontString and entry.creationTimestamp then 917 | local currentTimeStr = date("%H:%M:%S") 918 | local ageSeconds = timeToSeconds(currentTimeStr) - timeToSeconds(entry.creationTimestamp) 919 | if ageSeconds < 0 then ageSeconds = ageSeconds + 86400 end 920 | local expirationTime = tonumber(DifficultBulletinBoardVars.messageExpirationTime) 921 | if expirationTime and expirationTime > 0 and ageSeconds >= expirationTime then 922 | entry.nameButton:SetText("-") 923 | entry.messageFontString:SetText("-") 924 | entry.timeFontString:SetText("-") 925 | entry.creationTimestamp = nil 926 | if entry.icon then entry.icon:SetTexture(nil) end 927 | else 928 | local delta = calculateDelta(entry.creationTimestamp, currentTimeStr) 929 | entry.timeFontString:SetText(delta) 930 | end 931 | end 932 | end 933 | end 934 | end 935 | 936 | -- Function to expire messages older than specified seconds 937 | function DifficultBulletinBoardMessageProcessor.ExpireMessages(seconds) 938 | local secs = tonumber(seconds) 939 | if not secs then return end 940 | local currentTimeStr = date("%H:%M:%S") 941 | -- Hide/show entries 942 | local function expireEntry(entry) 943 | if entry and entry.creationTimestamp then 944 | local age = timeToSeconds(currentTimeStr) - timeToSeconds(entry.creationTimestamp) 945 | if age < 0 then age = age + 86400 end 946 | if age >= secs then 947 | -- Prevent reprocessing 948 | entry.creationTimestamp = nil 949 | if entry.nameButton then entry.nameButton:Hide() end 950 | entry.messageFontString:Hide() 951 | entry.timeFontString:Hide() 952 | if entry.icon then entry.icon:Hide() end 953 | else 954 | if entry.nameButton then entry.nameButton:Show() end 955 | entry.messageFontString:Show() 956 | entry.timeFontString:Show() 957 | if entry.icon then entry.icon:Show() end 958 | end 959 | end 960 | end 961 | -- Process all tabs 962 | for _, entries in pairs(groupTopicPlaceholders) do 963 | for _, entry in ipairs(entries) do expireEntry(entry) end 964 | end 965 | if groupsLogsPlaceholders["Group Logs"] then 966 | for _, entry in ipairs(groupsLogsPlaceholders["Group Logs"]) do expireEntry(entry) end 967 | end 968 | for _, entries in pairs(professionTopicPlaceholders) do 969 | for _, entry in ipairs(entries) do expireEntry(entry) end 970 | end 971 | for _, entries in pairs(hardcoreTopicPlaceholders) do 972 | for _, entry in ipairs(entries) do expireEntry(entry) end 973 | end 974 | 975 | 976 | -- Repack and reflow all placeholder lists to collapse gaps and reposition rows 977 | if RepackEntries and ReflowTopicEntries then 978 | for _, entries in pairs(groupTopicPlaceholders) do 979 | RepackEntries(entries) 980 | ReflowTopicEntries(entries) 981 | end 982 | if groupsLogsPlaceholders["Group Logs"] then 983 | RepackEntries(groupsLogsPlaceholders["Group Logs"]) 984 | ReflowTopicEntries(groupsLogsPlaceholders["Group Logs"]) 985 | end 986 | for _, entries in pairs(professionTopicPlaceholders) do 987 | RepackEntries(entries) 988 | ReflowTopicEntries(entries) 989 | end 990 | for _, entries in pairs(hardcoreTopicPlaceholders) do 991 | RepackEntries(entries) 992 | ReflowTopicEntries(entries) 993 | end 994 | end 995 | end -------------------------------------------------------------------------------- /DifficultBulletinBoardMinimapButton.xml: -------------------------------------------------------------------------------- 1 | 2 | 73 | 74 | -------------------------------------------------------------------------------- /DifficultBulletinBoardOptionFrame.lua: -------------------------------------------------------------------------------- 1 | -- DifficultBulletinBoardOptionFrame.lua 2 | -- Handles the options interface and configuration settings for Difficult Bulletin Board 3 | DifficultBulletinBoard = DifficultBulletinBoard or {} 4 | DifficultBulletinBoardVars = DifficultBulletinBoardVars or {} 5 | DifficultBulletinBoardDefaults = DifficultBulletinBoardDefaults or {} 6 | DifficultBulletinBoardSavedVariables = DifficultBulletinBoardSavedVariables or {} 7 | 8 | local optionFrame = DifficultBulletinBoardOptionFrame 9 | 10 | local optionYOffset = 30 -- Starting vertical offset for the first option 11 | 12 | local optionScrollChild 13 | 14 | -- Standard WoW scrollbar width including padding 15 | local SCROLL_BAR_WIDTH = 16 16 | 17 | local tagsTextBoxWidthDelta = 260 18 | 19 | -- Constants for dynamic button sizing (matching main panel) 20 | local OPTION_BUTTON_SPACING = 10 -- Fixed spacing between buttons 21 | local OPTION_MIN_TEXT_PADDING = 10 -- Minimum spacing between text and button edges 22 | local OPTION_NUM_BUTTONS = 4 -- Total number of option tab buttons 23 | local OPTION_SIDE_PADDING = 10 -- Padding on left and right sides of frame 24 | 25 | -- Improved spacing constants for better layout 26 | local SECTION_SPACING = 30 -- Space between major sections (proper separation) 27 | local OPTION_SPACING = 25 -- Space between individual options 28 | local LABEL_SPACING = 20 -- Space between label and input 29 | 30 | -- Tab system state 31 | DifficultBulletinBoardOptionFrame.currentTab = "general" 32 | DifficultBulletinBoardOptionFrame.tabs = { 33 | general = {}, 34 | groups = {}, 35 | professions = {}, 36 | hardcore = {} 37 | } 38 | DifficultBulletinBoardOptionFrame.allDropdowns = {} -- Track all dropdown buttons 39 | 40 | local tempGroupTags = {} 41 | local tempProfessionTags = {} 42 | local tempHardcoreTags = {} 43 | 44 | -- Option Data for Base Font Size 45 | local baseFontSizeOptionObject = { 46 | frameName = "DifficultBulletinBoardOptionFrame_Font_Size_Input", 47 | labelText = "Base Font Size:", 48 | labelToolTip = "Adjusts the base font size for text. Other font sizes (e.g., titles) are calculated relative to this value. For example, if the base font size is 14, titles may be set 2 points higher.", 49 | width = 40, -- match expiration-time width 50 | } 51 | 52 | -- Option Data for Placeholders per Group Topic 53 | local groupPlaceholdersOptionObject = { 54 | frameName = "DifficultBulletinBoardOptionFrame_Group_Placeholder_Input", 55 | labelText = "Entries per Group Topic:", 56 | labelToolTip = "Defines the number of entries displayed for each group topic entry.", 57 | width = 40, -- match expiration-time width 58 | } 59 | 60 | -- Option Data for Placeholders per Profession Topic 61 | local professionPlaceholdersOptionObject = { 62 | frameName = "DifficultBulletinBoardOptionFrame_Profession_Placeholder_Input", 63 | labelText = "Entries per Profession Topic:", 64 | labelToolTip = "Specifies the number of entries displayed for each profession topic entry.", 65 | width = 40, -- match expiration-time width 66 | } 67 | 68 | -- Option Data for Placeholders per Hardcore Topic 69 | local hardcorePlaceholdersOptionObject = { 70 | frameName = "DifficultBulletinBoardOptionFrame_Hardcore_Placeholder_Input", 71 | labelText = "Entries per Hardcore Topic:", 72 | labelToolTip = "Sets the number of entries displayed for each hardcore topic entry.", 73 | width = 40, -- match expiration-time width 74 | } 75 | 76 | local groupTopicListObject = { 77 | frameName = "DifficultBulletinBoardOptionFrame_Group_TopicList", 78 | labelText = "Select the Group Topics to Observe:", 79 | labelToolTip = "Check to enable scanning for messages related to this group topic in chat. Uncheck to stop searching.\n\nTags should be separated by spaces, and only the first match will be searched. Once a match is found, the message will be added to the bulletin board.", 80 | } 81 | 82 | local professionTopicListObject = { 83 | frameName = "DifficultBulletinBoardOptionFrame_Profession_TopicList", 84 | labelText = "Select the Profession Topics to Observe:", 85 | labelToolTip = "Check to enable scanning for messages related to this profession topic in chat. Uncheck to stop searching.\n\nTags should be separated by spaces, and only the first match will be searched. Once a match is found, the message will be added to the bulletin board.", 86 | } 87 | 88 | local hardcoreTopicListObject = { 89 | frameName = "DifficultBulletinBoardOptionFrame_Hardcore_TopicList", 90 | labelText = "Select the Hardcore Topics to Observe:", 91 | labelToolTip = "Check to enable scanning for messages related to this hardcore topic in chat. Uncheck to stop searching.\n\nTags should be separated by spaces, and only the first match will be searched. Once a match is found, the message will be added to the bulletin board.", 92 | } 93 | 94 | -- Option Data for the timestamp format 95 | local timeFormatDropDownOptionObject = { 96 | frameName = "DifficultBulletinBoardOptionFrame_Time_Dropdown", 97 | labelText = "Select Time Format:", 98 | labelToolTip = "Choose a time format for displaying timestamps.\n\nFixed format displays the exact time, while elapsed format shows the time since the message was posted.", 99 | items = { 100 | { text = "Fixed Time (HH:MM:SS)", value = "fixed"}, 101 | { text = "Elapsed Time (MM:SS)", value = "elapsed"} 102 | } 103 | } 104 | 105 | -- Option Data for hardcore only messages 106 | local hardcoreOnlyDropDownOptionObject = { 107 | frameName = "DifficultBulletinBoardOptionFrame_HardcoreOnly_Dropdown", 108 | labelText = "Show Hardcore Messages Only:", 109 | labelToolTip = "When enabled, only messages pertaining to hardcore characters (e.g. containing \"HC\" or \"Hardcore\") will appear.", 110 | items = { 111 | { text = "Enable Hardcore Filter", value = "true" }, 112 | { text = "Disable Hardcore Filter", value = "false" } 113 | } 114 | } 115 | 116 | -- Option Data for filtering matched messages 117 | local filterMatchedMessagesDropDownOptionObject = { 118 | frameName = "DifficultBulletinBoardOptionFrame_FilterMatched_Dropdown", 119 | labelText = "Filter Matched Messages from Chat:", 120 | labelToolTip = "When enabled, messages that match criteria and are added to the bulletin board will be hidden from your chat window.", 121 | items = { 122 | { text = "Enable Filtering", value = "true" }, 123 | { text = "Disable Filtering", value = "false" } 124 | } 125 | } 126 | 127 | -- Option Data for MainFrame sound being played 128 | local mainFrameSoundDropDownOptionObject = { 129 | frameName = "DifficultBulletinBoardOptionFrame_MainFrame_Sound_Dropdown", 130 | labelText = "Play Sound for Bulletin Board:", 131 | labelToolTip = "Enable or disable the sound that plays when the Bulletin Board is opened and closed.", 132 | items = { 133 | { text = "Enable Sound", value = "true" }, 134 | { text = "Disable Sound", value = "false" } 135 | } 136 | } 137 | 138 | -- Option Data for OptionFrame sound being played 139 | local optionFrameSoundDropDownOptionObject = { 140 | frameName = "DifficultBulletinBoardOptionFrame_OptionFrame_Sound_Dropdown", 141 | labelText = "Play Sound for Option Window:", 142 | labelToolTip = "Enable or disable the sound that plays when the Optin Window is opened and closed.", 143 | items = { 144 | { text = "Enable Sound", value = "true" }, 145 | { text = "Disable Sound", value = "false" } 146 | } 147 | } 148 | 149 | -- Option Data for the timestamp format 150 | local serverTimePositionDropDownOptionObject = { 151 | frameName = "DifficultBulletinBoardOptionFrame_Server_Time_Dropdown", 152 | labelText = "Select Server Time Position:", 153 | labelToolTip = "Choose where to display the server time or disable it:\n\n" .. 154 | "Disabled: Hides the server time completely.\n\n" .. 155 | "Top Left: Displays the server time to the left of the title, at the top of the bulletin board.\n\n" .. 156 | "To the Right of Tab Buttons: Displays the server time on the same level as the tab buttons, sticking to the right above where the time columns normally are.", 157 | items = { 158 | { text = "Top Left of Title", value = "top-left" }, 159 | { text = "Top Right of Title", value = "right-of-tabs" }, 160 | { text = "Disable Time Display", value = "disabled" } 161 | } 162 | } 163 | 164 | -- Option Data for message expiration time 165 | local messageExpirationTimeOptionObject = { 166 | frameName = "DifficultBulletinBoardOptionFrame_Expiration_Time_Input", 167 | labelText = "Message Expiration Time (seconds):", 168 | labelToolTip = "How many seconds before messages automatically expire from the bulletin board.\n\nSet to 0 to disable message expiration completely (messages will never be automatically removed).", 169 | width = 40, -- Wider input for up to 3-digit values 170 | } 171 | 172 | local fontSizeOptionInputBox 173 | local serverTimePositionDropDown 174 | local timeFormatDropDown 175 | local mainFrameSoundDropDown 176 | local optionFrameSoundDropDown 177 | local filterMatchedMessagesDropDown 178 | local hardcoreOnlyDropDown 179 | local groupOptionInputBox 180 | local professionOptionInputBox 181 | local hardcoreOptionInputBox 182 | 183 | local optionControlsToResize = {} 184 | 185 | -- Function to update option tab button widths based on frame size (matching main panel logic) 186 | local function updateOptionButtonWidths() 187 | -- Get button references 188 | local buttons = { 189 | getglobal("DifficultBulletinBoardOptionFrame_GeneralTab"), 190 | getglobal("DifficultBulletinBoardOptionFrame_GroupsTab"), 191 | getglobal("DifficultBulletinBoardOptionFrame_ProfessionsTab"), 192 | getglobal("DifficultBulletinBoardOptionFrame_HardcoreTab") 193 | } 194 | 195 | -- Safety check for buttons 196 | for i = 1, OPTION_NUM_BUTTONS do 197 | if not buttons[i] then 198 | return 199 | end 200 | end 201 | 202 | -- Get button text widths to calculate minimum required widths 203 | local textWidths = {} 204 | local totalTextWidth = 0 205 | local buttonCount = OPTION_NUM_BUTTONS 206 | 207 | -- Check all buttons have text elements and get their widths 208 | for i = 1, buttonCount do 209 | local button = buttons[i] 210 | if not button:GetName() then 211 | return 212 | end 213 | 214 | local textObjName = button:GetName() .. "_Text" 215 | local textObj = getglobal(textObjName) 216 | 217 | if not textObj then 218 | return 219 | end 220 | 221 | local textWidth = textObj:GetStringWidth() 222 | if not textWidth then 223 | return 224 | end 225 | 226 | textWidths[i] = textWidth 227 | totalTextWidth = totalTextWidth + textWidth 228 | end 229 | 230 | -- Calculate minimum padded widths for each button (text + minimum padding) 231 | local minButtonWidths = {} 232 | for i = 1, buttonCount do 233 | minButtonWidths[i] = textWidths[i] + (2 * OPTION_MIN_TEXT_PADDING) 234 | end 235 | 236 | -- Find the button that needs the most minimum width 237 | local maxMinWidth = 0 238 | for i = 1, buttonCount do 239 | if minButtonWidths[i] > maxMinWidth then 240 | maxMinWidth = minButtonWidths[i] 241 | end 242 | end 243 | 244 | -- Calculate total content width if all buttons had the same width (maxMinWidth) 245 | -- This ensures all buttons have at least minimum padding 246 | local totalEqualContentWidth = maxMinWidth * buttonCount 247 | 248 | -- Add spacing to total minimum width 249 | local totalMinFrameWidth = totalEqualContentWidth + ((OPTION_NUM_BUTTONS - 1) * OPTION_BUTTON_SPACING) + (2 * OPTION_SIDE_PADDING) 250 | 251 | -- Get current frame width 252 | local frameWidth = optionFrame:GetWidth() 253 | 254 | -- Set the minimum resizable width of the frame directly 255 | -- This prevents the user from dragging it smaller than the minimum width 256 | optionFrame:SetMinResize(totalMinFrameWidth, 300) 257 | 258 | -- If frame is somehow smaller than minimum (should not happen), force a resize 259 | if frameWidth < totalMinFrameWidth then 260 | optionFrame:SetWidth(totalMinFrameWidth) 261 | frameWidth = totalMinFrameWidth 262 | end 263 | 264 | -- Calculate available width for buttons 265 | local availableWidth = frameWidth - (2 * OPTION_SIDE_PADDING) - ((OPTION_NUM_BUTTONS - 1) * OPTION_BUTTON_SPACING) 266 | 267 | -- Calculate equal width distribution 268 | local equalWidth = availableWidth / OPTION_NUM_BUTTONS 269 | 270 | -- Always try to use equal widths first 271 | if equalWidth >= maxMinWidth then 272 | -- We can use equal widths for all buttons 273 | for i = 1, buttonCount do 274 | buttons[i]:SetWidth(equalWidth) 275 | end 276 | else 277 | -- We can't use equal widths because some text would have less than minimum padding 278 | -- Set all buttons to the maximum minimum width to ensure all have same width 279 | -- unless that would mean having less than minimum padding 280 | for i = 1, buttonCount do 281 | buttons[i]:SetWidth(maxMinWidth) 282 | end 283 | end 284 | end 285 | 286 | -- Tab System Functions 287 | function DifficultBulletinBoardOptionFrame.ShowTab(tabName) 288 | -- Force close all dropdowns first 289 | DifficultBulletinBoardOptionFrame.HideAllDropdownMenus() 290 | 291 | -- Update current tab 292 | DifficultBulletinBoardOptionFrame.currentTab = tabName 293 | 294 | -- Update tab button appearances 295 | DifficultBulletinBoardOptionFrame.UpdateTabButtons() 296 | 297 | -- Hide all content 298 | DifficultBulletinBoardOptionFrame.HideAllTabContent() 299 | 300 | -- Show content for selected tab 301 | DifficultBulletinBoardOptionFrame.ShowTabContent(tabName) 302 | 303 | -- Force close dropdowns again after showing content 304 | DifficultBulletinBoardOptionFrame.HideAllDropdownMenus() 305 | end 306 | 307 | function DifficultBulletinBoardOptionFrame.UpdateTabButtons() 308 | local currentTab = DifficultBulletinBoardOptionFrame.currentTab 309 | local tabs = {"General", "Groups", "Professions", "Hardcore"} 310 | 311 | for _, tabName in ipairs(tabs) do 312 | local tabButton = getglobal("DifficultBulletinBoardOptionFrame_" .. tabName .. "Tab") 313 | local tabText = getglobal("DifficultBulletinBoardOptionFrame_" .. tabName .. "Tab_Text") 314 | 315 | if tabButton and tabText then 316 | local isActive = (string.lower(tabName) == currentTab) 317 | 318 | if isActive then 319 | -- Active tab styling - matching main panel exactly 320 | tabButton:SetBackdropColor(0.25, 0.25, 0.3, 1.0) -- Darker color for active tab 321 | tabButton:SetBackdropBorderColor(0.3, 0.3, 0.3, 1.0) -- Same border as main panel 322 | tabText:SetTextColor(1.0, 1.0, 1.0, 1.0) -- Brighter text for active tab 323 | else 324 | -- Inactive tab styling - matching main panel exactly 325 | tabButton:SetBackdropColor(0.15, 0.15, 0.15, 1.0) -- Normal color 326 | tabButton:SetBackdropBorderColor(0.3, 0.3, 0.3, 1.0) -- Same border as main panel 327 | tabText:SetTextColor(0.9, 0.9, 0.9, 1.0) -- Normal text color 328 | end 329 | end 330 | end 331 | end 332 | 333 | function DifficultBulletinBoardOptionFrame.HideAllTabContent() 334 | -- Close all open dropdown menus first 335 | DifficultBulletinBoardOptionFrame.HideAllDropdownMenus() 336 | 337 | -- Hide all option elements 338 | for _, tab in pairs(DifficultBulletinBoardOptionFrame.tabs) do 339 | for _, element in pairs(tab) do 340 | if element and element.Hide then 341 | element:Hide() 342 | end 343 | end 344 | end 345 | end 346 | 347 | function DifficultBulletinBoardOptionFrame.ShowTabContent(tabName) 348 | local tab = DifficultBulletinBoardOptionFrame.tabs[tabName] 349 | if tab then 350 | for _, element in pairs(tab) do 351 | if element and element.Show then 352 | element:Show() 353 | end 354 | end 355 | 356 | -- Update scroll height for this specific tab and reset scroll position 357 | local tabHeight = DifficultBulletinBoardOptionFrame.tabHeights[tabName] 358 | if tabHeight and optionScrollChild then 359 | -- Get the scroll frame to calculate if we need to ensure scrollable content 360 | local scrollFrame = getglobal("DifficultBulletinBoardOptionFrame_ScrollFrame") 361 | if scrollFrame then 362 | local scrollFrameHeight = scrollFrame:GetHeight() 363 | local actualMaxScroll = math.max(0, tabHeight - scrollFrameHeight) 364 | 365 | -- If there's no natural scrolling, add enough height to make scrolling possible 366 | -- This ensures the thumb always has a reason to exist 367 | local finalHeight = tabHeight 368 | if actualMaxScroll < 25 then 369 | finalHeight = scrollFrameHeight + 25 -- Always ensure at least 25px of scrollable content 370 | end 371 | 372 | optionScrollChild:SetHeight(finalHeight) 373 | 374 | -- Reset scroll position first 375 | scrollFrame:SetVerticalScroll(0) 376 | 377 | -- Calculate final scroll range with the adjusted height 378 | local maxScroll = math.max(0, finalHeight - scrollFrameHeight) 379 | 380 | -- Update the scrollbar range and position 381 | local scrollBar = getglobal(scrollFrame:GetName().."ScrollBar") 382 | if scrollBar then 383 | scrollBar:SetMinMaxValues(0, maxScroll) 384 | scrollBar:SetValue(0) 385 | scrollBar:Show() 386 | 387 | -- Configure thumb texture 388 | local thumbTexture = scrollBar:GetThumbTexture() 389 | if thumbTexture then 390 | thumbTexture:SetWidth(8) 391 | thumbTexture:SetTexture("Interface\\ChatFrame\\ChatFrameBackground") 392 | thumbTexture:SetGradientAlpha("VERTICAL", 0.504, 0.504, 0.576, 0.7, 0.648, 0.648, 0.72, 0.9) 393 | thumbTexture:Show() 394 | end 395 | end 396 | 397 | -- Force update the scroll frame's child rect 398 | scrollFrame:UpdateScrollChildRect() 399 | end 400 | end 401 | end 402 | end 403 | 404 | -- Helper function to show consistent styled tooltips matching the main panel 405 | local function showStyledTooltip(frame, text) 406 | if not text or text == "" then 407 | return 408 | end 409 | 410 | -- Check if GameTooltip exists 411 | if not GameTooltip then 412 | return 413 | end 414 | 415 | -- Ensure GameTooltip is properly reset before use 416 | GameTooltip:Hide() 417 | GameTooltip:ClearLines() 418 | 419 | -- Set owner and anchor with error checking 420 | if GameTooltip.SetOwner then 421 | GameTooltip:SetOwner(frame, "ANCHOR_RIGHT") 422 | else 423 | return -- GameTooltip not available 424 | end 425 | 426 | -- Set the text with word wrapping enabled (white text) 427 | GameTooltip:SetText(text, 1, 1, 1, 1, true) 428 | 429 | 430 | --save the old font settings 431 | local leftLine = GameTooltipTextLeft1 432 | local oldR, oldG, oldB, oldA = GameTooltip:GetBackdropBorderColor() 433 | local oldFontPath, oldFontSize, oldFontFlags = leftLine:GetFont() 434 | 435 | -- Ensure proper font and sizing for better readability using user's font size setting + 2 436 | if GameTooltipTextLeft1 and GameTooltipTextLeft1.SetFont then 437 | local tooltipFontSize = (DifficultBulletinBoardVars.fontSize or 12) + 2 438 | GameTooltipTextLeft1:SetFont("Fonts\\ARIALN.TTF", tooltipFontSize, "") 439 | end 440 | 441 | -- Set tooltip border color to match header color (light blue-white) 442 | if GameTooltip.SetBackdropBorderColor then 443 | GameTooltip:SetBackdropBorderColor(0.9, 0.9, 1.0, 1.0) 444 | end 445 | 446 | -- Force the tooltip to appear on top of other UI elements 447 | GameTooltip:SetFrameStrata("TOOLTIP") 448 | 449 | -- Show the tooltip 450 | GameTooltip:Show() 451 | 452 | --swap in a temporary OnHide handler 453 | local origOnHide = GameTooltip:GetScript("OnHide") 454 | GameTooltip:SetScript("OnHide", function() 455 | leftLine:SetFont(oldFontPath, oldFontSize, oldFontFlags) 456 | this:SetBackdropBorderColor(oldR, oldG, oldB, oldA) 457 | 458 | --restore the original script 459 | this:SetScript("OnHide", origOnHide) 460 | 461 | --call the original if it existed 462 | if origOnHide then 463 | origOnHide() 464 | end 465 | end) 466 | end 467 | 468 | -- Helper function to hide tooltips safely 469 | local function hideStyledTooltip(frame) 470 | if GameTooltip and GameTooltip.IsOwned and GameTooltip:IsOwned(frame) then 471 | GameTooltip:Hide() 472 | elseif GameTooltip and GameTooltip.Hide then 473 | -- Fallback for cases where IsOwned might not work 474 | GameTooltip:Hide() 475 | end 476 | end 477 | 478 | -- Create a global registry to manage all dropdown menus 479 | local dropdownMenuRegistry = {} 480 | local currentTopMenu = nil 481 | local MENU_BASE_LEVEL = 100 482 | 483 | -- Margin for left alignment of option labels 484 | local OPTION_LABEL_LEFT_MARGIN = 5 -- Distance in pixels from left edge 485 | 486 | -- Extra padding to prevent text cutoff at larger font sizes 487 | local DROPDOWN_EXTRA_PADDING = 10 488 | 489 | local INPUT_BOX_TEXT_INSETS = { 5, 3, 2, 2 } -- Left, right, top, bottom 490 | 491 | -- Function to show a dropdown menu and ensure it's on top 492 | local function showDropdownMenu(dropdown) 493 | local menuFrame = dropdown.menuFrame 494 | 495 | -- Hide all other menus first 496 | for _, otherDropdown in ipairs(dropdownMenuRegistry) do 497 | if otherDropdown ~= dropdown and otherDropdown.menuFrame:IsShown() then 498 | otherDropdown.menuFrame:Hide() 499 | end 500 | end 501 | 502 | -- Set this as the current top menu 503 | currentTopMenu = dropdown 504 | 505 | -- Ensure this menu is at the highest strata and level 506 | menuFrame:SetFrameStrata("FULLSCREEN_DIALOG") 507 | menuFrame:SetFrameLevel(MENU_BASE_LEVEL) 508 | 509 | -- Show the menu 510 | menuFrame:Show() 511 | end 512 | 513 | local function overwriteTagsForAllTopics(allTopics, tempTags) 514 | for _, topic in ipairs(allTopics) do 515 | if tempTags[topic.name] then 516 | local newTags = tempTags[topic.name] 517 | topic.tags = newTags 518 | end 519 | end 520 | end 521 | 522 | -- Create scroll frame with hidden arrows and modern styling 523 | local function addScrollFrameToOptionFrame() 524 | local parentFrame = optionFrame 525 | 526 | -- Create the ScrollFrame with modern styling 527 | local optionScrollFrame = CreateFrame("ScrollFrame", "DifficultBulletinBoardOptionFrame_ScrollFrame", parentFrame, "UIPanelScrollFrameTemplate") 528 | optionScrollFrame:EnableMouseWheel(true) 529 | 530 | -- Get the scroll bar reference 531 | local scrollBar = getglobal(optionScrollFrame:GetName().."ScrollBar") 532 | 533 | -- Get references to the scroll buttons 534 | local upButton = getglobal(scrollBar:GetName().."ScrollUpButton") 535 | local downButton = getglobal(scrollBar:GetName().."ScrollDownButton") 536 | 537 | -- Completely remove the scroll buttons from the layout 538 | upButton:SetHeight(0.001) 539 | upButton:SetWidth(0.001) 540 | upButton:SetAlpha(0) 541 | upButton:EnableMouse(false) 542 | upButton:ClearAllPoints() 543 | upButton:SetPoint("TOP", scrollBar, "TOP", 0, 1000) 544 | 545 | -- Same for down button 546 | downButton:SetHeight(0.001) 547 | downButton:SetWidth(0.001) 548 | downButton:SetAlpha(0) 549 | downButton:EnableMouse(false) 550 | downButton:ClearAllPoints() 551 | downButton:SetPoint("BOTTOM", scrollBar, "BOTTOM", 0, -1000) 552 | 553 | -- Adjust scroll bar position - changed from 2 to 8 pixels for consistency 554 | scrollBar:ClearAllPoints() 555 | scrollBar:SetPoint("TOPLEFT", optionScrollFrame, "TOPRIGHT", 8, 0) 556 | scrollBar:SetPoint("BOTTOMLEFT", optionScrollFrame, "BOTTOMRIGHT", 8, 0) 557 | 558 | -- Style the scroll bar to be slimmer 559 | scrollBar:SetWidth(8) 560 | 561 | -- Set up the thumb texture with blue-tinted colors to match main panel 562 | local thumbTexture = scrollBar:GetThumbTexture() 563 | thumbTexture:SetWidth(8) 564 | thumbTexture:SetHeight(50) 565 | thumbTexture:SetTexture("Interface\\ChatFrame\\ChatFrameBackground") 566 | -- Blue-tinted gradient to match main panel 567 | thumbTexture:SetGradientAlpha("VERTICAL", 0.504, 0.504, 0.576, 0.7, 0.648, 0.648, 0.72, 0.9) 568 | 569 | -- Style the scroll bar track with darker background to match main panel 570 | scrollBar:SetBackdrop({ 571 | bgFile = "Interface\\ChatFrame\\ChatFrameBackground", 572 | edgeFile = nil, 573 | tile = true, tileSize = 8, 574 | insets = { left = 0, right = 0, top = 0, bottom = 0 } 575 | }) 576 | scrollBar:SetBackdropColor(0.072, 0.072, 0.108, 0.3) 577 | 578 | -- Close dropdowns when scroll bar is used (while preserving scroll functionality) 579 | scrollBar:SetScript("OnValueChanged", function() 580 | -- Preserve the original scroll functionality 581 | optionScrollFrame:SetVerticalScroll(this:GetValue()) 582 | 583 | -- Close dropdowns to prevent positioning issues 584 | DifficultBulletinBoardOptionFrame.HideAllDropdownMenus() 585 | end) 586 | 587 | -- FIXED: Set ScrollFrame anchors to match main panel positioning exactly 588 | optionScrollFrame:SetPoint("TOPLEFT", parentFrame, "TOPLEFT", 15, -55) -- Match main panel: -55px from top 589 | optionScrollFrame:SetPoint("BOTTOMRIGHT", parentFrame, "BOTTOMRIGHT", -26, 50) 590 | 591 | -- Create the ScrollChild with proper styling 592 | optionScrollChild = CreateFrame("Frame", nil, optionScrollFrame) 593 | optionScrollChild:SetWidth(optionScrollFrame:GetWidth() - 10) -- Adjusted width calculation 594 | optionScrollChild:SetHeight(1) 595 | 596 | -- Set the background for better visual distinction 597 | local background = optionScrollChild:CreateTexture(nil, "BACKGROUND") 598 | background:SetAllPoints() 599 | background:SetTexture("Interface\\ChatFrame\\ChatFrameBackground") 600 | background:SetGradientAlpha("VERTICAL", 0.1, 0.1, 0.1, 0.5, 0.1, 0.1, 0.1, 0.0) 601 | 602 | -- Use both mouse wheel directions for scrolling 603 | optionScrollFrame:SetScript("OnMouseWheel", function() 604 | local scrollBar = getglobal(this:GetName().."ScrollBar") 605 | local currentValue = scrollBar:GetValue() 606 | 607 | if arg1 > 0 then 608 | scrollBar:SetValue(currentValue - (scrollBar:GetHeight() / 2)) 609 | else 610 | scrollBar:SetValue(currentValue + (scrollBar:GetHeight() / 2)) 611 | end 612 | 613 | -- Close all open dropdown menus when scrolling to prevent positioning issues 614 | DifficultBulletinBoardOptionFrame.HideAllDropdownMenus() 615 | end) 616 | 617 | optionScrollFrame:SetScrollChild(optionScrollChild) 618 | end 619 | 620 | -- Dropdown option function with proper z-ordering and dynamic width 621 | local function addDropDownOptionToOptionFrame(options, defaultValue) 622 | -- Adjust vertical offset for the dropdown with improved spacing 623 | optionYOffset = optionYOffset - OPTION_SPACING 624 | 625 | -- Create a frame to hold the label and enable mouse interactions 626 | local labelFrame = CreateFrame("Frame", nil, optionScrollChild) 627 | labelFrame:SetPoint("TOPLEFT", optionScrollChild, "TOPLEFT", OPTION_LABEL_LEFT_MARGIN, optionYOffset) 628 | labelFrame:SetHeight(20) 629 | 630 | -- Create the label (FontString) inside the frame 631 | local label = labelFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlight") 632 | label:SetAllPoints(labelFrame) 633 | label:SetText(options.labelText) 634 | label:SetFont("Fonts\\FRIZQT__.TTF", DifficultBulletinBoardVars.fontSize) 635 | label:SetTextColor(0.9, 0.9, 0.9, 1.0) 636 | label:SetJustifyH("LEFT") -- Explicitly set left alignment for consistent text starting position 637 | 638 | --set labelFrame width afterwards with padding so the label is not cut off 639 | labelFrame:SetWidth(label:GetStringWidth() + 40) 640 | 641 | -- Add a GameTooltip to the labelFrame 642 | labelFrame:EnableMouse(true) 643 | labelFrame:SetScript("OnEnter", function() 644 | showStyledTooltip(labelFrame, options.labelToolTip) 645 | end) 646 | labelFrame:SetScript("OnLeave", function() 647 | hideStyledTooltip(labelFrame) 648 | end) 649 | 650 | -- Create temporary exact clone of the GameFontHighlight to measure text properly 651 | local tempFont = UIParent:CreateFontString(nil, "OVERLAY", "GameFontHighlight") 652 | local fontPath, fontSize, fontFlags = GameFontHighlight:GetFont() 653 | tempFont:SetFont(fontPath, fontSize, fontFlags) 654 | 655 | -- Set extra wide width to ensure accurate measurements 656 | tempFont:SetWidth(500) 657 | 658 | local maxTextWidth = 0 659 | local itemsWithLongText = {} 660 | 661 | -- First pass: Check width of dropdown items and identify longer items 662 | for i, item in ipairs(options.items) do 663 | tempFont:SetText(item.text) 664 | local width = tempFont:GetStringWidth() 665 | 666 | -- Store the actual width for each item 667 | itemsWithLongText[i] = width 668 | 669 | if width > maxTextWidth then 670 | maxTextWidth = width 671 | end 672 | end 673 | 674 | -- Add proper padding for dropdown elements with extra buffer 675 | -- Account for: left padding (8px) + arrow width (16px) + arrow right padding (8px) + right text padding (4px) 676 | local ARROW_WIDTH = 16 677 | local TEXT_PADDING = 12 -- Total horizontal text padding (left + right) 678 | local ARROW_PADDING = 8 -- Space to the right of text before arrow 679 | local BORDER_PADDING = 4 -- Extra space for border elements 680 | local DROPDOWN_EXTRA_PADDING = 10 -- Extra padding to prevent text cutoff at larger font sizes 681 | 682 | -- Calculate required dropdown width with proper padding and font size adjustment 683 | local fontSizeAdjustment = (tonumber(DifficultBulletinBoardVars.fontSize) - 11) * 1.5 -- Additional width per font size point above default 684 | local dropdownWidth = maxTextWidth + TEXT_PADDING + ARROW_WIDTH + ARROW_PADDING + BORDER_PADDING + DROPDOWN_EXTRA_PADDING 685 | 686 | -- Add extra width for larger font sizes 687 | if tonumber(DifficultBulletinBoardVars.fontSize) > 11 then 688 | dropdownWidth = dropdownWidth + fontSizeAdjustment 689 | end 690 | 691 | -- Minimum width with proper padding 692 | local MIN_WIDTH = 120 -- Base minimum without text 693 | if dropdownWidth < MIN_WIDTH then 694 | dropdownWidth = MIN_WIDTH 695 | end 696 | 697 | -- Round up to nearest even number for better visual appearance 698 | dropdownWidth = math.ceil(dropdownWidth / 2) * 2 699 | 700 | -- Clean up temporary font 701 | tempFont:Hide() 702 | 703 | -- adjust the optionYOffset for the dropdown box 704 | optionYOffset = optionYOffset - LABEL_SPACING 705 | 706 | -- Create a container frame for our custom dropdown 707 | local dropdownContainer = CreateFrame("Frame", options.frameName.."Container", optionScrollChild) 708 | dropdownContainer:SetPoint("LEFT", labelFrame, "LEFT", 0, -20) 709 | dropdownContainer:SetWidth(dropdownWidth) 710 | dropdownContainer:SetHeight(22) 711 | 712 | -- Create the dropdown button with modern styling 713 | local dropdown = CreateFrame("Button", options.frameName, dropdownContainer) 714 | dropdown:SetPoint("TOPLEFT", dropdownContainer, "TOPLEFT", 0, 0) 715 | dropdown:SetPoint("BOTTOMRIGHT", dropdownContainer, "BOTTOMRIGHT", 0, 0) 716 | 717 | -- Add modern backdrop 718 | dropdown:SetBackdrop({ 719 | bgFile = "Interface\\ChatFrame\\ChatFrameBackground", 720 | edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", 721 | tile = true, tileSize = 16, edgeSize = 8, 722 | insets = { left = 2, right = 2, top = 2, bottom = 2 } 723 | }) 724 | dropdown:SetBackdropColor(0.1, 0.1, 0.1, 0.8) 725 | dropdown:SetBackdropBorderColor(0.3, 0.3, 0.3, 1.0) 726 | 727 | -- Create the selected text display with proper padding for arrow 728 | local selectedText = dropdown:CreateFontString(nil, "OVERLAY", "GameFontHighlight") 729 | selectedText:SetPoint("LEFT", dropdown, "LEFT", 8, 0) 730 | selectedText:SetPoint("RIGHT", dropdown, "RIGHT", -(ARROW_WIDTH + ARROW_PADDING), 0) 731 | selectedText:SetJustifyH("LEFT") 732 | selectedText:SetTextColor(0.9, 0.9, 0.9, 1.0) 733 | 734 | -- Create dropdown arrow texture using down.tga as default 735 | local arrow = dropdown:CreateTexture(nil, "OVERLAY") 736 | arrow:SetTexture("Interface\\AddOns\\DifficultBulletinBoard\\icons\\down.tga") 737 | arrow:SetWidth(ARROW_WIDTH) 738 | arrow:SetHeight(16) 739 | arrow:SetPoint("RIGHT", dropdown, "RIGHT", -4, 0) 740 | arrow:SetTexCoord(0, 1, 0, 1) -- Use full texture 741 | 742 | -- Store references to the text object and value 743 | dropdown.value = defaultValue 744 | dropdown.text = selectedText 745 | dropdown.arrow = arrow 746 | dropdown.menuOpen = false -- Track menu state 747 | 748 | -- Initialize with default value 749 | local matchFound = false 750 | for _, item in ipairs(options.items) do 751 | if item.value == defaultValue then 752 | selectedText:SetText(item.text) 753 | matchFound = true 754 | break 755 | end 756 | end 757 | 758 | -- Fallback: Set to first option if no match found 759 | if not matchFound then 760 | if options.items and options.items[1] then 761 | selectedText:SetText(options.items[1].text) 762 | dropdown.value = options.items[1].value 763 | end 764 | end 765 | 766 | -- Create the menu frame with matching width 767 | local menuFrame = CreateFrame("Frame", options.frameName.."Menu", UIParent) 768 | menuFrame:SetFrameStrata("TOOLTIP") 769 | menuFrame:SetWidth(dropdownWidth) 770 | menuFrame:SetBackdrop({ 771 | bgFile = "Interface\\ChatFrame\\ChatFrameBackground", 772 | edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", 773 | tile = true, tileSize = 16, edgeSize = 8, 774 | insets = { left = 3, right = 3, top = 3, bottom = 3 } 775 | }) 776 | menuFrame:SetBackdropColor(0.1, 0.1, 0.1, 0.95) 777 | menuFrame:SetBackdropBorderColor(0.4, 0.4, 0.4, 1.0) 778 | menuFrame:Hide() 779 | 780 | -- Create menu items 781 | local menuHeight = 0 782 | local itemHeight = 24 783 | 784 | for i, item in ipairs(options.items) do 785 | local menuItem = CreateFrame("Button", options.frameName.."MenuItem"..i, menuFrame) 786 | menuItem:SetHeight(itemHeight) 787 | menuItem:SetPoint("TOPLEFT", menuFrame, "TOPLEFT", 4, -4 - (i-1)*itemHeight) 788 | menuItem:SetPoint("TOPRIGHT", menuFrame, "TOPRIGHT", -4, -4 - (i-1)*itemHeight) 789 | 790 | menuItem.value = item.value 791 | menuItem.text = item.text 792 | 793 | -- Normal state 794 | menuItem:SetBackdrop({ 795 | bgFile = "Interface\\ChatFrame\\ChatFrameBackground", 796 | edgeFile = nil, 797 | tile = true, tileSize = 16, edgeSize = 0, 798 | insets = { left = 0, right = 0, top = 0, bottom = 0 } 799 | }) 800 | menuItem:SetBackdropColor(0.12, 0.12, 0.12, 0.0) 801 | 802 | -- Item text 803 | local itemText = menuItem:CreateFontString(nil, "OVERLAY", "GameFontHighlight") 804 | itemText:SetPoint("LEFT", menuItem, "LEFT", 8, 0) 805 | itemText:SetPoint("RIGHT", menuItem, "RIGHT", -8, 0) 806 | itemText:SetJustifyH("LEFT") 807 | itemText:SetText(item.text) 808 | itemText:SetTextColor(0.9, 0.9, 0.9, 1.0) 809 | 810 | -- Highlight state - update to match headline color 811 | menuItem:SetScript("OnEnter", function() 812 | this:SetBackdropColor(0.2, 0.2, 0.25, 1.0) 813 | itemText:SetTextColor(0.9, 0.9, 1.0, 1.0) 814 | end) 815 | 816 | menuItem:SetScript("OnLeave", function() 817 | this:SetBackdropColor(0.12, 0.12, 0.12, 0.0) 818 | itemText:SetTextColor(0.9, 0.9, 0.9, 1.0) 819 | end) 820 | 821 | -- Click handler 822 | menuItem:SetScript("OnClick", function() 823 | dropdown.value = this.value 824 | dropdown.text:SetText(this.text) 825 | menuFrame:Hide() 826 | dropdown.menuOpen = false 827 | -- Reset arrow to normal state when menu closes 828 | dropdown.arrow:SetTexture("Interface\\AddOns\\DifficultBulletinBoard\\icons\\down.tga") 829 | end) 830 | 831 | menuHeight = menuHeight + itemHeight 832 | end 833 | 834 | -- Set menu height 835 | menuFrame:SetHeight(menuHeight + 8) 836 | 837 | -- Position update function 838 | local function updateMenuPosition() 839 | local dropLeft, dropBottom = dropdown:GetLeft(), dropdown:GetBottom() 840 | if dropLeft and dropBottom then 841 | menuFrame:ClearAllPoints() 842 | menuFrame:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT", dropLeft, dropBottom - 2) 843 | end 844 | end 845 | 846 | -- Toggle menu 847 | dropdown:SetScript("OnClick", function() 848 | if menuFrame:IsShown() then 849 | -- Menu is closing 850 | menuFrame:Hide() 851 | dropdown.menuOpen = false 852 | -- Reset arrow to normal state 853 | this.arrow:SetTexture("Interface\\AddOns\\DifficultBulletinBoard\\icons\\down.tga") 854 | else 855 | -- Menu is opening 856 | -- Hide all other open dropdown menus first 857 | if DROPDOWN_MENUS_LIST then 858 | for _, menu in ipairs(DROPDOWN_MENUS_LIST) do 859 | if menu ~= menuFrame and menu:IsShown() then 860 | menu:Hide() 861 | end 862 | end 863 | end 864 | 865 | -- Change arrow to gradient when menu opens 866 | this.arrow:SetTexture("Interface\\AddOns\\DifficultBulletinBoard\\icons\\gradient_down.tga") 867 | dropdown.menuOpen = true 868 | 869 | updateMenuPosition() 870 | menuFrame:Show() 871 | end 872 | end) 873 | 874 | -- Hover effect with blue-tinted colors to match main panel 875 | dropdown:SetScript("OnEnter", function() 876 | this:SetBackdropColor(0.15, 0.15, 0.18, 0.8) 877 | this:SetBackdropBorderColor(0.4, 0.4, 0.5, 1.0) 878 | selectedText:SetTextColor(0.9, 0.9, 1.0, 1.0) 879 | end) 880 | 881 | dropdown:SetScript("OnLeave", function() 882 | this:SetBackdropColor(0.1, 0.1, 0.1, 0.8) 883 | this:SetBackdropBorderColor(0.3, 0.3, 0.3, 1.0) 884 | selectedText:SetTextColor(0.9, 0.9, 0.9, 1.0) 885 | end) 886 | 887 | -- Store menu reference 888 | dropdown.menuFrame = menuFrame 889 | 890 | -- Add menu hide handler to reset arrow 891 | menuFrame:SetScript("OnHide", function() 892 | dropdown.menuOpen = false 893 | dropdown.arrow:SetTexture("Interface\\AddOns\\DifficultBulletinBoard\\icons\\down.tga") 894 | end) 895 | 896 | -- Close menu when clicking elsewhere 897 | if not DROPDOWN_MENUS_LIST then 898 | DROPDOWN_MENUS_LIST = {} 899 | 900 | -- Global click handler 901 | local clickHandler = CreateFrame("Frame") 902 | clickHandler:SetScript("OnEvent", function() 903 | if event == "GLOBAL_MOUSE_DOWN" then 904 | for _, menu in ipairs(DROPDOWN_MENUS_LIST) do 905 | if menu:IsShown() then 906 | menu:Hide() 907 | end 908 | end 909 | end 910 | end) 911 | clickHandler:RegisterEvent("GLOBAL_MOUSE_DOWN") 912 | end 913 | 914 | table.insert(DROPDOWN_MENUS_LIST, menuFrame) 915 | 916 | -- Custom functions 917 | dropdown.GetSelectedValue = function(self) 918 | return self.value 919 | end 920 | 921 | dropdown.SetSelectedValue = function(self, value, text) 922 | self.value = value 923 | 924 | if text then 925 | self.text:SetText(text) 926 | else 927 | for _, item in ipairs(options.items) do 928 | if item.value == value then 929 | self.text:SetText(item.text) 930 | break 931 | end 932 | end 933 | end 934 | end 935 | 936 | dropdown.GetText = function(self) 937 | return self.text:GetText() 938 | end 939 | 940 | dropdown.SetText = function(self, text) 941 | self.text:SetText(text) 942 | end 943 | 944 | -- Add a reset function to force close the dropdown 945 | dropdown.ForceClose = function(self) 946 | if self.menuFrame and self.menuFrame:IsShown() then 947 | self.menuFrame:Hide() 948 | end 949 | self.menuOpen = false 950 | if self.arrow then 951 | self.arrow:SetTexture("Interface\\AddOns\\DifficultBulletinBoard\\icons\\down.tga") 952 | end 953 | end 954 | 955 | table.insert(optionControlsToResize, dropdownContainer) 956 | 957 | -- Track elements for tab assignment 958 | table.insert(DifficultBulletinBoardOptionFrame.lastCreatedElements, labelFrame) 959 | table.insert(DifficultBulletinBoardOptionFrame.lastCreatedElements, dropdownContainer) 960 | table.insert(DifficultBulletinBoardOptionFrame.lastCreatedElements, menuFrame) 961 | 962 | -- Register this dropdown for state management 963 | table.insert(DifficultBulletinBoardOptionFrame.allDropdowns, dropdown) 964 | 965 | return dropdown 966 | end 967 | 968 | -- Creates an input box option with label and tooltip 969 | local function addInputBoxOptionToOptionFrame(option, value) 970 | -- Adjust Y offset for the new option with improved spacing 971 | optionYOffset = optionYOffset - OPTION_SPACING 972 | 973 | -- Create a frame to hold the label and allow for mouse interactions 974 | local labelFrame = CreateFrame("Frame", nil, optionScrollChild) 975 | labelFrame:SetPoint("TOPLEFT", optionScrollChild, "TOPLEFT", OPTION_LABEL_LEFT_MARGIN, optionYOffset) 976 | labelFrame:SetHeight(20) 977 | 978 | -- Create the label (FontString) inside the frame 979 | local label = labelFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlight") 980 | label:SetAllPoints(labelFrame) -- Make the label take up the full frame 981 | label:SetText(option.labelText) 982 | label:SetFont("Fonts\\FRIZQT__.TTF", DifficultBulletinBoardVars.fontSize) 983 | label:SetTextColor(0.9, 0.9, 0.9, 1.0) 984 | label:SetJustifyH("LEFT") -- Explicitly set left alignment for consistent text starting position 985 | 986 | --set labelFrame width afterwards with padding so the label is not cut off 987 | labelFrame:SetWidth(label:GetStringWidth() + 20) 988 | 989 | -- Add a GameTooltip to the labelFrame 990 | labelFrame:EnableMouse(true) -- Enable mouse interactions for the frame 991 | labelFrame:SetScript("OnEnter", function() 992 | showStyledTooltip(labelFrame, option.labelToolTip) 993 | end) 994 | labelFrame:SetScript("OnLeave", function() 995 | hideStyledTooltip(labelFrame) 996 | end) 997 | 998 | -- Create a backdrop for the input box for a modern look 999 | local inputBackdrop = { 1000 | bgFile = "Interface\\ChatFrame\\ChatFrameBackground", 1001 | edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", 1002 | tile = true, tileSize = 16, edgeSize = 8, 1003 | insets = { left = 2, right = 2, top = 2, bottom = 2 } 1004 | } 1005 | 1006 | -- adjust the optionYOffset for the inputBox 1007 | optionYOffset = optionYOffset - LABEL_SPACING 1008 | 1009 | -- Create the input field (EditBox) with modern styling 1010 | local inputBox = CreateFrame("EditBox", option.frameName, optionScrollChild) 1011 | inputBox:SetPoint("LEFT", labelFrame, "LEFT", 0, -20) 1012 | -- Use custom width if provided, else default 1013 | inputBox:SetWidth(option.width or 33) 1014 | inputBox:SetHeight(20) 1015 | inputBox:SetBackdrop(inputBackdrop) 1016 | inputBox:SetBackdropColor(0.1, 0.1, 0.1, 0.8) 1017 | inputBox:SetBackdropBorderColor(0.3, 0.3, 0.3, 1.0) 1018 | inputBox:SetTextInsets(unpack(INPUT_BOX_TEXT_INSETS)) 1019 | inputBox:SetText(value or "") 1020 | inputBox:SetFontObject(GameFontHighlight) 1021 | inputBox:SetTextColor(1, 1, 1, 1) 1022 | inputBox:SetAutoFocus(false) 1023 | inputBox:SetJustifyH("LEFT") 1024 | 1025 | -- Add highlight effect on focus with blue-tinted colors to match main panel 1026 | inputBox:SetScript("OnEditFocusGained", function() 1027 | this:SetBackdropBorderColor(0.4, 0.4, 0.5, 1.0) -- Subtle blue-tinted highlight 1028 | end) 1029 | inputBox:SetScript("OnEditFocusLost", function() 1030 | this:SetBackdropBorderColor(0.3, 0.3, 0.3, 1.0) 1031 | end) 1032 | 1033 | -- Track elements for tab assignment 1034 | table.insert(DifficultBulletinBoardOptionFrame.lastCreatedElements, labelFrame) 1035 | table.insert(DifficultBulletinBoardOptionFrame.lastCreatedElements, inputBox) 1036 | 1037 | return inputBox 1038 | end 1039 | 1040 | local tempTagsTextBoxes = {} 1041 | local checkboxes = {} -- Track all checkboxes we create for texture updates 1042 | 1043 | -- Helper function to update checkbox textures after they've been created 1044 | local function updateCheckboxTextures() 1045 | for i, checkbox in ipairs(checkboxes) do 1046 | if checkbox and checkbox:GetChecked() and checkbox.customCheckTexture then 1047 | checkbox.customCheckTexture:Show() 1048 | elseif checkbox and checkbox.customCheckTexture then 1049 | checkbox.customCheckTexture:Hide() 1050 | end 1051 | end 1052 | end 1053 | 1054 | -- Creates a customized topic list with checkboxes and tag inputs 1055 | local function addTopicListToOptionFrame(topicObject, topicList) 1056 | local parentFrame = optionScrollChild 1057 | local tempTags = {} 1058 | 1059 | optionYOffset = optionYOffset - SECTION_SPACING 1060 | 1061 | -- Create a frame to hold the label and allow for mouse interactions 1062 | local labelFrame = CreateFrame("Frame", nil, optionScrollChild) 1063 | labelFrame:SetPoint("TOPLEFT", optionScrollChild, "TOPLEFT", OPTION_LABEL_LEFT_MARGIN, optionYOffset) 1064 | labelFrame:SetHeight(20) 1065 | 1066 | -- Create the label (FontString) inside the frame with blue-tinted color to match main panel 1067 | local scrollLabel = labelFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlight") 1068 | scrollLabel:SetAllPoints(labelFrame) -- Make the label take up the full frame 1069 | scrollLabel:SetText(topicObject.labelText) 1070 | scrollLabel:SetFont("Fonts\\FRIZQT__.TTF", DifficultBulletinBoardVars.fontSize) 1071 | scrollLabel:SetTextColor(0.9, 0.9, 1.0, 1.0) -- Blue-tinted header color to match main panel 1072 | scrollLabel:SetJustifyH("LEFT") -- Explicitly set left alignment for consistent text starting position 1073 | 1074 | -- Set width based on actual text width plus padding 1075 | labelFrame:SetWidth(scrollLabel:GetStringWidth() + 40) 1076 | 1077 | -- Add a GameTooltip to the labelFrame 1078 | labelFrame:EnableMouse(true) -- Enable mouse interactions for the frame 1079 | labelFrame:SetScript("OnEnter", function() 1080 | showStyledTooltip(labelFrame, topicObject.labelToolTip) 1081 | end) 1082 | labelFrame:SetScript("OnLeave", function() 1083 | hideStyledTooltip(labelFrame) 1084 | end) 1085 | 1086 | -- Create a separator line 1087 | local separator = parentFrame:CreateTexture(nil, "BACKGROUND") 1088 | separator:SetHeight(1) 1089 | separator:SetWidth(1000) 1090 | separator:SetPoint("TOPLEFT", labelFrame, "BOTTOMLEFT", -5, -5) 1091 | separator:SetTexture(1, 1, 1, 0.2) 1092 | 1093 | -- Add extra spacing after the separator line to prevent overlap 1094 | optionYOffset = optionYOffset - 15 1095 | 1096 | for _, topic in ipairs(topicList) do 1097 | optionYOffset = optionYOffset - OPTION_SPACING -- Adjust the vertical offset for the next row 1098 | 1099 | -- Create a custom checkbox button (without using the template) 1100 | local checkbox = CreateFrame("Button", nil, parentFrame) 1101 | checkbox:SetWidth(20) 1102 | checkbox:SetHeight(20) 1103 | checkbox:SetPoint("TOPLEFT", parentFrame, "TOPLEFT", 10, optionYOffset) 1104 | 1105 | -- Create empty checkbox background texture 1106 | local emptyBoxTexture = checkbox:CreateTexture(nil, "BACKGROUND") 1107 | emptyBoxTexture:SetTexture("Interface\\Buttons\\UI-CheckBox-Up") 1108 | emptyBoxTexture:SetAllPoints(checkbox) 1109 | checkbox:SetNormalTexture(emptyBoxTexture) 1110 | 1111 | -- Create pushed state texture 1112 | local pushedTexture = checkbox:CreateTexture(nil, "BACKGROUND") 1113 | pushedTexture:SetTexture("Interface\\Buttons\\UI-CheckBox-Down") 1114 | pushedTexture:SetAllPoints(checkbox) 1115 | checkbox:SetPushedTexture(pushedTexture) 1116 | 1117 | -- Create highlight texture 1118 | local highlightTexture = checkbox:CreateTexture(nil, "HIGHLIGHT") 1119 | highlightTexture:SetTexture("Interface\\Buttons\\UI-CheckBox-Highlight") 1120 | highlightTexture:SetAllPoints(checkbox) 1121 | highlightTexture:SetBlendMode("ADD") 1122 | checkbox:SetHighlightTexture(highlightTexture) 1123 | 1124 | -- Create custom check mark frame with adjustable dimensions and position 1125 | local checkMarkFrame = CreateFrame("Frame", nil, checkbox) 1126 | 1127 | -- Set the desired size for the check mark 1128 | local checkMarkWidth = 12 -- Width of check mark 1129 | local checkMarkHeight = 12 -- Height of check mark 1130 | local xOffset = 0 -- Horizontal positioning (negative = left, positive = right) 1131 | local yOffset = 2 -- Vertical positioning (negative = down, positive = up) 1132 | 1133 | checkMarkFrame:SetWidth(checkMarkWidth) 1134 | checkMarkFrame:SetHeight(checkMarkHeight) 1135 | checkMarkFrame:SetPoint("CENTER", checkbox, "CENTER", xOffset, yOffset) 1136 | checkMarkFrame:SetFrameLevel(checkbox:GetFrameLevel() + 5) 1137 | 1138 | local checkMarkTexture = checkMarkFrame:CreateTexture(nil, "OVERLAY") 1139 | checkMarkTexture:SetTexture("Interface\\AddOns\\DifficultBulletinBoard\\icons\\check_sign.tga") 1140 | checkMarkTexture:SetAllPoints(checkMarkFrame) 1141 | 1142 | -- Store state and references 1143 | checkbox.isChecked = topic.selected 1144 | checkbox.checkMarkFrame = checkMarkFrame 1145 | checkbox.topicRef = topic 1146 | 1147 | -- Apply initial state 1148 | if checkbox.isChecked then 1149 | checkMarkFrame:Show() 1150 | else 1151 | checkMarkFrame:Hide() 1152 | end 1153 | 1154 | -- Handle clicking on the checkbox 1155 | checkbox:SetScript("OnClick", function() 1156 | local self = this 1157 | 1158 | -- Toggle checked state 1159 | self.isChecked = not self.isChecked 1160 | 1161 | -- Update topic data 1162 | self.topicRef.selected = self.isChecked 1163 | 1164 | -- Update visual state 1165 | if self.isChecked then 1166 | self.checkMarkFrame:Show() 1167 | else 1168 | self.checkMarkFrame:Hide() 1169 | end 1170 | end) 1171 | 1172 | -- Add a label next to the checkbox 1173 | local topicLabel = parentFrame:CreateFontString(nil, "OVERLAY", "GameFontNormal") 1174 | topicLabel:SetPoint("LEFT", checkbox, "RIGHT", 10, 0) 1175 | topicLabel:SetText(topic.name) 1176 | topicLabel:SetFont("Fonts\\FRIZQT__.TTF", DifficultBulletinBoardVars.fontSize - 2) 1177 | topicLabel:SetTextColor(0.9, 0.9, 0.9, 1.0) 1178 | topicLabel:SetJustifyH("LEFT") 1179 | topicLabel:SetWidth(175) 1180 | 1181 | -- Make the label clickable too 1182 | local labelClickArea = CreateFrame("Button", nil, parentFrame) 1183 | labelClickArea:SetPoint("LEFT", checkbox, "RIGHT", 10, 0) 1184 | labelClickArea:SetWidth(175) 1185 | labelClickArea:SetHeight(20) 1186 | labelClickArea:SetScript("OnClick", function() 1187 | -- Forward clicks to the checkbox 1188 | checkbox:Click() 1189 | end) 1190 | 1191 | -- Create a backdrop for tags textbox 1192 | local tagsBackdrop = { 1193 | bgFile = "Interface\\ChatFrame\\ChatFrameBackground", 1194 | edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", 1195 | tile = true, tileSize = 16, edgeSize = 8, 1196 | insets = { left = 2, right = 2, top = 2, bottom = 2 } 1197 | } 1198 | 1199 | -- Add a text box next to the topic label for tags input 1200 | local tagsTextBox = CreateFrame("EditBox", "$parent_" .. topic.name .. "_TagsTextBox", parentFrame) 1201 | tagsTextBox:SetPoint("LEFT", topicLabel, "RIGHT", 10, 0) 1202 | tagsTextBox:SetWidth(200) 1203 | tagsTextBox:SetHeight(24) 1204 | tagsTextBox:SetBackdrop(tagsBackdrop) 1205 | tagsTextBox:SetBackdropColor(0.1, 0.1, 0.1, 0.8) 1206 | tagsTextBox:SetBackdropBorderColor(0.3, 0.3, 0.3, 1.0) 1207 | tagsTextBox:SetText(topic.tags and table.concat(topic.tags, " ") or "") 1208 | tagsTextBox:SetFont("Fonts\\FRIZQT__.TTF", DifficultBulletinBoardVars.fontSize + 1) 1209 | tagsTextBox:SetTextColor(1, 1, 1, 1) 1210 | tagsTextBox:SetAutoFocus(false) 1211 | tagsTextBox:SetJustifyH("LEFT") 1212 | tagsTextBox:SetTextInsets(unpack(INPUT_BOX_TEXT_INSETS)) 1213 | 1214 | -- Add highlight effect on focus with blue-tinted colors to match main panel 1215 | tagsTextBox:SetScript("OnEditFocusGained", function() 1216 | this:SetBackdropBorderColor(0.4, 0.4, 0.5, 1.0) -- Subtle blue-tinted highlight 1217 | end) 1218 | tagsTextBox:SetScript("OnEditFocusLost", function() 1219 | this:SetBackdropBorderColor(0.3, 0.3, 0.3, 1.0) 1220 | end) 1221 | 1222 | local topicName = topic.name -- save a reference for the onTextChanged event 1223 | tagsTextBox:SetScript("OnTextChanged", function() 1224 | local enteredText = this:GetText() 1225 | tempTags[topicName] = DifficultBulletinBoard.SplitIntoLowerWords(enteredText) 1226 | end) 1227 | 1228 | table.insert(tempTagsTextBoxes, tagsTextBox) 1229 | 1230 | -- Track elements for tab assignment 1231 | table.insert(DifficultBulletinBoardOptionFrame.lastCreatedElements, checkbox) 1232 | table.insert(DifficultBulletinBoardOptionFrame.lastCreatedElements, topicLabel) 1233 | table.insert(DifficultBulletinBoardOptionFrame.lastCreatedElements, labelClickArea) 1234 | table.insert(DifficultBulletinBoardOptionFrame.lastCreatedElements, tagsTextBox) 1235 | end 1236 | 1237 | -- Track the main elements for tab assignment 1238 | table.insert(DifficultBulletinBoardOptionFrame.lastCreatedElements, labelFrame) 1239 | table.insert(DifficultBulletinBoardOptionFrame.lastCreatedElements, separator) 1240 | 1241 | return tempTags 1242 | end 1243 | 1244 | local function normalizeFrameWidths(frames) 1245 | -- Find the maximum width 1246 | local maxWidth = 0 1247 | for _, frame in ipairs(frames) do 1248 | local w = frame:GetWidth() 1249 | if w and w > maxWidth then 1250 | maxWidth = w 1251 | end 1252 | end 1253 | 1254 | -- Apply that width to all 1255 | for _, frame in ipairs(frames) do 1256 | frame:SetWidth(maxWidth) 1257 | end 1258 | end 1259 | 1260 | -- Helper: finalize layout (normalize widths, adjust scroll height) 1261 | local function finalizeOptionFrame() 1262 | normalizeFrameWidths(optionControlsToResize) 1263 | 1264 | -- Set initial scroll height for the general tab (will be updated when switching tabs) 1265 | optionScrollChild:SetHeight(DifficultBulletinBoardOptionFrame.tabHeights.general) 1266 | end 1267 | 1268 | -- Add section headers for better organization (matching topic list style) 1269 | local function addSectionHeader(text) 1270 | optionYOffset = optionYOffset - SECTION_SPACING 1271 | 1272 | -- Create a frame to hold the header and allow for mouse interactions 1273 | local headerFrame = CreateFrame("Frame", nil, optionScrollChild) 1274 | headerFrame:SetPoint("TOPLEFT", optionScrollChild, "TOPLEFT", OPTION_LABEL_LEFT_MARGIN, optionYOffset) 1275 | headerFrame:SetHeight(20) 1276 | 1277 | -- Create the header text (matching topic list style) 1278 | local header = headerFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlight") 1279 | header:SetAllPoints(headerFrame) 1280 | header:SetText(text) 1281 | header:SetFont("Fonts\\FRIZQT__.TTF", DifficultBulletinBoardVars.fontSize) 1282 | header:SetTextColor(0.9, 0.9, 1.0, 1.0) -- Blue-tinted header color to match topic lists 1283 | header:SetJustifyH("LEFT") 1284 | 1285 | -- Set width based on actual text width plus padding 1286 | headerFrame:SetWidth(header:GetStringWidth() + 40) 1287 | 1288 | -- Create a separator line (matching topic list style) 1289 | local separator = optionScrollChild:CreateTexture(nil, "BACKGROUND") 1290 | separator:SetHeight(1) 1291 | separator:SetWidth(1000) 1292 | separator:SetPoint("TOPLEFT", headerFrame, "BOTTOMLEFT", -5, -5) 1293 | separator:SetTexture(1, 1, 1, 0.2) 1294 | 1295 | -- Add to tracking 1296 | table.insert(DifficultBulletinBoardOptionFrame.lastCreatedElements, headerFrame) 1297 | table.insert(DifficultBulletinBoardOptionFrame.lastCreatedElements, separator) 1298 | 1299 | optionYOffset = optionYOffset - LABEL_SPACING 1300 | end 1301 | 1302 | -- Track individual content height for each tab 1303 | DifficultBulletinBoardOptionFrame.tabHeights = { 1304 | general = 0, 1305 | groups = 0, 1306 | professions = 0, 1307 | hardcore = 0 1308 | } 1309 | 1310 | -- Initialize tab content with proper organization 1311 | function DifficultBulletinBoardOptionFrame.InitializeTabContent() 1312 | -- Initialize scroll frame first 1313 | addScrollFrameToOptionFrame() 1314 | 1315 | -- GENERAL TAB - Display, Filter, and Audio Settings 1316 | optionYOffset = 30 -- Start higher to compensate for larger section spacing 1317 | 1318 | -- General Settings Section 1319 | addSectionHeader("General Settings") 1320 | optionFrame.fontSizeOptionInputBox = addInputBoxOptionToOptionFrame(baseFontSizeOptionObject, DifficultBulletinBoardVars.fontSize) 1321 | optionFrame.expirationTimeOptionInputBox = addInputBoxOptionToOptionFrame(messageExpirationTimeOptionObject, DifficultBulletinBoardVars.messageExpirationTime) 1322 | optionFrame.serverTimePositionDropDown = addDropDownOptionToOptionFrame(serverTimePositionDropDownOptionObject, DifficultBulletinBoardVars.serverTimePosition) 1323 | optionFrame.timeFormatDropDown = addDropDownOptionToOptionFrame(timeFormatDropDownOptionObject, DifficultBulletinBoardVars.timeFormat) 1324 | 1325 | -- Filter Settings Section 1326 | addSectionHeader("Filter Settings") 1327 | optionFrame.hardcoreOnlyDropDown = addDropDownOptionToOptionFrame(hardcoreOnlyDropDownOptionObject, DifficultBulletinBoardVars.hardcoreOnly) 1328 | optionFrame.filterMatchedMessagesDropDown = addDropDownOptionToOptionFrame(filterMatchedMessagesDropDownOptionObject, DifficultBulletinBoardVars.filterMatchedMessages) 1329 | 1330 | -- Audio Settings Section 1331 | addSectionHeader("Audio Settings") 1332 | optionFrame.mainFrameSoundDropDown = addDropDownOptionToOptionFrame(mainFrameSoundDropDownOptionObject, DifficultBulletinBoardVars.mainFrameSound) 1333 | optionFrame.optionFrameSoundDropDown = addDropDownOptionToOptionFrame(optionFrameSoundDropDownOptionObject, DifficultBulletinBoardVars.optionFrameSound) 1334 | 1335 | -- Store height for this tab 1336 | DifficultBulletinBoardOptionFrame.tabHeights.general = math.abs(optionYOffset) + 35 1337 | 1338 | -- Store general tab elements 1339 | DifficultBulletinBoardOptionFrame.tabs.general = DifficultBulletinBoardOptionFrame.GetLastCreatedElements() 1340 | 1341 | -- GROUPS TAB 1342 | optionYOffset = 30 -- Start higher to compensate for larger section spacing 1343 | addSectionHeader("Group Settings") 1344 | optionFrame.groupOptionInputBox = addInputBoxOptionToOptionFrame(groupPlaceholdersOptionObject, DifficultBulletinBoardVars.numberOfGroupPlaceholders) 1345 | optionFrame.tempGroupTags = addTopicListToOptionFrame(groupTopicListObject, DifficultBulletinBoardVars.allGroupTopics) 1346 | 1347 | -- Store height for this tab 1348 | DifficultBulletinBoardOptionFrame.tabHeights.groups = math.abs(optionYOffset) + 35 1349 | 1350 | DifficultBulletinBoardOptionFrame.tabs.groups = DifficultBulletinBoardOptionFrame.GetLastCreatedElements() 1351 | 1352 | -- PROFESSIONS TAB 1353 | optionYOffset = 30 -- Start higher to compensate for larger section spacing 1354 | addSectionHeader("Profession Settings") 1355 | optionFrame.professionOptionInputBox = addInputBoxOptionToOptionFrame(professionPlaceholdersOptionObject, DifficultBulletinBoardVars.numberOfProfessionPlaceholders) 1356 | optionFrame.tempProfessionTags = addTopicListToOptionFrame(professionTopicListObject, DifficultBulletinBoardVars.allProfessionTopics) 1357 | 1358 | -- Store height for this tab 1359 | DifficultBulletinBoardOptionFrame.tabHeights.professions = math.abs(optionYOffset) + 35 1360 | 1361 | DifficultBulletinBoardOptionFrame.tabs.professions = DifficultBulletinBoardOptionFrame.GetLastCreatedElements() 1362 | 1363 | -- HARDCORE TAB 1364 | optionYOffset = 30 -- Start higher to compensate for larger section spacing 1365 | addSectionHeader("Hardcore Settings") 1366 | optionFrame.hardcoreOptionInputBox = addInputBoxOptionToOptionFrame(hardcorePlaceholdersOptionObject, DifficultBulletinBoardVars.numberOfHardcorePlaceholders) 1367 | optionFrame.tempHardcoreTags = addTopicListToOptionFrame(hardcoreTopicListObject, DifficultBulletinBoardVars.allHardcoreTopics) 1368 | 1369 | -- Store height for this tab 1370 | DifficultBulletinBoardOptionFrame.tabHeights.hardcore = math.abs(optionYOffset) + 35 1371 | 1372 | DifficultBulletinBoardOptionFrame.tabs.hardcore = DifficultBulletinBoardOptionFrame.GetLastCreatedElements() 1373 | 1374 | finalizeOptionFrame() 1375 | 1376 | -- Initialize with general tab 1377 | DifficultBulletinBoardOptionFrame.ShowTab("general") 1378 | end 1379 | 1380 | -- Helper functions for tab management 1381 | function DifficultBulletinBoardOptionFrame.ResetYOffset() 1382 | optionYOffset = 30 -- Start higher to compensate for larger section spacing 1383 | end 1384 | 1385 | -- Track created elements for tab assignment 1386 | DifficultBulletinBoardOptionFrame.lastCreatedElements = {} 1387 | 1388 | function DifficultBulletinBoardOptionFrame.GetLastCreatedElements() 1389 | local elements = DifficultBulletinBoardOptionFrame.lastCreatedElements 1390 | DifficultBulletinBoardOptionFrame.lastCreatedElements = {} 1391 | return elements 1392 | end 1393 | 1394 | function DifficultBulletinBoardOptionFrame.InitializeOptionFrame() 1395 | DifficultBulletinBoardOptionFrame.InitializeTabContent() 1396 | DifficultBulletinBoardOptionFrame.UpdateTabButtons() 1397 | 1398 | -- Initialize button widths based on current frame size 1399 | updateOptionButtonWidths() 1400 | end 1401 | 1402 | function DifficultBulletinBoard_ResetVariablesAndReload() 1403 | DifficultBulletinBoardSavedVariables.version = DifficultBulletinBoardDefaults.version 1404 | 1405 | DifficultBulletinBoardSavedVariables.fontSize = DifficultBulletinBoardDefaults.defaultFontSize 1406 | 1407 | DifficultBulletinBoardSavedVariables.serverTimePosition = DifficultBulletinBoardDefaults.defaultServerTimePosition 1408 | 1409 | DifficultBulletinBoardSavedVariables.timeFormat = DifficultBulletinBoardDefaults.defaultTimeFormat 1410 | 1411 | DifficultBulletinBoardSavedVariables.filterMatchedMessages = DifficultBulletinBoardDefaults.defaultFilterMatchedMessages 1412 | 1413 | DifficultBulletinBoardSavedVariables.hardcoreOnly = DifficultBulletinBoardDefaults.defaultHardcoreOnly 1414 | 1415 | DifficultBulletinBoardSavedVariables.mainFrameSound = DifficultBulletinBoardDefaults.defaultMainFrameSound 1416 | DifficultBulletinBoardSavedVariables.optionFrameSound = DifficultBulletinBoardDefaults.defaultOptionFrameSound 1417 | -- Default message expiration time 1418 | DifficultBulletinBoardSavedVariables.messageExpirationTime = DifficultBulletinBoardDefaults.defaultMessageExpirationTime 1419 | 1420 | DifficultBulletinBoardSavedVariables.numberOfGroupPlaceholders = DifficultBulletinBoardDefaults.defaultNumberOfGroupPlaceholders 1421 | DifficultBulletinBoardSavedVariables.numberOfProfessionPlaceholders = DifficultBulletinBoardDefaults.defaultNumberOfProfessionPlaceholders 1422 | DifficultBulletinBoardSavedVariables.numberOfHardcorePlaceholders = DifficultBulletinBoardDefaults.defaultNumberOfHardcorePlaceholders 1423 | 1424 | DifficultBulletinBoardSavedVariables.activeGroupTopics = DifficultBulletinBoardDefaults.defaultGroupTopics 1425 | DifficultBulletinBoardSavedVariables.activeProfessionTopics = DifficultBulletinBoardDefaults.defaultProfessionTopics 1426 | DifficultBulletinBoardSavedVariables.activeHardcoreTopics = DifficultBulletinBoardDefaults.defaultHardcoreTopics 1427 | 1428 | ReloadUI(); 1429 | end 1430 | 1431 | function DifficultBulletinBoard_SaveVariablesAndReload() 1432 | -- Save basic settings from optionFrame fields with safety checks 1433 | DifficultBulletinBoardSavedVariables.fontSize = optionFrame.fontSizeOptionInputBox and optionFrame.fontSizeOptionInputBox:GetText() or DifficultBulletinBoardDefaults.defaultFontSize 1434 | DifficultBulletinBoardSavedVariables.timeFormat = optionFrame.timeFormatDropDown and optionFrame.timeFormatDropDown:GetSelectedValue() or DifficultBulletinBoardDefaults.defaultTimeFormat 1435 | DifficultBulletinBoardSavedVariables.serverTimePosition = optionFrame.serverTimePositionDropDown and optionFrame.serverTimePositionDropDown:GetSelectedValue() or DifficultBulletinBoardDefaults.defaultServerTimePosition 1436 | DifficultBulletinBoardSavedVariables.filterMatchedMessages = optionFrame.filterMatchedMessagesDropDown and optionFrame.filterMatchedMessagesDropDown:GetSelectedValue() or DifficultBulletinBoardDefaults.defaultFilterMatchedMessages 1437 | DifficultBulletinBoardSavedVariables.hardcoreOnly = optionFrame.hardcoreOnlyDropDown and optionFrame.hardcoreOnlyDropDown:GetSelectedValue() or DifficultBulletinBoardDefaults.defaultHardcoreOnly 1438 | DifficultBulletinBoardSavedVariables.mainFrameSound = optionFrame.mainFrameSoundDropDown and optionFrame.mainFrameSoundDropDown:GetSelectedValue() or DifficultBulletinBoardDefaults.defaultMainFrameSound 1439 | DifficultBulletinBoardSavedVariables.optionFrameSound = optionFrame.optionFrameSoundDropDown and optionFrame.optionFrameSoundDropDown:GetSelectedValue() or DifficultBulletinBoardDefaults.defaultOptionFrameSound 1440 | DifficultBulletinBoardSavedVariables.numberOfGroupPlaceholders = optionFrame.groupOptionInputBox and optionFrame.groupOptionInputBox:GetText() or DifficultBulletinBoardDefaults.defaultNumberOfGroupPlaceholders 1441 | DifficultBulletinBoardSavedVariables.numberOfProfessionPlaceholders = optionFrame.professionOptionInputBox and optionFrame.professionOptionInputBox:GetText() or DifficultBulletinBoardDefaults.defaultNumberOfProfessionPlaceholders 1442 | DifficultBulletinBoardSavedVariables.numberOfHardcorePlaceholders = optionFrame.hardcoreOptionInputBox and optionFrame.hardcoreOptionInputBox:GetText() or DifficultBulletinBoardDefaults.defaultNumberOfHardcorePlaceholders 1443 | -- Save custom message expiration time with safety check 1444 | DifficultBulletinBoardSavedVariables.messageExpirationTime = optionFrame.expirationTimeOptionInputBox and optionFrame.expirationTimeOptionInputBox:GetText() or DifficultBulletinBoardDefaults.defaultMessageExpirationTime 1445 | -- Overwrite tags saved from temp fields 1446 | overwriteTagsForAllTopics(DifficultBulletinBoardVars.allGroupTopics, optionFrame.tempGroupTags) 1447 | overwriteTagsForAllTopics(DifficultBulletinBoardVars.allProfessionTopics, optionFrame.tempProfessionTags) 1448 | overwriteTagsForAllTopics(DifficultBulletinBoardVars.allHardcoreTopics, optionFrame.tempHardcoreTags) 1449 | ReloadUI() 1450 | end 1451 | 1452 | -- Function to update option tab button widths based on frame size (matching main panel logic) 1453 | local function updateOptionButtonWidths() 1454 | -- Get button references 1455 | local buttons = { 1456 | getglobal("DifficultBulletinBoardOptionFrame_GeneralTab"), 1457 | getglobal("DifficultBulletinBoardOptionFrame_GroupsTab"), 1458 | getglobal("DifficultBulletinBoardOptionFrame_ProfessionsTab"), 1459 | getglobal("DifficultBulletinBoardOptionFrame_HardcoreTab") 1460 | } 1461 | 1462 | -- Safety check for buttons 1463 | for i = 1, OPTION_NUM_BUTTONS do 1464 | if not buttons[i] then 1465 | return 1466 | end 1467 | end 1468 | 1469 | -- Get button text widths to calculate minimum required widths 1470 | local textWidths = {} 1471 | local totalTextWidth = 0 1472 | local buttonCount = OPTION_NUM_BUTTONS 1473 | 1474 | -- Check all buttons have text elements and get their widths 1475 | for i = 1, buttonCount do 1476 | local button = buttons[i] 1477 | if not button:GetName() then 1478 | return 1479 | end 1480 | 1481 | local textObjName = button:GetName() .. "_Text" 1482 | local textObj = getglobal(textObjName) 1483 | 1484 | if not textObj then 1485 | return 1486 | end 1487 | 1488 | local textWidth = textObj:GetStringWidth() 1489 | if not textWidth then 1490 | return 1491 | end 1492 | 1493 | textWidths[i] = textWidth 1494 | totalTextWidth = totalTextWidth + textWidth 1495 | end 1496 | 1497 | -- Calculate minimum padded widths for each button (text + minimum padding) 1498 | local minButtonWidths = {} 1499 | for i = 1, buttonCount do 1500 | minButtonWidths[i] = textWidths[i] + (2 * OPTION_MIN_TEXT_PADDING) 1501 | end 1502 | 1503 | -- Find the button that needs the most minimum width 1504 | local maxMinWidth = 0 1505 | for i = 1, buttonCount do 1506 | if minButtonWidths[i] > maxMinWidth then 1507 | maxMinWidth = minButtonWidths[i] 1508 | end 1509 | end 1510 | 1511 | -- Calculate total content width if all buttons had the same width (maxMinWidth) 1512 | -- This ensures all buttons have at least minimum padding 1513 | local totalEqualContentWidth = maxMinWidth * buttonCount 1514 | 1515 | -- Add spacing to total minimum width 1516 | local totalMinFrameWidth = totalEqualContentWidth + ((OPTION_NUM_BUTTONS - 1) * OPTION_BUTTON_SPACING) + (2 * OPTION_SIDE_PADDING) 1517 | 1518 | -- Get current frame width 1519 | local frameWidth = optionFrame:GetWidth() 1520 | 1521 | -- Set the minimum resizable width of the frame directly 1522 | -- This prevents the user from dragging it smaller than the minimum width 1523 | optionFrame:SetMinResize(totalMinFrameWidth, 300) 1524 | 1525 | -- If frame is somehow smaller than minimum (should not happen), force a resize 1526 | if frameWidth < totalMinFrameWidth then 1527 | optionFrame:SetWidth(totalMinFrameWidth) 1528 | frameWidth = totalMinFrameWidth 1529 | end 1530 | 1531 | -- Calculate available width for buttons 1532 | local availableWidth = frameWidth - (2 * OPTION_SIDE_PADDING) - ((OPTION_NUM_BUTTONS - 1) * OPTION_BUTTON_SPACING) 1533 | 1534 | -- Calculate equal width distribution 1535 | local equalWidth = availableWidth / OPTION_NUM_BUTTONS 1536 | 1537 | -- Always try to use equal widths first 1538 | if equalWidth >= maxMinWidth then 1539 | -- We can use equal widths for all buttons 1540 | for i = 1, buttonCount do 1541 | buttons[i]:SetWidth(equalWidth) 1542 | end 1543 | else 1544 | -- We can't use equal widths because some text would have less than minimum padding 1545 | -- Set all buttons to the maximum minimum width to ensure all have same width 1546 | -- unless that would mean having less than minimum padding 1547 | for i = 1, buttonCount do 1548 | buttons[i]:SetWidth(maxMinWidth) 1549 | end 1550 | end 1551 | end 1552 | 1553 | -- Function to hide all dropdown menus and reset their states 1554 | function DifficultBulletinBoardOptionFrame.HideAllDropdownMenus() 1555 | -- Hide menus in the global list 1556 | if DROPDOWN_MENUS_LIST then 1557 | for _, menu in ipairs(DROPDOWN_MENUS_LIST) do 1558 | if menu:IsShown() then 1559 | menu:Hide() 1560 | end 1561 | end 1562 | end 1563 | 1564 | -- Reset all tracked dropdown button states 1565 | for _, dropdown in ipairs(DifficultBulletinBoardOptionFrame.allDropdowns) do 1566 | if dropdown and dropdown.ForceClose then 1567 | dropdown:ForceClose() 1568 | end 1569 | end 1570 | end 1571 | 1572 | optionFrame:SetScript("OnSizeChanged", function() 1573 | -- Update tab button widths based on new frame size (matching main panel behavior) 1574 | updateOptionButtonWidths() 1575 | 1576 | -- Adjust the width calculation to account for scrollbar and padding 1577 | local tagsTextBoxWidth = optionFrame:GetWidth() - tagsTextBoxWidthDelta - SCROLL_BAR_WIDTH 1578 | for _, msgFrame in ipairs(tempTagsTextBoxes) do 1579 | msgFrame:SetWidth(tagsTextBoxWidth) 1580 | end 1581 | end) -------------------------------------------------------------------------------- /DifficultBulletinBoardOptionFrame.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 100 | 101 | 102 | 148 | 149 | 150 | 196 | 197 | 198 | 244 | 245 | 308 | 309 | 310 | 352 | 353 | 354 | 396 | 397 | 398 | 421 | 422 | 423 | 424 | 425 | 426 | if not DifficultBulletinBoardOptionFrame.isMoving then 427 | DifficultBulletinBoardOptionFrame:StartMoving(); 428 | DifficultBulletinBoardOptionFrame.isMoving = true; 429 | end 430 | 431 | 432 | if DifficultBulletinBoardOptionFrame.isMoving then 433 | DifficultBulletinBoardOptionFrame:StopMovingOrSizing(); 434 | DifficultBulletinBoardOptionFrame.isMoving = false; 435 | end 436 | 437 | 438 | 439 | -------------------------------------------------------------------------------- /DifficultBulletinBoardPlayerScanner.lua: -------------------------------------------------------------------------------- 1 | DifficultBulletinBoardVars = DifficultBulletinBoardVars or {} 2 | 3 | local playerScannerFrame = CreateFrame("Frame") 4 | 5 | 6 | 7 | 8 | playerScannerFrame:RegisterEvent("PLAYER_ENTERING_WORLD") 9 | playerScannerFrame:RegisterEvent("PLAYER_TARGET_CHANGED") 10 | playerScannerFrame:RegisterEvent("UPDATE_MOUSEOVER_UNIT") 11 | playerScannerFrame:RegisterEvent("WHO_LIST_UPDATE") 12 | playerScannerFrame:RegisterEvent("CHAT_MSG_SYSTEM") 13 | playerScannerFrame:SetScript("OnEvent", function() 14 | 15 | if event == "PLAYER_ENTERING_WORLD" then 16 | local playerName = UnitName("player") 17 | local localizedClass, englishClass = UnitClass("player") 18 | 19 | DifficultBulletinBoardVars.AddPlayerToDatabase(playerName, localizedClass) 20 | end 21 | 22 | if event == "WHO_LIST_UPDATE" then 23 | for index = 1, GetNumWhoResults() do 24 | local playerName, guild, level, race, class, zone, classFileName = GetWhoInfo(index) 25 | 26 | DifficultBulletinBoardVars.AddPlayerToDatabase(playerName, class) 27 | end 28 | end 29 | 30 | if event == "UPDATE_MOUSEOVER_UNIT" then 31 | if UnitIsPlayer("mouseover") then 32 | local playerName = UnitName("mouseover") 33 | local localizedClass, englishClass = UnitClass("mouseover") 34 | 35 | DifficultBulletinBoardVars.AddPlayerToDatabase(playerName, localizedClass) 36 | end 37 | end 38 | 39 | if event == "PLAYER_TARGET_CHANGED" then 40 | if UnitIsPlayer("target") then 41 | local playerName = UnitName("target") 42 | local localizedClass, englishClass = UnitClass("target") 43 | 44 | DifficultBulletinBoardVars.AddPlayerToDatabase(playerName, localizedClass) 45 | end 46 | end 47 | 48 | -- for /who results 49 | if event == "CHAT_MSG_SYSTEM" then 50 | for i = 1, GetNumWhoResults() do 51 | local playerName, _, _, _, localizedClass, _ = GetWhoInfo(i) 52 | DifficultBulletinBoardVars.AddPlayerToDatabase(playerName, localizedClass) 53 | end 54 | end 55 | end) -------------------------------------------------------------------------------- /DifficultBulletinBoardVars.lua: -------------------------------------------------------------------------------- 1 | -- DifficultBulletinBoardVars.lua 2 | -- Handles variable initialization and loading of saved variables 3 | 4 | DifficultBulletinBoardSavedVariables = DifficultBulletinBoardSavedVariables or {} 5 | DifficultBulletinBoardVars = DifficultBulletinBoardVars or {} 6 | DifficultBulletinBoardDefaults = DifficultBulletinBoardDefaults or {} 7 | 8 | DifficultBulletinBoardVars.version = DifficultBulletinBoardDefaults.version 9 | 10 | DifficultBulletinBoardVars.fontSize = DifficultBulletinBoardDefaults.defaultFontSize 11 | 12 | DifficultBulletinBoardVars.serverTimePosition = DifficultBulletinBoardDefaults.defaultServerTimePosition 13 | 14 | DifficultBulletinBoardVars.timeFormat = DifficultBulletinBoardDefaults.defaultTimeFormat 15 | 16 | DifficultBulletinBoardVars.numberOfGroupPlaceholders = DifficultBulletinBoardDefaults.defaultNumberOfGroupPlaceholders 17 | DifficultBulletinBoardVars.numberOfProfessionPlaceholders = DifficultBulletinBoardDefaults.defaultNumberOfProfessionPlaceholders 18 | DifficultBulletinBoardVars.numberOfHardcorePlaceholders = DifficultBulletinBoardDefaults.defaultNumberOfHardcorePlaceholders 19 | 20 | DifficultBulletinBoardVars.allGroupTopics = {} 21 | DifficultBulletinBoardVars.allProfessionTopics = {} 22 | DifficultBulletinBoardVars.allHardcoreTopics = {} 23 | 24 | DifficultBulletinBoardSavedVariables.playerList = DifficultBulletinBoardSavedVariables.playerList or {} 25 | DifficultBulletinBoardSavedVariables.keywordBlacklist = DifficultBulletinBoardSavedVariables.keywordBlacklist or "" 26 | 27 | 28 | 29 | 30 | -- Retrieves a player's class from the saved database 31 | function DifficultBulletinBoardVars.GetPlayerClassFromDatabase(name) 32 | local realmName = GetRealmName() 33 | 34 | -- Add or update the player entry 35 | if DifficultBulletinBoardSavedVariables.playerList[realmName][name] then 36 | return DifficultBulletinBoardSavedVariables.playerList[realmName][name].class 37 | else 38 | return nil 39 | end 40 | end 41 | 42 | -- Adds a player to the class database 43 | function DifficultBulletinBoardVars.AddPlayerToDatabase(name, class) 44 | local realmName = GetRealmName() 45 | 46 | -- Add or update the player entry 47 | DifficultBulletinBoardSavedVariables.playerList[realmName][name] = { 48 | class = class 49 | } 50 | 51 | 52 | end 53 | 54 | -- Helper function to get saved variable or default 55 | local function setSavedVariable(savedVar, defaultVar, savedName) 56 | if savedVar and savedVar ~= "" then 57 | return savedVar 58 | else 59 | -- Handle nil default values gracefully 60 | local fallbackValue = defaultVar or "" 61 | DifficultBulletinBoardSavedVariables[savedName] = fallbackValue 62 | return fallbackValue 63 | end 64 | end 65 | 66 | -- Loads saved variables or initializes defaults 67 | function DifficultBulletinBoardVars.LoadSavedVariables() 68 | 69 | -- Ensure the root and container tables exist 70 | DifficultBulletinBoardSavedVariables = DifficultBulletinBoardSavedVariables or {} 71 | DifficultBulletinBoardSavedVariables.playerList = DifficultBulletinBoardSavedVariables.playerList or {} 72 | DifficultBulletinBoardSavedVariables.keywordBlacklist = DifficultBulletinBoardSavedVariables.keywordBlacklist or "" 73 | 74 | local realmName = GetRealmName() 75 | DifficultBulletinBoardSavedVariables.playerList[realmName] = DifficultBulletinBoardSavedVariables.playerList[realmName] or {} 76 | 77 | if DifficultBulletinBoardSavedVariables.version then 78 | local savedVersion = DifficultBulletinBoardSavedVariables.version 79 | 80 | -- update the saved activeTopics if a new version of the topic list was released 81 | if savedVersion < DifficultBulletinBoardVars.version then 82 | 83 | DifficultBulletinBoardVars.allGroupTopics = DifficultBulletinBoardDefaults.deepCopy(DifficultBulletinBoardDefaults.defaultGroupTopics) 84 | DifficultBulletinBoardSavedVariables.activeGroupTopics = DifficultBulletinBoardVars.allGroupTopics 85 | 86 | DifficultBulletinBoardVars.allProfessionTopics = DifficultBulletinBoardDefaults.deepCopy(DifficultBulletinBoardDefaults.defaultProfessionTopics) 87 | DifficultBulletinBoardSavedVariables.activeProfessionTopics = DifficultBulletinBoardVars.allProfessionTopics 88 | 89 | DifficultBulletinBoardVars.allHardcoreTopics = DifficultBulletinBoardDefaults.deepCopy(DifficultBulletinBoardDefaults.defaultHardcoreTopics) 90 | DifficultBulletinBoardSavedVariables.activeHardcoreTopics = DifficultBulletinBoardVars.allHardcoreTopics 91 | 92 | DifficultBulletinBoardSavedVariables.version = DifficultBulletinBoardVars.version 93 | end 94 | else 95 | DifficultBulletinBoardSavedVariables.version = DifficultBulletinBoardVars.version 96 | DifficultBulletinBoardVars.allGroupTopics = DifficultBulletinBoardDefaults.deepCopy(DifficultBulletinBoardDefaults.defaultGroupTopics) 97 | DifficultBulletinBoardSavedVariables.activeGroupTopics = DifficultBulletinBoardVars.allGroupTopics 98 | 99 | DifficultBulletinBoardVars.allProfessionTopics = DifficultBulletinBoardDefaults.deepCopy(DifficultBulletinBoardDefaults.defaultProfessionTopics) 100 | DifficultBulletinBoardSavedVariables.activeProfessionTopics = DifficultBulletinBoardVars.allProfessionTopics 101 | 102 | DifficultBulletinBoardVars.allHardcoreTopics = DifficultBulletinBoardDefaults.deepCopy(DifficultBulletinBoardDefaults.defaultHardcoreTopics) 103 | DifficultBulletinBoardSavedVariables.activeHardcoreTopics = DifficultBulletinBoardVars.allHardcoreTopics 104 | end 105 | 106 | -- Set the saved or default variables for different settings 107 | DifficultBulletinBoardVars.serverTimePosition = setSavedVariable(DifficultBulletinBoardSavedVariables.serverTimePosition, DifficultBulletinBoardDefaults.defaultServerTimePosition, "serverTimePosition") 108 | DifficultBulletinBoardVars.fontSize = setSavedVariable(DifficultBulletinBoardSavedVariables.fontSize, DifficultBulletinBoardDefaults.defaultFontSize, "fontSize") 109 | DifficultBulletinBoardVars.timeFormat = setSavedVariable(DifficultBulletinBoardSavedVariables.timeFormat, DifficultBulletinBoardDefaults.defaultTimeFormat, "timeFormat") 110 | DifficultBulletinBoardVars.mainFrameSound = setSavedVariable(DifficultBulletinBoardSavedVariables.mainFrameSound, DifficultBulletinBoardDefaults.defaultMainFrameSound, "mainFrameSound") 111 | DifficultBulletinBoardVars.optionFrameSound = setSavedVariable(DifficultBulletinBoardSavedVariables.optionFrameSound, DifficultBulletinBoardDefaults.defaultOptionFrameSound, "optionFrameSound") 112 | DifficultBulletinBoardVars.filterMatchedMessages = setSavedVariable(DifficultBulletinBoardSavedVariables.filterMatchedMessages, DifficultBulletinBoardDefaults.defaultFilterMatchedMessages, "filterMatchedMessages") 113 | DifficultBulletinBoardVars.hardcoreOnly = setSavedVariable(DifficultBulletinBoardSavedVariables.hardcoreOnly, DifficultBulletinBoardDefaults.defaultHardcoreOnly, "hardcoreOnly") 114 | DifficultBulletinBoardVars.messageExpirationTime = setSavedVariable(DifficultBulletinBoardSavedVariables.messageExpirationTime, DifficultBulletinBoardDefaults.defaultMessageExpirationTime, "messageExpirationTime") 115 | 116 | -- Set placeholders variables 117 | DifficultBulletinBoardVars.numberOfGroupPlaceholders = setSavedVariable(DifficultBulletinBoardSavedVariables.numberOfGroupPlaceholders, DifficultBulletinBoardDefaults.defaultNumberOfGroupPlaceholders, "numberOfGroupPlaceholders") 118 | DifficultBulletinBoardVars.numberOfProfessionPlaceholders = setSavedVariable(DifficultBulletinBoardSavedVariables.numberOfProfessionPlaceholders, DifficultBulletinBoardDefaults.defaultNumberOfProfessionPlaceholders, "numberOfProfessionPlaceholders") 119 | DifficultBulletinBoardVars.numberOfHardcorePlaceholders = setSavedVariable(DifficultBulletinBoardSavedVariables.numberOfHardcorePlaceholders, DifficultBulletinBoardDefaults.defaultNumberOfHardcorePlaceholders, "numberOfHardcorePlaceholders") 120 | 121 | -- Set active topics, or default if not found 122 | DifficultBulletinBoardVars.allGroupTopics = setSavedVariable(DifficultBulletinBoardSavedVariables.activeGroupTopics, DifficultBulletinBoardDefaults.deepCopy(DifficultBulletinBoardDefaults.defaultGroupTopics), "activeGroupTopics") 123 | DifficultBulletinBoardVars.allProfessionTopics = setSavedVariable(DifficultBulletinBoardSavedVariables.activeProfessionTopics, DifficultBulletinBoardDefaults.deepCopy(DifficultBulletinBoardDefaults.defaultProfessionTopics), "activeProfessionTopics") 124 | DifficultBulletinBoardVars.allHardcoreTopics = setSavedVariable(DifficultBulletinBoardSavedVariables.activeHardcoreTopics, DifficultBulletinBoardDefaults.deepCopy(DifficultBulletinBoardDefaults.defaultHardcoreTopics), "activeHardcoreTopics") 125 | 126 | 127 | end -------------------------------------------------------------------------------- /DifficultBulletinBoardVersionCheck.lua: -------------------------------------------------------------------------------- 1 | -- DifficultBulletinBoardVersionCheck.lua 2 | -- Version check system with player sync capabilities for WoW 1.12.1 3 | -- Notifies users at login if a newer version exists 4 | 5 | -- Constants 6 | local UPDATE_URL = "https://github.com/DeterminedPanda/DifficultBulletinBoard" 7 | local ADDON_PREFIX = "DBBVERSION" -- For addon communication 8 | local hasNotified = false 9 | local hasCheckedThisSession = false 10 | 11 | -- ONLY UPDATE THIS NUMBER when a new version is available 12 | -- Everyone with an older installed version will be notified 13 | DifficultBulletinBoardDefaults.latestVersion = 10 14 | 15 | -- Register addon communication 16 | local versionCheckFrame = CreateFrame("Frame") 17 | versionCheckFrame:RegisterEvent("PLAYER_LOGIN") 18 | versionCheckFrame:RegisterEvent("CHAT_MSG_ADDON") 19 | versionCheckFrame:RegisterEvent("PLAYER_ENTERING_WORLD") 20 | 21 | -- Broadcast version to other players 22 | local function BroadcastVersion() 23 | local latestVersion = DifficultBulletinBoardDefaults.latestVersion 24 | 25 | -- Send our version in all available channels 26 | if GetNumRaidMembers() > 0 then 27 | SendAddonMessage(ADDON_PREFIX, tostring(latestVersion), "RAID") 28 | elseif GetNumPartyMembers() > 0 then 29 | SendAddonMessage(ADDON_PREFIX, tostring(latestVersion), "PARTY") 30 | end 31 | 32 | -- Also send in guild chat if in a guild 33 | if IsInGuild() then 34 | SendAddonMessage(ADDON_PREFIX, tostring(latestVersion), "GUILD") 35 | end 36 | end 37 | 38 | -- Update known latest version without showing notification 39 | local function UpdateLatestVersionSilently(newVersion) 40 | -- Store the highest known version in saved variables 41 | if not DifficultBulletinBoardSavedVariables.knownLatestVersion or 42 | DifficultBulletinBoardSavedVariables.knownLatestVersion < newVersion then 43 | DifficultBulletinBoardSavedVariables.knownLatestVersion = newVersion 44 | end 45 | end 46 | 47 | -- Check if addon version is outdated and show notification if needed 48 | local function CheckVersion() 49 | -- Skip if already notified this session (only notify once per login) 50 | if hasNotified or hasCheckedThisSession then 51 | return 52 | end 53 | 54 | -- Initialize saved variables if they don't exist 55 | DifficultBulletinBoardSavedVariables.installedVersion = 56 | DifficultBulletinBoardSavedVariables.installedVersion or DifficultBulletinBoardDefaults.latestVersion 57 | 58 | DifficultBulletinBoardSavedVariables.knownLatestVersion = 59 | DifficultBulletinBoardSavedVariables.knownLatestVersion or DifficultBulletinBoardDefaults.latestVersion 60 | 61 | -- Get installed version and the highest known version 62 | local installedVersion = DifficultBulletinBoardSavedVariables.installedVersion 63 | local knownLatestVersion = DifficultBulletinBoardSavedVariables.knownLatestVersion 64 | 65 | -- Determine the highest version available (local, saved, or default) 66 | local highestVersion = DifficultBulletinBoardDefaults.latestVersion 67 | if knownLatestVersion > highestVersion then 68 | highestVersion = knownLatestVersion 69 | end 70 | 71 | -- Display notification only at login and only if user has an older version 72 | if installedVersion < highestVersion then 73 | DEFAULT_CHAT_FRAME:AddMessage("|cFFFFCC00Difficult Bulletin Board:|r Version " .. 74 | highestVersion .. " is now available at " .. UPDATE_URL) 75 | hasNotified = true 76 | else 77 | -- If user has latest version, update their saved version number 78 | DifficultBulletinBoardSavedVariables.installedVersion = highestVersion 79 | end 80 | 81 | -- Mark that we've performed the check this session 82 | hasCheckedThisSession = true 83 | end 84 | 85 | -- Process addon sync messages 86 | local function ProcessAddonMessage(prefix, message, channel, sender) 87 | -- Ignore our own messages 88 | if sender == UnitName("player") then return end 89 | 90 | -- Check if it's a version message 91 | if prefix == ADDON_PREFIX then 92 | local syncedVersion = tonumber(message) 93 | if syncedVersion and syncedVersion > 0 then 94 | -- Found a version from another player, silently update our known version 95 | UpdateLatestVersionSilently(syncedVersion) 96 | end 97 | end 98 | end 99 | 100 | -- Event handler 101 | versionCheckFrame:SetScript("OnEvent", function() 102 | if event == "PLAYER_LOGIN" then 103 | -- Do login check after short delay 104 | versionCheckFrame:SetScript("OnUpdate", function() 105 | if not this.delay then 106 | this.delay = 0 107 | end 108 | 109 | this.delay = this.delay + arg1 110 | 111 | if this.delay >= 2 then 112 | -- Check version at login 113 | CheckVersion() 114 | 115 | -- Initial broadcast of our version 116 | BroadcastVersion() 117 | 118 | -- Stop the update timer 119 | this:SetScript("OnUpdate", nil) 120 | end 121 | end) 122 | elseif event == "CHAT_MSG_ADDON" then 123 | -- Process addon messages for version checking 124 | ProcessAddonMessage(arg1, arg2, arg3, arg4) 125 | elseif event == "PLAYER_ENTERING_WORLD" then 126 | -- Broadcast our version when entering world or changing zones 127 | BroadcastVersion() 128 | end 129 | end) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Difficult Bulletin Board - Addon for Turtle WoW 2 | 3 | **Difficult Bulletin Board** is a World of Warcraft 1.12 addon inspired by the LFG Group Bulletin Board. It scans world chat and organizes messages into an easy-to-read UI, making it more convenient to find what you need. 4 | 5 | ## Features 6 | 7 | - **Group Finder**: Easily locate groups for the content you want to do. 8 | - **Groups Logs**: Track and filter all group-related messages in one consolidated view. 9 | - **Profession Search**: Quickly find players offering or seeking crafting services. 10 | - **Hardcore Messages**: Stay updated with hardcore-related events, such as deaths or level-ups. 11 | - **Hardcore-Only Chat Filter**: Show only messages aimed at hardcore characters. 12 | - **Blacklist Filter**: Hide all messages matching your blacklist. 13 | - **Expiration Date**: Set an expiration date to automatically delete old messages—or turn automatic deletion off entirely. 14 | - **Collapsible Topics**: Collapse topics to hide unwanted messages and keep your feed tidy. 15 | - **Notification System**: Be notified if an entry gets added to a specific topic. 16 | 17 | ### Group Finder 18 | 19 | ![groups](https://github.com/user-attachments/assets/59403fa8-df0c-4136-9a40-8c0bc2c61e1b) 20 | 21 | 22 | 23 | ### Group Logs 24 | 25 | ![groups logs](https://github.com/user-attachments/assets/b1da8aba-0f01-48ff-8cdd-67d91b81d9c4) 26 | 27 | 28 | 29 | ### Profession Search 30 | 31 | ![professions](https://github.com/user-attachments/assets/c2a1ed20-a78c-4485-bcc4-e32f6b2d6cac) 32 | 33 | 34 | 35 | ### Hardcore Messages 36 | 37 | ![hardcore](https://github.com/user-attachments/assets/5c2a96c9-14d3-4c5f-a745-d96469043f3c) 38 | 39 | 40 | 41 | ## Settings 42 | 43 | ![settings](https://github.com/user-attachments/assets/b5cc8ec3-367e-4bb7-862e-4f966027ee49) 44 | 45 | 46 | 47 | ## Installation 48 | 49 | 1. Download the addon by clicking the link below: 50 | - [Download Difficult Bulletin Board](https://github.com/DeterminedPanda/DifficultBulletinBoard/archive/refs/heads/master.zip) 51 | 52 | 2. Unzip the `DifficultBulletinBoard-master.zip` archive. 53 | 54 | 3. Rename the directory inside the zip from `DifficultBulletinBoard-master` to `DifficultBulletinBoard`. 55 | 56 | 4. Move the `DifficultBulletinBoard` folder to your World of Warcraft AddOns directory: 57 | - Example path: `C:\World of Warcraft Turtle\Interface\AddOns` 58 | 59 | 5. If you have WoW open, make sure to restart the game for the addon to load properly. 60 | 61 | 6. To verify the installation, type `/dbb` in the game chat. If the installation was successful, the addon will open. 62 | 63 | 64 | ## Usage 65 | 66 | ### Accessing the Bulletin Board 67 | 68 | You can open the bulletin board interface by left-clicking on the DifficultBulletinBoard Minimap icon or by typing ```/dbb``` into your chat window. 69 | The interface will show an ordered list of messages from the world chat. 70 | 71 | ### Editing and Managing Entries 72 | 73 | To manage topics, right-click the minimap icon to open the options window and select which topics to follow by selecting or unselecting the corresponding checkbox. 74 | 75 | ## Troubleshooting 76 | 77 | ### No Messages Show Up? 78 | If no messages appear on your bulletin board and no errors are displayed, make sure you are in the **world chat** by typing `/join world` in-game. 79 | 80 | ### Tags List Issues? 81 | If your tags list seems incorrect or disorganized, try pressing the **Reset** button in the options window. 82 | It’s also a good idea to reset the tags after updating the addon, as I may improve or adjust the tags list in future updates. 83 | 84 | ## Contact 85 | 86 | Have feature suggestions or need further assistance? Feel free to [create an issue](https://github.com/DeterminedPanda/DifficultBulletinBoard/issues) on this repository and I will help you as soon as possible. 87 | 88 | 89 | ## To-Do List: 90 | 91 | - [x] Ensure that when a person already in the list sends a new message, their old entry is removed and they are moved to the top of the list. 92 | - [x] Add a reset button to the options frame. 93 | - [x] Expand options (e.g., placeholder number adjustments, etc.). 94 | - [x] Implement left-click whisper, shift-left-click /who, and right-click /invite functionality for buttons 95 | - [x] Implement tabs 96 | - [x] Implement a tab for Hardcore messages 97 | - [ ] Implement more customization options (e.g., classic WoW border styles, etc.). 98 | -------------------------------------------------------------------------------- /icons/UI-ChatIM-SizeGrabber-Down.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeterminedPanda/DifficultBulletinBoard/5c66d874ba4d9527819749c12f9ea940d9700948/icons/UI-ChatIM-SizeGrabber-Down.tga -------------------------------------------------------------------------------- /icons/UI-ChatIM-SizeGrabber-Highlight.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeterminedPanda/DifficultBulletinBoard/5c66d874ba4d9527819749c12f9ea940d9700948/icons/UI-ChatIM-SizeGrabber-Highlight.tga -------------------------------------------------------------------------------- /icons/UI-ChatIM-SizeGrabber-Up.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeterminedPanda/DifficultBulletinBoard/5c66d874ba4d9527819749c12f9ea940d9700948/icons/UI-ChatIM-SizeGrabber-Up.tga -------------------------------------------------------------------------------- /icons/bell.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeterminedPanda/DifficultBulletinBoard/5c66d874ba4d9527819749c12f9ea940d9700948/icons/bell.tga -------------------------------------------------------------------------------- /icons/check_sign.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeterminedPanda/DifficultBulletinBoard/5c66d874ba4d9527819749c12f9ea940d9700948/icons/check_sign.tga -------------------------------------------------------------------------------- /icons/close.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeterminedPanda/DifficultBulletinBoard/5c66d874ba4d9527819749c12f9ea940d9700948/icons/close.tga -------------------------------------------------------------------------------- /icons/close_light.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeterminedPanda/DifficultBulletinBoard/5c66d874ba4d9527819749c12f9ea940d9700948/icons/close_light.tga -------------------------------------------------------------------------------- /icons/down.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeterminedPanda/DifficultBulletinBoard/5c66d874ba4d9527819749c12f9ea940d9700948/icons/down.tga -------------------------------------------------------------------------------- /icons/druid_class_icon.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeterminedPanda/DifficultBulletinBoard/5c66d874ba4d9527819749c12f9ea940d9700948/icons/druid_class_icon.tga -------------------------------------------------------------------------------- /icons/gradient_down.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeterminedPanda/DifficultBulletinBoard/5c66d874ba4d9527819749c12f9ea940d9700948/icons/gradient_down.tga -------------------------------------------------------------------------------- /icons/hunter_class_icon.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeterminedPanda/DifficultBulletinBoard/5c66d874ba4d9527819749c12f9ea940d9700948/icons/hunter_class_icon.tga -------------------------------------------------------------------------------- /icons/icon.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeterminedPanda/DifficultBulletinBoard/5c66d874ba4d9527819749c12f9ea940d9700948/icons/icon.tga -------------------------------------------------------------------------------- /icons/mage_class_icon.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeterminedPanda/DifficultBulletinBoard/5c66d874ba4d9527819749c12f9ea940d9700948/icons/mage_class_icon.tga -------------------------------------------------------------------------------- /icons/paladin_class_icon.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeterminedPanda/DifficultBulletinBoard/5c66d874ba4d9527819749c12f9ea940d9700948/icons/paladin_class_icon.tga -------------------------------------------------------------------------------- /icons/priest_class_icon.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeterminedPanda/DifficultBulletinBoard/5c66d874ba4d9527819749c12f9ea940d9700948/icons/priest_class_icon.tga -------------------------------------------------------------------------------- /icons/rogue_class_icon.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeterminedPanda/DifficultBulletinBoard/5c66d874ba4d9527819749c12f9ea940d9700948/icons/rogue_class_icon.tga -------------------------------------------------------------------------------- /icons/shaman_class_icon.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeterminedPanda/DifficultBulletinBoard/5c66d874ba4d9527819749c12f9ea940d9700948/icons/shaman_class_icon.tga -------------------------------------------------------------------------------- /icons/warlock_class_icon.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeterminedPanda/DifficultBulletinBoard/5c66d874ba4d9527819749c12f9ea940d9700948/icons/warlock_class_icon.tga -------------------------------------------------------------------------------- /icons/warrior_class_icon.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeterminedPanda/DifficultBulletinBoard/5c66d874ba4d9527819749c12f9ea940d9700948/icons/warrior_class_icon.tga -------------------------------------------------------------------------------- /images/grouplogs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeterminedPanda/DifficultBulletinBoard/5c66d874ba4d9527819749c12f9ea940d9700948/images/grouplogs.png -------------------------------------------------------------------------------- /images/groups.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeterminedPanda/DifficultBulletinBoard/5c66d874ba4d9527819749c12f9ea940d9700948/images/groups.png -------------------------------------------------------------------------------- /images/hardcore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeterminedPanda/DifficultBulletinBoard/5c66d874ba4d9527819749c12f9ea940d9700948/images/hardcore.png -------------------------------------------------------------------------------- /images/professions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeterminedPanda/DifficultBulletinBoard/5c66d874ba4d9527819749c12f9ea940d9700948/images/professions.png --------------------------------------------------------------------------------