-- VirtualKeyboard.lua -- -- Written by Kip Turner, copyright ROBLOX 2016 -- local CoreGui = game:GetService('CoreGui') local RunService = game:GetService('RunService') local UserInputService = game:GetService('UserInputService') local GuiService = game:GetService('GuiService') local HttpService = game:GetService('HttpService') local ContextActionService = game:GetService('ContextActionService') local PlayersService = game:GetService('Players') local TextService = game:GetService('TextService') local RobloxGui = CoreGui:WaitForChild("RobloxGui") local Util = require(RobloxGui.Modules.Settings.Utility) local vrKeyboardSuccess, vrKeyboardFlagValue = pcall(function() return settings():GetFFlag("UseVRKeyboardInLua") end) local useVRKeyboard = (vrKeyboardSuccess and vrKeyboardFlagValue == true) local BACKGROUND_OPACITY = 0.3 local NORMAL_KEY_COLOR = Color3.new(49/255,49/255,49/255) local HOVER_KEY_COLOR = Color3.new(49/255,49/255,49/255) local PRESSED_KEY_COLOR = Color3.new(0,162/255,1) local SET_KEY_COLOR = Color3.new(0,162/255,1) local KEY_TEXT_COLOR = Color3.new(1,1,1) ---------------------------------------- KEYBOARD LAYOUT -------------------------------------- local MINIMAL_KEYBOARD_LAYOUT = HttpService:JSONDecode([==[ [ [ { "a": 7, "w": 0.8 }, "*", "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", { "w": 1.8 }, "Delete" ], [ { "w": 1.6 }, "Caps", "A", "S", "D", "F", "G", "H", "J", "K", "L", "?", { "h": 2, "w2": 2.4, "h2": 1, "x2": -1.4, "y2": 1 }, "Enter" ], [ { "w": 2.2 }, "Shift", "Z", "X", "C", "V", "B", "N", "M", "." ], [ { "w": 2.2 }, "123/sym", { "w": 8 }, "", { "w": 2.4 }, "" ] ] ]==]) local MINIMAL_KEYBOARD_LAYOUT_SYMBOLS = HttpService:JSONDecode([==[ [ [ { "a": 7, "w": 0.8 }, "*", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", { "w": 1.8 }, "Delete" ], [ { "w": 1.6 }, "!", "@", "#", "$", "%", "^", "&", "(", ")", "=", "?", { "h": 2, "w2": 2.4, "h2": 1, "x2": -1.4, "y2": 1 }, "Enter" ], [ { "w": 1.2 }, "/", "-", "+", "_", ":", ";", "'", "\"", ",", "." ], [ { "w": 2.2 }, "abc", { "w": 8 }, "", { "w": 2.4 }, "" ] ] ]==]) ---------------------------------------- END KEYBOARD LAYOUT -------------------------------------- local function tokenizeString(str, tokenChar) local words = {} for word in string.gmatch(str, '([^' .. tokenChar .. ']+)') do table.insert(words, word) end return words end local function ConvertFontSizeEnumToInt(fontSizeEnum) local result = string.match(fontSizeEnum.Name, '%d+') return (result and tostring(result)) or 12 end -- RayPlaneIntersection -- http://www.siggraph.org/education/materials/HyperGraph/raytrace/rayplane_intersection.htm local function RayPlaneIntersection(ray, planeNormal, pointOnPlane) planeNormal = planeNormal.unit ray = ray.Unit -- compute Pn (dot) Rd = Vd and check if Vd == 0 then we know ray is parallel to plane local Vd = planeNormal:Dot(ray.Direction) -- could fuzzy equals this a little bit to account for imprecision or very close angles to zero if Vd == 0 then -- parallel, no intersection return nil end local V0 = planeNormal:Dot(pointOnPlane - ray.Origin) local t = V0 / Vd if t < 0 then --plane is behind ray origin, and thus there is no intersection return nil end return ray.Origin + ray.Direction * t end function Clamp(low, high, input) return math.max(low, math.min(high, input)) end -- No rotation as of yet local function PointInGuiObject(object, x, y) local minPt = object.AbsolutePosition local maxPt = object.AbsolutePosition + object.AbsoluteSize if minPt.X <= x and maxPt.X >= x and minPt.Y <= y and maxPt.Y >= y then return true end return false end local function ExtendedInstance(instance) local this = {} do local mt = { __index = function (t, k) return instance[k] end; __newindex = function (t, k, v) instance[k] = v end; } setmetatable(this, mt) end return this end local function IsVoiceToTextEnabled() return false end local function CreateVRButton(instance) local newButton = ExtendedInstance(instance) rawset(newButton, "OnEnter", function(self) end) rawset(newButton, "OnLeave", function(self) end) rawset(newButton, "OnDown", function(self) end) rawset(newButton, "OnUp", function(self) end) rawset(newButton, "ContainsPoint", function(self, x, y) return PointInGuiObject(instance, x, y) end) rawset(newButton, "Update", function(self) end) return newButton end local selectionRing = Util:Create'ImageLabel' { Name = 'SelectionRing'; Size = UDim2.new(1, -6, 1, -6); Position = UDim2.new(0, 4, 0, 3); Image = 'rbxasset://textures/ui/menu/buttonHover.png'; ScaleType = Enum.ScaleType.Slice; SliceCenter = Rect.new(94/2, 94/2, 94/2, 94/2); BackgroundTransparency = 1; } local KEY_ICONS = { [""] = {Asset = "rbxasset://textures/ui/Keyboard/mic_icon.png", AspectRatio = 0.615}; } local function CreateKeyboardKey(keyboard, layoutData, keyData) local isSpecialShapeKey = layoutData['width2'] and layoutData['height2'] and layoutData['x2'] and layoutData['y2'] local newKeyElement = Util:Create'ImageButton' { Name = keyData[1]; Position = UDim2.new(layoutData['x'], 0, layoutData['y'], 0); Size = UDim2.new(layoutData['width'], 0, layoutData['height'], 0); BorderSizePixel = 0; Image = ""; BackgroundTransparency = 1; ZIndex = 1; } local keyText = Util:Create'TextLabel' { Name = "KeyText"; Text = keyData[#keyData]; Position = UDim2.new(0, -10, 0, -10); Size = UDim2.new(1, 0, 1, 0); Font = Enum.Font.SourceSansBold; FontSize = Enum.FontSize.Size96; TextColor3 = KEY_TEXT_COLOR; BackgroundTransparency = 1; Selectable = true; ZIndex = 2; Parent = newKeyElement; } local backgroundImage = Util:Create'Frame' { Name = 'KeyBackground'; Size = UDim2.new(1,-10,1,-10); Position = UDim2.new(0,-5,0,-5); BackgroundColor3 = NORMAL_KEY_COLOR; BackgroundTransparency = BACKGROUND_OPACITY; BorderSizePixel = 0; Parent = newKeyElement; } local selectionObject = Util:Create'ImageLabel' { Name = 'SelectionObject'; Size = UDim2.new(1,0,1,0); BackgroundTransparency = 1; Image = "rbxasset://textures/ui/Keyboard/key_selection_9slice.png"; ImageTransparency = 0; ScaleType = Enum.ScaleType.Slice; SliceCenter = Rect.new(12,12,52,52); BorderSizePixel = 0; } newKeyElement.SelectionImageObject = Util:Create'ImageLabel' { Visible = false; } -- Special silly enter key nonsense local secondBackgroundImage = nil local specialSelectionObject, specialSelectionObject2, specialSelectionObject3 = nil, nil, nil if isSpecialShapeKey then secondBackgroundImage = Util:Create'ImageButton' { Name = 'KeyBackground'; Position = UDim2.new(layoutData['x2'] / layoutData['width'], -5, layoutData['y2'] / layoutData['height'], -5); Size = UDim2.new(layoutData['width2'] / layoutData['width'], 0, layoutData['height2'] / layoutData['height'], -10); BackgroundColor3 = NORMAL_KEY_COLOR; BackgroundTransparency = BACKGROUND_OPACITY; BorderSizePixel = 0; AutoButtonColor = false; Parent = newKeyElement; } if layoutData['x2'] <= 0 then keyText.Size = secondBackgroundImage.Size - UDim2.new(0,10,0,0) keyText.Position = secondBackgroundImage.Position secondBackgroundImage.Size = secondBackgroundImage.Size - UDim2.new(1,0,0,0) end do specialSelectionObject = Util:Create'Frame' { Name = 'SpecialSelectionObject'; Size = UDim2.new(1,0,0.5,0); Position = UDim2.new(0,0,0.5,0); BackgroundTransparency = 1; ClipsDescendants = true; Util:Create'ImageLabel' { Name = 'Borders'; Position = UDim2.new(-1,0,-1,0); Size = UDim2.new(2,0,2,0); BackgroundTransparency = 1; Image = "rbxasset://textures/ui/Keyboard/key_selection_9slice.png"; ImageTransparency = 0; ScaleType = Enum.ScaleType.Slice; SliceCenter = Rect.new(12,12,52,52); }; } specialSelectionObject2 = specialSelectionObject:Clone() specialSelectionObject2.Size = UDim2.new(1,0,0.5,5) specialSelectionObject2.Position = UDim2.new(0,0,0,0) specialSelectionObject2.Borders.Size = UDim2.new(1,0,1,30) specialSelectionObject2.Borders.Position = UDim2.new(0,0,0,0) specialSelectionObject3 = specialSelectionObject:Clone() specialSelectionObject3.Size = UDim2.new(1,5,1,0) specialSelectionObject3.Position = UDim2.new(0,0,0,0) specialSelectionObject3.Borders.Size = UDim2.new(1,30,1,0) specialSelectionObject3.Borders.Position = UDim2.new(0,0,0,0) end -- End of nonsense end local newKey = CreateVRButton(newKeyElement) local hovering = false local pressed = false local isAlpha = #keyData == 1 and type(keyData[1]) == 'string' and #keyData[1] == 1 and string.byte(keyData[1]) >= string.byte("A") and string.byte(keyData[1]) <= string.byte("z") local icon = nil if keyData[1] and KEY_ICONS[keyData[1]] then keyText.Visible = false icon = Util:Create'ImageLabel' { Name = 'KeyIcon'; Size = UDim2.new(KEY_ICONS[keyData[1]].AspectRatio, -20, 1, -20); SizeConstraint = Enum.SizeConstraint.RelativeYY; BackgroundTransparency = 1; Image = KEY_ICONS[keyData[1]].Asset; Parent = backgroundImage; } local function onChanged(prop) if prop == 'AbsoluteSize' then icon.Position = UDim2.new(0.5,-icon.AbsoluteSize.X/2,0.5,-icon.AbsoluteSize.Y/2); end end icon.Changed:connect(onChanged) onChanged('AbsoluteSize') end local function isEnabled() if newKey:GetCurrentKeyValue() == "" then return IsVoiceToTextEnabled() end return true end local function onClicked() local keyValue = nil local currentKeySetting = newKey:GetCurrentKeyValue() if currentKeySetting == 'Shift' then keyboard:SetShift(not keyboard:GetShift()) elseif currentKeySetting == 'Caps' then keyboard:SetCaps(not keyboard:GetCaps()) elseif currentKeySetting == 'Enter' then keyboard:SubmitText(true, true) elseif currentKeySetting == 'Delete' then keyboard:BackspaceAtCursor() elseif currentKeySetting == "123/sym" then keyboard:SetCurrentKeyset(2) elseif currentKeySetting == "abc" then keyboard:SetCurrentKeyset(1) elseif currentKeySetting == "" then keyboard:SetVoiceMode(true) elseif currentKeySetting == 'Tab' then keyValue = '\t' else keyValue = currentKeySetting end if keyValue ~= nil then keyboard:SubmitCharacter(keyValue, isAlpha) end end local function setKeyColor(newColor, hovering) backgroundImage.BackgroundColor3 = newColor if secondBackgroundImage then secondBackgroundImage.BackgroundColor3 = newColor end if isSpecialShapeKey then specialSelectionObject.Parent = hovering and backgroundImage or nil specialSelectionObject2.Parent = hovering and backgroundImage or nil specialSelectionObject3.Parent = hovering and secondBackgroundImage or nil else selectionObject.Parent = hovering and backgroundImage or nil end end local function update() local currentKey = newKey:GetCurrentKeyValue() if pressed then setKeyColor(PRESSED_KEY_COLOR, false) elseif hovering then setKeyColor(HOVER_KEY_COLOR, true) elseif currentKey == 'Caps' and keyboard:GetCaps() then setKeyColor(SET_KEY_COLOR, false) elseif currentKey == 'Shift' and keyboard:GetShift() then setKeyColor(SET_KEY_COLOR, false) elseif currentKey == 'abc' then setKeyColor(SET_KEY_COLOR, false) else setKeyColor(NORMAL_KEY_COLOR, false) end if icon then icon.ImageTransparency = isEnabled() and 0 or 0.5 end keyText.Text = newKey:GetCurrentKeyValue() end local hoveringGuiElements = {} rawset(newKey, "OnEnter", function(self) hovering = true update() end) rawset(newKey, "OnLeave", function(self) if not next(hoveringGuiElements) then hovering = false pressed = false update() end end) rawset(newKey, "OnDown", function(self) pressed = true update() end) rawset(newKey, "OnUp", function(self) pressed = false update() end) rawset(newKey, "GetCurrentKeyValue", function(self) local shiftEnabled = keyboard:GetShift() local capsEnabled = keyboard:GetCaps() if isAlpha then if capsEnabled and shiftEnabled then return string.lower(keyData[#keyData]) elseif capsEnabled or shiftEnabled then return keyData[1] else return string.lower(keyData[#keyData]) end end if shiftEnabled then return keyData[1] end return keyData[#keyData] end) rawset(newKey, "ContainsPoint", function(self, x, y) return PointInGuiObject(backgroundImage, x, y) or (secondBackgroundImage and PointInGuiObject(secondBackgroundImage, x, y)) end) rawset(newKey, "Update", function(self) update() end) rawset(newKey, "GetInstance", function(self) return newKeyElement end) newKeyElement.MouseButton1Down:connect(function() newKey:OnDown() end) newKeyElement.MouseButton1Up:connect(function() newKey:OnUp() end) newKeyElement.SelectionGained:connect(function() hoveringGuiElements[newKeyElement] = true newKey:OnEnter() end) newKeyElement.SelectionLost:connect(function() hoveringGuiElements[newKeyElement] = nil newKey:OnLeave() end) newKeyElement.MouseButton1Click:connect(function() onClicked() end) if secondBackgroundImage then secondBackgroundImage.MouseButton1Click:connect(onClicked) secondBackgroundImage.MouseButton1Down:connect(function() newKey:OnDown() end) secondBackgroundImage.MouseButton1Up:connect(function() newKey:OnUp() end) secondBackgroundImage.SelectionGained:connect(function() hoveringGuiElements[secondBackgroundImage] = true newKey:OnEnter() end) secondBackgroundImage.SelectionLost:connect(function() hoveringGuiElements[secondBackgroundImage] = nil newKey:OnLeave() end) end update() return newKey end local function ConstructKeyboardUI(keyboardLayoutDefinitions) local Panel3D = require(RobloxGui.Modules.VR.Panel3D) local panel = Panel3D.Get("Keyboard") panel:SetVisible(false) -- DEBUG setting -- panel:GetGUI().Parent = workspace local buttons = {} local keyboardContainer = Util:Create'Frame' { Name = 'VirtualKeyboard'; Size = UDim2.new(1, 0, 1, 0); Position = UDim2.new(0, 0, 0, 0); BackgroundTransparency = 1; Active = true; Visible = false; }; local textEntryBackground = Util:Create'ImageLabel' { Name = 'TextEntryBackground'; Size = UDim2.new(0.5,0,0.125,0); Position = UDim2.new(0.25,0,0,0); Image = ""; BackgroundTransparency = 0.5; BackgroundColor3 = Color3.new(31/255,31/255,31/255); BorderSizePixel = 0; ClipsDescendants = true; Parent = keyboardContainer; } local textfieldBackground = Util:Create'Frame' { Name = 'TextfieldBackground'; Position = UDim2.new(0,2,0,2); Size = UDim2.new(1, -4, 1, -4); BackgroundTransparency = 0; BackgroundColor3 = Color3.new(209/255,216/255,221/255); BorderSizePixel = 0; Visible = true; Parent = textEntryBackground; }; local textEntryField = Util:Create'TextButton' { Name = "TextEntryField"; Text = ""; Position = UDim2.new(0,4,0,4); Size = UDim2.new(1, -8, 1, -8); Font = Enum.Font.SourceSans; FontSize = Enum.FontSize.Size60; TextXAlignment = Enum.TextXAlignment.Left; BackgroundTransparency = 1; BorderSizePixel = 0; Parent = textfieldBackground; } local textfieldCursor = Util:Create'Frame' { Name = 'TextfieldCursor'; Size = UDim2.new(0, 5, 0.9, 0); Position = UDim2.new(0, 0, 0.05, 0); BackgroundTransparency = 0; BackgroundColor3 = SET_KEY_COLOR; BorderSizePixel = 0; Visible = true; ZIndex = 2; Parent = textEntryField; }; local closeButtonElement = Util:Create'ImageButton' { Name = 'CloseButton'; Size = UDim2.new(0.075,-10,0.198,-10); Position = UDim2.new(0,-5,0,-35); Image = "rbxasset://textures/ui/Keyboard/close_button_background.png"; BackgroundTransparency = 1; AutoButtonColor = false; Parent = keyboardContainer; } do closeButtonElement.SelectionImageObject = Util:Create'ImageLabel' { Name = 'Selection'; Size = UDim2.new(0.9,0,0.9,0); Position = UDim2.new(0.05,0,0.05,0); Image = "rbxasset://textures/ui/Keyboard/close_button_selection.png"; BackgroundTransparency = 1; } Util:Create'ImageLabel' { Name = 'Icon'; Size = UDim2.new(0.5,0,0.5,0); Position = UDim2.new(0.25,0,0.25,0); Image = "rbxasset://textures/ui/Keyboard/close_button_icon.png"; BackgroundTransparency = 1; Parent = closeButtonElement; } end local closeButton = CreateVRButton(closeButtonElement) table.insert(buttons, closeButton) local voiceRecognitionContainer = Util:Create'Frame' { Name = 'VoiceRecognitionContainer'; Size = UDim2.new(1, 0, 0.85, 0); Position = UDim2.new(0, 0, 0.15, 0); BackgroundTransparency = 1; Active = true; Visible = false; Parent = keyboardContainer; }; do local voiceRecognitionBackground1 = Util:Create'Frame' { Name = 'voiceRecognitionBackground1'; Size = UDim2.new(1, 0, 0.75, 0); Position = UDim2.new(0, 0, 0, 0); BackgroundColor3 = Color3.new(0,0,0); BackgroundTransparency = 0.5; BorderSizePixel = 0; Active = true; Parent = voiceRecognitionContainer; }; local voiceRecognitionBackground2 = voiceRecognitionBackground1:Clone() voiceRecognitionBackground2.Size = UDim2.new(0.75, 0, 0.25, 0) voiceRecognitionBackground2.Position = UDim2.new(0, 0, 0.75, 0) voiceRecognitionBackground2.Parent = voiceRecognitionContainer end local voiceDoneButton = CreateVRButton(Util:Create'ImageButton' { Name = 'DoneButton'; Size = UDim2.new(0.25,-5,0.25,-5); Position = UDim2.new(0.75,5,0.75,5); Image = ""; BackgroundTransparency = 0; AutoButtonColor = false; BorderSizePixel = 0; Parent = voiceRecognitionContainer; }) table.insert(buttons, voiceDoneButton) local newKeyboard = ExtendedInstance(keyboardContainer) local keyboardOptions = nil local keysets = {} local capsLockEnabled = false local shiftEnabled = false local textfieldCursorPosition = 0 local closedEvent = Instance.new('BindableEvent') local opened = false local function SetTextFieldCursorPosition(newPosition) textfieldCursorPosition = Clamp(0, #textEntryField.Text, newPosition) if not textEntryField.TextFits then textfieldCursorPosition = #textEntryField.Text end local textSize = TextService:GetTextSize( string.sub(textEntryField.Text, 1, textfieldCursorPosition), ConvertFontSizeEnumToInt(textEntryField.FontSize), textEntryField.Font, textEntryField.AbsoluteSize) textfieldCursor.Position = UDim2.new(0, textSize.x, textfieldCursor.Position.Y.Scale, textfieldCursor.Position.Y.Offset) end local function UpdateTextEntryFieldText(newText) textEntryField.Text = newText SetTextFieldCursorPosition(textfieldCursorPosition) end local buffer = "" local function getBufferText() if keyboardOptions and keyboardOptions.TextBox then return keyboardOptions.TextBox.Text end return buffer end local function setBufferText(newBufferText) if keyboardOptions and keyboardOptions.TextBox then keyboardOptions.TextBox.Text = newBufferText elseif buffer ~= newBufferText then buffer = newBufferText UpdateTextEntryFieldText(buffer) end end local function calculateTextCursorPosition(x, y) x = x - textEntryField.AbsolutePosition.x y = y - textEntryField.AbsolutePosition.y for i = 1, #textEntryField.Text do local textSize = TextService:GetTextSize( string.sub(textEntryField.Text, 1, i), ConvertFontSizeEnumToInt(textEntryField.FontSize), textEntryField.Font, textEntryField.AbsoluteSize) if textSize.x > x then return i - 1 end end return #textEntryField.Text end local currentKeyset = nil rawset(newKeyboard, "ClosedEvent", closedEvent.Event) rawset(newKeyboard, "GetCurrentKeyset", function(self) return keysets[currentKeyset] end) rawset(newKeyboard, "SetCurrentKeyset", function(self, newKeyset) if newKeyset ~= currentKeyset and keysets[newKeyset] ~= nil then if keysets[currentKeyset] and keysets[currentKeyset].container then keysets[currentKeyset].container.Visible = false end currentKeyset = newKeyset if keysets[currentKeyset] and keysets[currentKeyset].container then keysets[currentKeyset].container.Visible = true end end end) rawset(newKeyboard, "SetVoiceMode", function(self, inVoiceMode) inVoiceMode = inVoiceMode and IsVoiceToTextEnabled() local currentKeysetObject = self:GetCurrentKeyset() if currentKeysetObject and currentKeysetObject.container then currentKeysetObject.container.Visible = not inVoiceMode end voiceRecognitionContainer.Visible = inVoiceMode end) rawset(newKeyboard, "GetCaps", function(self) return capsLockEnabled end) rawset(newKeyboard, "SetCaps", function(self, newCaps) capsLockEnabled = newCaps for _, key in pairs(self:GetCurrentKeyset().keys) do key:Update() end end) rawset(newKeyboard, "GetShift", function(self) return shiftEnabled end) rawset(newKeyboard, "SetShift", function(self, newShift) shiftEnabled = newShift for _, key in pairs(self:GetCurrentKeyset().keys) do key:Update() end end) local textChangedConn = nil local panelClosedConn = nil rawset(newKeyboard, "Open", function(self, options) if opened then return end opened = true keyboardOptions = options self:SetCurrentKeyset(1) self:SetVoiceMode(false) keyboardContainer.Visible = true panel:ResizeStuds(5.9, 2.25, 320) local localCF = CFrame.new() if textChangedConn then textChangedConn:disconnect() end textChangedConn = nil if options.TextBox then textChangedConn = options.TextBox.Changed:connect(function(prop) if prop == 'Text' then UpdateTextEntryFieldText(options.TextBox.Text) end end) options.TextBox.FocusLost:connect(function(submitted) self:Close(submitted) end) if options.TextBox.ClearTextOnFocus then setBufferText("") else UpdateTextEntryFieldText(options.TextBox.Text) end local textboxPanel = Panel3D.FindContainerOf(options.TextBox) if textboxPanel then panelClosedConn = Panel3D.OnPanelClosed.Event:connect(function(closedPanelName) if closedPanelName == textboxPanel.name then self:Close(false) end end) --Attach to it if it's in the same space if textboxPanel.panelType == Panel3D.Type.Fixed then local panelCF = textboxPanel.localCF localCF = panelCF * CFrame.new(0, (-textboxPanel.height / 2) - 0.5, 0) * CFrame.Angles(math.rad(30), 0, 0) * CFrame.new(0, (-panel.height / 2) - 0.5, 0) else --Otherwise, best-guess where it should go based on the user's head. local headForwardCF = Panel3D.GetHeadLookXZ(true) localCF = headForwardCF * CFrame.Angles(math.rad(22.5), 0, 0) * CFrame.new(0, -1, 5) end end else setBufferText("") end ContextActionService:BindCoreAction("VirtualKeyboardControllerInput", function(actionName, inputState, inputObject) if inputState == Enum.UserInputState.End then if inputObject.KeyCode == Enum.KeyCode.ButtonL1 then SetTextFieldCursorPosition(textfieldCursorPosition - 1) elseif inputObject.KeyCode == Enum.KeyCode.ButtonR1 then SetTextFieldCursorPosition(textfieldCursorPosition + 1) elseif inputObject.KeyCode == Enum.KeyCode.ButtonX then self:BackspaceAtCursor() elseif inputObject.KeyCode == Enum.KeyCode.ButtonY then self:SubmitCharacter(" ", false) elseif inputObject.KeyCode == Enum.KeyCode.ButtonL2 then if currentKeyset then -- Go to the next keyset self:SetCurrentKeyset((currentKeyset % #keysets) + 1) end elseif inputObject.KeyCode == Enum.KeyCode.ButtonL3 then self:SetCaps(not self:GetCaps()) end end end, false, Enum.KeyCode.ButtonL1, Enum.KeyCode.ButtonR1, Enum.KeyCode.ButtonL2, Enum.KeyCode.ButtonL3, Enum.KeyCode.ButtonX, Enum.KeyCode.ButtonY) self.Parent = panel:GetGUI() panel:SetType(Panel3D.Type.Fixed, { CFrame = localCF }) panel:SetCanFade(false) panel:SetVisible(true, true) panel:ForceShowUntilLookedAt() function panel:OnUpdate() end end) rawset(newKeyboard, "Close", function(self, submit) submit = (submit == true) if not opened then return end opened = false if textChangedConn then textChangedConn:disconnect() end textChangedConn = nil if panelClosedConn then panelClosedConn:disconnect() end panelClosedConn = nil ContextActionService:UnbindCoreAction("VirtualKeyboardControllerInput") -- Clean-up panel:OnMouseLeave() panel:SetVisible(false, true) keyboardContainer.Visible = false self:SubmitText(submit, false) end) rawset(newKeyboard, "SubmitText", function(self, submit, keepKeyboardOpen) local keyboardTextbox = keyboardOptions and keyboardOptions.TextBox if keyboardTextbox then if submit then keyboardTextbox.Text = getBufferText() end keyboardTextbox:ReleaseFocus(submit) if keepKeyboardOpen then keyboardTextbox:CaptureFocus() end end end) rawset(newKeyboard, "GetCurrentOptions", function(self) return keyboardOptions end) rawset(newKeyboard, "BackspaceAtCursor", function(self) if textfieldCursorPosition >= 1 then local bufferText = getBufferText() local newBufferText = string.sub(bufferText, 1, textfieldCursorPosition - 1) .. string.sub(bufferText, textfieldCursorPosition + 1, #bufferText) local newCursorPosition = textfieldCursorPosition - 1 setBufferText(newBufferText) SetTextFieldCursorPosition(newCursorPosition) end end) rawset(newKeyboard, "SubmitCharacter", function(self, character, isAnAlphaKey) local bufferText = getBufferText() local newBufferText = string.sub(bufferText, 1, textfieldCursorPosition) .. character .. string.sub(bufferText, textfieldCursorPosition + 1, #bufferText) setBufferText(newBufferText) SetTextFieldCursorPosition(textfieldCursorPosition + #character) if isAnAlphaKey and self:GetShift() then self:SetShift(false) end end) do -- Parse input definition for _, keyboardKeyset in pairs(keyboardLayoutDefinitions) do local keys = {} local keyboardSizeConstrainer = Util:Create'Frame' { Name = 'KeyboardSizeConstrainer'; Size = UDim2.new(1, 0, 1, -20); Position = UDim2.new(0, 0, 0, 20); BackgroundTransparency = 1; Parent = keyboardContainer; }; local maxWidth = 0 local maxHeight = 0 local y = 0 for rowNum, rowData in pairs(keyboardKeyset) do local x = 0 local width = 1 local height = 1 local width2, height2, x2, y2; for columnNum, columnData in pairs(rowData) do if type(columnData) == 'table' then if columnData['w'] then width = columnData['w'] end if columnData['h'] then height = columnData['h'] end if columnData['x'] then x = x + columnData['x'] end if columnData['y'] then y = y + columnData['y'] end if columnData['x2'] then x2 = columnData['x2'] end if columnData['y2'] then y2 = columnData['y2'] end if columnData['w2'] then width2 = columnData['w2'] end if columnData['h2'] then height2 = columnData['h2'] end elseif type(columnData) == 'string' then if columnData == "" then columnData = " " end -- put key local key = CreateKeyboardKey( newKeyboard, {x = x, y = y, width = width, height = height, x2 = x2, y2 = y2, width2 = width2, height2 = height2}, tokenizeString(columnData, '\n')) table.insert(keys, key) x = x + width maxWidth = math.max(maxWidth, x) maxHeight = math.max(maxHeight, y + height) -- reset for the next key width = 1 height = 1 width2, height2, x2, y2 = nil, nil, nil, nil end end y = y + 1 end -- Fix the positions and sizes to fit in our KeyboardContainer for _, element in pairs(keys) do element.Position = UDim2.new(element.Position.X.Scale / maxWidth, 0, element.Position.Y.Scale / maxHeight, 0) element.Size = UDim2.new(element.Size.X.Scale / maxWidth, 0, element.Size.Y.Scale / maxHeight, 0) element.Parent = keyboardSizeConstrainer end keyboardSizeConstrainer.SizeConstraint = Enum.SizeConstraint.RelativeXX keyboardSizeConstrainer.Size = UDim2.new(1, 0, -maxHeight / maxWidth, 0) keyboardSizeConstrainer.Position = UDim2.new(0, 0, 1, 0) keyboardSizeConstrainer.Visible = false table.insert(keysets, {keys = keys, container = keyboardSizeConstrainer}) end newKeyboard:SetCurrentKeyset(1) end textEntryField.MouseButton1Click:connect(function() SetTextFieldCursorPosition(calculateTextCursorPosition(panel.lookAtPixel.X, panel.lookAtPixel.Y)) end) closeButton.MouseButton1Click:connect(function() newKeyboard:Close(false) closedEvent:Fire() end) voiceDoneButton.MouseButton1Click:connect(function() newKeyboard:SetVoiceMode(false) end) return newKeyboard end local Keyboard = nil; local function GetKeyboard() if Keyboard == nil then Keyboard = ConstructKeyboardUI({MINIMAL_KEYBOARD_LAYOUT, MINIMAL_KEYBOARD_LAYOUT_SYMBOLS}) end return Keyboard end local VirtualKeyboardClass = {} function VirtualKeyboardClass:CreateVirtualKeyboardOptions(textbox) local keyboardOptions = {} keyboardOptions.TextBox = textbox return keyboardOptions end local VirtualKeyboardPlatform = false do -- iOS, Android and Xbox already have platform specific keyboards local platform = UserInputService:GetPlatform() VirtualKeyboardPlatform = platform == Enum.Platform.Windows or platform == Enum.Platform.OSX end function VirtualKeyboardClass:ShowVirtualKeyboard(virtualKeyboardOptions) if VirtualKeyboardPlatform and UserInputService.VREnabled then GetKeyboard():Open(virtualKeyboardOptions) end end function VirtualKeyboardClass:CloseVirtualKeyboard() if VirtualKeyboardPlatform and UserInputService.VREnabled then local currentKeyboard = GetKeyboard() currentKeyboard:Close(false) end end VirtualKeyboardClass.ClosedEvent = GetKeyboard().ClosedEvent if VirtualKeyboardPlatform and useVRKeyboard then UserInputService.TextBoxFocused:connect(function(textbox) VirtualKeyboardClass:ShowVirtualKeyboard(VirtualKeyboardClass:CreateVirtualKeyboardOptions(textbox)) end) UserInputService.TextBoxFocusReleased:connect(function(textbox) VirtualKeyboardClass:CloseVirtualKeyboard() end) end return VirtualKeyboardClass