--[[ | This file was obtained through the combined efforts | of Madbluntz & Plymouth Antiquarian Society. | | Credits: lifestorm, Gregory Wayne Rossel JR., | Maloy, DrPepper10 @ RIP, Atle! | | Visit for more: https://plymouth.thetwilightzone.ru/ --]] local PLUGIN = PLUGIN local animationTime = 0.5 local chatBorder = 32 local sizingBorder = 20 local maxChatEntries = 100 -- called when a markup object should paint its text -- gamemode/core/libs/cl_markup/MarkupObject:draw() override -- NOTE: alpha is some sort of override, which is ALWAYS 255 afaik local function PaintMarkupOverride(text, font, x, y, color, alignX, alignY, alpha, block) -- afaik this is always a string wich breaks SetTextColor silently -- this code (and cl_markup) is a nightmare. local textAlpha = tonumber(block.colour["a"]) or 255 if (ix.option.Get("chatOutline", false)) then -- outlined background for even more visibility draw.SimpleTextOutlined(text, font, x, y, ColorAlpha(color, textAlpha), alignX, alignY, 1, Color(0, 0, 0, textAlpha)) else -- background for easier reading surface.SetTextPos(x + SScaleMin(1 / 3), y + SScaleMin(1 / 3)) surface.SetFont(font) surface.SetTextColor(0, 0, 0, textAlpha) surface.DrawText(text) surface.SetTextPos(x, y) surface.SetFont(font) surface.SetTextColor(color.r, color.g, color.b, textAlpha) surface.DrawText(text) end end -- chat message local PANEL = {} AccessorFunc(PANEL, "fadeDelay", "FadeDelay", FORCE_NUMBER) AccessorFunc(PANEL, "fadeDuration", "FadeDuration", FORCE_NUMBER) function PANEL:Init() self.text = "" self.alpha = 255 self.fadeDelay = 15 self.fadeDuration = 5 end function PANEL:SetMarkup(text) self.text = text self.markup = ix.markup.Parse(self.text, self:GetWide()) self.markup.onDrawText = PaintMarkupOverride self:SetTall(self.markup:GetHeight()) timer.Simple(self.fadeDelay, function() if (!IsValid(self)) then return end self:CreateAnimation(self.fadeDuration, { index = 3, target = {alpha = 0} }) end) local parent = self:GetParent() if !parent then return end local parentParent = parent:GetParent() if !parentParent then return end if !parentParent.ScrollToBottom then return end if !ix.option.Get("disableAutoScrollChat", false) then self:GetParent():GetParent():ScrollToBottom() end end function PANEL:PerformLayout(width, height) if ((IsValid(ix.gui.chat) and ix.gui.chat.bSizing) or width == self.markup:GetWidth()) then return end self.markup = ix.markup.Parse(self.text, width) self.markup.onDrawText = PaintMarkupOverride self:SetTall(self.markup:GetHeight()) end function PANEL:Paint(width, height) local newAlpha -- we'll want to hide the chat while some important menus are open if (IsValid(ix.gui.characterMenu)) then newAlpha = math.min(255 - ix.gui.characterMenu.currentAlpha, self.alpha) elseif (IsValid(ix.gui.menu)) then newAlpha = math.min(255 - ix.gui.menu.currentAlpha, self.alpha) elseif (ix.gui.chat:GetActive()) then newAlpha = math.max(ix.gui.chat.alpha, self.alpha) else newAlpha = self.alpha end if (newAlpha < 1) then return end self.markup:draw(0, 0, nil, nil, newAlpha) end vgui.Register("ixChatMessage", PANEL, "Panel") -- chatbox tab button PANEL = {} AccessorFunc(PANEL, "bActive", "Active", FORCE_BOOL) AccessorFunc(PANEL, "bUnread", "Unread", FORCE_BOOL) function PANEL:Init() self:SetFont("ixChatFont") self:SetContentAlignment(5) self.unreadAlpha = 0 end function PANEL:SetUnread(bValue) self.bUnread = bValue self:CreateAnimation(animationTime, { index = 4, target = {unreadAlpha = bValue and 1 or 0}, easing = "outQuint" }) end function PANEL:SizeToContents() local width, height = self:GetContentSize() self:SetSize(width + SScaleMin(12 / 3), height + SScaleMin(6 / 3)) end function PANEL:Paint(width, height) derma.SkinFunc("PaintChatboxTabButton", self, width, height) end vgui.Register("ixChatboxTabButton", PANEL, "DButton") -- chatbox tab panel -- holds all tab buttons and corresponding history panels PANEL = {} function PANEL:Init() -- holds all tab buttons self.buttons = self:Add("Panel") self.buttons:Dock(TOP) self.buttons:DockPadding(1, 1, 0, 0) self.buttons.OnMousePressed = ix.util.Bind(ix.gui.chat, ix.gui.chat.OnMousePressed) -- we want mouse events to fall through self.buttons.OnMouseReleased = ix.util.Bind(ix.gui.chat, ix.gui.chat.OnMouseReleased) self.buttons.Paint = function(_, w, h) surface.SetDrawColor(Color(34, 32, 64, 100)) surface.DrawRect(0, 0, w, h) end self.tabs = {} end function PANEL:GetTabs() return self.tabs end function PANEL:AddTab(id, filter) local button = self.buttons:Add("ixChatboxTabButton") button:Dock(LEFT) button:SetText(id) -- display name is also the ID button:SetActive(false) button:SetMouseInputEnabled(true) button:SizeToContents() button.DoClick = function(this) self:SetActiveTab(this:GetText()) end local panel = self:Add("ixChatboxHistory") panel:SetButton(button) panel:SetID(id) panel:Dock(FILL) panel:SetVisible(false) panel:SetFilter(filter or {}) button.DoRightClick = function(this) ix.gui.chat:OnTabRightClick(this, panel, id) end self.tabs[id] = panel return panel end function PANEL:RemoveTab(id) local tab = self.tabs[id] if (!tab) then return end tab:GetButton():Remove() tab:Remove() self.tabs[id] = nil -- add default tab if we don't have any tabs left if (table.IsEmpty(self.tabs)) then self:AddTab(L("chat"), {}) self:SetActiveTab(L("chat")) elseif (id == self:GetActiveTabID()) then -- set a different active tab if we've removed a tab that is currently active self:SetActiveTab(next(self.tabs)) end end function PANEL:RenameTab(id, newID) local tab = self.tabs[id] if (!tab) then return end tab:GetButton():SetText(newID) tab:GetButton():SizeToContents() self.tabs[id] = nil self.tabs[newID] = tab end function PANEL:SetActiveTab(id) local tab = self.tabs[id] if (!tab) then error("attempted to set non-existent active tab") end for _, v in ipairs(self.buttons:GetChildren()) do v:SetActive(v:GetText() == id) end for _, v in pairs(self.tabs) do v:SetVisible(v:GetID() == id) end tab:GetButton():SetUnread(false) self.activeTab = id self:OnTabChanged(tab) end function PANEL:GetActiveTabID() return self.activeTab end function PANEL:GetActiveTab() return self.tabs[self.activeTab] end -- called when the active tab is changed -- `panel` is the corresponding history panel function PANEL:OnTabChanged(panel) end vgui.Register("ixChatboxTabs", PANEL, "EditablePanel") -- chatbox history panel -- holds individual messages in a scrollable panel PANEL = {} AccessorFunc(PANEL, "filter", "Filter") -- blacklist of message classes AccessorFunc(PANEL, "id", "ID", FORCE_STRING) AccessorFunc(PANEL, "button", "Button") -- button panel that this panel corresponds to function PANEL:Init() -- smaller top margin to help blend tab button/history panel transition self:DockMargin(SScaleMin(4 / 3), SScaleMin(2 / 3), SScaleMin(4 / 3), SScaleMin(4 / 3)) self:SetPaintedManually(true) local bar = self:GetVBar() bar:SetWide(0) self.entries = {} self.filter = {} end DEFINE_BASECLASS("Panel") -- DScrollPanel doesn't have SetVisible member function PANEL:SetVisible(bState) self:GetCanvas():SetVisible(bState) BaseClass.SetVisible(self, bState) end DEFINE_BASECLASS("DScrollPanel") function PANEL:PerformLayout(width, height) local bar = self:GetVBar() -- only scroll when we're not at the bottom/inactive local bScroll = !ix.gui.chat:GetActive() or bar.Scroll == bar.CanvasSize BaseClass.PerformLayout(self, width, height) if (bScroll) then self:ScrollToBottom() end end function PANEL:ScrollToBottom() local bar = self:GetVBar() bar:SetScroll(bar.CanvasSize) end -- adds a line of text as described by its elements function PANEL:AddLine(elements, bShouldScroll) -- table.concat is faster than regular string concatenation where there are lots of strings to concatenate local buffer = { "" } if (ix.option.Get("chatTimestamps", false)) then buffer[#buffer + 1] = "(" if (ix.option.Get("24hourTime", false)) then buffer[#buffer + 1] = os.date("%H:%M") else buffer[#buffer + 1] = os.date("%I:%M %p") end buffer[#buffer + 1] = ") " end if (CHAT_CLASS) then buffer[#buffer + 1] = " ", texture, v:Width(), v:Height()) end elseif (istable(v) and v.r and v.g and v.b) then if (v.a) then buffer[#buffer + 1] = string.format("", v.r, v.g, v.b, v.a) else buffer[#buffer + 1] = string.format("", v.r, v.g, v.b) end elseif (type(v) == "Player") then local color = team.GetColor(v:Team()) buffer[#buffer + 1] = string.format("%s", color.r, color.g, color.b, v:GetName():gsub("<", "<"):gsub(">", ">")) else buffer[#buffer + 1] = tostring(v):gsub("<", "<"):gsub(">", ">"):gsub("%b**", function(value) local inner = value:utf8sub(2, -2) if (inner:find("%S")) then if CHAT_CLASS.font == nil then CHAT_CLASS.font = "ixChatFont" end if CHAT_CLASS.font == "ixChatFont" then return "" .. value:utf8sub(2, -2) .. "" else return "" .. value:utf8sub(2, -2) .. "" end end end) end end local panel = self:Add("ixChatMessage") panel:Dock(TOP) panel:DockMargin(SScaleMin(10 / 3), 0, SScaleMin(10 / 3), 0) panel:InvalidateParent(true) panel:SetMarkup(table.concat(buffer)) if (#self.entries >= maxChatEntries) then local oldPanel = table.remove(self.entries, 1) if (IsValid(oldPanel)) then oldPanel:Remove() end end self.entries[#self.entries + 1] = panel return panel end vgui.Register("ixChatboxHistory", PANEL, "DScrollPanel") PANEL = {} DEFINE_BASECLASS("DTextEntry") function PANEL:Init() self:SetFont("ixChatFont") self:SetUpdateOnType(true) self:SetHistoryEnabled(true) self.History = ix.chat.history self.m_bLoseFocusOnClickAway = false end function PANEL:SetFont(font) BaseClass.SetFont(self, font) surface.SetFont(font) local _, height = surface.GetTextSize("W@") self:SetTall(height + SScaleMin(8 / 3)) end function PANEL:AllowInput(newCharacter) local text = self:GetText() local maxLength = ix.config.Get("chatMax") -- we can't check for the proper length using utf-8 since AllowInput is called for single bytes instead of full characters if (string.len(text .. newCharacter) > maxLength) then surface.PlaySound("common/talk.wav") return true end end function PANEL:Think() local text = self:GetText() local maxLength = ix.config.Get("chatMax", 256) if (text:utf8len() > maxLength) then local newText = text:utf8sub(0, maxLength) self:SetText(newText) self:SetCaretPos(newText:utf8len()) end end function PANEL:Paint(width, height) derma.SkinFunc("PaintChatboxEntry", self, width, height) end vgui.Register("ixChatboxEntry", PANEL, "DTextEntry") -- chatbox additional command info panel PANEL = {} AccessorFunc(PANEL, "text", "Text", FORCE_STRING) AccessorFunc(PANEL, "padding", "Padding", FORCE_NUMBER) AccessorFunc(PANEL, "backgroundColor", "BackgroundColor") AccessorFunc(PANEL, "textColor", "TextColor") function PANEL:Init() self.text = "" self.padding = SScaleMin(4 / 3) self.currentWidth = 0 self.currentMargin = 0 self.backgroundColor = ix.config.Get("color") self.textColor = color_white self:SetWide(0) self:DockMargin(0, 0, 0, 0) end function PANEL:SetText(text) self:SetVisible(true) if (!isstring(text) or text == "") then self:CreateAnimation(animationTime, { index = 9, easing = "outQuint", target = { currentWidth = 0, currentMargin = 0 }, Think = function(animation, panel) panel:SetWide(panel.currentWidth) panel:DockMargin(0, 0, panel.currentMargin, 0) end, OnComplete = function(animation, panel) panel:SetVisible(false) self.text = "" end }) else text = tostring(text) surface.SetFont("ixChatFont") local textWidth = surface.GetTextSize(text) self:CreateAnimation(animationTime, { index = 9, easing = "outQuint", target = { currentWidth = textWidth + self.padding * 2, currentMargin = SScaleMin(4 / 3) }, Think = function(animation, panel) panel:SetWide(panel.currentWidth) panel:DockMargin(0, 0, panel.currentMargin, 0) end, }) self.text = text end end function PANEL:Paint(width, height) derma.SkinFunc("DrawChatboxPrefixBox", self, width, height) surface.SetFont("ixChatFont") local textWidth, textHeight = surface.GetTextSize(self.text) surface.SetTextColor(self.textColor) surface.SetTextPos(width * 0.5 - textWidth * 0.5, height * 0.5 - textHeight * 0.5) surface.DrawText(self.text) end vgui.Register("ixChatboxPrefix", PANEL, "Panel") -- chatbox command preview panel PANEL = {} DEFINE_BASECLASS("Panel") AccessorFunc(PANEL, "targetHeight", "TargetHeight", FORCE_NUMBER) AccessorFunc(PANEL, "command", "Command", FORCE_STRING) function PANEL:Init() self:SetTall(0) self:SetVisible(false, true) self.height = 0 self.targetHeight = 16 self.margin = 0 self.command = "" end function PANEL:SetCommand(command) -- if we're setting it to an empty command, then we'll hold the reference to the old command table to render it for the -- fade out animation if (command == "") then self.command = "" ix.chat.currentCommand = "" return end local commandTable = ix.command.list[command] if (!commandTable) then return end self.command = command self.commandTable = commandTable self.arguments = {} ix.chat.currentCommand = command:lower() end function PANEL:UpdateArguments(text) if (self.command == "") then ix.chat.currentArguments = {} return end local commandName = text:match("(/(%w+)%s)") or self.command -- we could be using a chat class prefix and not a proper command local givenArguments = ix.command.ExtractArgs(text:utf8sub(commandName:utf8len())) local commandArguments = self.commandTable.arguments or {} local arguments = {} -- we want to concat any text types so they show up as one argument at the end of the list, this is so the argument -- highlighting is accurate since ExtractArgs will not account because it has no type context for k, v in ipairs(givenArguments) do if (k == #commandArguments) then arguments[#arguments + 1] = table.concat(givenArguments, " ", k) break end arguments[#arguments + 1] = v end self.arguments = arguments ix.chat.currentArguments = table.Copy(arguments) end -- returns the target SetVisible value function PANEL:IsOpen() return self.bOpen end function PANEL:SetVisible(bValue, bForce) if (bForce) then BaseClass.SetVisible(self, bValue) return end BaseClass.SetVisible(self, true) -- make sure this panel is visible during animation self.bOpen = bValue self:CreateAnimation(animationTime * 0.5, { index = 5, target = { height = bValue and self.targetHeight or 0, margin = bValue and 4 or 0 }, easing = "outQuint", Think = function(animation, panel) panel:SetTall(math.ceil(panel.height)) panel:DockMargin(4, 0, 4, math.ceil(panel.margin)) end, OnComplete = function(animation, panel) BaseClass.SetVisible(panel, bValue) end }) end function PANEL:Paint(width, height) local command = self.commandTable if (!command) then return end local color = ix.config.Get("color") surface.SetFont("ixChatFont") -- command name local x = derma.SkinFunc("DrawChatboxPreviewBox", 0, 0, "/" .. command.name) + 6 -- command arguments if (istable(command.arguments)) then for k, v in ipairs(command.arguments) do local bOptional = bit.band(v, ix.type.optional) > 0 local type = bOptional and bit.bxor(v, ix.type.optional) or v x = x + derma.SkinFunc( "DrawChatboxPreviewBox", x, 0, -- draw text in format of or [name: type] if it's optional string.format(bOptional and "[%s: %s]" or "<%s: %s>", command.argumentNames[k], ix.type[type]), -- fill in the color for arguments that are before the one the user is currently typing, otherwise draw a faded -- color instead (optional arguments will not have any background color unless it's been filled out by user) (k <= #self.arguments) and color or (bOptional and Color(0, 0, 0, 66) or ColorAlpha(color, 100)) ) + 6 end end end vgui.Register("ixChatboxPreview", PANEL, "Panel") -- chatbox autocomplete panel -- holds and displays similar commands based on the textentry PANEL = {} DEFINE_BASECLASS("Panel") AccessorFunc(PANEL, "maxEntries", "MaxEntries", FORCE_NUMBER) function PANEL:Init() self:SetVisible(false, true) self:SetMouseInputEnabled(true) self.maxEntries = 20 self.currentAlpha = 0 self.commandIndex = 0 -- currently selected entry in command list self.commands = {} self.commandPanels = {} end function PANEL:GetCommands() return self.commands end function PANEL:IsOpen() return self.bOpen end function PANEL:SetVisible(bValue, bForce) if (bForce) then BaseClass.SetVisible(self, bValue) return end BaseClass.SetVisible(self, true) -- make sure this panel is visible during animation self.bOpen = bValue self:CreateAnimation(animationTime, { index = 6, target = { currentAlpha = bValue and 255 or 0 }, easing = "outQuint", Think = function(animation, panel) panel:SetAlpha(math.ceil(panel.currentAlpha)) end, OnComplete = function(animation, panel) BaseClass.SetVisible(panel, bValue) if (!bValue) then self.commands = {} end end }) end function PANEL:Update(text) local commands = ix.command.FindAll(text, true, true, true) self.commandIndex = 0 -- reset the command index because the command list could be different self.commands = {} for _, v in ipairs(self.commandPanels) do v:Remove() end self.commandPanels = {} -- manually loop over the found commands so we can ignore commands the user doesn't have access to local i = 1 local bSelected -- just to make sure we don't reset it during the loop for whatever reason for _, v in ipairs(commands) do -- @todo chat classes aren't checked since they're done through the class's OnCanSay callback if (v.OnCheckAccess and !v:OnCheckAccess(LocalPlayer())) then continue end local panel = self:Add("ixChatboxAutocompleteEntry") panel:SetCommand(v) if (!bSelected and text:utf8lower():utf8sub(1, v.uniqueID:utf8len()) == v.uniqueID) then panel:SetHighlighted(true) self.commandIndex = i bSelected = true end self.commandPanels[i] = panel self.commands[i] = v if (i == self.maxEntries) then break end i = i + 1 end end -- selects the next entry in the autocomplete if possible and returns the text that should replace the textentry function PANEL:SelectNext() -- wrap back to beginning if we're past the end if (self.commandIndex == #self.commands) then self.commandIndex = 1 else self.commandIndex = self.commandIndex + 1 end for k, v in ipairs(self.commandPanels) do if (k == self.commandIndex) then v:SetHighlighted(true) self:ScrollToChild(v) else v:SetHighlighted(false) end end return "/" .. self.commands[self.commandIndex].uniqueID end function PANEL:Paint(width, height) ix.util.DrawBlur(self) surface.SetDrawColor(0, 0, 0, 200) surface.DrawRect(0, 0, width, height) end vgui.Register("ixChatboxAutocomplete", PANEL, "DScrollPanel") -- autocomplete entry PANEL = {} AccessorFunc(PANEL, "bSelected", "Highlighted", FORCE_BOOL) function PANEL:Init() self:Dock(TOP) self.name = self:Add("DLabel") self.name:Dock(TOP) self.name:DockMargin(SScaleMin(4 / 3), SScaleMin(4 / 3), 0, 0) self.name:SetContentAlignment(4) self.name:SetFont("ixChatFont") self.name:SetTextColor(ix.config.Get("color")) self.name:SetExpensiveShadow(1, color_black) self.description = self:Add("DLabel") self.description:Dock(BOTTOM) self.description:DockMargin(SScaleMin(4 / 3), SScaleMin(4 / 3), 0, SScaleMin(4 / 3)) self.description:SetContentAlignment(4) self.description:SetFont("ixChatFont") self.description:SetTextColor(color_white) self.description:SetExpensiveShadow(1, color_black) self.highlightAlpha = 0 end function PANEL:SetHighlighted(bValue) self:CreateAnimation(animationTime * 2, { index = 7, target = {highlightAlpha = bValue and 1 or 0}, easing = "outQuint" }) self.bHighlighted = true end function PANEL:SetCommand(command) local description = command:GetDescription() self.name:SetText("/" .. command.name) if (description and description != "") then self.description:SetText(command:GetDescription()) else self.description:SetVisible(false) end self:SizeToContents() self.command = command end function PANEL:SizeToContents() local bDescriptionVisible = self.description:IsVisible() local _, height = self.name:GetContentSize() self.name:SetTall(height) if (bDescriptionVisible) then _, height = self.description:GetContentSize() self.description:SetTall(height) else self.description:SetTall(0) end self:SetTall(self.name:GetTall() + self.description:GetTall() + (bDescriptionVisible and 12 or 8)) end function PANEL:Paint(width, height) derma.SkinFunc("PaintChatboxAutocompleteEntry", self, width, height) end vgui.Register("ixChatboxAutocompleteEntry", PANEL, "Panel") -- main chatbox panel -- this contains the text entry, tab sheets, and callbacks for other panel events PANEL = {} AccessorFunc(PANEL, "bActive", "Active", FORCE_BOOL) function PANEL:Init() ix.gui.chat = self self:SetSize(self:GetDefaultSize()) self:SetPos(self:GetDefaultPosition()) local entryPanel = self:Add("Panel") entryPanel:SetZPos(1) entryPanel:Dock(BOTTOM) entryPanel:DockMargin(SScaleMin(4 / 3), 0, SScaleMin(4 / 3), SScaleMin(4 / 3)) self.entry = entryPanel:Add("ixChatboxEntry") self.entry:Dock(FILL) self.entry.OnValueChange = ix.util.Bind(self, self.OnTextChanged) self.entry.OnKeyCodeTyped = ix.util.Bind(self, self.OnKeyCodeTyped) self.entry.OnEnter = ix.util.Bind(self, self.OnMessageSent) self.prefix = entryPanel:Add("ixChatboxPrefix") self.prefix:Dock(LEFT) self.preview = self:Add("ixChatboxPreview") self.preview:SetZPos(2) -- ensure the preview is docked above the text entry self.preview:Dock(BOTTOM) self.preview:SetTargetHeight(self.entry:GetTall()) self.tabs = self:Add("ixChatboxTabs") self.tabs:Dock(FILL) self.tabs.OnTabChanged = ix.util.Bind(self, self.OnTabChanged) self.autocomplete = self.tabs:Add("ixChatboxAutocomplete") self.autocomplete:Dock(FILL) -- top margin is 3 to account for tab 1px border self.autocomplete:DockMargin(SScaleMin(4 / 3), SScaleMin(3 / 3), SScaleMin(4 / 3), SScaleMin(4 / 3)) self.autocomplete:SetZPos(3) self.alpha = 0 self:SetActive(false) -- luacheck: globals chat chat.GetChatBoxPos = function() return self:GetPos() end chat.GetChatBoxSize = function() return self:GetSize() end end function PANEL:GetDefaultSize() return ScrW() * 0.4, ScrH() * 0.375 end function PANEL:GetDefaultPosition() return chatBorder, ScrH() - self:GetTall() - chatBorder end DEFINE_BASECLASS("Panel") function PANEL:SetAlpha(amount, duration) self:CreateAnimation(duration or animationTime, { index = 1, target = {alpha = amount}, easing = "outQuint", Think = function(animation, panel) BaseClass.SetAlpha(panel, panel.alpha) end }) end function PANEL:SizingInBounds() local screenX, screenY = self:LocalToScreen(0, 0) local mouseX, mouseY = gui.MousePos() return mouseX > screenX + self:GetWide() - sizingBorder and mouseY > screenY + self:GetTall() - sizingBorder end function PANEL:DraggingInBounds() local _, screenY = self:LocalToScreen(0, 0) local mouseY = gui.MouseY() return mouseY > screenY and mouseY < screenY + self.tabs.buttons:GetTall() end function PANEL:SetActive(bActive) if (bActive) then self:SetAlpha(255) self:MakePopup() self.entry:RequestFocus() input.SetCursorPos(self:LocalToScreen(-1, -1)) hook.Run("StartChat") self.prefix:SetText(hook.Run("GetChatPrefixInfo", "")) else -- make sure we aren't still sizing/dragging anything if (self.bSizing or self.DragOffset) then self:OnMouseReleased(MOUSE_LEFT) end self:SetAlpha(0) self:SetMouseInputEnabled(false) self:SetKeyboardInputEnabled(false) self.autocomplete:SetVisible(false) self.preview:SetVisible(false) self.entry:SetText("") self.preview:SetCommand("") self.prefix:SetText(hook.Run("GetChatPrefixInfo", "")) CloseDermaMenus() gui.EnableScreenClicker(false) hook.Run("FinishChat") end local tab = self.tabs:GetActiveTab() if (tab) then -- we'll scroll to bottom even if we're opening since the SetVisible for the textentry will shift things a bit tab:ScrollToBottom() end self.bActive = tobool(bActive) end function PANEL:SetupTabs(tabs) if (!tabs or table.IsEmpty(tabs)) then self.tabs:AddTab(L("chat"), {}) self.tabs:SetActiveTab(L("chat")) return end for id, filter in pairs(tabs) do self.tabs:AddTab(id, filter) end self.tabs:SetActiveTab(next(tabs)) end function PANEL:SetupPosition(info) local x, y, width, height if (!istable(info)) then x, y = self:GetDefaultPosition() width, height = self:GetDefaultSize() else -- screen size may have changed so we'll need to clamp the values width = math.Clamp(info[3], 32, ScrW() - chatBorder * 2) height = math.Clamp(info[4], 32, ScrH() - chatBorder * 2) x = math.Clamp(info[1], 0, ScrW() - width) y = math.Clamp(info[2], 0, ScrH() - height) end self:SetSize(width, height) self:SetPos(x, y) PLUGIN:SavePosition() end function PANEL:OnMousePressed(key) if (key == MOUSE_RIGHT) then local menu = DermaMenu() menu:AddOption(L("chatNewTab"), function() if (IsValid(ix.gui.chatTabCustomize)) then ix.gui.chatTabCustomize:Remove() end local panel = vgui.Create("ixChatboxTabCustomize") panel.OnTabCreated = ix.util.Bind(self, self.OnTabCreated) end) menu:AddOption(L("chatMarkRead"), function() for _, v in pairs(self.tabs:GetTabs()) do v:GetButton():SetUnread(false) end end) menu:AddSpacer() menu:AddOption(L("chatReset"), function() local x, y = self:GetDefaultPosition() local width, height = self:GetDefaultSize() self:SetSize(width, height) self:SetPos(x, y) ix.option.Set("chatPosition", "") hook.Run("ChatboxPositionChanged", x, y, width, height) end) menu:AddOption(L("chatResetTabs"), function() for id, _ in pairs(self.tabs:GetTabs()) do self.tabs:RemoveTab(id) end ix.option.Set("chatTabs", "") end) menu:Open() menu:MakePopup() return end if (key != MOUSE_LEFT) then return end -- capture the mouse if we're in bounds for sizing this panel if (self:SizingInBounds()) then self.bSizing = true self:MouseCapture(true) elseif (self:DraggingInBounds()) then local mouseX, mouseY = self:ScreenToLocal(gui.MousePos()) -- mouse offset relative to the panel self.DragOffset = {mouseX, mouseY} self:MouseCapture(true) end end function PANEL:OnMouseReleased() self:MouseCapture(false) self:SetCursor("arrow") -- save new position/size if we were dragging/resizing if (self.bSizing or self.DragOffset) then PLUGIN:SavePosition() self.bSizing = nil self.DragOffset = nil -- resize chat messages to fit new width self:InvalidateChildren(true) local x, y = self:GetPos() local width, height = self:GetSize() hook.Run("ChatboxPositionChanged", x, y, width, height) end end function PANEL:Think() if (!self.bActive) then return end if (gui.IsGameUIVisible()) then self:SetActive(false) gui.HideGameUI() return end local mouseX = math.Clamp(gui.MouseX(), 0, ScrW()) local mouseY = math.Clamp(gui.MouseY(), 0, ScrH()) if (self.bSizing) then local x, y = self:GetPos() local width = math.Clamp(mouseX - x, chatBorder, ScrW() - chatBorder * 2) local height = math.Clamp(mouseY - y, chatBorder, ScrH() - chatBorder * 2) self:SetSize(width, height) self:SetCursor("sizenwse") elseif (self.DragOffset) then local x = math.Clamp(mouseX - self.DragOffset[1], 0, ScrW() - self:GetWide()) local y = math.Clamp(mouseY - self.DragOffset[2], 0, ScrH() - self:GetTall()) self:SetPos(x, y) elseif (self:SizingInBounds()) then self:SetCursor("sizenwse") elseif (self:DraggingInBounds()) then -- we have to set the cursor on the list panel since that's the actual hovered panel self.tabs.buttons:SetCursor("sizeall") else self:SetCursor("arrow") end end function PANEL:Paint(width, height) local tab = self.tabs:GetActiveTab() local alpha = self:GetAlpha() derma.SkinFunc("PaintChatboxBackground", self, width, height) if (tab) then -- manually paint active tab since messages handle their own alpha lifetime surface.SetAlphaMultiplier(1) tab:PaintManual() surface.SetAlphaMultiplier(alpha / 255) end if (alpha > 0) then hook.Run("PostChatboxDraw", width, height, self:GetAlpha()) end end -- get the command of the current chat class in the textentry if possible function PANEL:GetTextEntryChatClass(text) text = text or self.entry:GetText() local chatType = ix.chat.Parse(LocalPlayer(), text, true) if (chatType and chatType != "ic") then -- OOC is the only one with two slashes as its prefix, so we'll make a special case for it here if (chatType == "ooc") then return "ooc" end local class = ix.chat.classes[chatType] if (istable(class.prefix)) then for _, v in ipairs(class.prefix) do if (v:utf8sub(1, 1) == "/") then return v:utf8sub(2):utf8lower() end end elseif (class.prefix:utf8sub(1, 1) == "/") then return class.prefix:utf8sub(2):utf8lower() end end end -- chatbox panel hooks -- called when the textentry value changes function PANEL:OnTextChanged(text) hook.Run("ChatTextChanged", text) local preview = self.preview local autocomplete = self.autocomplete local chatClassCommand = self:GetTextEntryChatClass(text) self.prefix:SetText(hook.Run("GetChatPrefixInfo", text)) if (chatClassCommand) then preview:SetCommand(chatClassCommand) preview:SetVisible(true) preview:UpdateArguments(text) autocomplete:SetVisible(false) return end local start, _, command = text:find("(/(%w+)%s)") command = ix.command.list[tostring(command):utf8sub(2, tostring(command):utf8len() - 1):utf8lower()] -- update preview if we've found a command if (start == 1 and command) then preview:SetCommand(command.uniqueID) preview:SetVisible(true) preview:UpdateArguments(text) -- we don't need the autocomplete because we have a command already typed out autocomplete:SetVisible(false) return -- if there's a slash then we're probably going to be (or are currently) typing out a command elseif (text:utf8sub(1, 1) == "/") then command = text:match("(/(%w+))") or "/" preview:SetVisible(false) -- we don't have a valid command yet autocomplete:Update(command:utf8sub(2)) autocomplete:SetVisible(true) return end if (preview:GetCommand() != "") then preview:SetCommand("") preview:SetVisible(false) end if (autocomplete:IsVisible()) then autocomplete:SetVisible(false) end end DEFINE_BASECLASS("DTextEntry") function PANEL:OnKeyCodeTyped(key) if (key == KEY_TAB) then if (self.autocomplete:IsOpen() and #self.autocomplete:GetCommands() > 0) then local newText = self.autocomplete:SelectNext() self.entry:SetText(newText) self.entry:SetCaretPos(newText:utf8len()) end return true end return BaseClass.OnKeyCodeTyped(self.entry, key) end -- called when player types something and presses enter in the textentry function PANEL:OnMessageSent() local text = self.entry:GetText() if (text:find("%S")) then local lastEntry = ix.chat.history[#ix.chat.history] -- only add line to textentry history if it isn't the same message if (lastEntry != text) then if (#ix.chat.history >= 20) then table.remove(ix.chat.history, 1) end ix.chat.history[#ix.chat.history + 1] = text end net.Start("ixChatMessage") net.WriteString(text) net.SendToServer() end self:SetActive(false) -- textentry is set to "" in SetActive end -- called when the player changes the currently active tab function PANEL:OnTabChanged(panel) panel:InvalidateLayout(true) panel:ScrollToBottom() end -- called when the player creates a new tab function PANEL:OnTabCreated(id, filter) self.tabs:AddTab(id, filter) PLUGIN:SaveTabs() end -- called when the player updates a tab's filter function PANEL:OnTabUpdated(id, filter, newID) local tab = self.tabs:GetTabs()[id] if (!tab) then return end tab:SetFilter(filter) self.tabs:RenameTab(id, newID) PLUGIN:SaveTabs() end -- called when a tab's button was right-clicked function PANEL:OnTabRightClick(button, tab, id) local menu = DermaMenu() menu:AddOption(L("chatCustomize"), function() if (IsValid(ix.gui.chatTabCustomize)) then ix.gui.chatTabCustomize:Remove() end local panel = vgui.Create("ixChatboxTabCustomize") panel:PopulateFromTab(id, tab:GetFilter()) panel.OnTabUpdated = ix.util.Bind(self, self.OnTabUpdated) end) menu:AddSpacer() menu:AddOption(L("chatCloseTab"), function() self.tabs:RemoveTab(id) PLUGIN:SaveTabs() end) menu:Open() menu:MakePopup() -- HACK: mouse input doesn't work when created immediately after opening chatbox end -- called when a message needs to be added to applicable tabs function PANEL:AddMessage(...) local class = CHAT_CLASS and CHAT_CLASS.uniqueID or "notice" local activeTab = self.tabs:GetActiveTab() -- track whether or not the message was filtered out in the active tab local bShown = false if (activeTab and !activeTab:GetFilter()[class]) then activeTab:AddLine({...}, true) bShown = true end for _, v in pairs(self.tabs:GetTabs()) do if (v:GetID() == activeTab:GetID()) then continue -- we already added it to the active tab end if (!v:GetFilter()[class]) then v:AddLine({...}, true) -- mark other tabs as unread if we didn't show the message in the active tab if (!bShown) then v:GetButton():SetUnread(true) end end end if (bShown) then chat.PlaySound() end end vgui.Register("ixChatbox", PANEL, "EditablePanel")