---------------------------------------------------- -- Eine vollständige UI-Alternative -- WoW Classic Anniversary TBC -- Interface 20506 -- Build 69110 ---------------------------------------------------- local _, ns = ... local module = ns:new_module("chat") local chat_width = 400 local chat_font_size = 14 local alert_duration = 20 local alert_pulse_period = 1 local setting_order = { "buttons_hidden", "free_movement", "message_fading", } local default_settings = { buttons_hidden = true, free_movement = true, message_fading = true, } local chat_frame_states = setmetatable({}, { __mode = "k" }) local control_visibility_states = setmetatable({}, { __mode = "k" }) local clamp_states = setmetatable({}, { __mode = "k" }) local hooked_chat_frames = setmetatable({}, { __mode = "k" }) local hooked_edit_boxes = setmetatable({}, { __mode = "k" }) local hooked_control_regions = setmetatable({}, { __mode = "k" }) local hooked_alert_reset_regions = setmetatable({}, { __mode = "k" }) local resolved_chat_entries = setmetatable({}, { __mode = "k" }) local resolved_chat_frames_by_id = setmetatable({}, { __mode = "v" }) local alert_chat_frame local alert_color_token local alert_started_at local alert_driver local function copy_default_settings() local settings = {} for index = 1, #setting_order do local key = setting_order[index] settings[key] = default_settings[key] end return settings end local function report_missing_element(key, label) module.missing_element_reports = module.missing_element_reports or {} if module.missing_element_reports[key] then return end module.missing_element_reports[key] = true ns:chat_message("Blizzard-Element '" .. label .. "' konnte nicht ermittelt werden", true) end local function resolve_blizzard_chat_environment(report_missing) local constants = _G.Constants local chat_constants = constants and constants.ChatFrameConstants or nil local max_chat_windows = chat_constants and chat_constants.MaxChatWindows or nil local minimap = _G.Minimap if type(max_chat_windows) ~= "number" then if report_missing then report_missing_element("max_chat_windows", "Constants.ChatFrameConstants.MaxChatWindows") end return minimap, resolved_chat_entries end for chat_frame in pairs(resolved_chat_entries) do resolved_chat_entries[chat_frame] = nil end for chat_id in pairs(resolved_chat_frames_by_id) do resolved_chat_frames_by_id[chat_id] = nil end for chat_id = 1, max_chat_windows do local chat_frame = _G["ChatFrame" .. chat_id] if chat_frame and not chat_frame.isTemporary then resolved_chat_frames_by_id[chat_id] = chat_frame local frame_name = type(chat_frame.GetName) == "function" and chat_frame:GetName() or nil local edit_box = chat_frame.editBox or (frame_name and _G[frame_name .. "EditBox"] or nil) local edit_textures = {} if edit_box and type(edit_box.GetName) == "function" then local edit_box_name = edit_box:GetName() if edit_box_name then local texture_suffixes = { "Left", "Mid", "Right", "FocusLeft", "FocusMid", "FocusRight", } for texture_index = 1, #texture_suffixes do local texture = _G[edit_box_name .. texture_suffixes[texture_index]] if texture then edit_textures[#edit_textures + 1] = texture end end end end local entry = { edit_box = edit_box, edit_textures = edit_textures, tab = frame_name and _G[frame_name .. "Tab"] or nil, button_frame = chat_frame.buttonFrame or (frame_name and _G[frame_name .. "ButtonFrame"] or nil), minimize_button = chat_frame.minimizeButton or (frame_name and _G[frame_name .. "MinimizeButton"] or nil), resize_button = chat_frame.ResizeButton or (frame_name and _G[frame_name .. "ResizeButton"] or nil), scroll_to_bottom_button = chat_frame.ScrollToBottomButton, } resolved_chat_entries[chat_frame] = entry if report_missing then if not entry.edit_box then report_missing_element("edit_box_" .. chat_id, "ChatFrame" .. chat_id .. "EditBox") end if not entry.tab then report_missing_element("tab_" .. chat_id, "ChatFrame" .. chat_id .. "Tab") end if not entry.button_frame then report_missing_element("button_frame_" .. chat_id, "ChatFrame" .. chat_id .. "ButtonFrame") end if not entry.minimize_button then report_missing_element("minimize_button_" .. chat_id, "ChatFrame" .. chat_id .. "MinimizeButton") end if not entry.resize_button then report_missing_element("resize_button_" .. chat_id, "ChatFrame" .. chat_id .. "ResizeButton") end if not entry.scroll_to_bottom_button then report_missing_element( "scroll_to_bottom_button_" .. chat_id, "ChatFrame" .. chat_id .. ".ScrollToBottomButton" ) end end elseif report_missing and not chat_frame then report_missing_element("chat_frame_" .. chat_id, "ChatFrame" .. chat_id) end end if report_missing and not minimap then report_missing_element("minimap", "Minimap") end return minimap, resolved_chat_entries end local function can_modify_foreign_region(region) if not region then return false end if type(region.IsProtected) == "function" and region:IsProtected() and InCombatLockdown() then module.pending_foreign_update = true return false end return true end local function get_chat_frame_state(chat_frame) local state = chat_frame_states[chat_frame] if state then return state end state = {} chat_frame_states[chat_frame] = state return state end local function update_border_color(border, chat_frame) local palette = ns.palette if not border or not palette then return end if chat_frame == alert_chat_frame and alert_color_token and alert_started_at then local base_r, base_g, base_b = palette:get_rgb("border") local alert_r, alert_g, alert_b = palette:get_rgb(alert_color_token) if base_r and alert_r then local elapsed = GetTime() - alert_started_at local blend = (math.cos(elapsed * math.pi * 2 / alert_pulse_period) + 1) / 2 local r = base_r + (alert_r - base_r) * blend local g = base_g + (alert_g - base_g) * blend local b = base_b + (alert_b - base_b) * blend border:SetBackdropBorderColor(r, g, b, palette:get_alpha("opaque")) return end end border:SetBackdropBorderColor( palette:get_rgba("border", palette:get_alpha("opaque")) ) end local function stop_chat_alert() local chat_frame = alert_chat_frame alert_chat_frame = nil alert_color_token = nil alert_started_at = nil if alert_driver then alert_driver:Hide() end local state = chat_frame and chat_frame_states[chat_frame] or nil if state and state.border then update_border_color(state.border) end end local function hook_alert_reset_region(region) if not region or hooked_alert_reset_regions[region] then return end hooked_alert_reset_regions[region] = true region:HookScript("OnMouseDown", function() if alert_chat_frame then stop_chat_alert() end end) end local function ensure_chat_border(chat_frame) local designelemente = ns.designelemente if not chat_frame or not designelemente then return false end local state = get_chat_frame_state(chat_frame) if not state.border then state.border = designelemente:create_border_overlay( chat_frame, chat_frame, "frame" ) end if state.border then local border_offset = designelemente:get_border_offset("frame") if type(border_offset) == "number" then state.border:ClearAllPoints() state.border:SetPoint("TOPLEFT", chat_frame, "TOPLEFT", -border_offset, border_offset) state.border:SetPoint("BOTTOMRIGHT", chat_frame, "BOTTOMRIGHT", border_offset, -(border_offset + 3)) end state.border:SetFrameLevel(chat_frame:GetFrameLevel() + 10) end update_border_color(state.border, chat_frame) return state.border ~= nil end local function ensure_edit_box_design(edit_box) local palette = ns.palette local designelemente = ns.designelemente if not edit_box or not palette or not designelemente then return false end local state = get_chat_frame_state(edit_box) local r, g, b, a = palette:get_surface("unit_bar") if not r then return false end if not state.background then local background = edit_box:CreateTexture(nil, "BACKGROUND") background:SetAllPoints(edit_box) state.background = background end state.background:SetColorTexture(r, g, b, a) if not state.border then state.border = designelemente:create_border_overlay( edit_box, edit_box, "frame" ) end update_border_color(state.border) return state.border ~= nil end local function hide_edit_box_textures(entry) if not entry or type(entry.edit_textures) ~= "table" then return end for index = 1, #entry.edit_textures do local texture = entry.edit_textures[index] if texture then -- Direkte Anforderung des Chat-Prompts: native Rand-/Fokustexturen per Alpha und Hide ausblenden. texture:SetAlpha(0) texture:Hide() end end end local function apply_edit_box(chat_frame) local entry = resolved_chat_entries[chat_frame] local edit_box = entry and entry.edit_box or nil if not edit_box or not can_modify_foreign_region(edit_box) then return false end local gap local height if type(ns.get_minimap_top_bar_metrics) == "function" then gap, height = ns:get_minimap_top_bar_metrics() end if type(ns.get_minimap_top_bar) == "function" then local top_bar = ns:get_minimap_top_bar() local top_bar_height = top_bar and type(top_bar.GetHeight) == "function" and top_bar:GetHeight() or nil if type(top_bar_height) == "number" and top_bar_height > 0 then height = top_bar_height end end if type(gap) ~= "number" or type(height) ~= "number" or height <= 0 then report_missing_element("minimap_top_bar_metrics", "Minimap-Top-Bar-Metriken") return false end edit_box:ClearAllPoints() edit_box:SetPoint("BOTTOMLEFT", chat_frame, "TOPLEFT", 0, gap) edit_box:SetPoint("TOPRIGHT", chat_frame, "TOPRIGHT", 0, gap + height) edit_box:SetHeight(height) hide_edit_box_textures(entry) ensure_edit_box_design(edit_box) return true end local function hook_edit_box(chat_frame, edit_box) if not edit_box or hooked_edit_boxes[edit_box] then return end hooked_edit_boxes[edit_box] = true edit_box:HookScript("OnShow", function() apply_edit_box(chat_frame) end) edit_box:HookScript("OnEditFocusGained", function() apply_edit_box(chat_frame) end) end local function hook_control_region(region) if not region or hooked_control_regions[region] then return end hooked_control_regions[region] = true region:HookScript("OnShow", function(self) if module.settings and module.settings.buttons_hidden == true then self:Hide() end end) end local function apply_control_visibility(region, hidden) if not region then return end hook_control_region(region) if hidden then if control_visibility_states[region] == nil then control_visibility_states[region] = region:IsShown() end if can_modify_foreign_region(region) then region:Hide() end return end local previous_visibility = control_visibility_states[region] if previous_visibility == nil or not can_modify_foreign_region(region) then return end if previous_visibility then region:Show() else region:Hide() end control_visibility_states[region] = nil end local function apply_chat_controls(entry) if not entry then return end local hidden = module.settings and module.settings.buttons_hidden == true apply_control_visibility(entry.button_frame, hidden) apply_control_visibility(entry.minimize_button, hidden) apply_control_visibility(entry.resize_button, hidden) apply_control_visibility(entry.scroll_to_bottom_button, hidden) end local function apply_chat_clamping(chat_frame) if not chat_frame or not can_modify_foreign_region(chat_frame) then return false end local free_movement = module.settings and module.settings.free_movement == true if free_movement then if clamp_states[chat_frame] == nil then clamp_states[chat_frame] = chat_frame:IsClampedToScreen() end chat_frame:SetClampedToScreen(false) return true end local previous_clamping = clamp_states[chat_frame] if previous_clamping ~= nil then chat_frame:SetClampedToScreen(previous_clamping) clamp_states[chat_frame] = nil end return true end local function position_chat_frame(chat_frame, minimap) local ui_parent = _G.UIParent if not chat_frame or not minimap or not ui_parent or not can_modify_foreign_region(chat_frame) then return false end local parent_right = ui_parent:GetRight() local parent_top = ui_parent:GetTop() local minimap_right = minimap:GetRight() local minimap_top = minimap:GetTop() if type(parent_right) ~= "number" or type(parent_top) ~= "number" or type(minimap_right) ~= "number" or type(minimap_top) ~= "number" then return false end local horizontal_margin = parent_right - minimap_right local vertical_margin = parent_top - minimap_top chat_frame:ClearAllPoints() chat_frame:SetPoint( "TOPLEFT", ui_parent, "TOPLEFT", horizontal_margin, -vertical_margin ) return true end local function apply_chat_appearance(chat_frame, minimap) if not chat_frame or not minimap or not can_modify_foreign_region(chat_frame) then return false end local minimap_height = minimap:GetHeight() if type(minimap_height) ~= "number" or minimap_height <= 0 then return false end chat_frame:SetSize(chat_width, minimap_height) position_chat_frame(chat_frame, minimap) local palette = ns.palette if not palette then return false end local r, g, b, a = palette:get_surface("unit_bar") if not r then return false end if type(_G.FCF_SetWindowColor) == "function" then _G.FCF_SetWindowColor(chat_frame, r, g, b, true) else report_missing_element("fcf_set_window_color", "FCF_SetWindowColor") end if type(_G.FCF_SetWindowAlpha) == "function" then _G.FCF_SetWindowAlpha(chat_frame, a, true) else report_missing_element("fcf_set_window_alpha", "FCF_SetWindowAlpha") end if type(_G.FCF_SetChatWindowFontSize) == "function" then local _, current_font_size = chat_frame:GetFont() if type(current_font_size) ~= "number" or math.floor(current_font_size + 0.5) ~= chat_font_size then _G.FCF_SetChatWindowFontSize(nil, chat_frame, chat_font_size) end else report_missing_element("fcf_set_chat_window_font_size", "FCF_SetChatWindowFontSize") end ensure_chat_border(chat_frame) return true end local function hook_chat_interaction_regions(chat_frame, entry) hook_alert_reset_region(chat_frame) if not entry then return end hook_alert_reset_region(entry.edit_box) hook_alert_reset_region(entry.tab) hook_alert_reset_region(entry.button_frame) hook_alert_reset_region(entry.minimize_button) hook_alert_reset_region(entry.resize_button) hook_alert_reset_region(entry.scroll_to_bottom_button) end local function apply_chat_frame(chat_frame, minimap) local entry = resolved_chat_entries[chat_frame] if not entry then return false end apply_chat_appearance(chat_frame, minimap) apply_chat_controls(entry) apply_chat_clamping(chat_frame) if type(chat_frame.SetFading) == "function" then chat_frame:SetFading(module.settings and module.settings.message_fading == true) end hook_edit_box(chat_frame, entry.edit_box) hook_chat_interaction_regions(chat_frame, entry) apply_edit_box(chat_frame) return true end local function hook_chat_frame(chat_frame) if not chat_frame or hooked_chat_frames[chat_frame] then return end hooked_chat_frames[chat_frame] = true chat_frame:HookScript("OnShow", function(self) local minimap = resolve_blizzard_chat_environment(false) apply_chat_frame(self, minimap) end) end local function apply_all_chat_frames(report_missing) local minimap, entries = resolve_blizzard_chat_environment(report_missing) if not minimap then return false end module.pending_foreign_update = false for chat_frame in pairs(entries) do hook_chat_frame(chat_frame) apply_chat_frame(chat_frame, minimap) end return true end local function chat_message_type_is_enabled(message_type) if type(_G.GetChatWindowMessages) ~= "function" then report_missing_element("get_chat_window_messages", "GetChatWindowMessages") return false end resolve_blizzard_chat_environment(false) for chat_id, chat_frame in pairs(resolved_chat_frames_by_id) do if chat_frame and not chat_frame.isTemporary then local message_types = { _G.GetChatWindowMessages(chat_id) } for index = 1, #message_types do if message_types[index] == message_type then return true end end end end return false end local function is_valid_alert_target(chat_frame) return chat_frame and resolved_chat_entries[chat_frame] ~= nil and type(chat_frame.IsShown) == "function" and chat_frame:IsShown() end local function get_active_visible_chat_frame() resolve_blizzard_chat_environment(false) local selected_chat_frame = _G.SELECTED_CHAT_FRAME if is_valid_alert_target(selected_chat_frame) then return selected_chat_frame end local chat_dock = _G.GENERAL_CHAT_DOCK if chat_dock and type(_G.FCFDock_GetSelectedWindow) == "function" then local docked_chat_frame = _G.FCFDock_GetSelectedWindow(chat_dock) if is_valid_alert_target(docked_chat_frame) then return docked_chat_frame end end local default_chat_frame = _G.DEFAULT_CHAT_FRAME if is_valid_alert_target(default_chat_frame) then return default_chat_frame end return nil end local function is_own_guild_message(sender_name, sender_guid) local player_guid = type(_G.UnitGUID) == "function" and _G.UnitGUID("player") or nil if sender_guid and player_guid and sender_guid == player_guid then return true end local player_name = type(_G.UnitName) == "function" and _G.UnitName("player") or nil if type(sender_name) ~= "string" or type(player_name) ~= "string" then return false end local realm_separator = string.find(sender_name, "-", 1, true) local sender_base_name = realm_separator and string.sub(sender_name, 1, realm_separator - 1) or sender_name return sender_base_name == player_name end local function play_alert_sound() local soundkit = _G.SOUNDKIT if type(_G.PlaySound) == "function" and soundkit and type(soundkit.TELL_MESSAGE) == "number" then _G.PlaySound(soundkit.TELL_MESSAGE) return end report_missing_element("tell_message_sound", "SOUNDKIT.TELL_MESSAGE") end local function start_chat_alert(color_token) if alert_chat_frame then return false end local chat_frame = get_active_visible_chat_frame() if not chat_frame or not ensure_chat_border(chat_frame) then return false end alert_chat_frame = chat_frame alert_color_token = color_token alert_started_at = GetTime() local state = chat_frame_states[chat_frame] if state and state.border then update_border_color(state.border, chat_frame) end play_alert_sound() if alert_driver then alert_driver:Show() end return true end local function collect_profile_settings() local settings = {} local source = type(module.settings) == "table" and module.settings or default_settings for index = 1, #setting_order do local key = setting_order[index] local value = source[key] if type(value) ~= "boolean" then value = default_settings[key] end settings[key] = value end return settings end local function apply_profile_settings(data) if type(module.settings) ~= "table" then module.settings = copy_default_settings() end for index = 1, #setting_order do local key = setting_order[index] local value if type(data) == "table" then value = data[key] end if type(value) ~= "boolean" then value = default_settings[key] end module.settings[key] = value end apply_all_chat_frames(false) return true end local function on_event(_, event, ...) if event == "CHAT_MSG_GUILD" then if alert_chat_frame or not chat_message_type_is_enabled("GUILD") then return end local sender_name = select(2, ...) local sender_guid = select(12, ...) if not is_own_guild_message(sender_name, sender_guid) then start_chat_alert("green") end return end if event == "CHAT_MSG_WHISPER" then if not alert_chat_frame and chat_message_type_is_enabled("WHISPER") then start_chat_alert("red") end return end if event == "PLAYER_REGEN_ENABLED" then if module.pending_foreign_update then apply_all_chat_frames(false) end return end if event == "ADDON_LOADED" then local loaded_addon_name = ... if loaded_addon_name == "Blizzard_ChatFrameBase" then apply_all_chat_frames(false) end return end if event == "PLAYER_ENTERING_WORLD" or event == "UPDATE_CHAT_WINDOWS" or event == "UPDATE_FLOATING_CHAT_WINDOWS" then apply_all_chat_frames(false) end end function module:initialize() self.settings = copy_default_settings() self.pending_foreign_update = false alert_driver = CreateFrame("Frame") alert_driver:Hide() alert_driver:SetScript("OnUpdate", function() if not alert_chat_frame or not alert_started_at then alert_driver:Hide() return end if GetTime() - alert_started_at >= alert_duration then stop_chat_alert() return end local state = chat_frame_states[alert_chat_frame] if state and state.border then update_border_color(state.border, alert_chat_frame) end end) ns:register_profile_provider("chat", collect_profile_settings, apply_profile_settings) ns:register_event("ADDON_LOADED", self, on_event) ns:register_event("PLAYER_ENTERING_WORLD", self, on_event) ns:register_event("UPDATE_CHAT_WINDOWS", self, on_event) ns:register_event("UPDATE_FLOATING_CHAT_WINDOWS", self, on_event) ns:register_event("PLAYER_REGEN_ENABLED", self, on_event) ns:register_event("CHAT_MSG_GUILD", self, on_event) ns:register_event("CHAT_MSG_WHISPER", self, on_event) if type(_G.FloatingChatFrame_Update) == "function" and type(hooksecurefunc) == "function" then hooksecurefunc("FloatingChatFrame_Update", function(chat_id) local minimap = resolve_blizzard_chat_environment(false) local chat_frame = resolved_chat_frames_by_id[chat_id] if chat_frame and not chat_frame.isTemporary then apply_chat_frame(chat_frame, minimap) end end) else report_missing_element("floating_chat_frame_update", "FloatingChatFrame_Update") end if ns.palette and type(ns.palette.register_callback) == "function" then ns.palette:register_callback("chat", function() apply_all_chat_frames(false) end) end apply_all_chat_frames(true) end ns:register_module("chat")