---------------------------------------------------- -- SamUI v0.3.3 -- Eine vollständige UI-Alternative -- WoW Classic Anniversary TBC -- Interface 20506 -- Build 69110 ---------------------------------------------------- local _, ns = ... local module = ns:new_module("tooltip") local default_settings = { player_name_colored = true, guild_name_shown = true, } local setting_order = { "player_name_colored", "guild_name_shown", } local required_tooltip_names = { "GameTooltip", "ShoppingTooltip1", "ShoppingTooltip2", "ItemRefTooltip", } local optional_tooltip_names = { "ShoppingTooltip3", } local tooltip_states = setmetatable({}, { __mode = "k" }) local configured_tooltips = setmetatable({}, { __mode = "k" }) local missing_tooltip_warnings = {} local api_warnings = {} local default_anchor_hooked = false local health_bar_height = 4 local health_bar_inset = 3 local health_bar_side_inset = 7 local tooltip_bottom_extension = 5 local health_bar_raise = 0 local game_tooltip_frame = nil local game_tooltip_health_bar = nil 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 collect_profile_settings() local settings = {} for index = 1, #setting_order do local key = setting_order[index] settings[key] = module.settings[key] == true end return settings end local function apply_profile_settings(profile_settings) if type(profile_settings) ~= "table" then return false end for index = 1, #setting_order do local key = setting_order[index] local value = profile_settings[key] if type(value) == "boolean" then module.settings[key] = value else module.settings[key] = default_settings[key] end end return true end local function warn_api_once(key, message) if api_warnings[key] then return end api_warnings[key] = true ns:chat_message(message, true) end local function resolve_tooltip_line(tooltip, line_index, side) if not tooltip or type(line_index) ~= "number" then return nil end local tooltip_name = tooltip:GetName() if type(tooltip_name) ~= "string" or tooltip_name == "" then return nil end local line_side = side == "right" and "Right" or "Left" return _G[tooltip_name .. "Text" .. line_side .. tostring(line_index)] end local function resolve_tooltips() local resolved = {} game_tooltip_health_bar = _G.GameTooltipStatusBar for index = 1, #required_tooltip_names do local tooltip_name = required_tooltip_names[index] local tooltip = _G[tooltip_name] if tooltip then resolved[#resolved + 1] = tooltip if tooltip_name == "GameTooltip" then game_tooltip_frame = tooltip end elseif not missing_tooltip_warnings[tooltip_name] then missing_tooltip_warnings[tooltip_name] = true ns:chat_message("Tooltip-Frame '" .. tooltip_name .. "' wurde nicht gefunden", true) end end for index = 1, #optional_tooltip_names do local tooltip = _G[optional_tooltip_names[index]] if tooltip then resolved[#resolved + 1] = tooltip end end return resolved end local function get_state(tooltip) local state = tooltip_states[tooltip] if state then return state end state = { color_source = "standard", hidden_regions = setmetatable({}, { __mode = "k" }), target_signature = nil, unit = nil, special_anchor_active = false, original_clamped = nil, pending_anchor = false, pending_clamp_restore = false, update_elapsed = 0, optical_extension_active = false, original_height = nil, pending_optical_extension = false, pending_optical_restore = false, adjusting_optical_height = false, } tooltip_states[tooltip] = state return state end local function restore_hidden_regions(state) if not state or type(state.hidden_regions) ~= "table" then return end for region in pairs(state.hidden_regions) do if region and type(region.Show) == "function" then region:Show() end state.hidden_regions[region] = nil end end local function hide_region(state, region) if not state or not region or type(region.Hide) ~= "function" then return end region:Hide() state.hidden_regions[region] = true end local function get_standard_border_color() local palette = ns.palette return palette:get_rgba("border", palette:get_alpha("opaque")) end local function get_neutral_gray_color() if GRAY_FONT_COLOR then return GRAY_FONT_COLOR.r, GRAY_FONT_COLOR.g, GRAY_FONT_COLOR.b end return ns.palette:get_rgb("text_muted") end local function get_reaction_color(unit) local reaction = UnitReaction(unit, "player") local reaction_color = reaction and FACTION_BAR_COLORS and FACTION_BAR_COLORS[reaction] or nil if reaction_color then return reaction_color.r, reaction_color.g, reaction_color.b end return ns.palette:get_rgb("text") end local function set_visual_color(tooltip, state, source, r, g, b) if not tooltip or not state then return end if source == "standard" then r, g, b = get_standard_border_color() end if type(r) ~= "number" or type(g) ~= "number" or type(b) ~= "number" then return end state.color_source = source or "runtime" state.border_r = r state.border_g = g state.border_b = b if state.border_frame then state.border_frame:SetBackdropBorderColor( r, g, b, ns.palette:get_alpha("opaque") ) end if state.health_bar then state.health_bar:SetStatusBarColor(r, g, b) end end local function apply_tooltip_backdrop(tooltip, state) local palette = ns.palette local designelemente = ns.designelemente if not tooltip or not state or not palette or not designelemente then return false end if type(tooltip.SetBackdrop) ~= "function" then if type(Mixin) ~= "function" or not BackdropTemplateMixin then return false end Mixin(tooltip, BackdropTemplateMixin) end local background_texture = designelemente:get_texture("statusbar_background") local border_size = designelemente:get_border_size("tooltip") local border_offset = designelemente:get_border_offset("tooltip") if not background_texture or not border_size or not border_offset then return false end tooltip:SetBackdrop({ bgFile = background_texture, }) tooltip:SetBackdropColor(palette:get_surface("tooltip")) if not state.border_frame then state.border_frame = designelemente:create_border_overlay( tooltip, tooltip, "tooltip", "border" ) if not state.border_frame then return false end end state.border_frame:SetFrameLevel(tooltip:GetFrameLevel() + 10) if state.color_source == "standard" then set_visual_color(tooltip, state, "standard") else set_visual_color( tooltip, state, state.color_source, state.border_r, state.border_g, state.border_b ) end return true end local function apply_health_bar_color(tooltip, state) if not tooltip or not state or not state.health_bar then return end local r = state.border_r local g = state.border_g local b = state.border_b if type(r) ~= "number" or type(g) ~= "number" or type(b) ~= "number" then return end state.health_bar:SetStatusBarColor(r, g, b) end local function configure_health_bar(tooltip, state) if tooltip ~= game_tooltip_frame then return true end local health_bar = game_tooltip_health_bar local designelemente = ns.designelemente local palette = ns.palette if not health_bar or not designelemente or not palette then return false end local status_bar_texture = designelemente:get_texture("statusbar") local background_texture = designelemente:get_texture("statusbar_background") local side_inset = health_bar_side_inset if not status_bar_texture or not background_texture then return false end local bottom_inset = health_bar_inset + tooltip_bottom_extension + health_bar_raise health_bar:ClearAllPoints() health_bar:SetPoint( "BOTTOMLEFT", tooltip, "BOTTOMLEFT", side_inset, bottom_inset ) health_bar:SetPoint( "BOTTOMRIGHT", tooltip, "BOTTOMRIGHT", -side_inset, bottom_inset ) health_bar:SetHeight(health_bar_height) health_bar:SetStatusBarTexture(status_bar_texture) if not state.health_bar_background then local background = health_bar:CreateTexture(nil, "BACKGROUND") background:SetAllPoints(health_bar) background:SetTexture(background_texture) state.health_bar_background = background end state.health_bar_background:SetVertexColor(palette:get_surface("unit_bar")) state.health_bar = health_bar apply_health_bar_color(tooltip, state) return true end local function on_health_bar_value_changed() local tooltip = game_tooltip_frame if not tooltip then return end local state = tooltip_states[tooltip] if state then apply_health_bar_color(tooltip, state) end end local function get_unit_status(unit) local palette = ns.palette if UnitIsPlayer(unit) and not UnitIsConnected(unit) then return palette:wrap("text_muted", ""), true end if UnitIsDeadOrGhost(unit) then return palette:wrap("red", "TOT"), true end if UnitIsPlayer(unit) then if UnitIsAFK(unit) then return palette:wrap("text_muted", ""), false end if UnitIsDND(unit) then return palette:wrap("text_muted", ""), false end end if not UnitIsPlayer(unit) and UnitIsTapDenied(unit) then return nil, true end return nil, false end local function get_unit_border_color(unit, neutral_gray) if neutral_gray then return get_neutral_gray_color() end if UnitIsPlayer(unit) then local _, class_file = UnitClass(unit) local r, g, b = ns.palette:get_class_color(class_file) if r then return r, g, b end return ns.palette:get_rgb("text") end return get_reaction_color(unit) end local function get_unit_name_color(unit, neutral_gray) if neutral_gray then return get_neutral_gray_color() end if UnitIsPlayer(unit) then if module.settings.player_name_colored then return get_unit_border_color(unit, false) end return ns.palette:get_rgb("text") end return get_reaction_color(unit) end local function get_level_text(unit) local level = UnitLevel(unit) if level == -1 then if UnitClassification(unit) == "worldboss" then return "BOSS" end return "??" end return tostring(level) end local function get_target_display(unit) if not UnitIsPlayer(unit) then return nil, nil end local target_unit = unit .. "target" if not UnitExists(target_unit) then return nil, nil end local target_name = UnitName(target_unit) if type(target_name) ~= "string" or target_name == "" then return nil, nil end local palette = ns.palette local display_text local signature_prefix local r local g local b if UnitIsUnit(target_unit, unit) then display_text = string.upper("Ziel: " .. target_name) signature_prefix = "self" r, g, b = palette:get_rgb("red") elseif UnitIsPlayer(target_unit) then local _, class_file = UnitClass(target_unit) display_text = "Ziel: [" .. target_name .. "]" signature_prefix = "player" r, g, b = palette:get_class_color(class_file) elseif UnitIsOtherPlayersPet(target_unit) then display_text = "Ziel: <" .. target_name .. ">" signature_prefix = "pet" r, g, b = get_reaction_color(target_unit) else display_text = "Ziel: [" .. target_name .. "]" signature_prefix = "unit" r, g, b = get_reaction_color(target_unit) end if not r then r, g, b = palette:get_rgb("text") end return { text = display_text, signature = table.concat({ signature_prefix, target_name, tostring(r), tostring(g), tostring(b), }, ":"), r = r, g = g, b = b, } end local function set_line_text(tooltip, state, line_index, text, r, g, b) local line = resolve_tooltip_line(tooltip, line_index, "left") if not line then return false end if not r then r, g, b = ns.palette:get_rgb("text") end line:Show() line:SetText(text) line:SetTextColor(r, g, b) local right_line = resolve_tooltip_line(tooltip, line_index, "right") if right_line then hide_region(state, right_line) end return true end local function ensure_line_count(tooltip, minimum_lines) while tooltip:NumLines() < minimum_lines do tooltip:AddLine(" ") end end local function style_player_tooltip(tooltip, state, unit, name_text, name_r, name_g, name_b, target) local lines = { { text = name_text, r = name_r, g = name_g, b = name_b, }, } local guild_name = GetGuildInfo(unit) if module.settings.guild_name_shown and type(guild_name) == "string" and guild_name ~= "" then lines[#lines + 1] = { text = guild_name } end local class_name = UnitClass(unit) or "" local race_name = UnitRace(unit) or "" lines[#lines + 1] = { text = "Spieler, " .. class_name .. " " .. race_name } lines[#lines + 1] = { text = "Stufe " .. get_level_text(unit) } if target then lines[#lines + 1] = target end local minimum_lines = #lines if not target then minimum_lines = minimum_lines + 1 end ensure_line_count(tooltip, minimum_lines) restore_hidden_regions(state) local total_lines = tooltip:NumLines() for line_index = 1, #lines do local line_data = lines[line_index] set_line_text( tooltip, state, line_index, line_data.text, line_data.r, line_data.g, line_data.b ) end for line_index = #lines + 1, total_lines do hide_region(state, resolve_tooltip_line(tooltip, line_index, "left")) hide_region(state, resolve_tooltip_line(tooltip, line_index, "right")) end end local function style_non_player_tooltip(tooltip, state, unit, name_text, r, g, b) local first_line = resolve_tooltip_line(tooltip, 1, "left") if first_line then first_line:SetText(name_text) first_line:SetTextColor(r, g, b) end if UnitLevel(unit) ~= -1 then return end ensure_line_count(tooltip, 2) set_line_text( tooltip, state, 2, "Stufe " .. get_level_text(unit) ) end local function style_unit_tooltip(tooltip) local state = get_state(tooltip) restore_hidden_regions(state) local _, unit = tooltip:GetUnit() if type(unit) ~= "string" or unit == "" or not UnitExists(unit) then state.unit = nil state.target_signature = nil set_visual_color(tooltip, state, "standard") return end state.unit = unit state.update_elapsed = 0 local unit_name = UnitName(unit) if type(unit_name) ~= "string" or unit_name == "" then set_visual_color(tooltip, state, "standard") return end local status_text, neutral_gray = get_unit_status(unit) local name_r, name_g, name_b = get_unit_name_color(unit, neutral_gray) local border_r, border_g, border_b = get_unit_border_color(unit, neutral_gray) local name_text = unit_name if status_text then name_text = status_text .. " " .. name_text end local target = get_target_display(unit) state.target_signature = target and target.signature or nil if UnitIsPlayer(unit) then style_player_tooltip( tooltip, state, unit, name_text, name_r, name_g, name_b, target ) else style_non_player_tooltip( tooltip, state, unit, name_text, name_r, name_g, name_b ) end set_visual_color(tooltip, state, "unit", border_r, border_g, border_b) end local function style_item_tooltip(tooltip) local state = get_state(tooltip) state.unit = nil state.target_signature = nil restore_hidden_regions(state) local _, item_link = tooltip:GetItem() if type(item_link) ~= "string" or item_link == "" then set_visual_color(tooltip, state, "standard") return end if not C_Item or type(C_Item.GetItemInfo) ~= "function" then warn_api_once( "c_item_get_item_info", "C_Item.GetItemInfo ist für die Tooltip-Qualitätsfarbe nicht verfügbar" ) set_visual_color(tooltip, state, "standard") return end local item_quality = select(3, C_Item.GetItemInfo(item_link)) local r, g, b = ns.palette:get_quality_color(item_quality) if r then set_visual_color(tooltip, state, "quality", r, g, b) else set_visual_color(tooltip, state, "standard") end end local function reset_tooltip(tooltip) local state = get_state(tooltip) state.unit = nil state.target_signature = nil state.update_elapsed = 0 restore_hidden_regions(state) set_visual_color(tooltip, state, "standard") end local function refresh_tooltip_content(tooltip) local _, unit = tooltip:GetUnit() if type(unit) == "string" and unit ~= "" and UnitExists(unit) then style_unit_tooltip(tooltip) return end local _, item_link = tooltip:GetItem() if type(item_link) == "string" and item_link ~= "" then style_item_tooltip(tooltip) return end reset_tooltip(tooltip) end local function can_modify_tooltip_geometry(tooltip) if type(tooltip.IsProtected) == "function" and tooltip:IsProtected() and InCombatLockdown() then return false end return true end local function set_optical_extension_height(tooltip, state, base_height) if type(base_height) ~= "number" or base_height <= 0 then return false end state.original_height = base_height state.adjusting_optical_height = true tooltip:SetHeight(base_height + tooltip_bottom_extension) state.adjusting_optical_height = false state.optical_extension_active = true state.pending_optical_extension = false state.pending_optical_restore = false return true end local function apply_optical_extension(tooltip) if tooltip ~= game_tooltip_frame then return true end local state = get_state(tooltip) if not can_modify_tooltip_geometry(tooltip) then state.pending_optical_extension = true return false end local current_height = tooltip:GetHeight() if type(current_height) ~= "number" or current_height <= 0 then return false end if state.optical_extension_active and type(state.original_height) == "number" then local expected_height = state.original_height + tooltip_bottom_extension if math.abs(current_height - expected_height) < 0.01 then state.pending_optical_extension = false return true end end return set_optical_extension_height(tooltip, state, current_height) end local function on_tooltip_size_changed(tooltip, _, height) if tooltip ~= game_tooltip_frame or not tooltip:IsShown() then return end local state = tooltip_states[tooltip] if not state or state.adjusting_optical_height or not state.optical_extension_active then return end if type(height) ~= "number" or height <= 0 then return end local expected_height = type(state.original_height) == "number" and state.original_height + tooltip_bottom_extension or nil if expected_height and math.abs(height - expected_height) < 0.01 then return end if not can_modify_tooltip_geometry(tooltip) then state.original_height = height state.optical_extension_active = false state.pending_optical_extension = true return end set_optical_extension_height(tooltip, state, height) end local function restore_optical_extension(tooltip) if tooltip ~= game_tooltip_frame then return end local state = get_state(tooltip) if not state.optical_extension_active then state.pending_optical_restore = false return end if not can_modify_tooltip_geometry(tooltip) then state.pending_optical_restore = true return end if type(state.original_height) == "number" and state.original_height > 0 then state.adjusting_optical_height = true tooltip:SetHeight(state.original_height) state.adjusting_optical_height = false end state.original_height = nil state.optical_extension_active = false state.pending_optical_extension = false state.pending_optical_restore = false end local function restore_clamping(tooltip) local state = get_state(tooltip) if not state.special_anchor_active then state.pending_clamp_restore = false return end if not can_modify_tooltip_geometry(tooltip) then state.pending_clamp_restore = true return end if state.original_clamped ~= nil then tooltip:SetClampedToScreen(state.original_clamped) end state.original_clamped = nil state.special_anchor_active = false state.pending_anchor = false state.pending_clamp_restore = false end local function position_game_tooltip(tooltip) if tooltip ~= game_tooltip_frame then return end local minimap = _G.Minimap if not minimap then warn_api_once("minimap_frame", "Minimap-Frame wurde für den Tooltip-Anker nicht gefunden") return end local state = get_state(tooltip) if not can_modify_tooltip_geometry(tooltip) then state.pending_anchor = true return end state.pending_anchor = false state.pending_clamp_restore = false if not state.special_anchor_active then state.original_clamped = tooltip:IsClampedToScreen() tooltip:SetClampedToScreen(false) state.special_anchor_active = true end tooltip:ClearAllPoints() tooltip:SetPoint( "TOPRIGHT", minimap, "TOPLEFT", -14, 0 ) end local function update_target_of_target(tooltip, elapsed) local state = tooltip_states[tooltip] if not state or not state.unit or not UnitIsPlayer(state.unit) then return end local update_interval = TOOLTIP_UPDATE_TIME if type(update_interval) ~= "number" or update_interval <= 0 then return end state.update_elapsed = state.update_elapsed + elapsed if state.update_elapsed < update_interval then return end state.update_elapsed = 0 local _, current_unit = tooltip:GetUnit() if current_unit ~= state.unit or not UnitExists(state.unit) then return end local target = get_target_display(state.unit) local target_signature = target and target.signature or nil if target_signature ~= state.target_signature then style_unit_tooltip(tooltip) tooltip:Show() end end local function on_tooltip_show(tooltip) local state = get_state(tooltip) apply_optical_extension(tooltip) configure_health_bar(tooltip, state) apply_tooltip_backdrop(tooltip, state) refresh_tooltip_content(tooltip) end local function on_tooltip_hide(tooltip) reset_tooltip(tooltip) restore_optical_extension(tooltip) restore_clamping(tooltip) end local function hook_tooltip_handler(tooltip, script_name, handler) local success = pcall(tooltip.HookScript, tooltip, script_name, handler) return success == true end local function configure_tooltip(tooltip) if configured_tooltips[tooltip] then return true end local state = get_state(tooltip) if not apply_tooltip_backdrop(tooltip, state) then local tooltip_name = tooltip:GetName() or "" ns:chat_message("Tooltip-Design für '" .. tooltip_name .. "' konnte nicht gesetzt werden", true) return false end if not configure_health_bar(tooltip, state) then local tooltip_name = tooltip:GetName() or "" ns:chat_message("Tooltip-Gesundheitsbalken für '" .. tooltip_name .. "' konnte nicht eingerichtet werden", true) return false end set_visual_color(tooltip, state, "standard") tooltip:HookScript("OnShow", on_tooltip_show) tooltip:HookScript("OnHide", on_tooltip_hide) hook_tooltip_handler(tooltip, "OnTooltipCleared", reset_tooltip) local item_hooked = hook_tooltip_handler( tooltip, "OnTooltipSetItem", style_item_tooltip ) local unit_hooked = hook_tooltip_handler( tooltip, "OnTooltipSetUnit", style_unit_tooltip ) if not item_hooked or not unit_hooked then warn_api_once( "tooltip_handlers", "Tooltip-Datenhooks sind für diesen Client nicht vollständig verfügbar" ) end if tooltip == game_tooltip_frame then tooltip:HookScript("OnUpdate", update_target_of_target) tooltip:HookScript("OnSizeChanged", on_tooltip_size_changed) if state.health_bar then state.health_bar:HookScript("OnValueChanged", on_health_bar_value_changed) end end configured_tooltips[tooltip] = true return true end local function configure_resolved_tooltips() local tooltips = resolve_tooltips() for index = 1, #tooltips do configure_tooltip(tooltips[index]) end end local function on_event(_, event) if event == "ADDON_LOADED" then configure_resolved_tooltips() return end if event == "PLAYER_REGEN_ENABLED" then local tooltip = game_tooltip_frame if not tooltip then return end local state = tooltip_states[tooltip] if not state then return end if state.pending_optical_restore then restore_optical_extension(tooltip) elseif state.pending_optical_extension and tooltip:IsShown() then apply_optical_extension(tooltip) end if state.pending_clamp_restore then restore_clamping(tooltip) elseif state.pending_anchor and tooltip:IsShown() then position_game_tooltip(tooltip) end end end local function on_palette_changed() for tooltip, state in pairs(tooltip_states) do if tooltip and state then configure_health_bar(tooltip, state) apply_tooltip_backdrop(tooltip, state) if tooltip:IsShown() then refresh_tooltip_content(tooltip) elseif state.color_source == "standard" then set_visual_color(tooltip, state, "standard") end end end end function module:initialize() self.settings = copy_default_settings() ns:register_profile_provider("tooltip", collect_profile_settings, apply_profile_settings) ns:register_event("ADDON_LOADED", self, on_event) ns:register_event("PLAYER_REGEN_ENABLED", self, on_event) configure_resolved_tooltips() if type(GameTooltip_SetDefaultAnchor) == "function" and type(hooksecurefunc) == "function" then hooksecurefunc("GameTooltip_SetDefaultAnchor", position_game_tooltip) default_anchor_hooked = true end if not default_anchor_hooked then ns:chat_message("GameTooltip-Standardanker konnte nicht eingebunden werden", true) end if ns.palette and type(ns.palette.register_callback) == "function" then ns.palette:register_callback("tooltip", on_palette_changed) end end ns:register_module("tooltip")