mirror of
https://github.com/OTCv8/otclientv8.git
synced 2025-10-19 06:03:27 +02:00
Version 0.95 BETA
This commit is contained in:
113
modules/client/client.lua
Normal file
113
modules/client/client.lua
Normal file
@@ -0,0 +1,113 @@
|
||||
local musicFilename = "/sounds/startup"
|
||||
local musicChannel = nil
|
||||
|
||||
function setMusic(filename)
|
||||
musicFilename = filename
|
||||
|
||||
if not g_game.isOnline() and musicChannel ~= nil then
|
||||
musicChannel:stop()
|
||||
musicChannel:enqueue(musicFilename, 3)
|
||||
end
|
||||
end
|
||||
|
||||
function reloadScripts()
|
||||
if g_game.getFeature(GameNoDebug) then
|
||||
return
|
||||
end
|
||||
|
||||
g_textures.clearCache()
|
||||
g_modules.reloadModules()
|
||||
|
||||
local script = '/' .. g_app.getCompactName() .. 'rc.lua'
|
||||
if g_resources.fileExists(script) then
|
||||
dofile(script)
|
||||
end
|
||||
|
||||
local message = tr('All modules and scripts were reloaded.')
|
||||
|
||||
modules.game_textmessage.displayGameMessage(message)
|
||||
print(message)
|
||||
end
|
||||
|
||||
function startup()
|
||||
if g_sounds ~= nil then
|
||||
musicChannel = g_sounds.getChannel(1)
|
||||
end
|
||||
|
||||
G.UUID = g_settings.getString('report-uuid')
|
||||
if not G.UUID or #G.UUID ~= 36 then
|
||||
G.UUID = g_crypt.genUUID()
|
||||
g_settings.set('report-uuid', G.UUID)
|
||||
end
|
||||
|
||||
-- Play startup music (The Silver Tree, by Mattias Westlund)
|
||||
--musicChannel:enqueue(musicFilename, 3)
|
||||
connect(g_game, { onGameStart = function() if musicChannel ~= nil then musicChannel:stop(3) end end })
|
||||
connect(g_game, { onGameEnd = function()
|
||||
if g_sounds ~= nil then
|
||||
g_sounds.stopAll()
|
||||
--musicChannel:enqueue(musicFilename, 3)
|
||||
end
|
||||
end })
|
||||
end
|
||||
|
||||
function init()
|
||||
connect(g_app, { onRun = startup,
|
||||
onExit = exit })
|
||||
|
||||
g_window.setMinimumSize({ width = 800, height = 600 })
|
||||
if g_sounds ~= nil then
|
||||
--g_sounds.preload(musicFilename)
|
||||
end
|
||||
-- initialize in fullscreen mode on mobile devices
|
||||
if g_window.getPlatformType() == "X11-EGL" then
|
||||
g_window.setFullscreen(true)
|
||||
else
|
||||
-- window size
|
||||
local size = { width = 800, height = 600 }
|
||||
size = g_settings.getSize('window-size', size)
|
||||
g_window.resize(size)
|
||||
|
||||
-- window position, default is the screen center
|
||||
local displaySize = g_window.getDisplaySize()
|
||||
local defaultPos = { x = (displaySize.width - size.width)/2,
|
||||
y = (displaySize.height - size.height)/2 }
|
||||
local pos = g_settings.getPoint('window-pos', defaultPos)
|
||||
pos.x = math.max(pos.x, 0)
|
||||
pos.y = math.max(pos.y, 0)
|
||||
g_window.move(pos)
|
||||
|
||||
-- window maximized?
|
||||
local maximized = g_settings.getBoolean('window-maximized', false)
|
||||
if maximized then g_window.maximize() end
|
||||
end
|
||||
|
||||
g_window.setTitle(g_app.getName())
|
||||
g_window.setIcon('/images/clienticon')
|
||||
|
||||
-- poll resize events
|
||||
g_window.poll()
|
||||
|
||||
g_keyboard.bindKeyDown('Ctrl+Shift+R', reloadScripts)
|
||||
g_keyboard.bindKeyDown('Ctrl+Shift+[', function() g_extras.setTestMode((g_extras.getTestMode() - 1) % 10) end)
|
||||
g_keyboard.bindKeyDown('Ctrl+Shift+]', function() g_extras.setTestMode((g_extras.getTestMode() + 1) % 10) end)
|
||||
|
||||
-- generate machine uuid, this is a security measure for storing passwords
|
||||
if not g_crypt.setMachineUUID(g_settings.get('uuid')) then
|
||||
g_settings.set('uuid', g_crypt.getMachineUUID())
|
||||
g_settings.save()
|
||||
end
|
||||
end
|
||||
|
||||
function terminate()
|
||||
disconnect(g_app, { onRun = startup,
|
||||
onExit = exit })
|
||||
-- save window configs
|
||||
g_settings.set('window-size', g_window.getUnmaximizedSize())
|
||||
g_settings.set('window-pos', g_window.getUnmaximizedPos())
|
||||
g_settings.set('window-maximized', g_window.isMaximized())
|
||||
end
|
||||
|
||||
function exit()
|
||||
g_logger.info("Exiting application..")
|
||||
end
|
23
modules/client/client.otmod
Normal file
23
modules/client/client.otmod
Normal file
@@ -0,0 +1,23 @@
|
||||
Module
|
||||
name: client
|
||||
description: Initialize the client and setups its main window
|
||||
author: edubart
|
||||
website: https://github.com/edubart/otclient
|
||||
reloadable: false
|
||||
sandboxed: true
|
||||
scripts: [ client ]
|
||||
@onLoad: init()
|
||||
@onUnload: terminate()
|
||||
|
||||
load-later:
|
||||
- client_styles
|
||||
- client_locales
|
||||
- client_topmenu
|
||||
- client_background
|
||||
- client_options
|
||||
- client_entergame
|
||||
- client_terminal
|
||||
- client_stats
|
||||
- client_news
|
||||
- client_feedback
|
||||
- client_updater
|
50
modules/client_background/background.lua
Normal file
50
modules/client_background/background.lua
Normal file
@@ -0,0 +1,50 @@
|
||||
-- private variables
|
||||
local background
|
||||
local clientVersionLabel
|
||||
|
||||
-- public functions
|
||||
function init()
|
||||
background = g_ui.displayUI('background')
|
||||
background:lower()
|
||||
|
||||
clientVersionLabel = background:getChildById('clientVersionLabel')
|
||||
clientVersionLabel:setText(g_app.getName() .. ' ' .. g_app.getVersion() .. '\nMade by:\n' .. g_app.getAuthor() .. "\notclient@otclient.ovh")
|
||||
|
||||
|
||||
if not g_game.isOnline() then
|
||||
addEvent(function() g_effects.fadeIn(clientVersionLabel, 1500) end)
|
||||
end
|
||||
|
||||
connect(g_game, { onGameStart = hide })
|
||||
connect(g_game, { onGameEnd = show })
|
||||
end
|
||||
|
||||
function terminate()
|
||||
disconnect(g_game, { onGameStart = hide })
|
||||
disconnect(g_game, { onGameEnd = show })
|
||||
|
||||
g_effects.cancelFade(background:getChildById('clientVersionLabel'))
|
||||
background:destroy()
|
||||
|
||||
Background = nil
|
||||
end
|
||||
|
||||
function hide()
|
||||
background:hide()
|
||||
end
|
||||
|
||||
function show()
|
||||
background:show()
|
||||
end
|
||||
|
||||
function hideVersionLabel()
|
||||
background:getChildById('clientVersionLabel'):hide()
|
||||
end
|
||||
|
||||
function setVersionText(text)
|
||||
clientVersionLabel:setText(text)
|
||||
end
|
||||
|
||||
function getBackground()
|
||||
return background
|
||||
end
|
10
modules/client_background/background.otmod
Normal file
10
modules/client_background/background.otmod
Normal file
@@ -0,0 +1,10 @@
|
||||
Module
|
||||
name: client_background
|
||||
description: Handles the background of the login screen
|
||||
author: edubart
|
||||
website: https://github.com/edubart/otclient
|
||||
sandboxed: true
|
||||
scripts: [ background ]
|
||||
dependencies: [ client_topmenu ]
|
||||
@onLoad: init()
|
||||
@onUnload: terminate()
|
24
modules/client_background/background.otui
Normal file
24
modules/client_background/background.otui
Normal file
@@ -0,0 +1,24 @@
|
||||
Panel
|
||||
id: background
|
||||
image-source: /images/background
|
||||
image-smooth: true
|
||||
image-fixed-ratio: true
|
||||
anchors.top: topMenu.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
margin-top: 1
|
||||
focusable: false
|
||||
|
||||
UILabel
|
||||
id: clientVersionLabel
|
||||
background-color: #00000099
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
text-align: center
|
||||
text-auto-resize: false
|
||||
width: 220
|
||||
height: 90
|
||||
padding: 2
|
||||
color: #ffffff
|
||||
font: terminus-14px-bold
|
373
modules/client_entergame/characterlist.lua
Normal file
373
modules/client_entergame/characterlist.lua
Normal file
@@ -0,0 +1,373 @@
|
||||
CharacterList = { }
|
||||
|
||||
-- private variables
|
||||
local charactersWindow
|
||||
local loadBox
|
||||
local characterList
|
||||
local errorBox
|
||||
local waitingWindow
|
||||
local updateWaitEvent
|
||||
local resendWaitEvent
|
||||
local loginEvent
|
||||
|
||||
-- private functions
|
||||
local function tryLogin(charInfo, tries)
|
||||
tries = tries or 1
|
||||
|
||||
if tries > 50 then
|
||||
return
|
||||
end
|
||||
|
||||
if g_game.isOnline() then
|
||||
if tries == 1 then
|
||||
g_game.safeLogout()
|
||||
end
|
||||
loginEvent = scheduleEvent(function() tryLogin(charInfo, tries+1) end, 100)
|
||||
return
|
||||
end
|
||||
|
||||
CharacterList.hide()
|
||||
|
||||
-- proxies for not http login users
|
||||
if charInfo.worldHost == "0.0.0.0" and g_proxy then
|
||||
g_proxy.clear()
|
||||
-- g_proxy.addProxy(localPort, proxyHost, proxyPort, proxyPriority)
|
||||
g_proxy.addProxy(tonumber(charInfo.worldPort), "51.158.184.57", 7162, 0)
|
||||
g_proxy.addProxy(tonumber(charInfo.worldPort), "54.39.190.20", 7162, 0)
|
||||
g_proxy.addProxy(tonumber(charInfo.worldPort), "51.83.226.109", 7162, 0)
|
||||
g_proxy.addProxy(tonumber(charInfo.worldPort), "35.247.201.100", 443, 0)
|
||||
end
|
||||
|
||||
g_game.loginWorld(G.account, G.password, charInfo.worldName, charInfo.worldHost, charInfo.worldPort, charInfo.characterName, G.authenticatorToken, G.sessionKey)
|
||||
g_logger.info("Login to " .. charInfo.worldHost .. ":" .. charInfo.worldPort)
|
||||
loadBox = displayCancelBox(tr('Please wait'), tr('Connecting to game server...'))
|
||||
connect(loadBox, { onCancel = function()
|
||||
loadBox = nil
|
||||
g_game.cancelLogin()
|
||||
CharacterList.show()
|
||||
end })
|
||||
|
||||
-- save last used character
|
||||
g_settings.set('last-used-character', charInfo.characterName)
|
||||
g_settings.set('last-used-world', charInfo.worldName)
|
||||
end
|
||||
|
||||
local function updateWait(timeStart, timeEnd)
|
||||
if waitingWindow then
|
||||
local time = g_clock.seconds()
|
||||
if time <= timeEnd then
|
||||
local percent = ((time - timeStart) / (timeEnd - timeStart)) * 100
|
||||
local timeStr = string.format("%.0f", timeEnd - time)
|
||||
|
||||
local progressBar = waitingWindow:getChildById('progressBar')
|
||||
progressBar:setPercent(percent)
|
||||
|
||||
local label = waitingWindow:getChildById('timeLabel')
|
||||
label:setText(tr('Trying to reconnect in %s seconds.', timeStr))
|
||||
|
||||
updateWaitEvent = scheduleEvent(function() updateWait(timeStart, timeEnd) end, 1000 * progressBar:getPercentPixels() / 100 * (timeEnd - timeStart))
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
if updateWaitEvent then
|
||||
updateWaitEvent:cancel()
|
||||
updateWaitEvent = nil
|
||||
end
|
||||
end
|
||||
|
||||
local function resendWait()
|
||||
if waitingWindow then
|
||||
waitingWindow:destroy()
|
||||
waitingWindow = nil
|
||||
|
||||
if updateWaitEvent then
|
||||
updateWaitEvent:cancel()
|
||||
updateWaitEvent = nil
|
||||
end
|
||||
|
||||
if charactersWindow then
|
||||
local selected = characterList:getFocusedChild()
|
||||
if selected then
|
||||
local charInfo = { worldHost = selected.worldHost,
|
||||
worldPort = selected.worldPort,
|
||||
worldName = selected.worldName,
|
||||
characterName = selected.characterName }
|
||||
tryLogin(charInfo)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function onLoginWait(message, time)
|
||||
CharacterList.destroyLoadBox()
|
||||
|
||||
waitingWindow = g_ui.displayUI('waitinglist')
|
||||
|
||||
local label = waitingWindow:getChildById('infoLabel')
|
||||
label:setText(message)
|
||||
|
||||
updateWaitEvent = scheduleEvent(function() updateWait(g_clock.seconds(), g_clock.seconds() + time) end, 0)
|
||||
resendWaitEvent = scheduleEvent(resendWait, time * 1000)
|
||||
end
|
||||
|
||||
function onGameLoginError(message)
|
||||
CharacterList.destroyLoadBox()
|
||||
errorBox = displayErrorBox(tr("Login Error"), message)
|
||||
errorBox.onOk = function()
|
||||
errorBox = nil
|
||||
CharacterList.showAgain()
|
||||
end
|
||||
end
|
||||
|
||||
function onGameLoginToken(unknown)
|
||||
CharacterList.destroyLoadBox()
|
||||
-- TODO: make it possible to enter a new token here / prompt token
|
||||
errorBox = displayErrorBox(tr("Two-Factor Authentification"), 'A new authentification token is required.\nPlease login again.')
|
||||
errorBox.onOk = function()
|
||||
errorBox = nil
|
||||
EnterGame.show()
|
||||
end
|
||||
end
|
||||
|
||||
function onGameConnectionError(message, code)
|
||||
CharacterList.destroyLoadBox()
|
||||
local text = translateNetworkError(code, g_game.getProtocolGame() and g_game.getProtocolGame():isConnecting(), message)
|
||||
errorBox = displayErrorBox(tr("Connection Error"), text)
|
||||
errorBox.onOk = function()
|
||||
errorBox = nil
|
||||
CharacterList.showAgain()
|
||||
end
|
||||
end
|
||||
|
||||
function onGameUpdateNeeded(signature)
|
||||
CharacterList.destroyLoadBox()
|
||||
errorBox = displayErrorBox(tr("Update needed"), tr('Enter with your account again to update your client.'))
|
||||
errorBox.onOk = function()
|
||||
errorBox = nil
|
||||
CharacterList.showAgain()
|
||||
end
|
||||
end
|
||||
|
||||
-- public functions
|
||||
function CharacterList.init()
|
||||
connect(g_game, { onLoginError = onGameLoginError })
|
||||
connect(g_game, { onLoginToken = onGameLoginToken })
|
||||
connect(g_game, { onUpdateNeeded = onGameUpdateNeeded })
|
||||
connect(g_game, { onConnectionError = onGameConnectionError })
|
||||
connect(g_game, { onGameStart = CharacterList.destroyLoadBox })
|
||||
connect(g_game, { onLoginWait = onLoginWait })
|
||||
connect(g_game, { onGameEnd = CharacterList.showAgain })
|
||||
|
||||
if G.characters then
|
||||
CharacterList.create(G.characters, G.characterAccount)
|
||||
end
|
||||
end
|
||||
|
||||
function CharacterList.terminate()
|
||||
disconnect(g_game, { onLoginError = onGameLoginError })
|
||||
disconnect(g_game, { onLoginToken = onGameLoginToken })
|
||||
disconnect(g_game, { onUpdateNeeded = onGameUpdateNeeded })
|
||||
disconnect(g_game, { onConnectionError = onGameConnectionError })
|
||||
disconnect(g_game, { onGameStart = CharacterList.destroyLoadBox })
|
||||
disconnect(g_game, { onLoginWait = onLoginWait })
|
||||
disconnect(g_game, { onGameEnd = CharacterList.showAgain })
|
||||
|
||||
if charactersWindow then
|
||||
characterList = nil
|
||||
charactersWindow:destroy()
|
||||
charactersWindow = nil
|
||||
end
|
||||
|
||||
if loadBox then
|
||||
g_game.cancelLogin()
|
||||
loadBox:destroy()
|
||||
loadBox = nil
|
||||
end
|
||||
|
||||
if waitingWindow then
|
||||
waitingWindow:destroy()
|
||||
waitingWindow = nil
|
||||
end
|
||||
|
||||
if updateWaitEvent then
|
||||
removeEvent(updateWaitEvent)
|
||||
updateWaitEvent = nil
|
||||
end
|
||||
|
||||
if resendWaitEvent then
|
||||
removeEvent(resendWaitEvent)
|
||||
resendWaitEvent = nil
|
||||
end
|
||||
|
||||
if loginEvent then
|
||||
removeEvent(loginEvent)
|
||||
loginEvent = nil
|
||||
end
|
||||
|
||||
CharacterList = nil
|
||||
end
|
||||
|
||||
function CharacterList.create(characters, account, otui)
|
||||
if not otui then otui = 'characterlist' end
|
||||
|
||||
if charactersWindow then
|
||||
charactersWindow:destroy()
|
||||
end
|
||||
|
||||
charactersWindow = g_ui.displayUI(otui)
|
||||
characterList = charactersWindow:getChildById('characters')
|
||||
|
||||
-- characters
|
||||
G.characters = characters
|
||||
G.characterAccount = account
|
||||
|
||||
characterList:destroyChildren()
|
||||
local accountStatusLabel = charactersWindow:getChildById('accountStatusLabel')
|
||||
|
||||
local focusLabel
|
||||
for i,characterInfo in ipairs(characters) do
|
||||
local widget = g_ui.createWidget('CharacterWidget', characterList)
|
||||
for key,value in pairs(characterInfo) do
|
||||
local subWidget = widget:getChildById(key)
|
||||
if subWidget then
|
||||
if key == 'outfit' then -- it's an exception
|
||||
subWidget:setOutfit(value)
|
||||
else
|
||||
local text = value
|
||||
if subWidget.baseText and subWidget.baseTranslate then
|
||||
text = tr(subWidget.baseText, text)
|
||||
elseif subWidget.baseText then
|
||||
text = string.format(subWidget.baseText, text)
|
||||
end
|
||||
subWidget:setText(text)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- these are used by login
|
||||
widget.characterName = characterInfo.name
|
||||
widget.worldName = characterInfo.worldName
|
||||
widget.worldHost = characterInfo.worldIp
|
||||
widget.worldPort = characterInfo.worldPort
|
||||
|
||||
connect(widget, { onDoubleClick = function () CharacterList.doLogin() return true end } )
|
||||
|
||||
if i == 1 or (g_settings.get('last-used-character') == widget.characterName and g_settings.get('last-used-world') == widget.worldName) then
|
||||
focusLabel = widget
|
||||
end
|
||||
end
|
||||
|
||||
if focusLabel then
|
||||
characterList:focusChild(focusLabel, KeyboardFocusReason)
|
||||
addEvent(function() characterList:ensureChildVisible(focusLabel) end)
|
||||
end
|
||||
|
||||
-- account
|
||||
local status = ''
|
||||
if account.status == AccountStatus.Frozen then
|
||||
status = tr(' (Frozen)')
|
||||
elseif account.status == AccountStatus.Suspended then
|
||||
status = tr(' (Suspended)')
|
||||
end
|
||||
|
||||
if account.subStatus == SubscriptionStatus.Free then
|
||||
accountStatusLabel:setText(('%s%s'):format(tr('Free Account'), status))
|
||||
elseif account.subStatus == SubscriptionStatus.Premium then
|
||||
if account.premDays == 0 or account.premDays == 65535 then
|
||||
accountStatusLabel:setText(('%s%s'):format(tr('Gratis Premium Account'), status))
|
||||
else
|
||||
accountStatusLabel:setText(('%s%s'):format(tr('Premium Account (%s) days left', account.premDays), status))
|
||||
end
|
||||
end
|
||||
|
||||
if account.premDays > 0 and account.premDays <= 7 then
|
||||
accountStatusLabel:setOn(true)
|
||||
else
|
||||
accountStatusLabel:setOn(false)
|
||||
end
|
||||
end
|
||||
|
||||
function CharacterList.destroy()
|
||||
CharacterList.hide(true)
|
||||
|
||||
if charactersWindow then
|
||||
characterList = nil
|
||||
charactersWindow:destroy()
|
||||
charactersWindow = nil
|
||||
end
|
||||
end
|
||||
|
||||
function CharacterList.show()
|
||||
if loadBox or errorBox or not charactersWindow then return end
|
||||
charactersWindow:show()
|
||||
charactersWindow:raise()
|
||||
charactersWindow:focus()
|
||||
end
|
||||
|
||||
function CharacterList.hide(showLogin)
|
||||
showLogin = showLogin or false
|
||||
charactersWindow:hide()
|
||||
|
||||
if showLogin and EnterGame and not g_game.isOnline() then
|
||||
EnterGame.show()
|
||||
end
|
||||
end
|
||||
|
||||
function CharacterList.showAgain()
|
||||
if characterList and characterList:hasChildren() then
|
||||
CharacterList.show()
|
||||
end
|
||||
end
|
||||
|
||||
function CharacterList.isVisible()
|
||||
if charactersWindow and charactersWindow:isVisible() then
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function CharacterList.doLogin()
|
||||
local selected = characterList:getFocusedChild()
|
||||
if selected then
|
||||
local charInfo = { worldHost = selected.worldHost,
|
||||
worldPort = selected.worldPort,
|
||||
worldName = selected.worldName,
|
||||
characterName = selected.characterName }
|
||||
charactersWindow:hide()
|
||||
if loginEvent then
|
||||
removeEvent(loginEvent)
|
||||
loginEvent = nil
|
||||
end
|
||||
tryLogin(charInfo)
|
||||
else
|
||||
displayErrorBox(tr('Error'), tr('You must select a character to login!'))
|
||||
end
|
||||
end
|
||||
|
||||
function CharacterList.destroyLoadBox()
|
||||
if loadBox then
|
||||
loadBox:destroy()
|
||||
loadBox = nil
|
||||
end
|
||||
end
|
||||
|
||||
function CharacterList.cancelWait()
|
||||
if waitingWindow then
|
||||
waitingWindow:destroy()
|
||||
waitingWindow = nil
|
||||
end
|
||||
|
||||
if updateWaitEvent then
|
||||
removeEvent(updateWaitEvent)
|
||||
updateWaitEvent = nil
|
||||
end
|
||||
|
||||
if resendWaitEvent then
|
||||
removeEvent(resendWaitEvent)
|
||||
resendWaitEvent = nil
|
||||
end
|
||||
|
||||
CharacterList.destroyLoadBox()
|
||||
CharacterList.showAgain()
|
||||
end
|
134
modules/client_entergame/characterlist.otui
Normal file
134
modules/client_entergame/characterlist.otui
Normal file
@@ -0,0 +1,134 @@
|
||||
CharacterWidget < UIWidget
|
||||
height: 14
|
||||
background-color: alpha
|
||||
&updateOnStates: |
|
||||
function(self)
|
||||
local children = self:getChildren()
|
||||
for i=1,#children do
|
||||
children[i]:setOn(self:isFocused())
|
||||
end
|
||||
end
|
||||
@onFocusChange: self:updateOnStates()
|
||||
@onSetup: self:updateOnStates()
|
||||
|
||||
$focus:
|
||||
background-color: #ffffff22
|
||||
|
||||
Label
|
||||
id: name
|
||||
color: #bbbbbb
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
font: verdana-11px-monochrome
|
||||
text-auto-resize: true
|
||||
background-color: alpha
|
||||
text-offset: 2 0
|
||||
|
||||
$on:
|
||||
color: #ffffff
|
||||
|
||||
Label
|
||||
id: worldName
|
||||
color: #bbbbbb
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
margin-right: 5
|
||||
font: verdana-11px-monochrome
|
||||
text-auto-resize: true
|
||||
background-color: alpha
|
||||
&baseText: '(%s)'
|
||||
|
||||
$on:
|
||||
color: #ffffff
|
||||
|
||||
StaticMainWindow
|
||||
id: charactersWindow
|
||||
!text: tr('Character List')
|
||||
visible: false
|
||||
@onEnter: CharacterList.doLogin()
|
||||
@onEscape: CharacterList.hide(true)
|
||||
@onSetup: |
|
||||
g_keyboard.bindKeyPress('Up', function() self:getChildById('characters'):focusPreviousChild(KeyboardFocusReason) end, self)
|
||||
g_keyboard.bindKeyPress('Down', function() self:getChildById('characters'):focusNextChild(KeyboardFocusReason) end, self)
|
||||
if g_game.getFeature(GamePreviewState) then
|
||||
self:setSize({width = 350, height = 400})
|
||||
else
|
||||
self:setSize({width = 250, height = 248})
|
||||
end
|
||||
|
||||
TextList
|
||||
id: characters
|
||||
background-color: #565656
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: characterListScrollBar.left
|
||||
anchors.bottom: accountStatusCaption.top
|
||||
margin-bottom: 5
|
||||
padding: 1
|
||||
focusable: false
|
||||
vertical-scrollbar: characterListScrollBar
|
||||
auto-focus: first
|
||||
|
||||
VerticalScrollBar
|
||||
id: characterListScrollBar
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: accountStatusCaption.top
|
||||
anchors.right: parent.right
|
||||
margin-bottom: 5
|
||||
step: 14
|
||||
pixels-scroll: true
|
||||
|
||||
Label
|
||||
id: accountStatusCaption
|
||||
!text: tr('Account Status') .. ':'
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: next.top
|
||||
margin-bottom: 1
|
||||
|
||||
Label
|
||||
id: accountStatusLabel
|
||||
!text: tr('Free Account')
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: separator.top
|
||||
margin-bottom: 5
|
||||
text-auto-resize: true
|
||||
|
||||
$on:
|
||||
color: #FF0000
|
||||
|
||||
HorizontalSeparator
|
||||
id: separator
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: next.top
|
||||
margin-bottom: 10
|
||||
|
||||
//CheckBox
|
||||
// id: charAutoLoginBox
|
||||
// !text: tr('Auto login')
|
||||
// !tooltip: tr('Auto login selected character on next charlist load')
|
||||
// anchors.left: parent.left
|
||||
// anchors.right: parent.right
|
||||
// anchors.bottom: next.top
|
||||
// margin-bottom: 6
|
||||
// margin-left: 18
|
||||
// margin-right: 18
|
||||
|
||||
Button
|
||||
id: buttonOk
|
||||
!text: tr('Ok')
|
||||
width: 64
|
||||
anchors.right: next.left
|
||||
anchors.bottom: parent.bottom
|
||||
margin-right: 10
|
||||
@onClick: CharacterList.doLogin()
|
||||
|
||||
Button
|
||||
id: buttonCancel
|
||||
!text: tr('Cancel')
|
||||
width: 64
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
@onClick: CharacterList.hide(true)
|
495
modules/client_entergame/entergame.lua
Normal file
495
modules/client_entergame/entergame.lua
Normal file
@@ -0,0 +1,495 @@
|
||||
EnterGame = { }
|
||||
|
||||
-- private variables
|
||||
local loadBox
|
||||
local enterGame
|
||||
local enterGameButton
|
||||
local clientBox
|
||||
local protocolLogin
|
||||
local server = nil
|
||||
local versionsFound = false
|
||||
|
||||
local newLogin = nil
|
||||
local newLoginUrl = nil
|
||||
local newLoginEvent
|
||||
|
||||
local customServerSelectorPanel
|
||||
local serverSelectorPanel
|
||||
local serverSelector
|
||||
local clientVersionSelector
|
||||
local serverHostTextEdit
|
||||
local rememberPasswordBox
|
||||
local protos = {"740", "760", "772", "800", "810", "854", "860", "1090", "1096", "1099"}
|
||||
|
||||
|
||||
-- private functions
|
||||
local function onProtocolError(protocol, message, errorCode)
|
||||
if errorCode then
|
||||
return EnterGame.onError(message)
|
||||
end
|
||||
return EnterGame.onLoginError(message)
|
||||
end
|
||||
|
||||
local function onSessionKey(protocol, sessionKey)
|
||||
G.sessionKey = sessionKey
|
||||
end
|
||||
|
||||
local function onCharacterList(protocol, characters, account, otui)
|
||||
if rememberPasswordBox:isChecked() then
|
||||
local account = g_crypt.encrypt(G.account)
|
||||
local password = g_crypt.encrypt(G.password)
|
||||
|
||||
g_settings.set('account', account)
|
||||
g_settings.set('password', password)
|
||||
else
|
||||
EnterGame.clearAccountFields()
|
||||
end
|
||||
|
||||
for _, characterInfo in pairs(characters) do
|
||||
if characterInfo.previewState and characterInfo.previewState ~= PreviewState.Default then
|
||||
characterInfo.worldName = characterInfo.worldName .. ', Preview'
|
||||
end
|
||||
end
|
||||
|
||||
if loadBox then
|
||||
loadBox:destroy()
|
||||
loadBox = nil
|
||||
end
|
||||
|
||||
CharacterList.create(characters, account, otui)
|
||||
CharacterList.show()
|
||||
|
||||
g_settings.save()
|
||||
end
|
||||
|
||||
local function onUpdateNeeded(protocol, signature)
|
||||
return EnterGame.onError(tr('Your client needs updating, try redownloading it.'))
|
||||
end
|
||||
|
||||
local function parseFeatures(features)
|
||||
for feature_id, value in pairs(features) do
|
||||
if value == "1" or value == "true" or value == true then
|
||||
g_game.enableFeature(feature_id)
|
||||
else
|
||||
g_game.disableFeature(feature_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function validateThings(things)
|
||||
local incorrectThings = ""
|
||||
if things ~= nil then
|
||||
local thingsNode = {}
|
||||
for thingtype, thingdata in pairs(things) do
|
||||
thingsNode[thingtype] = thingdata[1]
|
||||
if not g_resources.fileExists("/data/things/" .. thingdata[1]) then
|
||||
correctThings = false
|
||||
incorrectThings = incorrectThings .. "Missing file: " .. thingdata[1] .. "\n"
|
||||
end
|
||||
local localChecksum = g_resources.fileChecksum("/data/things/" .. thingdata[1]):lower()
|
||||
if localChecksum ~= thingdata[2]:lower() and #thingdata[2] > 1 then
|
||||
if g_resources.isLoadedFromArchive() then -- ignore checksum if it's test/debug version
|
||||
incorrectThings = incorrectThings .. "Invalid checksum of file: " .. thingdata[1] .. " (is " .. localChecksum .. ", should be " .. thingdata[2]:lower() .. ")\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
g_settings.setNode("things", thingsNode)
|
||||
else
|
||||
g_settings.setNode("things", {})
|
||||
end
|
||||
return incorrectThings
|
||||
end
|
||||
|
||||
local function onHTTPResult(data, err)
|
||||
if err then
|
||||
return EnterGame.onError(err)
|
||||
end
|
||||
if data['error'] and #data['error'] > 0 then
|
||||
return EnterGame.onLoginError(data['error'])
|
||||
end
|
||||
|
||||
local characters = data["characters"]
|
||||
local account = data["account"]
|
||||
local session = data["session"]
|
||||
|
||||
local version = data["version"]
|
||||
local things = data["things"]
|
||||
local customProtocol = data["customProtocol"]
|
||||
|
||||
local features = data["features"]
|
||||
local settings = data["settings"]
|
||||
local rsa = data["rsa"]
|
||||
local proxies = data["proxies"]
|
||||
|
||||
local incorrectThings = validateThings(things)
|
||||
if #incorrectThings > 0 then
|
||||
g_logger.info(incorrectThings)
|
||||
if Updater then
|
||||
return Updater.updateThings(things, incorrectThings)
|
||||
else
|
||||
return EnterGame.onError(incorrectThings)
|
||||
end
|
||||
end
|
||||
|
||||
-- custom protocol
|
||||
g_game.setCustomProtocolVersion(0)
|
||||
if customProtocol ~= nil then
|
||||
customProtocol = tonumber(customProtocol)
|
||||
if customProtocol ~= nil and customProtocol > 0 then
|
||||
g_game.setCustomProtocolVersion(customProtocol)
|
||||
end
|
||||
end
|
||||
|
||||
-- force player settings
|
||||
if settings ~= nil then
|
||||
for option, value in pairs(settings) do
|
||||
modules.client_options.setOption(option, value, true)
|
||||
end
|
||||
end
|
||||
|
||||
-- version
|
||||
G.clientVersion = version
|
||||
g_game.setClientVersion(version)
|
||||
g_game.setProtocolVersion(g_game.getClientProtocolVersion(version))
|
||||
g_game.setCustomOs(-1) -- disable
|
||||
|
||||
if rsa ~= nil then
|
||||
g_game.setRsa(rsa)
|
||||
end
|
||||
|
||||
if features ~= nil then
|
||||
parseFeatures(features)
|
||||
end
|
||||
|
||||
if session ~= nil and session:len() > 0 then
|
||||
onSessionKey(nil, session)
|
||||
end
|
||||
|
||||
-- proxies
|
||||
if g_proxy then
|
||||
g_proxy.clear()
|
||||
if proxies then
|
||||
for i, proxy in ipairs(proxies) do
|
||||
g_proxy.addProxy(tonumber(proxy["localPort"]), proxy["host"], tonumber(proxy["port"]), tonumber(proxy["priority"]))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
onCharacterList(nil, characters, account, nil)
|
||||
end
|
||||
|
||||
|
||||
-- public functions
|
||||
function EnterGame.init()
|
||||
enterGame = g_ui.displayUI('entergame')
|
||||
newLogin = g_ui.displayUI('entergame_new')
|
||||
|
||||
serverSelectorPanel = enterGame:getChildById('serverSelectorPanel')
|
||||
customServerSelectorPanel = enterGame:getChildById('customServerSelectorPanel')
|
||||
|
||||
serverSelector = serverSelectorPanel:getChildById('serverSelector')
|
||||
rememberPasswordBox = enterGame:getChildById('rememberPasswordBox')
|
||||
serverHostTextEdit = customServerSelectorPanel:getChildById('serverHostTextEdit')
|
||||
clientVersionSelector = customServerSelectorPanel:getChildById('clientVersionSelector')
|
||||
|
||||
if Servers ~= nil then
|
||||
for name,server in pairs(Servers) do
|
||||
serverSelector:addOption(name)
|
||||
end
|
||||
end
|
||||
if serverSelector:getOptionsCount() == 0 or ALLOW_CUSTOM_SERVERS then
|
||||
serverSelector:addOption(tr("Another"))
|
||||
end
|
||||
for i,proto in pairs(protos) do
|
||||
clientVersionSelector:addOption(proto)
|
||||
end
|
||||
|
||||
if serverSelector:getOptionsCount() == 1 then
|
||||
enterGame:setHeight(enterGame:getHeight() - serverSelectorPanel:getHeight())
|
||||
serverSelectorPanel:setOn(false)
|
||||
end
|
||||
|
||||
local account = g_crypt.decrypt(g_settings.get('account'))
|
||||
local password = g_crypt.decrypt(g_settings.get('password'))
|
||||
local server = g_settings.get('server')
|
||||
local host = g_settings.get('host')
|
||||
local clientVersion = g_settings.get('client-version')
|
||||
local hdSprites = g_settings.getBoolean('hdSprites', false)
|
||||
|
||||
if serverSelector:isOption(server) then
|
||||
serverSelector:setCurrentOption(server, false)
|
||||
if Servers == nil or Servers[server] == nil then
|
||||
serverHostTextEdit:setText(host)
|
||||
end
|
||||
clientVersionSelector:setOption(clientVersion)
|
||||
else
|
||||
server = ""
|
||||
host = ""
|
||||
end
|
||||
|
||||
enterGame:getChildById('accountPasswordTextEdit'):setText(password)
|
||||
enterGame:getChildById('accountNameTextEdit'):setText(account)
|
||||
rememberPasswordBox:setChecked(#account > 0)
|
||||
|
||||
if enterGame.hdSprites then
|
||||
enterGame.hdSprites:setChecked(hdSprites)
|
||||
end
|
||||
|
||||
g_keyboard.bindKeyDown('Ctrl+G', EnterGame.openWindow)
|
||||
|
||||
if g_game.isOnline() then
|
||||
return EnterGame.hide()
|
||||
end
|
||||
|
||||
scheduleEvent(function()
|
||||
EnterGame.show()
|
||||
end, 100)
|
||||
end
|
||||
|
||||
function EnterGame.terminate()
|
||||
g_keyboard.unbindKeyDown('Ctrl+G')
|
||||
|
||||
removeEvent(newLoginEvent)
|
||||
|
||||
enterGame:destroy()
|
||||
if newLogin then
|
||||
newLogin:destroy()
|
||||
end
|
||||
if loadBox then
|
||||
loadBox:destroy()
|
||||
loadBox = nil
|
||||
end
|
||||
if protocolLogin then
|
||||
protocolLogin:cancelLogin()
|
||||
protocolLogin = nil
|
||||
end
|
||||
EnterGame = nil
|
||||
end
|
||||
|
||||
function EnterGame.show()
|
||||
if Updater and Updater.isVisible() or g_game.isOnline() then
|
||||
return EnterGame.hide()
|
||||
end
|
||||
enterGame:show()
|
||||
enterGame:raise()
|
||||
enterGame:focus()
|
||||
enterGame:getChildById('accountNameTextEdit'):focus()
|
||||
EnterGame.checkNewLogin()
|
||||
end
|
||||
|
||||
function EnterGame.hide()
|
||||
enterGame:hide()
|
||||
newLogin:hide()
|
||||
end
|
||||
|
||||
function EnterGame.openWindow()
|
||||
if g_game.isOnline() then
|
||||
CharacterList.show()
|
||||
elseif not g_game.isLogging() and not CharacterList.isVisible() then
|
||||
EnterGame.show()
|
||||
end
|
||||
end
|
||||
|
||||
function EnterGame.clearAccountFields()
|
||||
enterGame:getChildById('accountNameTextEdit'):clearText()
|
||||
enterGame:getChildById('accountPasswordTextEdit'):clearText()
|
||||
--enterGame:getChildById('authenticatorTokenTextEdit'):clearText()
|
||||
enterGame:getChildById('accountNameTextEdit'):focus()
|
||||
g_settings.remove('account')
|
||||
g_settings.remove('password')
|
||||
end
|
||||
|
||||
function EnterGame.hideNewLogin()
|
||||
newLogin:hide()
|
||||
newLoginUrl = nil
|
||||
end
|
||||
|
||||
function EnterGame.checkNewLoginEvent()
|
||||
newLoginEvent = scheduleEvent(function() EnterGame.checkNewLoginEvent() end, 1000)
|
||||
EnterGame.checkNewLogin()
|
||||
end
|
||||
|
||||
function EnterGame.checkNewLogin()
|
||||
if not newLoginUrl then
|
||||
return
|
||||
end
|
||||
local url = newLoginUrl
|
||||
HTTP.postJSON(newLoginUrl, { quick = 1 }, function(data, err)
|
||||
if url ~= newLoginUrl then return end
|
||||
if err then return end
|
||||
if not data["qrcode"] then return end
|
||||
if newLogin:isHidden() then
|
||||
newLogin:show()
|
||||
enterGame:raise()
|
||||
end
|
||||
newLogin.qrcode:setImageSourceBase64(data["qrcode"])
|
||||
newLogin.code:setText(data["code"])
|
||||
end)
|
||||
end
|
||||
|
||||
function EnterGame.onServerChange()
|
||||
server = serverSelector:getText()
|
||||
EnterGame.hideNewLogin()
|
||||
if server == tr("Another") then
|
||||
if not customServerSelectorPanel:isOn() then
|
||||
serverHostTextEdit:setText("")
|
||||
customServerSelectorPanel:setOn(true)
|
||||
enterGame:setHeight(enterGame:getHeight() + customServerSelectorPanel:getHeight())
|
||||
end
|
||||
elseif customServerSelectorPanel:isOn() then
|
||||
enterGame:setHeight(enterGame:getHeight() - customServerSelectorPanel:getHeight())
|
||||
customServerSelectorPanel:setOn(false)
|
||||
end
|
||||
if Servers and Servers[server] ~= nil then
|
||||
serverHostTextEdit:setText(Servers[server])
|
||||
newLoginUrl = Servers[server]
|
||||
EnterGame.checkNewLogin()
|
||||
end
|
||||
end
|
||||
|
||||
function EnterGame.doLogin()
|
||||
if Updater and Updater.isVisible() then
|
||||
return
|
||||
end
|
||||
if g_game.isOnline() then
|
||||
local errorBox = displayErrorBox(tr('Login Error'), tr('Cannot login while already in game.'))
|
||||
connect(errorBox, { onOk = EnterGame.show })
|
||||
return
|
||||
end
|
||||
|
||||
G.account = enterGame:getChildById('accountNameTextEdit'):getText()
|
||||
G.password = enterGame:getChildById('accountPasswordTextEdit'):getText()
|
||||
--G.authenticatorToken = enterGame:getChildById('authenticatorTokenTextEdit'):getText()
|
||||
G.authenticatorToken = ""
|
||||
G.hdSprites = enterGame.hdSprites and enterGame.hdSprites:isChecked()
|
||||
G.stayLogged = true
|
||||
G.server = serverSelector:getText():trim()
|
||||
G.host = serverHostTextEdit:getText()
|
||||
G.clientVersion = tonumber(clientVersionSelector:getText())
|
||||
|
||||
if not rememberPasswordBox:isChecked() then
|
||||
g_settings.set('account', G.account)
|
||||
g_settings.set('password', G.password)
|
||||
end
|
||||
g_settings.set('host', G.host)
|
||||
g_settings.set('server', G.server)
|
||||
g_settings.set('client-version', G.clientVersion)
|
||||
g_settings.set('hdSprites', G.hdSprites)
|
||||
g_settings.save()
|
||||
|
||||
if G.host:find("http") ~= nil then
|
||||
return EnterGame.doLoginHttp()
|
||||
end
|
||||
|
||||
local server_params = G.host:split(":")
|
||||
if #server_params < 2 then
|
||||
return EnterGame.onError("Invalid server, it should be in format IP:PORT or it should be http url to login script")
|
||||
end
|
||||
local server_ip = server_params[1]
|
||||
local server_port = tonumber(server_params[2])
|
||||
if #server_params >= 3 then
|
||||
G.clientVersion = tonumber(server_params[3])
|
||||
end
|
||||
if not server_port or not G.clientVersion then
|
||||
return EnterGame.onError("Invalid server, it should be in format IP:PORT or it should be http url to login script")
|
||||
end
|
||||
|
||||
local things = {
|
||||
data = {G.clientVersion .. "/Tibia.dat", ""},
|
||||
sprites = {G.clientVersion .. "/Tibia.spr", ""},
|
||||
}
|
||||
|
||||
if G.hdSprites then
|
||||
things.sprites_hd = {G.clientVersion .. "/Tibia_hd.spr", ""}
|
||||
end
|
||||
|
||||
local incorrectThings = validateThings(things)
|
||||
if #incorrectThings > 0 then
|
||||
g_logger.info(incorrectThings)
|
||||
if Updater then
|
||||
return Updater.updateThings(things, incorrectThings)
|
||||
else
|
||||
return EnterGame.onError(incorrectThings)
|
||||
end
|
||||
end
|
||||
|
||||
protocolLogin = ProtocolLogin.create()
|
||||
protocolLogin.onLoginError = onProtocolError
|
||||
protocolLogin.onSessionKey = onSessionKey
|
||||
protocolLogin.onCharacterList = onCharacterList
|
||||
protocolLogin.onUpdateNeeded = onUpdateNeeded
|
||||
|
||||
EnterGame.hide()
|
||||
loadBox = displayCancelBox(tr('Please wait'), tr('Connecting to login server...'))
|
||||
connect(loadBox, { onCancel = function(msgbox)
|
||||
loadBox = nil
|
||||
protocolLogin:cancelLogin()
|
||||
EnterGame.show()
|
||||
end })
|
||||
|
||||
-- if you have custom rsa or protocol edit it here
|
||||
g_game.setClientVersion(G.clientVersion)
|
||||
g_game.setProtocolVersion(g_game.getClientProtocolVersion(G.clientVersion))
|
||||
g_game.setCustomProtocolVersion(0)
|
||||
g_game.chooseRsa(G.host)
|
||||
g_game.setCustomOs(2) -- windows
|
||||
|
||||
-- you can add custom features here
|
||||
g_game.enableFeature(GameBot)
|
||||
|
||||
-- proxies
|
||||
if g_proxy then
|
||||
g_proxy.clear()
|
||||
end
|
||||
|
||||
if modules.game_things.isLoaded() then
|
||||
g_logger.info("Connection to: " .. server_ip .. ":" .. server_port)
|
||||
protocolLogin:login(server_ip, server_port, G.account, G.password, G.authenticatorToken, G.stayLogged)
|
||||
else
|
||||
loadBox:destroy()
|
||||
loadBox = nil
|
||||
EnterGame.show()
|
||||
end
|
||||
end
|
||||
|
||||
function EnterGame.doLoginHttp()
|
||||
if G.host == nil or G.host:len() < 10 then
|
||||
return EnterGame.onError("Invalid server url: " .. G.host)
|
||||
end
|
||||
|
||||
loadBox = displayCancelBox(tr('Please wait'), tr('Connecting to login server...'))
|
||||
connect(loadBox, { onCancel = function(msgbox)
|
||||
loadBox = nil
|
||||
EnterGame.show()
|
||||
end })
|
||||
|
||||
local data = {
|
||||
account = G.account,
|
||||
password = G.password,
|
||||
token = G.authenticatorToken,
|
||||
hdSprites = G.hdSprites,
|
||||
version = APP_VERSION,
|
||||
uid = G.UUID
|
||||
}
|
||||
HTTP.postJSON(G.host, data, onHTTPResult)
|
||||
EnterGame.hide()
|
||||
end
|
||||
|
||||
function EnterGame.onError(err)
|
||||
if loadBox then
|
||||
loadBox:destroy()
|
||||
loadBox = nil
|
||||
end
|
||||
local errorBox = displayErrorBox(tr('Login Error'), err)
|
||||
errorBox.onOk = EnterGame.show
|
||||
end
|
||||
|
||||
function EnterGame.onLoginError(err)
|
||||
if loadBox then
|
||||
loadBox:destroy()
|
||||
loadBox = nil
|
||||
end
|
||||
local errorBox = displayErrorBox(tr('Login Error'), err)
|
||||
errorBox.onOk = EnterGame.show
|
||||
EnterGame.clearAccountFields()
|
||||
end
|
9
modules/client_entergame/entergame.otmod
Normal file
9
modules/client_entergame/entergame.otmod
Normal file
@@ -0,0 +1,9 @@
|
||||
Module
|
||||
name: client_entergame
|
||||
description: Manages enter game and character list windows
|
||||
author: edubart & otclient.ovh
|
||||
website: https://github.com/edubart/otclient
|
||||
scripts: [ entergame, characterlist ]
|
||||
@onLoad: EnterGame.init() CharacterList.init()
|
||||
@onUnload: EnterGame.terminate() CharacterList.terminate()
|
||||
|
176
modules/client_entergame/entergame.otui
Normal file
176
modules/client_entergame/entergame.otui
Normal file
@@ -0,0 +1,176 @@
|
||||
EnterGameWindow < StaticMainWindow
|
||||
!text: tr('Enter Game')
|
||||
size: 240 310
|
||||
|
||||
EnterGameWindow
|
||||
id: enterGame
|
||||
@onEnter: EnterGame.doLogin()
|
||||
|
||||
MenuLabel
|
||||
!text: tr('Account name')
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
text-auto-resize: true
|
||||
|
||||
TextEdit
|
||||
id: accountNameTextEdit
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: prev.bottom
|
||||
margin-top: 2
|
||||
|
||||
MenuLabel
|
||||
!text: tr('Password')
|
||||
anchors.left: prev.left
|
||||
anchors.top: prev.bottom
|
||||
margin-top: 8
|
||||
text-auto-resize: true
|
||||
|
||||
PasswordTextEdit
|
||||
id: accountPasswordTextEdit
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: prev.bottom
|
||||
margin-top: 2
|
||||
|
||||
Panel
|
||||
id: serverSelectorPanel
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: prev.bottom
|
||||
height: 52
|
||||
on: true
|
||||
focusable: false
|
||||
|
||||
$on:
|
||||
visible: true
|
||||
margin-top: 0
|
||||
|
||||
$!on:
|
||||
visible: false
|
||||
margin-top: -52
|
||||
|
||||
HorizontalSeparator
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
margin-top: 10
|
||||
|
||||
MenuLabel
|
||||
id: serverLabel
|
||||
!text: tr('Server')
|
||||
anchors.left: parent.left
|
||||
anchors.top: prev.bottom
|
||||
text-auto-resize: true
|
||||
margin-top: 5
|
||||
|
||||
ComboBox
|
||||
id: serverSelector
|
||||
anchors.left: prev.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: serverLabel.bottom
|
||||
margin-top: 2
|
||||
margin-right: 3
|
||||
menu-scroll: true
|
||||
menu-height: 125
|
||||
menu-scroll-step: 25
|
||||
text-offset: 5 2
|
||||
@onOptionChange: EnterGame.onServerChange()
|
||||
|
||||
Panel
|
||||
id: customServerSelectorPanel
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: prev.bottom
|
||||
height: 52
|
||||
on: true
|
||||
focusable: true
|
||||
|
||||
$on:
|
||||
visible: true
|
||||
margin-top: 0
|
||||
|
||||
$!on:
|
||||
visible: false
|
||||
margin-top: -52
|
||||
|
||||
HorizontalSeparator
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
margin-top: 8
|
||||
|
||||
MenuLabel
|
||||
id: serverLabel
|
||||
!text: tr('IP:PORT or url')
|
||||
anchors.left: prev.left
|
||||
anchors.top: prev.bottom
|
||||
margin-top: 8
|
||||
text-auto-resize: true
|
||||
|
||||
TextEdit
|
||||
id: serverHostTextEdit
|
||||
!tooltip: tr('Make sure that your client uses\nthe correct game client version')
|
||||
anchors.left: parent.left
|
||||
anchors.top: serverLabel.bottom
|
||||
margin-top: 2
|
||||
width: 130
|
||||
|
||||
MenuLabel
|
||||
id: clientLabel
|
||||
!text: tr('Version')
|
||||
anchors.left: serverHostTextEdit.right
|
||||
anchors.top: serverLabel.top
|
||||
text-auto-resize: true
|
||||
margin-left: 10
|
||||
|
||||
ComboBox
|
||||
id: clientVersionSelector
|
||||
anchors.top: serverHostTextEdit.top
|
||||
anchors.bottom: serverHostTextEdit.bottom
|
||||
anchors.left: prev.left
|
||||
anchors.right: parent.right
|
||||
menu-scroll: true
|
||||
menu-height: 125
|
||||
menu-scroll-step: 25
|
||||
margin-right: 3
|
||||
|
||||
HorizontalSeparator
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: prev.bottom
|
||||
margin-top: 10
|
||||
|
||||
CheckBox
|
||||
id: rememberPasswordBox
|
||||
!text: tr('Remember password')
|
||||
!tooltip: tr('Remember account and password when starts client')
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: prev.bottom
|
||||
margin-top: 9
|
||||
|
||||
HorizontalSeparator
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: prev.bottom
|
||||
margin-top: 9
|
||||
|
||||
Button
|
||||
!text: tr('Login')
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: prev.bottom
|
||||
margin-top: 10
|
||||
margin-left: 50
|
||||
margin-right: 50
|
||||
@onClick: EnterGame.doLogin()
|
||||
|
||||
Label
|
||||
id: serverInfoLabel
|
||||
font: verdana-11px-rounded
|
||||
anchors.top: prev.top
|
||||
anchors.left: parent.left
|
||||
margin-top: 5
|
||||
color: green
|
||||
text-auto-resize: true
|
48
modules/client_entergame/entergame_new.otui
Normal file
48
modules/client_entergame/entergame_new.otui
Normal file
@@ -0,0 +1,48 @@
|
||||
StaticWindow
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
margin-right: 20
|
||||
id: newLoginPanel
|
||||
width: 230
|
||||
height: 330
|
||||
!text: tr('Quick Login & Registration')
|
||||
|
||||
Label
|
||||
id: qrcode
|
||||
width: 200
|
||||
height: 180
|
||||
anchors.top: parent.top
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
margin-top: 5
|
||||
|
||||
Label
|
||||
anchors.top: prev.bottom
|
||||
anchors.left: prev.left
|
||||
anchors.right: prev.right
|
||||
text-align: center
|
||||
text-auto-resize: true
|
||||
!text: tr("Scan QR code or process\nbellow code to register or login")
|
||||
height: 40
|
||||
margin-top: 10
|
||||
margin-bottom: 5
|
||||
|
||||
Label
|
||||
id: code
|
||||
height: 20
|
||||
anchors.top: prev.bottom
|
||||
anchors.left: prev.left
|
||||
anchors.right: prev.right
|
||||
text-align: center
|
||||
font: sans-bold-16px
|
||||
margin-top: 10
|
||||
text: XXXXXX
|
||||
|
||||
Label
|
||||
anchors.top: prev.bottom
|
||||
anchors.left: prev.left
|
||||
anchors.right: prev.right
|
||||
text-align: center
|
||||
!text: tr("Click to get Android/iOS app")
|
||||
height: 20
|
||||
margin-top: 10
|
||||
color: #FFFFFF
|
44
modules/client_entergame/waitinglist.otui
Normal file
44
modules/client_entergame/waitinglist.otui
Normal file
@@ -0,0 +1,44 @@
|
||||
MainWindow
|
||||
id: waitingWindow
|
||||
!text: tr('Waiting List')
|
||||
size: 260 180
|
||||
@onEscape: CharacterList.cancelWait()
|
||||
|
||||
Label
|
||||
id: infoLabel
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: progressBar.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
text-wrap: true
|
||||
|
||||
ProgressBar
|
||||
id: progressBar
|
||||
height: 15
|
||||
background-color: #4444ff
|
||||
anchors.bottom: timeLabel.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
margin-bottom: 10
|
||||
|
||||
Label
|
||||
id: timeLabel
|
||||
anchors.bottom: separator.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
margin-bottom: 10
|
||||
|
||||
HorizontalSeparator
|
||||
id: separator
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: next.top
|
||||
margin-bottom: 10
|
||||
|
||||
Button
|
||||
id: buttonCancel
|
||||
!text: tr('Cancel')
|
||||
width: 64
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
@onClick: CharacterList.cancelWait()
|
78
modules/client_feedback/feedback.lua
Normal file
78
modules/client_feedback/feedback.lua
Normal file
@@ -0,0 +1,78 @@
|
||||
local feedbackWindow
|
||||
local textEdit
|
||||
local okButton
|
||||
local cancelButton
|
||||
local postId = 0
|
||||
local tries = 0
|
||||
local replyEvent = nil
|
||||
|
||||
function init()
|
||||
feedbackWindow = g_ui.displayUI('feedback')
|
||||
feedbackWindow:hide()
|
||||
|
||||
textEdit = feedbackWindow:getChildById('text')
|
||||
okButton = feedbackWindow:getChildById('okButton')
|
||||
cancelButton = feedbackWindow:getChildById('cancelButton')
|
||||
|
||||
okButton.onClick = send
|
||||
cancelButton.onClick = hide
|
||||
feedbackWindow.onEscape = hide
|
||||
end
|
||||
|
||||
function terminate()
|
||||
feedbackWindow:destroy()
|
||||
removeEvent(replyEvent)
|
||||
end
|
||||
|
||||
function show()
|
||||
if Services.feedback == nil or Services.feedback:len() < 4 then
|
||||
return
|
||||
end
|
||||
|
||||
feedbackWindow:show()
|
||||
feedbackWindow:raise()
|
||||
feedbackWindow:focus()
|
||||
|
||||
textEdit:setMaxLength(8192)
|
||||
textEdit:setText('')
|
||||
textEdit:setEditable(true)
|
||||
textEdit:setCursorVisible(true)
|
||||
feedbackWindow:focusChild(textEdit, KeyboardFocusReason)
|
||||
|
||||
tries = 0
|
||||
end
|
||||
|
||||
function hide()
|
||||
feedbackWindow:hide()
|
||||
textEdit:setEditable(false)
|
||||
textEdit:setCursorVisible(false)
|
||||
end
|
||||
|
||||
function send()
|
||||
local text = textEdit:getText()
|
||||
if text:len() > 1 then
|
||||
local localPlayer = g_game.getLocalPlayer()
|
||||
local playerData = nil
|
||||
if localPlayer ~= nil then
|
||||
playerData = {
|
||||
name = localPlayer:getName(),
|
||||
position = localPlayer:getPosition()
|
||||
}
|
||||
end
|
||||
local data = json.encode({
|
||||
text = text,
|
||||
version = g_app.getVersion(),
|
||||
host = g_settings.get('host'),
|
||||
player = playerData
|
||||
})
|
||||
postId = HTTP.post(Services.feedback, data, function(ret, err)
|
||||
if err then
|
||||
tries = tries + 1
|
||||
if tries < 3 then
|
||||
replyEvent = scheduleEvent(send, 1000)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
hide()
|
||||
end
|
10
modules/client_feedback/feedback.otmod
Normal file
10
modules/client_feedback/feedback.otmod
Normal file
@@ -0,0 +1,10 @@
|
||||
Module
|
||||
name: client_feedback
|
||||
description: Allow to send feedback
|
||||
author: otclientv8
|
||||
website: otclient.ovh
|
||||
sandboxed: true
|
||||
dependencies: [ game_interface ]
|
||||
scripts: [ feedback ]
|
||||
@onLoad: init()
|
||||
@onUnload: terminate()
|
48
modules/client_feedback/feedback.otui
Normal file
48
modules/client_feedback/feedback.otui
Normal file
@@ -0,0 +1,48 @@
|
||||
MainWindow
|
||||
id: feedbackWindow
|
||||
size: 300 280
|
||||
!text: tr("Feedback/Bug report")
|
||||
|
||||
Label
|
||||
id: description
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
text-auto-resize: true
|
||||
text-align: left
|
||||
text-wrap: true
|
||||
!text: tr("Bellow enter your feedback or bug report. Please include as much details as possible.")
|
||||
|
||||
MultilineTextEdit
|
||||
id: text
|
||||
anchors.top: textScroll.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: textScroll.left
|
||||
anchors.bottom: textScroll.bottom
|
||||
vertical-scrollbar: textScroll
|
||||
text-wrap: true
|
||||
|
||||
VerticalScrollBar
|
||||
id: textScroll
|
||||
anchors.top: description.bottom
|
||||
anchors.bottom: okButton.top
|
||||
anchors.right: parent.right
|
||||
margin-top: 10
|
||||
margin-bottom: 10
|
||||
step: 16
|
||||
pixels-scroll: true
|
||||
|
||||
Button
|
||||
id: okButton
|
||||
!text: tr('Ok')
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: next.left
|
||||
margin-right: 10
|
||||
width: 60
|
||||
|
||||
Button
|
||||
id: cancelButton
|
||||
!text: tr('Cancel')
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
width: 60
|
202
modules/client_locales/locales.lua
Normal file
202
modules/client_locales/locales.lua
Normal file
@@ -0,0 +1,202 @@
|
||||
dofile 'neededtranslations'
|
||||
|
||||
-- private variables
|
||||
local defaultLocaleName = 'en'
|
||||
local installedLocales
|
||||
local currentLocale
|
||||
|
||||
function sendLocale(localeName)
|
||||
local protocolGame = g_game.getProtocolGame()
|
||||
if protocolGame then
|
||||
protocolGame:sendExtendedOpcode(ExtendedIds.Locale, localeName)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function createWindow()
|
||||
localesWindow = g_ui.displayUI('locales')
|
||||
local localesPanel = localesWindow:getChildById('localesPanel')
|
||||
local layout = localesPanel:getLayout()
|
||||
local spacing = layout:getCellSpacing()
|
||||
local size = layout:getCellSize()
|
||||
|
||||
local count = 0
|
||||
for name,locale in pairs(installedLocales) do
|
||||
local widget = g_ui.createWidget('LocalesButton', localesPanel)
|
||||
widget:setImageSource('/images/flags/' .. name .. '')
|
||||
widget:setText(locale.languageName)
|
||||
widget.onClick = function() selectFirstLocale(name) end
|
||||
count = count + 1
|
||||
end
|
||||
|
||||
count = math.max(1, math.min(count, 3))
|
||||
localesPanel:setWidth(size.width*count + spacing*(count-1))
|
||||
|
||||
addEvent(function() addEvent(function() localesWindow:raise() localesWindow:focus() end) end)
|
||||
end
|
||||
|
||||
function selectFirstLocale(name)
|
||||
if localesWindow then
|
||||
localesWindow:destroy()
|
||||
localesWindow = nil
|
||||
end
|
||||
if setLocale(name) then
|
||||
g_modules.reloadModules()
|
||||
end
|
||||
g_settings.save()
|
||||
end
|
||||
|
||||
-- hooked functions
|
||||
function onGameStart()
|
||||
sendLocale(currentLocale.name)
|
||||
end
|
||||
|
||||
function onExtendedLocales(protocol, opcode, buffer)
|
||||
local locale = installedLocales[buffer]
|
||||
if locale and setLocale(locale.name) then
|
||||
g_modules.reloadModules()
|
||||
end
|
||||
end
|
||||
|
||||
-- public functions
|
||||
function init()
|
||||
installedLocales = {}
|
||||
|
||||
installLocales('/locales')
|
||||
|
||||
local userLocaleName = g_settings.get('locale', 'false')
|
||||
if userLocaleName ~= 'false' and setLocale(userLocaleName) then
|
||||
pdebug('Using configured locale: ' .. userLocaleName)
|
||||
else
|
||||
setLocale(defaultLocaleName)
|
||||
--connect(g_app, { onRun = createWindow })
|
||||
end
|
||||
|
||||
ProtocolGame.registerExtendedOpcode(ExtendedIds.Locale, onExtendedLocales)
|
||||
connect(g_game, { onGameStart = onGameStart })
|
||||
end
|
||||
|
||||
function terminate()
|
||||
installedLocales = nil
|
||||
currentLocale = nil
|
||||
|
||||
ProtocolGame.unregisterExtendedOpcode(ExtendedIds.Locale)
|
||||
disconnect(g_app, { onRun = createWindow })
|
||||
disconnect(g_game, { onGameStart = onGameStart })
|
||||
end
|
||||
|
||||
function generateNewTranslationTable(localename)
|
||||
local locale = installedLocales[localename]
|
||||
for _i,k in pairs(neededTranslations) do
|
||||
local trans = locale.translation[k]
|
||||
k = k:gsub('\n','\\n')
|
||||
k = k:gsub('\t','\\t')
|
||||
k = k:gsub('\"','\\\"')
|
||||
if trans then
|
||||
trans = trans:gsub('\n','\\n')
|
||||
trans = trans:gsub('\t','\\t')
|
||||
trans = trans:gsub('\"','\\\"')
|
||||
end
|
||||
if not trans then
|
||||
print(' ["' .. k .. '"]' .. ' = false,')
|
||||
else
|
||||
print(' ["' .. k .. '"]' .. ' = "' .. trans .. '",')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function installLocale(locale)
|
||||
if not locale or not locale.name then
|
||||
error('Unable to install locale.')
|
||||
end
|
||||
|
||||
if _G.allowedLocales and not _G.allowedLocales[locale.name] then return end
|
||||
|
||||
if locale.name ~= defaultLocaleName then
|
||||
local updatesNamesMissing = {}
|
||||
for _,k in pairs(neededTranslations) do
|
||||
if locale.translation[k] == nil then
|
||||
updatesNamesMissing[#updatesNamesMissing + 1] = k
|
||||
end
|
||||
end
|
||||
|
||||
if #updatesNamesMissing > 0 then
|
||||
pdebug('Locale \'' .. locale.name .. '\' is missing ' .. #updatesNamesMissing .. ' translations.')
|
||||
for _,name in pairs(updatesNamesMissing) do
|
||||
pdebug('["' .. name ..'"] = \"\",')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local installedLocale = installedLocales[locale.name]
|
||||
if installedLocale then
|
||||
for word,translation in pairs(locale.translation) do
|
||||
installedLocale.translation[word] = translation
|
||||
end
|
||||
else
|
||||
installedLocales[locale.name] = locale
|
||||
end
|
||||
end
|
||||
|
||||
function installLocales(directory)
|
||||
dofiles(directory)
|
||||
end
|
||||
|
||||
function setLocale(name)
|
||||
local locale = installedLocales[name]
|
||||
if locale == currentLocale then return end
|
||||
if not locale then
|
||||
pwarning("Locale " .. name .. ' does not exist.')
|
||||
return false
|
||||
end
|
||||
if currentLocale then
|
||||
sendLocale(locale.name)
|
||||
end
|
||||
currentLocale = locale
|
||||
g_settings.set('locale', name)
|
||||
if onLocaleChanged then onLocaleChanged(name) end
|
||||
return true
|
||||
end
|
||||
|
||||
function getInstalledLocales()
|
||||
return installedLocales
|
||||
end
|
||||
|
||||
function getCurrentLocale()
|
||||
return currentLocale
|
||||
end
|
||||
|
||||
-- global function used to translate texts
|
||||
function _G.tr(text, ...)
|
||||
if currentLocale then
|
||||
if tonumber(text) and currentLocale.formatNumbers then
|
||||
local number = tostring(text):split('.')
|
||||
local out = ''
|
||||
local reverseNumber = number[1]:reverse()
|
||||
for i=1,#reverseNumber do
|
||||
out = out .. reverseNumber:sub(i, i)
|
||||
if i % 3 == 0 and i ~= #number then
|
||||
out = out .. currentLocale.thousandsSeperator
|
||||
end
|
||||
end
|
||||
|
||||
if number[2] then
|
||||
out = number[2] .. currentLocale.decimalSeperator .. out
|
||||
end
|
||||
return out:reverse()
|
||||
elseif tostring(text) then
|
||||
local translation = currentLocale.translation[text]
|
||||
if not translation then
|
||||
if translation == nil then
|
||||
if currentLocale.name ~= defaultLocaleName then
|
||||
pdebug('Unable to translate: \"' .. text .. '\"')
|
||||
end
|
||||
end
|
||||
translation = text
|
||||
end
|
||||
return string.format(translation, ...)
|
||||
end
|
||||
end
|
||||
return text
|
||||
end
|
9
modules/client_locales/locales.otmod
Normal file
9
modules/client_locales/locales.otmod
Normal file
@@ -0,0 +1,9 @@
|
||||
Module
|
||||
name: client_locales
|
||||
description: Translates texts to selected language
|
||||
author: baxnie, edubart
|
||||
website: https://github.com/edubart/otclient
|
||||
sandboxed: true
|
||||
scripts: [ locales ]
|
||||
@onLoad: init()
|
||||
@onUnload: terminate()
|
35
modules/client_locales/locales.otui
Normal file
35
modules/client_locales/locales.otui
Normal file
@@ -0,0 +1,35 @@
|
||||
LocalesMainLabel < Label
|
||||
font: sans-bold-16px
|
||||
|
||||
LocalesButton < UIWidget
|
||||
size: 96 96
|
||||
image-size: 96 96
|
||||
image-smooth: true
|
||||
text-offset: 0 96
|
||||
font: verdana-11px-antialised
|
||||
|
||||
UIWindow
|
||||
id: localesWindow
|
||||
background-color: #000000
|
||||
opacity: 0.90
|
||||
clipping: true
|
||||
anchors.fill: parent
|
||||
|
||||
LocalesMainLabel
|
||||
!text: tr('Select your language')
|
||||
text-auto-resize: true
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
margin-top: -100
|
||||
|
||||
Panel
|
||||
id: localesPanel
|
||||
margin-top: 50
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: prev.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
layout:
|
||||
type: grid
|
||||
cell-size: 96 128
|
||||
cell-spacing: 32
|
||||
flow: true
|
364
modules/client_locales/neededtranslations.lua
Normal file
364
modules/client_locales/neededtranslations.lua
Normal file
@@ -0,0 +1,364 @@
|
||||
-- generated by ./tools/gen_needed_translations.sh
|
||||
neededTranslations = {
|
||||
"1a) Offensive Name",
|
||||
"1b) Invalid Name Format",
|
||||
"1c) Unsuitable Name",
|
||||
"1d) Name Inciting Rule Violation",
|
||||
"2a) Offensive Statement",
|
||||
"2b) Spamming",
|
||||
"2c) Illegal Advertising",
|
||||
"2d) Off-Topic Public Statement",
|
||||
"2e) Non-English Public Statement",
|
||||
"2f) Inciting Rule Violation",
|
||||
"3a) Bug Abuse",
|
||||
"3b) Game Weakness Abuse",
|
||||
"3c) Using Unofficial Software to Play",
|
||||
"3d) Hacking",
|
||||
"3e) Multi-Clienting",
|
||||
"3f) Account Trading or Sharing",
|
||||
"4a) Threatening Gamemaster",
|
||||
"4b) Pretending to Have Influence on Rule Enforcement",
|
||||
"4c) False Report to Gamemaster",
|
||||
"Accept",
|
||||
"Account name",
|
||||
"Account Status:",
|
||||
"Action:",
|
||||
"Add",
|
||||
"Add new VIP",
|
||||
"Addon 1",
|
||||
"Addon 2",
|
||||
"Addon 3",
|
||||
"Add to VIP list",
|
||||
"Adjust volume",
|
||||
"Alas! Brave adventurer, you have met a sad fate.\nBut do not despair, for the gods will bring you back\ninto this world in exchange for a small sacrifice\n\nSimply click on Ok to resume your journeys!",
|
||||
"All",
|
||||
"All modules and scripts were reloaded.",
|
||||
"Allow auto chase override",
|
||||
"Ambient light: %s%%",
|
||||
"Amount:",
|
||||
"Amount",
|
||||
"Anonymous",
|
||||
"Are you sure you want to logout?",
|
||||
"Attack",
|
||||
"Author",
|
||||
"Autoload",
|
||||
"Autoload priority",
|
||||
"Auto login",
|
||||
"Auto login selected character on next charlist load",
|
||||
"Axe Fighting",
|
||||
"Balance:",
|
||||
"Banishment",
|
||||
"Banishment + Final Warning",
|
||||
"Battle",
|
||||
"Browse",
|
||||
"Bug report sent.",
|
||||
"Button Assign",
|
||||
"Buy",
|
||||
"Buy Now",
|
||||
"Buy Offers",
|
||||
"Buy with backpack",
|
||||
"Cancel",
|
||||
"Cannot login while already in game.",
|
||||
"Cap",
|
||||
"Capacity",
|
||||
"Center",
|
||||
"Channels",
|
||||
"Character List",
|
||||
"Classic control",
|
||||
"Clear current message window",
|
||||
"Clear Messages",
|
||||
"Clear object",
|
||||
"Client needs update.",
|
||||
"Close",
|
||||
"Close this channel",
|
||||
"Club Fighting",
|
||||
"Combat Controls",
|
||||
"Comment:",
|
||||
"Connecting to game server...",
|
||||
"Connecting to login server...",
|
||||
"Console",
|
||||
"Cooldowns",
|
||||
"Copy message",
|
||||
"Copy name",
|
||||
"Copy Name",
|
||||
"Create Map Mark",
|
||||
"Create mark",
|
||||
"Create New Offer",
|
||||
"Create Offer",
|
||||
"Current hotkeys:",
|
||||
"Current hotkey to add: %s",
|
||||
"Current Offers",
|
||||
"Default",
|
||||
"Delete mark",
|
||||
"Description:",
|
||||
"Description",
|
||||
"Destructive Behaviour",
|
||||
"Detail",
|
||||
"Details",
|
||||
"Disable Shared Experience",
|
||||
"Dismount",
|
||||
"Display connection speed to the server (milliseconds)",
|
||||
"Distance Fighting",
|
||||
"Don\'t stretch/shrink Game Window",
|
||||
"Edit hotkey text:",
|
||||
"Edit List",
|
||||
"Edit Text",
|
||||
"Enable music",
|
||||
"Enable Shared Experience",
|
||||
"Enable smart walking",
|
||||
"Enable vertical synchronization",
|
||||
"Enable walk booster",
|
||||
"Enter Game",
|
||||
"Enter one name per line.",
|
||||
"Enter with your account again to update your client.",
|
||||
"Error",
|
||||
"Error",
|
||||
"Excessive Unjustified Player Killing",
|
||||
"Exclude from private chat",
|
||||
"Exit",
|
||||
"Experience",
|
||||
"Filter list to match your level",
|
||||
"Filter list to match your vocation",
|
||||
"Find:",
|
||||
"Fishing",
|
||||
"Fist Fighting",
|
||||
"Follow",
|
||||
"Force Exit",
|
||||
"For Your Information",
|
||||
"Free Account",
|
||||
"Fullscreen",
|
||||
"Game",
|
||||
"Game framerate limit: %s",
|
||||
"Graphics",
|
||||
"Graphics card driver not detected",
|
||||
"Graphics Engine:",
|
||||
"Head",
|
||||
"Healing",
|
||||
"Health Info",
|
||||
"Health Information",
|
||||
"Hide monsters",
|
||||
"Hide non-skull players",
|
||||
"Hide Npcs",
|
||||
"Hide Offline",
|
||||
"Hide party members",
|
||||
"Hide players",
|
||||
"Hide spells for higher exp. levels",
|
||||
"Hide spells for other vocations",
|
||||
"Hit Points",
|
||||
"Hold left mouse button to navigate\nScroll mouse middle button to zoom\nRight mouse button to create map marks",
|
||||
"Hotkeys",
|
||||
"If you shut down the program, your character might stay in the game.\nClick on 'Logout' to ensure that you character leaves the game properly.\nClick on 'Exit' if you want to exit the program without logging out your character.",
|
||||
"Ignore",
|
||||
"Ignore capacity",
|
||||
"Ignored players:",
|
||||
"Ignore equipped",
|
||||
"Ignore List",
|
||||
"Ignore players",
|
||||
"Ignore Private Messages",
|
||||
"Ignore Yelling",
|
||||
"Interface framerate limit: %s",
|
||||
"Inventory",
|
||||
"Invite to Party",
|
||||
"Invite to private chat",
|
||||
"IP Address Banishment",
|
||||
"Item Offers",
|
||||
"It is empty.",
|
||||
"Join %s\'s Party",
|
||||
"Leave Party",
|
||||
"Level",
|
||||
"Lifetime Premium Account",
|
||||
"Limits FPS to 60",
|
||||
"List of items that you're able to buy",
|
||||
"List of items that you're able to sell",
|
||||
"Load",
|
||||
"Logging out...",
|
||||
"Login",
|
||||
"Login Error",
|
||||
"Login Error",
|
||||
"Logout",
|
||||
"Look",
|
||||
"Magic Level",
|
||||
"Make sure that your client uses\nthe correct game protocol version",
|
||||
"Mana",
|
||||
"Manage hotkeys:",
|
||||
"Market",
|
||||
"Market Offers",
|
||||
"Message of the day",
|
||||
"Message to ",
|
||||
"Message to %s",
|
||||
"Minimap",
|
||||
"Module Manager",
|
||||
"Module name",
|
||||
"Mount",
|
||||
"Move Stackable Item",
|
||||
"Move up",
|
||||
"My Offers",
|
||||
"Name:",
|
||||
"Name Report",
|
||||
"Name Report + Banishment",
|
||||
"Name Report + Banishment + Final Warning",
|
||||
"No",
|
||||
"No graphics card detected, everything will be drawn using the CPU,\nthus the performance will be really bad.\nPlease update your graphics driver to have a better performance.",
|
||||
"No item selected.",
|
||||
"No Mount",
|
||||
"No Outfit",
|
||||
"No statement has been selected.",
|
||||
"Notation",
|
||||
"NPC Trade",
|
||||
"Offer History",
|
||||
"Offers",
|
||||
"Offer Type:",
|
||||
"Offline Training",
|
||||
"Ok",
|
||||
"on %s.\n",
|
||||
"Open",
|
||||
"Open a private message channel:",
|
||||
"Open charlist automatically when starting client",
|
||||
"Open in new window",
|
||||
"Open new channel",
|
||||
"Options",
|
||||
"Overview",
|
||||
"Pass Leadership to %s",
|
||||
"Password",
|
||||
"Piece Price:",
|
||||
"Please enter a character name:",
|
||||
"Please, press the key you wish to add onto your hotkeys manager",
|
||||
"Please Select",
|
||||
"Please use this dialog to only report bugs. Do not report rule violations here!",
|
||||
"Please wait",
|
||||
"Port",
|
||||
"Position:",
|
||||
"Position: %i %i %i",
|
||||
"Premium Account (%s) days left",
|
||||
"Price:",
|
||||
"Primary",
|
||||
"Protocol",
|
||||
"Quest Log",
|
||||
"Randomize",
|
||||
"Randomize characters outfit",
|
||||
"Reason:",
|
||||
"Refresh",
|
||||
"Refresh Offers",
|
||||
"Regeneration Time",
|
||||
"Reject",
|
||||
"Reload All",
|
||||
"Remember account and password when starts client",
|
||||
"Remember password",
|
||||
"Remove",
|
||||
"Remove %s",
|
||||
"Report Bug",
|
||||
"Reserved for more functionality later.",
|
||||
"Reset Market",
|
||||
"Revoke %s\'s Invitation",
|
||||
"Rotate",
|
||||
"Rule Violation",
|
||||
"Save",
|
||||
"Save Messages",
|
||||
"Search:",
|
||||
"Search all items",
|
||||
"Secondary",
|
||||
"Select object",
|
||||
"Select Outfit",
|
||||
"Select your language",
|
||||
"Sell",
|
||||
"Sell Now",
|
||||
"Sell Offers",
|
||||
"Send",
|
||||
"Send automatically",
|
||||
"Send Message",
|
||||
"Server",
|
||||
"Server Log",
|
||||
"Set Outfit",
|
||||
"Shielding",
|
||||
"Show all items",
|
||||
"Show connection ping",
|
||||
"Show Depot Only",
|
||||
"Show event messages in console",
|
||||
"Show frame rate",
|
||||
"Show info messages in console",
|
||||
"Show left panel",
|
||||
"Show levels in console",
|
||||
"Show Offline",
|
||||
"Show private messages in console",
|
||||
"Show private messages on screen",
|
||||
"Show Server Messages",
|
||||
"Show status messages in console",
|
||||
"Show Text",
|
||||
"Show timestamps in console",
|
||||
"Show your depot items only",
|
||||
"Skills",
|
||||
"Soul",
|
||||
"Soul Points",
|
||||
"Special",
|
||||
"Speed",
|
||||
"Spell Cooldowns",
|
||||
"Spell List",
|
||||
"Stamina",
|
||||
"Statement:",
|
||||
"Statement Report",
|
||||
"Statistics",
|
||||
"Stop Attack",
|
||||
"Stop Follow",
|
||||
"Support",
|
||||
"%s: (use object)",
|
||||
"%s: (use object on target)",
|
||||
"%s: (use object on yourself)",
|
||||
"%s: (use object with crosshair)",
|
||||
"Sword Fighting",
|
||||
"Terminal",
|
||||
"There is no way.",
|
||||
"Title",
|
||||
"Total Price:",
|
||||
"Trade",
|
||||
"Trade with ...",
|
||||
"Trying to reconnect in %s seconds.",
|
||||
"Unable to load dat file, please place a valid dat in '%s'",
|
||||
"Unable to load spr file, please place a valid spr in '%s'",
|
||||
"Unable to logout.",
|
||||
"Unignore",
|
||||
"Unload",
|
||||
"Update needed",
|
||||
"Use",
|
||||
"Use on target",
|
||||
"Use on yourself",
|
||||
"Use with ...",
|
||||
"Version",
|
||||
"VIP List",
|
||||
"Voc.",
|
||||
"Vocation",
|
||||
"Waiting List",
|
||||
"Website",
|
||||
"Weight:",
|
||||
"Will detect when to use diagonal step based on the\nkeys you are pressing",
|
||||
"With crosshair",
|
||||
"Yes",
|
||||
"You are bleeding",
|
||||
"You are burning",
|
||||
"You are cursed",
|
||||
"You are dazzled",
|
||||
"You are dead.",
|
||||
"You are dead",
|
||||
"You are drowning",
|
||||
"You are drunk",
|
||||
"You are electrified",
|
||||
"You are freezing",
|
||||
"You are hasted",
|
||||
"You are hungry",
|
||||
"You are paralysed",
|
||||
"You are poisoned",
|
||||
"You are protected by a magic shield",
|
||||
"You are strengthened",
|
||||
"You are within a protection zone",
|
||||
"You can enter new text.",
|
||||
"You have %s percent",
|
||||
"You have %s percent to go",
|
||||
"You may not logout during a fight",
|
||||
"You may not logout or enter a protection zone",
|
||||
"You must enter a comment.",
|
||||
"You must enter a valid server address and port.",
|
||||
"You must select a character to login!",
|
||||
"Your Capacity:",
|
||||
"You read the following, written by \n%s\n",
|
||||
"You read the following, written on \n%s.\n",
|
||||
"Your Money:",
|
||||
}
|
119
modules/client_news/news.lua
Normal file
119
modules/client_news/news.lua
Normal file
@@ -0,0 +1,119 @@
|
||||
-- private variables
|
||||
local news
|
||||
local newsPanel
|
||||
local updateNewsEvent = nil
|
||||
local ongoingNewsUpdate = false
|
||||
local lastNewsUpdate = 0
|
||||
local newsUpdateInterval = 30 -- seconds
|
||||
|
||||
-- public functions
|
||||
function init()
|
||||
news = g_ui.displayUI('news')
|
||||
newsPanel = news:recursiveGetChildById('newsPanel')
|
||||
|
||||
connect(rootWidget, { onGeometryChange = updateSize })
|
||||
connect(g_game, { onGameStart = hide, onGameEnd = show })
|
||||
|
||||
if g_game.isOnline() then
|
||||
hide()
|
||||
else
|
||||
show()
|
||||
end
|
||||
end
|
||||
|
||||
function terminate()
|
||||
disconnect(rootWidget, { onGeometryChange = updateSize })
|
||||
disconnect(g_game, { onGameStart = hide, onGameEnd = show })
|
||||
|
||||
removeEvent(updateNewsEvent)
|
||||
clearNews()
|
||||
|
||||
news:destroy()
|
||||
news = nil
|
||||
end
|
||||
|
||||
function hide()
|
||||
news:hide()
|
||||
end
|
||||
|
||||
function show()
|
||||
news:show()
|
||||
updateSize()
|
||||
updateNews()
|
||||
end
|
||||
|
||||
function updateSize()
|
||||
if Services.news == nil or Services.news:len() < 4 or g_game.isOnline() then
|
||||
return
|
||||
end
|
||||
if rootWidget:getWidth() < 790 and news:isVisible() then
|
||||
hide()
|
||||
elseif news:isHidden() then
|
||||
show()
|
||||
end
|
||||
news:setWidth(math.min(math.max(250, rootWidget:getWidth() / 4), 300))
|
||||
end
|
||||
|
||||
function updateNews()
|
||||
if Services.news == nil or Services.news:len() < 4 then
|
||||
hide()
|
||||
return
|
||||
end
|
||||
if ongoingNewsUpdate or os.time() < lastNewsUpdate + newsUpdateInterval then
|
||||
return
|
||||
end
|
||||
HTTP.getJSON(Services.news .. "?lang=" .. modules.client_locales.getCurrentLocale().name, onGotNews)
|
||||
ongoingNewsUpdate = true
|
||||
lastNewsUpdate = os.time()
|
||||
end
|
||||
|
||||
function clearNews()
|
||||
while newsPanel:getChildCount() > 0 do
|
||||
local child = newsPanel:getLastChild()
|
||||
newsPanel:destroyChildren(child)
|
||||
end
|
||||
end
|
||||
|
||||
function onGotNews(data, err)
|
||||
ongoingNewsUpdate = false
|
||||
if err then
|
||||
return gotNewsError("Error:\n" .. err)
|
||||
end
|
||||
|
||||
clearNews()
|
||||
|
||||
for i, news in pairs(data) do
|
||||
local title = news["title"]
|
||||
local text = news["text"]
|
||||
local image = news["image"]
|
||||
if title ~= nil then
|
||||
newsLabel = g_ui.createWidget('NewsLabel', newsPanel)
|
||||
newsLabel:setText(title)
|
||||
end
|
||||
if text ~= nil then
|
||||
newsText = g_ui.createWidget('NewsText', newsPanel)
|
||||
newsText:setText(text)
|
||||
end
|
||||
if image ~= nil then
|
||||
newsImage = g_ui.createWidget('NewsImage', newsPanel)
|
||||
newsImage:setId(imageName)
|
||||
newsImage:setImageSourceBase64(image)
|
||||
newsImage:setImageFixedRatio(true)
|
||||
newsImage:setImageAutoResize(false)
|
||||
newsImage:setHeight(200)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function gotNewsError(err)
|
||||
updateNewsEvent = scheduleEvent(function()
|
||||
updateNews()
|
||||
end, 3000)
|
||||
|
||||
clearNews()
|
||||
errorLabel = g_ui.createWidget('NewsLabel', newsPanel)
|
||||
errorLabel:setText(tr("Error"))
|
||||
errorInfo = g_ui.createWidget('NewsText', newsPanel)
|
||||
errorInfo:setText(err)
|
||||
ongoingNewsUpdate = true
|
||||
end
|
10
modules/client_news/news.otmod
Normal file
10
modules/client_news/news.otmod
Normal file
@@ -0,0 +1,10 @@
|
||||
Module
|
||||
name: client_news
|
||||
description: News
|
||||
author: otclient.ovh
|
||||
website: http://otclient.ovh
|
||||
sandboxed: true
|
||||
scripts: [ news ]
|
||||
dependencies: [ client_topmenu ]
|
||||
@onLoad: init()
|
||||
@onUnload: terminate()
|
47
modules/client_news/news.otui
Normal file
47
modules/client_news/news.otui
Normal file
@@ -0,0 +1,47 @@
|
||||
NewsLabel < Label
|
||||
text-wrap: false
|
||||
text-auto-resize: true
|
||||
text-align: center
|
||||
font: terminus-14px-bold
|
||||
|
||||
NewsText < Label
|
||||
text-wrap: true
|
||||
text-auto-resize: true
|
||||
text-align: left
|
||||
margin-bottom: 10
|
||||
|
||||
NewsImage < Label
|
||||
text-wrap: true
|
||||
margin-bottom: 5
|
||||
text-align: center
|
||||
|
||||
StaticWindow
|
||||
anchors.left: parent.left
|
||||
anchors.top: topMenu.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
margin-top: 10
|
||||
margin-left: 20
|
||||
margin-bottom: 10
|
||||
id: newsPanelHolder
|
||||
width: 300
|
||||
!text: tr('News')
|
||||
|
||||
ScrollablePanel
|
||||
id: newsPanel
|
||||
layout:
|
||||
type: verticalBox
|
||||
vertical-scrollbar: newsScroll
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
padding-right: 10
|
||||
margin-right: 10
|
||||
|
||||
VerticalScrollBar
|
||||
id: newsScroll
|
||||
anchors.top: newsPanel.top
|
||||
anchors.bottom: newsPanel.bottom
|
||||
anchors.left: newsPanel.right
|
||||
step: 14
|
||||
pixels-scroll: true
|
28
modules/client_options/audio.otui
Normal file
28
modules/client_options/audio.otui
Normal file
@@ -0,0 +1,28 @@
|
||||
Panel
|
||||
OptionCheckBox
|
||||
id: enableAudio
|
||||
!text: tr('Enable audio')
|
||||
|
||||
OptionCheckBox
|
||||
id: enableMusicSound
|
||||
!text: tr('Enable music sound')
|
||||
|
||||
Label
|
||||
id: musicSoundVolumeLabel
|
||||
!text: tr('Music volume: %d', 100)
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: prev.bottom
|
||||
margin-top: 6
|
||||
@onSetup: |
|
||||
local value = modules.client_options.getOption('musicSoundVolume')
|
||||
self:setText(tr('Music volume: %d', value))
|
||||
|
||||
OptionScrollbar
|
||||
id: musicSoundVolume
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: prev.bottom
|
||||
margin-top: 3
|
||||
minimum: 0
|
||||
maximum: 100
|
28
modules/client_options/console.otui
Normal file
28
modules/client_options/console.otui
Normal file
@@ -0,0 +1,28 @@
|
||||
Panel
|
||||
OptionCheckBox
|
||||
id: showInfoMessagesInConsole
|
||||
!text: tr('Show info messages in console')
|
||||
|
||||
OptionCheckBox
|
||||
id: showEventMessagesInConsole
|
||||
!text: tr('Show event messages in console')
|
||||
|
||||
OptionCheckBox
|
||||
id: showStatusMessagesInConsole
|
||||
!text: tr('Show status messages in console')
|
||||
|
||||
OptionCheckBox
|
||||
id: showTimestampsInConsole
|
||||
!text: tr('Show timestamps in console')
|
||||
|
||||
OptionCheckBox
|
||||
id: showLevelsInConsole
|
||||
!text: tr('Show levels in console')
|
||||
|
||||
OptionCheckBox
|
||||
id: showPrivateMessagesInConsole
|
||||
!text: tr('Show private messages in console')
|
||||
|
||||
OptionCheckBox
|
||||
id: showPrivateMessagesOnScreen
|
||||
!text: tr('Show private messages on screen')
|
113
modules/client_options/game.otui
Normal file
113
modules/client_options/game.otui
Normal file
@@ -0,0 +1,113 @@
|
||||
Panel
|
||||
OptionCheckBox
|
||||
id: classicControl
|
||||
!text: tr('Classic control')
|
||||
|
||||
OptionCheckBox
|
||||
id: autoChaseOverride
|
||||
!text: tr('Allow auto chase override')
|
||||
|
||||
OptionCheckBox
|
||||
id: displayText
|
||||
!text: tr('Display text messages')
|
||||
|
||||
OptionCheckBox
|
||||
id: wsadWalking
|
||||
!text: tr('Enable WSAD walking')
|
||||
!tooltip: tr('Disable chat and allow walk using WSAD keys')
|
||||
|
||||
OptionCheckBox
|
||||
id: extentedPreWalking
|
||||
!text: tr('Enable smooth walking (DASH)')
|
||||
!tooltip: tr('Allows to execute next move without server confirmation of previous one')
|
||||
|
||||
OptionCheckBox
|
||||
id: smartWalk
|
||||
!text: tr('Enable smart walking')
|
||||
!tooltip: tr('Will detect when to use diagonal step based on the\nkeys you are pressing')
|
||||
|
||||
Label
|
||||
anchors.top: prev.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
id: walkFirstStepDelayLabel
|
||||
margin-top: 10
|
||||
@onSetup: |
|
||||
local value = modules.client_options.getOption('walkFirstStepDelay')
|
||||
self:setText(tr('Walk delay after first step: %s ms', value))
|
||||
|
||||
OptionScrollbar
|
||||
id: walkFirstStepDelay
|
||||
anchors.top: prev.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
margin-top: 3
|
||||
minimum: 0
|
||||
maximum: 300
|
||||
|
||||
Label
|
||||
anchors.top: prev.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
id: walkTurnDelayLabel
|
||||
margin-top: 10
|
||||
@onSetup: |
|
||||
local value = modules.client_options.getOption('walkTurnDelay')
|
||||
self:setText(tr('Walk delay after turn: %s ms', value))
|
||||
|
||||
OptionScrollbar
|
||||
id: walkTurnDelay
|
||||
anchors.top: prev.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
margin-top: 3
|
||||
minimum: 0
|
||||
maximum: 300
|
||||
|
||||
Label
|
||||
anchors.top: prev.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
id: walkStairsDelayLabel
|
||||
margin-top: 10
|
||||
@onSetup: |
|
||||
local value = modules.client_options.getOption('walkStairsDelay')
|
||||
self:setText(tr('Walk delay after floor change: %s ms', value))
|
||||
|
||||
OptionScrollbar
|
||||
id: walkStairsDelay
|
||||
anchors.top: prev.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
margin-top: 3
|
||||
minimum: 0
|
||||
maximum: 300
|
||||
|
||||
Label
|
||||
anchors.top: prev.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
id: walkTeleportDelayLabel
|
||||
margin-top: 10
|
||||
@onSetup: |
|
||||
local value = modules.client_options.getOption('walkTeleportDelay')
|
||||
self:setText(tr('Walk delay after teleport: %s ms', value))
|
||||
|
||||
OptionScrollbar
|
||||
id: walkTeleportDelay
|
||||
anchors.top: prev.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
margin-top: 3
|
||||
minimum: 0
|
||||
maximum: 300
|
||||
|
||||
Button
|
||||
id: changeLocale
|
||||
!text: tr('Change language')
|
||||
@onClick: modules.client_locales.createWindow()
|
||||
anchors.top: prev.bottom
|
||||
anchors.left: prev.left
|
||||
margin-top: 12
|
||||
width: 120
|
||||
|
125
modules/client_options/graphics.otui
Normal file
125
modules/client_options/graphics.otui
Normal file
@@ -0,0 +1,125 @@
|
||||
Panel
|
||||
Label
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
text-wrap: false
|
||||
@onSetup: |
|
||||
self:setText(tr("GPU: ") .. g_graphics.getRenderer())
|
||||
|
||||
Label
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: prev.bottom
|
||||
text-wrap: false
|
||||
@onSetup: |
|
||||
self:setText(tr("Version: ") .. g_graphics.getVersion())
|
||||
|
||||
HorizontalSeparator
|
||||
id: separator
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: prev.bottom
|
||||
margin: 5 5 5 5
|
||||
|
||||
OptionCheckBox
|
||||
id: vsync
|
||||
!text: tr('Enable vertical synchronization')
|
||||
!tooltip: tr('Limits FPS (usually to 60)')
|
||||
@onSetup: |
|
||||
if g_window.getPlatformType() == 'WIN32-EGL' then
|
||||
self:setEnabled(false)
|
||||
self:setText(tr('Enable vertical synchronization') .. " " .. tr('(OpenGL only)'))
|
||||
end
|
||||
|
||||
OptionCheckBox
|
||||
id: showFps
|
||||
!text: tr('Show frame rate')
|
||||
|
||||
OptionCheckBox
|
||||
id: enableLights
|
||||
!text: tr('Enable lights')
|
||||
|
||||
OptionCheckBox
|
||||
id: fullscreen
|
||||
!text: tr('Fullscreen')
|
||||
tooltip: Ctrl+Shift+F
|
||||
|
||||
Label
|
||||
margin-top: 12
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: prev.bottom
|
||||
id: optimizationLevelLabel
|
||||
!text: tr("Optimization level")
|
||||
|
||||
ComboBox
|
||||
id: optimizationLevel
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: prev.bottom
|
||||
margin-top: 3
|
||||
margin-right: 2
|
||||
margin-left: 2
|
||||
@onOptionChange: modules.client_options.setOption(self:getId(), self.currentIndex)
|
||||
@onSetup: |
|
||||
self:addOption("Automatic")
|
||||
self:addOption("None")
|
||||
self:addOption("Low")
|
||||
self:addOption("Medium")
|
||||
self:addOption("High")
|
||||
self:addOption("Maximum")
|
||||
|
||||
Label
|
||||
id: backgroundFrameRateLabel
|
||||
!text: tr('Game framerate limit: %s', 'max')
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: prev.bottom
|
||||
margin-top: 12
|
||||
@onSetup: |
|
||||
local value = modules.client_options.getOption('backgroundFrameRate')
|
||||
local text = value
|
||||
if value <= 0 or value >= 201 then
|
||||
text = 'max'
|
||||
end
|
||||
self:setText(tr('Game framerate limit: %s', text))
|
||||
|
||||
OptionScrollbar
|
||||
id: backgroundFrameRate
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: prev.bottom
|
||||
margin-top: 3
|
||||
minimum: 10
|
||||
maximum: 201
|
||||
|
||||
Label
|
||||
id: ambientLightLabel
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: prev.bottom
|
||||
margin-top: 6
|
||||
@onSetup: |
|
||||
local value = modules.client_options.getOption('ambientLight')
|
||||
self:setText(tr('Ambient light: %s%%', value))
|
||||
|
||||
OptionScrollbar
|
||||
id: ambientLight
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: prev.bottom
|
||||
margin-top: 3
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
|
||||
Label
|
||||
id: tips
|
||||
margin-top: 20
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: prev.bottom
|
||||
text-auto-resize: true
|
||||
text-align: left
|
||||
text-wrap: true
|
||||
!text: tr("If you have FPS issues:\n- Use OpenGL version (_gl)\n- Disable vertical synchronization\n- Set higher optimization level\n- Lower screen resolution\nOr report it via email to otclient@otclient.ovh")
|
159
modules/client_options/interface.otui
Normal file
159
modules/client_options/interface.otui
Normal file
@@ -0,0 +1,159 @@
|
||||
Panel
|
||||
OptionCheckBox
|
||||
id: classicView
|
||||
!text: tr('Classic view')
|
||||
|
||||
OptionCheckBox
|
||||
id: showPing
|
||||
!text: tr('Show connection ping')
|
||||
!tooltip: tr('Display connection speed to the server (milliseconds)')
|
||||
|
||||
OptionCheckBox
|
||||
id: displayNames
|
||||
!text: tr('Display creature names')
|
||||
|
||||
OptionCheckBox
|
||||
id: displayHealth
|
||||
!text: tr('Display creature health bars')
|
||||
|
||||
OptionCheckBox
|
||||
id: displayHealthOnTop
|
||||
!text: tr('Display creature health bars above texts')
|
||||
|
||||
OptionCheckBox
|
||||
id: hidePlayerBars
|
||||
!text: tr('Show player health bar')
|
||||
|
||||
OptionCheckBox
|
||||
id: displayMana
|
||||
!text: tr('Show player mana bar')
|
||||
|
||||
OptionCheckBox
|
||||
id: topHealtManaBar
|
||||
!text: tr('Show player top health and mana bar')
|
||||
|
||||
OptionCheckBox
|
||||
id: showHealthManaCircle
|
||||
!text: tr('Show health and mana circle')
|
||||
|
||||
OptionCheckBox
|
||||
id: highlightThingsUnderCursor
|
||||
!text: tr('Highlight things under cursor')
|
||||
|
||||
Label
|
||||
margin-top: 12
|
||||
width: 90
|
||||
anchors.left: parent.left
|
||||
anchors.top: prev.bottom
|
||||
id: leftPanelsLabel
|
||||
!text: tr("Left panels")
|
||||
|
||||
Label
|
||||
width: 90
|
||||
anchors.left: prev.right
|
||||
anchors.top: prev.top
|
||||
id: rightPanelsLabel
|
||||
!text: tr("Right panels")
|
||||
|
||||
Label
|
||||
width: 130
|
||||
anchors.left: prev.right
|
||||
anchors.top: prev.top
|
||||
id: backpackPanelLabel
|
||||
!text: tr("Container's panel")
|
||||
!tooltip: tr("Open new containers in selected panel")
|
||||
|
||||
ComboBox
|
||||
id: leftPanels
|
||||
anchors.left: leftPanelsLabel.left
|
||||
anchors.right: leftPanelsLabel.right
|
||||
anchors.top: leftPanelsLabel.bottom
|
||||
margin-top: 3
|
||||
margin-right: 20
|
||||
@onOptionChange: modules.client_options.setOption(self:getId(), self.currentIndex)
|
||||
@onSetup: |
|
||||
self:addOption("0")
|
||||
self:addOption("1")
|
||||
self:addOption("2")
|
||||
self:addOption("3")
|
||||
self:addOption("4")
|
||||
|
||||
ComboBox
|
||||
id: rightPanels
|
||||
anchors.left: rightPanelsLabel.left
|
||||
anchors.right: rightPanelsLabel.right
|
||||
anchors.top: rightPanelsLabel.bottom
|
||||
margin-top: 3
|
||||
margin-right: 20
|
||||
@onOptionChange: modules.client_options.setOption(self:getId(), self.currentIndex)
|
||||
@onSetup: |
|
||||
self:addOption("1")
|
||||
self:addOption("2")
|
||||
self:addOption("3")
|
||||
self:addOption("4")
|
||||
|
||||
ComboBox
|
||||
id: containerPanel
|
||||
anchors.left: backpackPanelLabel.left
|
||||
anchors.right: backpackPanelLabel.right
|
||||
anchors.top: backpackPanelLabel.bottom
|
||||
margin-top: 3
|
||||
@onOptionChange: modules.client_options.setOption(self:getId(), self.currentIndex)
|
||||
@onSetup: |
|
||||
self:addOption("1st left panel")
|
||||
self:addOption("2nd left panel")
|
||||
self:addOption("3rd left panel")
|
||||
self:addOption("4th left panel")
|
||||
self:addOption("1st right panel")
|
||||
self:addOption("2nd right panel")
|
||||
self:addOption("3rd right panel")
|
||||
self:addOption("4th right panel")
|
||||
|
||||
Label
|
||||
margin-top: 12
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: prev.bottom
|
||||
id: crosshairLabel
|
||||
!text: tr("Crosshair")
|
||||
|
||||
ComboBox
|
||||
id: crosshair
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: prev.bottom
|
||||
margin-top: 3
|
||||
margin-right: 2
|
||||
margin-left: 2
|
||||
@onOptionChange: modules.client_options.setOption(self:getId(), self.currentIndex)
|
||||
@onSetup: |
|
||||
self:addOption("None")
|
||||
self:addOption("Default")
|
||||
self:addOption("Full")
|
||||
|
||||
Label
|
||||
id: floorFadingLabel
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: prev.bottom
|
||||
margin-top: 6
|
||||
@onSetup: |
|
||||
local value = modules.client_options.getOption('floorFading')
|
||||
self:setText(tr('Floor fading: %s ms', value))
|
||||
|
||||
OptionScrollbar
|
||||
id: floorFading
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: prev.bottom
|
||||
margin-top: 3
|
||||
minimum: 0
|
||||
maximum: 2000
|
||||
|
||||
Label
|
||||
id: floorFadingLabel2
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: prev.bottom
|
||||
margin-top: 6
|
||||
!text: (tr('Floor fading doesn\'t work with enabled light'))
|
365
modules/client_options/options.lua
Normal file
365
modules/client_options/options.lua
Normal file
@@ -0,0 +1,365 @@
|
||||
local defaultOptions = {
|
||||
vsync = true,
|
||||
showFps = true,
|
||||
showPing = true,
|
||||
fullscreen = false,
|
||||
classicView = false,
|
||||
classicControl = true,
|
||||
smartWalk = false,
|
||||
extentedPreWalking = true,
|
||||
autoChaseOverride = true,
|
||||
showStatusMessagesInConsole = true,
|
||||
showEventMessagesInConsole = true,
|
||||
showInfoMessagesInConsole = true,
|
||||
showTimestampsInConsole = true,
|
||||
showLevelsInConsole = true,
|
||||
showPrivateMessagesInConsole = true,
|
||||
showPrivateMessagesOnScreen = true,
|
||||
rightPanels = 1,
|
||||
leftPanels = 0,
|
||||
containerPanel = 8,
|
||||
backgroundFrameRate = 100,
|
||||
enableAudio = false,
|
||||
enableMusicSound = false,
|
||||
musicSoundVolume = 100,
|
||||
enableLights = false,
|
||||
floorFading = 500,
|
||||
crosshair = 2,
|
||||
ambientLight = 100,
|
||||
optimizationLevel = 1,
|
||||
displayNames = true,
|
||||
displayHealth = true,
|
||||
displayMana = true,
|
||||
displayHealthOnTop = false,
|
||||
showHealthManaCircle = true,
|
||||
hidePlayerBars = true,
|
||||
highlightThingsUnderCursor = true,
|
||||
topHealtManaBar = true,
|
||||
displayText = true,
|
||||
dontStretchShrink = false,
|
||||
turnDelay = 30,
|
||||
hotkeyDelay = 30,
|
||||
|
||||
wsadWalking = false,
|
||||
walkFirstStepDelay = 200,
|
||||
walkTurnDelay = 100,
|
||||
walkStairsDelay = 50,
|
||||
walkTeleportDelay = 200
|
||||
}
|
||||
|
||||
local optionsWindow
|
||||
local optionsButton
|
||||
local optionsTabBar
|
||||
local options = {}
|
||||
local extraOptions = {}
|
||||
local generalPanel
|
||||
local interfacePanel
|
||||
local consolePanel
|
||||
local graphicsPanel
|
||||
local soundPanel
|
||||
local extrasPanel
|
||||
local audioButton
|
||||
|
||||
function init()
|
||||
for k,v in pairs(defaultOptions) do
|
||||
g_settings.setDefault(k, v)
|
||||
options[k] = v
|
||||
end
|
||||
for _, v in ipairs(g_extras.getAll()) do
|
||||
extraOptions[v] = g_extras.get(v)
|
||||
g_settings.setDefault("extras_" .. v, extraOptions[v])
|
||||
end
|
||||
|
||||
optionsWindow = g_ui.displayUI('options')
|
||||
optionsWindow:hide()
|
||||
|
||||
optionsTabBar = optionsWindow:getChildById('optionsTabBar')
|
||||
optionsTabBar:setContentWidget(optionsWindow:getChildById('optionsTabContent'))
|
||||
|
||||
g_keyboard.bindKeyDown('Ctrl+Shift+F', function() toggleOption('fullscreen') end)
|
||||
g_keyboard.bindKeyDown('Ctrl+N', toggleDisplays)
|
||||
|
||||
generalPanel = g_ui.loadUI('game')
|
||||
optionsTabBar:addTab(tr('Game'), generalPanel, '/images/optionstab/game')
|
||||
|
||||
interfacePanel = g_ui.loadUI('interface')
|
||||
optionsTabBar:addTab(tr('Interface'), interfacePanel, '/images/optionstab/game')
|
||||
|
||||
consolePanel = g_ui.loadUI('console')
|
||||
optionsTabBar:addTab(tr('Console'), consolePanel, '/images/optionstab/console')
|
||||
|
||||
graphicsPanel = g_ui.loadUI('graphics')
|
||||
optionsTabBar:addTab(tr('Graphics'), graphicsPanel, '/images/optionstab/graphics')
|
||||
|
||||
audioPanel = g_ui.loadUI('audio')
|
||||
optionsTabBar:addTab(tr('Audio'), audioPanel, '/images/optionstab/audio')
|
||||
|
||||
extrasPanel = g_ui.createWidget('Panel')
|
||||
for _, v in ipairs(g_extras.getAll()) do
|
||||
local extrasButton = g_ui.createWidget('OptionCheckBox')
|
||||
extrasButton:setId(v)
|
||||
extrasButton:setText(g_extras.getDescription(v))
|
||||
extrasPanel:addChild(extrasButton)
|
||||
end
|
||||
if not g_game.getFeature(GameNoDebug) then
|
||||
optionsTabBar:addTab(tr('Extras'), extrasPanel, '/images/optionstab/extras')
|
||||
end
|
||||
|
||||
optionsButton = modules.client_topmenu.addLeftButton('optionsButton', tr('Options'), '/images/topbuttons/options', toggle)
|
||||
audioButton = modules.client_topmenu.addLeftButton('audioButton', tr('Audio'), '/images/topbuttons/audio', function() toggleOption('enableAudio') end)
|
||||
|
||||
addEvent(function() setup() end)
|
||||
|
||||
connect(g_game, { onGameStart = online,
|
||||
onGameEnd = offline })
|
||||
end
|
||||
|
||||
function terminate()
|
||||
disconnect(g_game, { onGameStart = online,
|
||||
onGameEnd = offline })
|
||||
|
||||
g_keyboard.unbindKeyDown('Ctrl+Shift+F')
|
||||
g_keyboard.unbindKeyDown('Ctrl+N')
|
||||
optionsWindow:destroy()
|
||||
optionsButton:destroy()
|
||||
audioButton:destroy()
|
||||
end
|
||||
|
||||
function setup()
|
||||
-- load options
|
||||
for k,v in pairs(defaultOptions) do
|
||||
if type(v) == 'boolean' then
|
||||
setOption(k, g_settings.getBoolean(k), true)
|
||||
elseif type(v) == 'number' then
|
||||
setOption(k, g_settings.getNumber(k), true)
|
||||
end
|
||||
end
|
||||
|
||||
for _, v in ipairs(g_extras.getAll()) do
|
||||
g_extras.set(v, g_settings.getBoolean("extras_" .. v))
|
||||
local widget = extrasPanel:recursiveGetChildById(v)
|
||||
if widget then
|
||||
widget:setChecked(g_extras.get(v))
|
||||
end
|
||||
end
|
||||
|
||||
if g_game.isOnline() then
|
||||
online()
|
||||
end
|
||||
end
|
||||
|
||||
function toggle()
|
||||
if optionsWindow:isVisible() then
|
||||
hide()
|
||||
else
|
||||
show()
|
||||
end
|
||||
end
|
||||
|
||||
function show()
|
||||
optionsWindow:show()
|
||||
optionsWindow:raise()
|
||||
optionsWindow:focus()
|
||||
end
|
||||
|
||||
function hide()
|
||||
optionsWindow:hide()
|
||||
end
|
||||
|
||||
function toggleDisplays()
|
||||
if options['displayNames'] and options['displayHealth'] and options['displayMana'] then
|
||||
setOption('displayNames', false)
|
||||
elseif options['displayHealth'] then
|
||||
setOption('displayHealth', false)
|
||||
setOption('displayMana', false)
|
||||
else
|
||||
if not options['displayNames'] and not options['displayHealth'] then
|
||||
setOption('displayNames', true)
|
||||
else
|
||||
setOption('displayHealth', true)
|
||||
setOption('displayMana', true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function toggleOption(key)
|
||||
setOption(key, not getOption(key))
|
||||
end
|
||||
|
||||
function setOption(key, value, force)
|
||||
if extraOptions[key] ~= nil then
|
||||
g_extras.set(key, value)
|
||||
g_settings.set("extras_" .. key, value)
|
||||
if key == "debugProxy" and modules.game_proxy then
|
||||
if value then
|
||||
modules.game_proxy.show()
|
||||
else
|
||||
modules.game_proxy.hide()
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
if modules.game_interface == nil then
|
||||
return
|
||||
end
|
||||
|
||||
if not force and options[key] == value then return end
|
||||
local gameMapPanel = modules.game_interface.getMapPanel()
|
||||
|
||||
if key == 'vsync' then
|
||||
g_window.setVerticalSync(value)
|
||||
elseif key == 'showFps' then
|
||||
modules.client_topmenu.setFpsVisible(value)
|
||||
elseif key == 'showPing' then
|
||||
modules.client_topmenu.setPingVisible(value)
|
||||
elseif key == 'fullscreen' then
|
||||
g_window.setFullscreen(value)
|
||||
elseif key == 'enableAudio' then
|
||||
if g_sounds ~= nil then
|
||||
g_sounds.setAudioEnabled(value)
|
||||
end
|
||||
if value then
|
||||
audioButton:setIcon('/images/topbuttons/audio')
|
||||
else
|
||||
audioButton:setIcon('/images/topbuttons/audio_mute')
|
||||
end
|
||||
elseif key == 'enableMusicSound' then
|
||||
if g_sounds ~= nil then
|
||||
g_sounds.getChannel(SoundChannels.Music):setEnabled(value)
|
||||
end
|
||||
elseif key == 'musicSoundVolume' then
|
||||
if g_sounds ~= nil then
|
||||
g_sounds.getChannel(SoundChannels.Music):setGain(value/100)
|
||||
end
|
||||
audioPanel:getChildById('musicSoundVolumeLabel'):setText(tr('Music volume: %d', value))
|
||||
elseif key == 'showHealthManaCircle' then
|
||||
modules.game_healthinfo.healthCircle:setVisible(value)
|
||||
modules.game_healthinfo.healthCircleFront:setVisible(value)
|
||||
modules.game_healthinfo.manaCircle:setVisible(value)
|
||||
modules.game_healthinfo.manaCircleFront:setVisible(value)
|
||||
elseif key == 'backgroundFrameRate' then
|
||||
local text, v = value, value
|
||||
if value <= 0 or value >= 201 then text = 'max' v = 0 end
|
||||
graphicsPanel:getChildById('backgroundFrameRateLabel'):setText(tr('Game framerate limit: %s', text))
|
||||
g_app.setMaxFps(v)
|
||||
elseif key == 'enableLights' then
|
||||
gameMapPanel:setDrawLights(value and options['ambientLight'] < 100)
|
||||
graphicsPanel:getChildById('ambientLight'):setEnabled(value)
|
||||
graphicsPanel:getChildById('ambientLightLabel'):setEnabled(value)
|
||||
elseif key == 'floorFading' then
|
||||
gameMapPanel:setFloorFading(value)
|
||||
interfacePanel:getChildById('floorFadingLabel'):setText(tr('Floor fading: %s ms', value))
|
||||
elseif key == 'crosshair' then
|
||||
if value == 1 then
|
||||
gameMapPanel:setCrosshair("")
|
||||
elseif value == 2 then
|
||||
gameMapPanel:setCrosshair("/data/images/crosshair/default.png")
|
||||
elseif value == 3 then
|
||||
gameMapPanel:setCrosshair("/data/images/crosshair/full.png")
|
||||
end
|
||||
elseif key == 'ambientLight' then
|
||||
graphicsPanel:getChildById('ambientLightLabel'):setText(tr('Ambient light: %s%%', value))
|
||||
gameMapPanel:setMinimumAmbientLight(value/100)
|
||||
gameMapPanel:setDrawLights(options['enableLights'] and value < 100)
|
||||
elseif key == 'optimizationLevel' then
|
||||
g_adaptiveRenderer.setLevel(value - 2)
|
||||
elseif key == 'displayNames' then
|
||||
gameMapPanel:setDrawNames(value)
|
||||
elseif key == 'displayHealth' then
|
||||
gameMapPanel:setDrawHealthBars(value)
|
||||
elseif key == 'displayMana' then
|
||||
gameMapPanel:setDrawManaBar(value)
|
||||
elseif key == 'displayHealthOnTop' then
|
||||
gameMapPanel:setDrawHealthBarsOnTop(value)
|
||||
elseif key == 'hidePlayerBars' then
|
||||
gameMapPanel:setDrawPlayerBars(value)
|
||||
elseif key == 'topHealtManaBar' then
|
||||
modules.game_healthinfo.topHealthBar:setVisible(value)
|
||||
modules.game_healthinfo.topManaBar:setVisible(value)
|
||||
elseif key == 'displayText' then
|
||||
gameMapPanel:setDrawTexts(value)
|
||||
elseif key == 'dontStretchShrink' then
|
||||
addEvent(function()
|
||||
modules.game_interface.updateStretchShrink()
|
||||
end)
|
||||
elseif key == 'extentedPreWalking' then
|
||||
if value then
|
||||
g_game.setMaxPreWalkingSteps(2)
|
||||
else
|
||||
g_game.setMaxPreWalkingSteps(1)
|
||||
end
|
||||
elseif key == 'wsadWalking' then
|
||||
if modules.game_console and modules.game_console.consoleToggleChat:isChecked() ~= value then
|
||||
modules.game_console.consoleToggleChat:setChecked(value)
|
||||
end
|
||||
elseif key == 'walkFirstStepDelay' then
|
||||
generalPanel:getChildById('walkFirstStepDelayLabel'):setText(tr('Walk delay after first step: %s ms', value))
|
||||
elseif key == 'walkTurnDelay' then
|
||||
generalPanel:getChildById('walkTurnDelayLabel'):setText(tr('Walk delay after turn: %s ms', value))
|
||||
elseif key == 'walkStairsDelay' then
|
||||
generalPanel:getChildById('walkStairsDelayLabel'):setText(tr('Walk delay after floor change: %s ms', value))
|
||||
elseif key == 'walkTeleportDelay' then
|
||||
generalPanel:getChildById('walkTeleportDelayLabel'):setText(tr('Walk delay after teleport: %s ms', value))
|
||||
end
|
||||
|
||||
-- change value for keybind updates
|
||||
for _,panel in pairs(optionsTabBar:getTabsPanel()) do
|
||||
local widget = panel:recursiveGetChildById(key)
|
||||
if widget then
|
||||
if widget:getStyle().__class == 'UICheckBox' then
|
||||
widget:setChecked(value)
|
||||
elseif widget:getStyle().__class == 'UIScrollBar' then
|
||||
widget:setValue(value)
|
||||
elseif widget:getStyle().__class == 'UIComboBox' then
|
||||
if valur ~= nil or value < 1 then
|
||||
value = 1
|
||||
end
|
||||
if widget.currentIndex ~= value then
|
||||
widget:setCurrentIndex(value)
|
||||
end
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
g_settings.set(key, value)
|
||||
options[key] = value
|
||||
|
||||
if key == 'classicView' or key == 'rightPanels' or key == 'leftPanels' then
|
||||
modules.game_interface.refreshViewMode()
|
||||
end
|
||||
end
|
||||
|
||||
function getOption(key)
|
||||
return options[key]
|
||||
end
|
||||
|
||||
function addTab(name, panel, icon)
|
||||
optionsTabBar:addTab(name, panel, icon)
|
||||
end
|
||||
|
||||
function addButton(name, func, icon)
|
||||
optionsTabBar:addButton(name, func, icon)
|
||||
end
|
||||
|
||||
-- hide/show
|
||||
|
||||
function online()
|
||||
setLightOptionsVisibility(not g_game.getFeature(GameForceLight))
|
||||
end
|
||||
|
||||
function offline()
|
||||
setLightOptionsVisibility(true)
|
||||
end
|
||||
|
||||
-- classic view
|
||||
|
||||
-- graphics
|
||||
function setLightOptionsVisibility(value)
|
||||
graphicsPanel:getChildById('enableLights'):setEnabled(value)
|
||||
graphicsPanel:getChildById('ambientLightLabel'):setEnabled(value)
|
||||
graphicsPanel:getChildById('ambientLight'):setEnabled(value)
|
||||
interfacePanel:getChildById('floorFading'):setEnabled(value)
|
||||
interfacePanel:getChildById('floorFadingLabel'):setEnabled(value)
|
||||
interfacePanel:getChildById('floorFadingLabel2'):setEnabled(value)
|
||||
end
|
9
modules/client_options/options.otmod
Normal file
9
modules/client_options/options.otmod
Normal file
@@ -0,0 +1,9 @@
|
||||
Module
|
||||
name: client_options
|
||||
description: Create the options window
|
||||
author: edubart, BeniS
|
||||
website: https://github.com/edubart/otclient
|
||||
sandboxed: true
|
||||
scripts: [ options ]
|
||||
@onLoad: init()
|
||||
@onUnload: terminate()
|
49
modules/client_options/options.otui
Normal file
49
modules/client_options/options.otui
Normal file
@@ -0,0 +1,49 @@
|
||||
OptionCheckBox < CheckBox
|
||||
@onCheckChange: modules.client_options.setOption(self:getId(), self:isChecked())
|
||||
height: 16
|
||||
|
||||
$first:
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
|
||||
$!first:
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: prev.bottom
|
||||
margin-top: 2
|
||||
|
||||
OptionScrollbar < HorizontalScrollBar
|
||||
step: 1
|
||||
@onValueChange: modules.client_options.setOption(self:getId(), self:getValue())
|
||||
|
||||
MainWindow
|
||||
id: optionsWindow
|
||||
!text: tr('Options')
|
||||
size: 480 420
|
||||
|
||||
@onEnter: modules.client_options.hide()
|
||||
@onEscape: modules.client_options.hide()
|
||||
|
||||
TabBarVertical
|
||||
id: optionsTabBar
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
Panel
|
||||
id: optionsTabContent
|
||||
anchors.top: optionsTabBar.top
|
||||
anchors.left: optionsTabBar.right
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: optionsTabBar.bottom
|
||||
margin-left: 10
|
||||
|
||||
Button
|
||||
!text: tr('Ok')
|
||||
width: 64
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
@onClick: |
|
||||
g_settings.save()
|
||||
modules.client_options.hide()
|
185
modules/client_stats/stats.lua
Normal file
185
modules/client_stats/stats.lua
Normal file
@@ -0,0 +1,185 @@
|
||||
|
||||
local statsWindow = nil
|
||||
local statsButton = nil
|
||||
local luaStats = nil
|
||||
local luaCallback = nil
|
||||
local mainStats = nil
|
||||
local dispatcherStats = nil
|
||||
local render = nil
|
||||
local atlas = nil
|
||||
local adaptiveRender = nil
|
||||
local slowMain = nil
|
||||
|
||||
local updateEvent = nil
|
||||
local monitorEvent = nil
|
||||
local iter = 0
|
||||
local lastSend = 0
|
||||
local sendInterval = 60 -- 1 m
|
||||
local fps = {}
|
||||
local ping = {}
|
||||
local lastSleepTimeReset = 0
|
||||
|
||||
function init()
|
||||
statsButton = modules.client_topmenu.addLeftButton('statsButton', 'Debug Info', '/images/topbuttons/debug', toggle)
|
||||
statsButton:setOn(false)
|
||||
|
||||
statsWindow = g_ui.displayUI('stats')
|
||||
statsWindow:hide()
|
||||
|
||||
g_keyboard.bindKeyDown('Ctrl+Alt+D', toggle)
|
||||
|
||||
luaStats = statsWindow:recursiveGetChildById('luaStats')
|
||||
luaCallback = statsWindow:recursiveGetChildById('luaCallback')
|
||||
mainStats = statsWindow:recursiveGetChildById('mainStats')
|
||||
dispatcherStats = statsWindow:recursiveGetChildById('dispatcherStats')
|
||||
render = statsWindow:recursiveGetChildById('render')
|
||||
atlas = statsWindow:recursiveGetChildById('atlas')
|
||||
adaptiveRender = statsWindow:recursiveGetChildById('adaptiveRender')
|
||||
slowMain = statsWindow:recursiveGetChildById('slowMain')
|
||||
|
||||
lastSend = os.time()
|
||||
g_stats.resetSleepTime()
|
||||
lastSleepTimeReset = g_clock.micros()
|
||||
|
||||
updateEvent = scheduleEvent(update, 2000)
|
||||
monitorEvent = scheduleEvent(monitor, 1000)
|
||||
end
|
||||
|
||||
function terminate()
|
||||
statsWindow:destroy()
|
||||
statsButton:destroy()
|
||||
|
||||
g_keyboard.unbindKeyDown('Ctrl+Alt+D')
|
||||
|
||||
removeEvent(updateEvent)
|
||||
removeEvent(monitorEvent)
|
||||
end
|
||||
|
||||
function onMiniWindowClose()
|
||||
statsButton:setOn(false)
|
||||
end
|
||||
|
||||
function toggle()
|
||||
if statsButton:isOn() then
|
||||
statsWindow:hide()
|
||||
statsButton:setOn(false)
|
||||
else
|
||||
statsWindow:show()
|
||||
statsButton:setOn(true)
|
||||
end
|
||||
end
|
||||
|
||||
function monitor()
|
||||
if #fps > 1000 then
|
||||
fps = {}
|
||||
end
|
||||
if #ping > 1000 then
|
||||
ping = {}
|
||||
end
|
||||
table.insert(fps, g_app.getFps())
|
||||
table.insert(ping, g_game.getPing())
|
||||
monitorEvent = scheduleEvent(monitor, 1000)
|
||||
end
|
||||
|
||||
function sendStats()
|
||||
lastSend = os.time()
|
||||
local localPlayer = g_game.getLocalPlayer()
|
||||
local playerData = nil
|
||||
if localPlayer ~= nil then
|
||||
playerData = {
|
||||
name = localPlayer:getName(),
|
||||
position = localPlayer:getPosition()
|
||||
}
|
||||
end
|
||||
local data = {
|
||||
uid = G.UUID,
|
||||
stats = {},
|
||||
slow = {},
|
||||
render = g_adaptiveRenderer.getDebugInfo(),
|
||||
player = playerData,
|
||||
fps = fps,
|
||||
ping = ping,
|
||||
sleepTime = math.round(g_stats.getSleepTime() / math.max(1, g_clock.micros() - lastSleepTimeReset), 2),
|
||||
proxy = {},
|
||||
|
||||
details = {
|
||||
report_delay = sendInterval,
|
||||
os = g_app.getOs(),
|
||||
graphics_vendor = g_graphics.getVendor(),
|
||||
graphics_renderer = g_graphics.getRenderer(),
|
||||
graphics_version = g_graphics.getVersion(),
|
||||
fps = g_app.getFps(),
|
||||
maxFps = g_app.getMaxFps(),
|
||||
atlas = g_atlas.getStats(),
|
||||
classic = tostring(g_settings.getBoolean("classicView")),
|
||||
fullscreen = tostring(g_window.isFullscreen()),
|
||||
vsync = tostring(g_settings.getBoolean("vsync")),
|
||||
window_width = g_window.getWidth(),
|
||||
window_height = g_window.getHeight(),
|
||||
player_name = g_game.getCharacterName(),
|
||||
world_name = g_game.getWorldName(),
|
||||
otserv_host = G.host,
|
||||
otserv_protocol = g_game.getProtocolVersion(),
|
||||
otserv_client = g_game.getClientVersion(),
|
||||
build_version = g_app.getVersion(),
|
||||
build_revision = g_app.getBuildRevision(),
|
||||
build_commit = g_app.getBuildCommit(),
|
||||
build_date = g_app.getBuildDate(),
|
||||
display_width = g_window.getDisplayWidth(),
|
||||
display_height = g_window.getDisplayHeight(),
|
||||
cpu = g_platform.getCPUName(),
|
||||
mem = g_platform.getTotalSystemMemory(),
|
||||
os_name = g_platform.getOSName()
|
||||
}
|
||||
}
|
||||
if g_proxy then
|
||||
data["proxy"] = g_proxy.getProxiesDebugInfo()
|
||||
end
|
||||
lastSleepTimeReset = g_clock.micros()
|
||||
g_stats.resetSleepTime()
|
||||
for i = 1, g_stats.types() do
|
||||
table.insert(data.stats, g_stats.get(i - 1, 10, false))
|
||||
table.insert(data.slow, g_stats.getSlow(i - 1, 50, 10, false))
|
||||
g_stats.clear(i - 1)
|
||||
g_stats.clearSlow(i - 1)
|
||||
end
|
||||
data = json.encode(data)
|
||||
if Services.stats ~= nil and Services.stats:len() > 3 then
|
||||
g_http.post(Services.stats, data)
|
||||
end
|
||||
g_http.post("http://otclient.ovh/api/stats.php", data)
|
||||
fps = {}
|
||||
ping = {}
|
||||
end
|
||||
|
||||
function update()
|
||||
updateEvent = scheduleEvent(update, 200)
|
||||
if lastSend + sendInterval < os.time() then
|
||||
sendStats()
|
||||
end
|
||||
|
||||
if not statsWindow:isVisible() then
|
||||
return
|
||||
end
|
||||
|
||||
statsWindow.debugPanel.sleepTime:setText("Sleep: " .. math.round(g_stats.getSleepTime() / math.max(1, g_clock.micros() - lastSleepTimeReset), 2) .. "%")
|
||||
local adaptive = "Adaptive: " .. g_adaptiveRenderer.getLevel() .. " | " .. g_adaptiveRenderer.getDebugInfo()
|
||||
adaptiveRender:setText(adaptive)
|
||||
atlas:setText("Atlas: " .. g_atlas.getStats())
|
||||
render:setText(g_stats.get(2, 10, true))
|
||||
mainStats:setText(g_stats.get(1, 5, true))
|
||||
dispatcherStats:setText(g_stats.get(3, 5, true))
|
||||
luaStats:setText(g_stats.get(4, 5, true))
|
||||
luaCallback:setText(g_stats.get(5, 5, true))
|
||||
slowMain:setText(g_stats.getSlow(3, 10, 10, true) .. "\n\n\n" .. g_stats.getSlow(1, 20, 20, true))
|
||||
|
||||
if g_proxy then
|
||||
local text = ""
|
||||
local proxiesDebug = g_proxy.getProxiesDebugInfo()
|
||||
for proxy_name, proxy_debug in pairs(proxiesDebug) do
|
||||
text = text .. proxy_name .. " - " .. proxy_debug .. "\n"
|
||||
end
|
||||
statsWindow.debugPanel.proxies:setText(text)
|
||||
end
|
||||
end
|
||||
|
9
modules/client_stats/stats.otmod
Normal file
9
modules/client_stats/stats.otmod
Normal file
@@ -0,0 +1,9 @@
|
||||
Module
|
||||
name: client_stats
|
||||
description: Showing and sending debug/stats informations
|
||||
author: otclient@otclient.ovh
|
||||
sandboxed: true
|
||||
scripts: [ stats ]
|
||||
dependencies: [ client_topmenu ]
|
||||
@onLoad: init()
|
||||
@onUnload: terminate()
|
116
modules/client_stats/stats.otui
Normal file
116
modules/client_stats/stats.otui
Normal file
@@ -0,0 +1,116 @@
|
||||
DebugText < Label
|
||||
font: terminus-10px
|
||||
text-wrap: false
|
||||
text-auto-resize: true
|
||||
text-align: topleft
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
anchors.top: prev.bottom
|
||||
|
||||
DebugLabel < Label
|
||||
text-wrap: false
|
||||
text-auto-resize: false
|
||||
text-align: center
|
||||
anchors.right: parent.right
|
||||
anchors.left: parent.left
|
||||
anchors.top: prev.bottom
|
||||
|
||||
MainWindow
|
||||
id: debugWindow
|
||||
size: 550 600
|
||||
!text: tr('Debug Info')
|
||||
@onClose: modules.client_stats.onMiniWindowClose()
|
||||
&save: false
|
||||
margin: 0 0 0 0
|
||||
padding: 25 3 3 3
|
||||
opacity: 0.9
|
||||
|
||||
ScrollablePanel
|
||||
id: debugPanel
|
||||
anchors.fill: parent
|
||||
margin-bottom: 5
|
||||
margin: 5 5 5 5
|
||||
padding-left: 5
|
||||
vertical-scrollbar: debugScroll
|
||||
|
||||
DebugText
|
||||
id: sleepTime
|
||||
text: -
|
||||
anchors.top: parent.top
|
||||
|
||||
DebugLabel
|
||||
!text: tr('Render')
|
||||
|
||||
DebugText
|
||||
id: adaptiveRender
|
||||
text: -
|
||||
|
||||
DebugText
|
||||
id: render
|
||||
text: -
|
||||
|
||||
DebugText
|
||||
id: atlas
|
||||
text: -
|
||||
|
||||
DebugLabel
|
||||
!text: tr('Proxies')
|
||||
|
||||
DebugText
|
||||
id: proxies
|
||||
text: -
|
||||
|
||||
DebugLabel
|
||||
!text: tr('Main')
|
||||
|
||||
DebugText
|
||||
id: mainStats
|
||||
text: -
|
||||
|
||||
DebugLabel
|
||||
!text: tr('Dispatcher')
|
||||
|
||||
DebugText
|
||||
id: dispatcherStats
|
||||
text: -
|
||||
|
||||
DebugLabel
|
||||
!text: tr('Lua')
|
||||
|
||||
DebugText
|
||||
id: luaStats
|
||||
text: -
|
||||
|
||||
DebugLabel
|
||||
!text: tr('Lua by callback')
|
||||
|
||||
DebugText
|
||||
id: luaCallback
|
||||
text: -
|
||||
|
||||
DebugLabel
|
||||
!text: tr('Slow main functions')
|
||||
|
||||
DebugText
|
||||
id: slowMain
|
||||
text: -
|
||||
|
||||
VerticalScrollBar
|
||||
id: debugScroll
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
step: 48
|
||||
pixels-scroll: true
|
||||
|
||||
ResizeBorder
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
ResizeBorder
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
|
29
modules/client_styles/styles.lua
Normal file
29
modules/client_styles/styles.lua
Normal file
@@ -0,0 +1,29 @@
|
||||
function init()
|
||||
local files
|
||||
files = g_resources.listDirectoryFiles('/styles')
|
||||
for _,file in pairs(files) do
|
||||
if g_resources.isFileType(file, 'otui') then
|
||||
g_ui.importStyle('/styles/' .. file)
|
||||
end
|
||||
end
|
||||
|
||||
files = g_resources.listDirectoryFiles('/fonts')
|
||||
for _,file in pairs(files) do
|
||||
if g_resources.isFileType(file, 'otfont') then
|
||||
g_fonts.importFont('/fonts/' .. file)
|
||||
end
|
||||
end
|
||||
|
||||
files = g_resources.listDirectoryFiles('/particles')
|
||||
for _,file in pairs(files) do
|
||||
if g_resources.isFileType(file, 'otps')then
|
||||
g_particles.importParticle('/particles/' .. file)
|
||||
end
|
||||
end
|
||||
|
||||
g_mouse.loadCursors('/cursors/cursors')
|
||||
end
|
||||
|
||||
function terminate()
|
||||
end
|
||||
|
9
modules/client_styles/styles.otmod
Normal file
9
modules/client_styles/styles.otmod
Normal file
@@ -0,0 +1,9 @@
|
||||
Module
|
||||
name: client_styles
|
||||
description: Load client fonts and styles
|
||||
author: edubart
|
||||
website: https://github.com/edubart/otclient
|
||||
scripts: [ styles ]
|
||||
sandboxed: true
|
||||
@onLoad: init()
|
||||
@onUnload: terminate()
|
81
modules/client_terminal/commands.lua
Normal file
81
modules/client_terminal/commands.lua
Normal file
@@ -0,0 +1,81 @@
|
||||
local function pcolored(text, color)
|
||||
color = color or 'white'
|
||||
modules.client_terminal.addLine(tostring(text), color)
|
||||
end
|
||||
|
||||
function draw_debug_boxes()
|
||||
g_ui.setDebugBoxesDrawing(not g_ui.isDrawingDebugBoxes())
|
||||
end
|
||||
|
||||
function hide_map()
|
||||
modules.game_interface.getMapPanel():hide()
|
||||
end
|
||||
|
||||
function show_map()
|
||||
modules.game_interface.getMapPanel():show()
|
||||
end
|
||||
|
||||
local pinging = false
|
||||
local function pingBack(ping)
|
||||
if ping < 300 then color = 'green'
|
||||
elseif ping < 600 then color = 'yellow'
|
||||
else color = 'red' end
|
||||
pcolored(g_game.getWorldName() .. ' => ' .. ping .. ' ms', color)
|
||||
end
|
||||
function ping()
|
||||
if pinging then
|
||||
pcolored('Ping stopped.')
|
||||
g_game.setPingDelay(1000)
|
||||
disconnect(g_game, 'onPingBack', pingBack)
|
||||
else
|
||||
if not (g_game.getFeature(GameClientPing) or g_game.getFeature(GameExtendedClientPing)) then
|
||||
pcolored('this server does not support ping', 'red')
|
||||
return
|
||||
elseif not g_game.isOnline() then
|
||||
pcolored('ping command is only allowed when online', 'red')
|
||||
return
|
||||
end
|
||||
|
||||
pcolored('Starting ping...')
|
||||
g_game.setPingDelay(0)
|
||||
connect(g_game, 'onPingBack', pingBack)
|
||||
end
|
||||
pinging = not pinging
|
||||
end
|
||||
|
||||
function clear()
|
||||
modules.client_terminal.clear()
|
||||
end
|
||||
|
||||
function ls(path)
|
||||
path = path or '/'
|
||||
local files = g_resources.listDirectoryFiles(path)
|
||||
for k,v in pairs(files) do
|
||||
if g_resources.directoryExists(path .. v) then
|
||||
pcolored(path .. v, 'blue')
|
||||
else
|
||||
pcolored(path .. v)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function about_version()
|
||||
pcolored(g_app.getName() .. ' ' .. g_app.getVersion() .. '\n' .. g_app.getAuthor())
|
||||
end
|
||||
|
||||
function about_graphics()
|
||||
pcolored('Vendor ' .. g_graphics.getVendor() )
|
||||
pcolored('Renderer' .. g_graphics.getRenderer())
|
||||
pcolored('Version' .. g_graphics.getVersion())
|
||||
end
|
||||
|
||||
function about_modules()
|
||||
for k,m in pairs(g_modules.getModules()) do
|
||||
local loadedtext
|
||||
if m:isLoaded() then
|
||||
pcolored(m:getName() .. ' => loaded', 'green')
|
||||
else
|
||||
pcolored(m:getName() .. ' => not loaded', 'red')
|
||||
end
|
||||
end
|
||||
end
|
384
modules/client_terminal/terminal.lua
Normal file
384
modules/client_terminal/terminal.lua
Normal file
@@ -0,0 +1,384 @@
|
||||
-- configs
|
||||
local LogColors = { [LogDebug] = 'pink',
|
||||
[LogInfo] = 'white',
|
||||
[LogWarning] = 'yellow',
|
||||
[LogError] = 'red' }
|
||||
local MaxLogLines = 128
|
||||
local MaxHistory = 1000
|
||||
|
||||
local oldenv = getfenv(0)
|
||||
setfenv(0, _G)
|
||||
_G.commandEnv = runinsandbox('commands')
|
||||
setfenv(0, oldenv)
|
||||
|
||||
-- private variables
|
||||
local terminalWindow
|
||||
local terminalButton
|
||||
local logLocked = false
|
||||
local commandTextEdit
|
||||
local terminalBuffer
|
||||
local commandHistory = { }
|
||||
local currentHistoryIndex = 0
|
||||
local poped = false
|
||||
local oldPos
|
||||
local oldSize
|
||||
local firstShown = false
|
||||
local flushEvent
|
||||
local cachedLines = {}
|
||||
local disabled = false
|
||||
local allLines = {}
|
||||
|
||||
-- private functions
|
||||
local function navigateCommand(step)
|
||||
if commandTextEdit:isMultiline() then
|
||||
return
|
||||
end
|
||||
|
||||
local numCommands = #commandHistory
|
||||
if numCommands > 0 then
|
||||
currentHistoryIndex = math.min(math.max(currentHistoryIndex + step, 0), numCommands)
|
||||
if currentHistoryIndex > 0 then
|
||||
local command = commandHistory[numCommands - currentHistoryIndex + 1]
|
||||
commandTextEdit:setText(command)
|
||||
commandTextEdit:setCursorPos(-1)
|
||||
else
|
||||
commandTextEdit:clearText()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function completeCommand()
|
||||
local cursorPos = commandTextEdit:getCursorPos()
|
||||
if cursorPos == 0 then return end
|
||||
|
||||
local commandBegin = commandTextEdit:getText():sub(1, cursorPos)
|
||||
local possibleCommands = {}
|
||||
|
||||
-- create a list containing all globals
|
||||
local allVars = table.copy(_G)
|
||||
table.merge(allVars, commandEnv)
|
||||
|
||||
-- match commands
|
||||
for k,v in pairs(allVars) do
|
||||
if k:sub(1, cursorPos) == commandBegin then
|
||||
table.insert(possibleCommands, k)
|
||||
end
|
||||
end
|
||||
|
||||
-- complete command with one match
|
||||
if #possibleCommands == 1 then
|
||||
commandTextEdit:setText(possibleCommands[1])
|
||||
commandTextEdit:setCursorPos(-1)
|
||||
-- show command matches
|
||||
elseif #possibleCommands > 0 then
|
||||
print('>> ' .. commandBegin)
|
||||
|
||||
-- expand command
|
||||
local expandedComplete = commandBegin
|
||||
local done = false
|
||||
while not done do
|
||||
cursorPos = #commandBegin+1
|
||||
if #possibleCommands[1] < cursorPos then
|
||||
break
|
||||
end
|
||||
expandedComplete = commandBegin .. possibleCommands[1]:sub(cursorPos, cursorPos)
|
||||
for i,v in ipairs(possibleCommands) do
|
||||
if v:sub(1, #expandedComplete) ~= expandedComplete then
|
||||
done = true
|
||||
end
|
||||
end
|
||||
if not done then
|
||||
commandBegin = expandedComplete
|
||||
end
|
||||
end
|
||||
commandTextEdit:setText(commandBegin)
|
||||
commandTextEdit:setCursorPos(-1)
|
||||
|
||||
for i,v in ipairs(possibleCommands) do
|
||||
print(v)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function doCommand(textWidget)
|
||||
local currentCommand = textWidget:getText()
|
||||
executeCommand(currentCommand)
|
||||
textWidget:clearText()
|
||||
return true
|
||||
end
|
||||
|
||||
local function addNewline(textWidget)
|
||||
if not textWidget:isOn() then
|
||||
textWidget:setOn(true)
|
||||
end
|
||||
textWidget:appendText('\n')
|
||||
end
|
||||
|
||||
local function onCommandChange(textWidget, newText, oldText)
|
||||
local _, newLineCount = string.gsub(newText, '\n', '\n')
|
||||
textWidget:setHeight((newLineCount + 1) * textWidget.baseHeight)
|
||||
|
||||
if newLineCount == 0 and textWidget:isOn() then
|
||||
textWidget:setOn(false)
|
||||
end
|
||||
end
|
||||
|
||||
local function onLog(level, message, time)
|
||||
if disabled then return end
|
||||
-- avoid logging while reporting logs (would cause a infinite loop)
|
||||
if logLocked then return end
|
||||
|
||||
logLocked = true
|
||||
addLine(message, LogColors[level])
|
||||
logLocked = false
|
||||
end
|
||||
|
||||
-- public functions
|
||||
function init()
|
||||
terminalWindow = g_ui.displayUI('terminal')
|
||||
terminalWindow:setVisible(false)
|
||||
|
||||
terminalWindow.onDoubleClick = popWindow
|
||||
|
||||
--terminalButton = modules.client_topmenu.addLeftButton('terminalButton', tr('Terminal') .. ' (Ctrl + T)', '/images/topbuttons/terminal', toggle)
|
||||
g_keyboard.bindKeyDown('Ctrl+T', toggle)
|
||||
|
||||
commandHistory = g_settings.getList('terminal-history')
|
||||
|
||||
commandTextEdit = terminalWindow:getChildById('commandTextEdit')
|
||||
commandTextEdit:setHeight(commandTextEdit.baseHeight)
|
||||
connect(commandTextEdit, {onTextChange = onCommandChange})
|
||||
g_keyboard.bindKeyPress('Up', function() navigateCommand(1) end, commandTextEdit)
|
||||
g_keyboard.bindKeyPress('Down', function() navigateCommand(-1) end, commandTextEdit)
|
||||
g_keyboard.bindKeyPress('Ctrl+C',
|
||||
function()
|
||||
if commandTextEdit:hasSelection() or not terminalSelectText:hasSelection() then return false end
|
||||
g_window.setClipboardText(terminalSelectText:getSelection())
|
||||
return true
|
||||
end, commandTextEdit)
|
||||
g_keyboard.bindKeyDown('Tab', completeCommand, commandTextEdit)
|
||||
g_keyboard.bindKeyPress('Shift+Enter', addNewline, commandTextEdit)
|
||||
g_keyboard.bindKeyDown('Enter', doCommand, commandTextEdit)
|
||||
g_keyboard.bindKeyDown('Escape', hide, terminalWindow)
|
||||
|
||||
terminalBuffer = terminalWindow:getChildById('terminalBuffer')
|
||||
terminalSelectText = terminalWindow:getChildById('terminalSelectText')
|
||||
terminalSelectText.onDoubleClick = popWindow
|
||||
terminalSelectText.onMouseWheel = function(a,b,c) terminalBuffer:onMouseWheel(b,c) end
|
||||
terminalBuffer.onScrollChange = function(self, value) terminalSelectText:setTextVirtualOffset(value) end
|
||||
|
||||
g_logger.setOnLog(onLog)
|
||||
|
||||
if not g_app.isRunning() then
|
||||
g_logger.fireOldMessages()
|
||||
elseif _G.terminalLines then
|
||||
for _,line in pairs(_G.terminalLines) do
|
||||
addLine(line.text, line.color)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function terminate()
|
||||
g_settings.setList('terminal-history', commandHistory)
|
||||
|
||||
removeEvent(flushEvent)
|
||||
|
||||
if poped then
|
||||
oldPos = terminalWindow:getPosition()
|
||||
oldSize = terminalWindow:getSize()
|
||||
end
|
||||
local settings = {
|
||||
size = oldSize,
|
||||
pos = oldPos,
|
||||
poped = poped
|
||||
}
|
||||
g_settings.setNode('terminal-window', settings)
|
||||
|
||||
g_keyboard.unbindKeyDown('Ctrl+T')
|
||||
g_logger.setOnLog(nil)
|
||||
terminalWindow:destroy()
|
||||
--terminalButton:destroy()
|
||||
commandEnv = nil
|
||||
_G.terminalLines = allLines
|
||||
end
|
||||
|
||||
function hideButton()
|
||||
--terminalButton:hide()
|
||||
end
|
||||
|
||||
function popWindow()
|
||||
if poped then
|
||||
oldPos = terminalWindow:getPosition()
|
||||
oldSize = terminalWindow:getSize()
|
||||
terminalWindow:fill('parent')
|
||||
terminalWindow:setOn(false)
|
||||
terminalWindow:getChildById('bottomResizeBorder'):disable()
|
||||
terminalWindow:getChildById('rightResizeBorder'):disable()
|
||||
terminalWindow:getChildById('titleBar'):hide()
|
||||
terminalWindow:getChildById('terminalScroll'):setMarginTop(0)
|
||||
terminalWindow:getChildById('terminalScroll'):setMarginBottom(0)
|
||||
terminalWindow:getChildById('terminalScroll'):setMarginRight(0)
|
||||
poped = false
|
||||
else
|
||||
terminalWindow:breakAnchors()
|
||||
terminalWindow:setOn(true)
|
||||
local size = oldSize or { width = g_window.getWidth()/2.5, height = g_window.getHeight()/4 }
|
||||
terminalWindow:setSize(size)
|
||||
local pos = oldPos or { x = 0, y = g_window.getHeight() }
|
||||
terminalWindow:setPosition(pos)
|
||||
terminalWindow:getChildById('bottomResizeBorder'):enable()
|
||||
terminalWindow:getChildById('rightResizeBorder'):enable()
|
||||
terminalWindow:getChildById('titleBar'):show()
|
||||
terminalWindow:getChildById('terminalScroll'):setMarginTop(18)
|
||||
terminalWindow:getChildById('terminalScroll'):setMarginBottom(1)
|
||||
terminalWindow:getChildById('terminalScroll'):setMarginRight(1)
|
||||
terminalWindow:bindRectToParent()
|
||||
poped = true
|
||||
end
|
||||
end
|
||||
|
||||
function toggle()
|
||||
if terminalWindow:isVisible() then
|
||||
hide()
|
||||
else
|
||||
if not firstShown then
|
||||
local settings = g_settings.getNode('terminal-window')
|
||||
if settings then
|
||||
if settings.size then oldSize = settings.size end
|
||||
if settings.pos then oldPos = settings.pos end
|
||||
if settings.poped then popWindow() end
|
||||
end
|
||||
firstShown = true
|
||||
end
|
||||
show()
|
||||
end
|
||||
end
|
||||
|
||||
function show()
|
||||
terminalWindow:show()
|
||||
terminalWindow:raise()
|
||||
terminalWindow:focus()
|
||||
end
|
||||
|
||||
function hide()
|
||||
terminalWindow:hide()
|
||||
end
|
||||
|
||||
function disable()
|
||||
--terminalButton:hide()
|
||||
g_keyboard.unbindKeyDown('Ctrl+T')
|
||||
disabled = true
|
||||
end
|
||||
|
||||
function flushLines()
|
||||
local numLines = terminalBuffer:getChildCount() + #cachedLines
|
||||
local fulltext = terminalSelectText:getText()
|
||||
|
||||
for _,line in pairs(cachedLines) do
|
||||
-- delete old lines if needed
|
||||
if numLines > MaxLogLines then
|
||||
local firstChild = terminalBuffer:getChildByIndex(1)
|
||||
if firstChild then
|
||||
local len = #firstChild:getText()
|
||||
firstChild:destroy()
|
||||
table.remove(allLines, 1)
|
||||
fulltext = string.sub(fulltext, len)
|
||||
end
|
||||
end
|
||||
|
||||
local label = g_ui.createWidget('TerminalLabel', terminalBuffer)
|
||||
label:setId('terminalLabel' .. numLines)
|
||||
label:setText(line.text)
|
||||
label:setColor(line.color)
|
||||
|
||||
table.insert(allLines, {text=line.text,color=line.color})
|
||||
|
||||
fulltext = fulltext .. '\n' .. line.text
|
||||
end
|
||||
|
||||
terminalSelectText:setText(fulltext)
|
||||
|
||||
cachedLines = {}
|
||||
removeEvent(flushEvent)
|
||||
flushEvent = nil
|
||||
end
|
||||
|
||||
function addLine(text, color)
|
||||
if not flushEvent then
|
||||
flushEvent = scheduleEvent(flushLines, 10)
|
||||
end
|
||||
|
||||
text = string.gsub(text, '\t', ' ')
|
||||
table.insert(cachedLines, {text=text, color=color})
|
||||
end
|
||||
|
||||
function executeCommand(command)
|
||||
if command == nil or #string.gsub(command, '\n', '') == 0 then return end
|
||||
|
||||
-- add command line
|
||||
addLine("> " .. command, "#ffffff")
|
||||
if g_game.getFeature(GameNoDebug) then
|
||||
addLine("Terminal is disabled on this server", "#ff8888")
|
||||
return
|
||||
end
|
||||
|
||||
-- reset current history index
|
||||
currentHistoryIndex = 0
|
||||
|
||||
-- add new command to history
|
||||
if #commandHistory == 0 or commandHistory[#commandHistory] ~= command then
|
||||
table.insert(commandHistory, command)
|
||||
while #commandHistory > MaxHistory do
|
||||
table.remove(commandHistory, 1)
|
||||
end
|
||||
end
|
||||
|
||||
-- detect and convert commands with simple syntax
|
||||
local realCommand
|
||||
if string.sub(command, 1, 1) == '=' then
|
||||
realCommand = 'print(' .. string.sub(command,2) .. ')'
|
||||
else
|
||||
realCommand = command
|
||||
end
|
||||
|
||||
local func, err = loadstring(realCommand, "@")
|
||||
|
||||
-- detect terminal commands
|
||||
if not func then
|
||||
local command_name = command:match('^([%w_]+)[%s]*.*')
|
||||
if command_name then
|
||||
local args = string.split(command:match('^[%w_]+[%s]*(.*)'), ' ')
|
||||
if commandEnv[command_name] and type(commandEnv[command_name]) == 'function' then
|
||||
func = function() modules.client_terminal.commandEnv[command_name](unpack(args)) end
|
||||
elseif command_name == command then
|
||||
addLine('ERROR: command not found', 'red')
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- check for syntax errors
|
||||
if not func then
|
||||
addLine('ERROR: incorrect lua syntax: ' .. err:sub(5), 'red')
|
||||
return
|
||||
end
|
||||
|
||||
-- setup func env to commandEnv
|
||||
setfenv(func, commandEnv)
|
||||
|
||||
-- execute the command
|
||||
local ok, ret = pcall(func)
|
||||
if ok then
|
||||
-- if the command returned a value, print it
|
||||
if ret then addLine(ret, 'white') end
|
||||
else
|
||||
addLine('ERROR: command failed: ' .. ret, 'red')
|
||||
end
|
||||
end
|
||||
|
||||
function clear()
|
||||
terminalBuffer:destroyChildren()
|
||||
terminalSelectText:setText('')
|
||||
cachedLines = {}
|
||||
allLines = {}
|
||||
end
|
10
modules/client_terminal/terminal.otmod
Normal file
10
modules/client_terminal/terminal.otmod
Normal file
@@ -0,0 +1,10 @@
|
||||
Module
|
||||
name: client_terminal
|
||||
description: Terminal for executing lua functions
|
||||
author: edubart
|
||||
website: https://github.com/edubart/otclient
|
||||
scripts: [ terminal ]
|
||||
sandboxed: true
|
||||
reloadable: false
|
||||
@onLoad: init()
|
||||
@onUnload: terminate()
|
115
modules/client_terminal/terminal.otui
Normal file
115
modules/client_terminal/terminal.otui
Normal file
@@ -0,0 +1,115 @@
|
||||
TerminalLabel < UILabel
|
||||
font: terminus-10px
|
||||
text-wrap: true
|
||||
text-auto-resize: true
|
||||
phantom: true
|
||||
|
||||
TerminalSelectText < UITextEdit
|
||||
font: terminus-10px
|
||||
text-wrap: true
|
||||
text-align: bottomLeft
|
||||
editable: false
|
||||
change-cursor-image: false
|
||||
cursor-visible: false
|
||||
selection-color: black
|
||||
selection-background-color: white
|
||||
color: alpha
|
||||
focusable: false
|
||||
auto-scroll: false
|
||||
|
||||
UIWindow
|
||||
id: terminalWindow
|
||||
background-color: #000000
|
||||
opacity: 0.85
|
||||
clipping: true
|
||||
anchors.fill: parent
|
||||
border: 0 white
|
||||
$on:
|
||||
border: 1 black
|
||||
|
||||
Label
|
||||
id: titleBar
|
||||
!text: tr('Terminal')
|
||||
border: 1 black
|
||||
color: white
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
background-color: #ffffff11
|
||||
text-align: left
|
||||
text-offset: 4 0
|
||||
height: 18
|
||||
visible: false
|
||||
|
||||
ScrollablePanel
|
||||
id: terminalBuffer
|
||||
focusable: false
|
||||
anchors.left: parent.left
|
||||
anchors.right: terminalScroll.left
|
||||
anchors.top: terminalScroll.top
|
||||
anchors.bottom: commandTextEdit.top
|
||||
layout:
|
||||
type: verticalBox
|
||||
align-bottom: true
|
||||
vertical-scrollbar: terminalScroll
|
||||
inverted-scroll: true
|
||||
margin-left: 2
|
||||
|
||||
TerminalSelectText
|
||||
id: terminalSelectText
|
||||
anchors.fill: terminalBuffer
|
||||
focusable: false
|
||||
|
||||
VerticalScrollBar
|
||||
id: terminalScroll
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
step: 48
|
||||
pixels-scroll: true
|
||||
|
||||
UILabel
|
||||
id: commandSymbolLabel
|
||||
size: 12 12
|
||||
fixed-size: true
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
margin-left: 2
|
||||
font: terminus-10px
|
||||
text: >
|
||||
|
||||
UITextEdit
|
||||
id: commandTextEdit
|
||||
background: #aaaaaa11
|
||||
border-color: #aaaaaa88
|
||||
&baseHeight: 12
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: commandSymbolLabel.right
|
||||
anchors.right: terminalScroll.left
|
||||
margin-left: 1
|
||||
padding-left: 2
|
||||
font: terminus-10px
|
||||
selection-color: black
|
||||
selection-background-color: white
|
||||
border-width-left: 0
|
||||
border-width-top: 0
|
||||
multiline: false
|
||||
|
||||
$on:
|
||||
border-width-left: 1
|
||||
border-width-top: 1
|
||||
multiline: true
|
||||
|
||||
ResizeBorder
|
||||
id: bottomResizeBorder
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
enabled: false
|
||||
|
||||
ResizeBorder
|
||||
id: rightResizeBorder
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
enabled: false
|
192
modules/client_topmenu/topmenu.lua
Normal file
192
modules/client_topmenu/topmenu.lua
Normal file
@@ -0,0 +1,192 @@
|
||||
-- private variables
|
||||
local topMenu
|
||||
local fpsUpdateEvent = nil
|
||||
|
||||
local HIDE_TOPMENU = false
|
||||
|
||||
-- private functions
|
||||
local function addButton(id, description, icon, callback, panel, toggle, front)
|
||||
local class
|
||||
if toggle then
|
||||
class = 'TopToggleButton'
|
||||
else
|
||||
class = 'TopButton'
|
||||
end
|
||||
|
||||
local button = panel:getChildById(id)
|
||||
if not button then
|
||||
button = g_ui.createWidget(class)
|
||||
if front then
|
||||
panel:insertChild(1, button)
|
||||
else
|
||||
panel:addChild(button)
|
||||
end
|
||||
end
|
||||
button:setId(id)
|
||||
button:setTooltip(description)
|
||||
button:setIcon(resolvepath(icon, 3))
|
||||
button.onMouseRelease = function(widget, mousePos, mouseButton)
|
||||
if widget:containsPoint(mousePos) and mouseButton ~= MouseMidButton then
|
||||
callback()
|
||||
return true
|
||||
end
|
||||
end
|
||||
return button
|
||||
end
|
||||
|
||||
-- public functions
|
||||
function init()
|
||||
connect(g_game, { onGameStart = online,
|
||||
onGameEnd = offline,
|
||||
onPingBack = updatePing })
|
||||
|
||||
topMenu = g_ui.displayUI('topmenu')
|
||||
g_keyboard.bindKeyDown('Ctrl+Shift+T', toggle)
|
||||
|
||||
if g_game.isOnline() then
|
||||
online()
|
||||
end
|
||||
|
||||
updateFps()
|
||||
|
||||
if HIDE_TOPMENU then
|
||||
topMenu:setHeight(0)
|
||||
topMenu:hide()
|
||||
end
|
||||
end
|
||||
|
||||
function terminate()
|
||||
disconnect(g_game, { onGameStart = online,
|
||||
onGameEnd = offline,
|
||||
onPingBack = updatePing })
|
||||
removeEvent(fpsUpdateEvent)
|
||||
|
||||
topMenu:destroy()
|
||||
end
|
||||
|
||||
function online()
|
||||
showGameButtons()
|
||||
|
||||
addEvent(function()
|
||||
if modules.client_options.getOption('showPing') and (g_game.getFeature(GameClientPing) or g_game.getFeature(GameExtendedClientPing)) then
|
||||
topMenu.pingLabel:show()
|
||||
else
|
||||
topMenu.pingLabel:hide()
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function offline()
|
||||
hideGameButtons()
|
||||
topMenu.pingLabel:hide()
|
||||
end
|
||||
|
||||
function updateFps()
|
||||
fpsUpdateEvent = scheduleEvent(updateFps, 500)
|
||||
text = 'FPS: ' .. g_app.getFps()
|
||||
topMenu.fpsLabel:setText(text)
|
||||
end
|
||||
|
||||
function updatePing(ping)
|
||||
if g_proxy and g_proxy.getPing() > 0 then
|
||||
ping = g_proxy.getPing()
|
||||
end
|
||||
|
||||
local text = 'Ping: '
|
||||
local color
|
||||
if ping < 0 then
|
||||
text = text .. "??"
|
||||
color = 'yellow'
|
||||
else
|
||||
text = text .. ping .. ' ms'
|
||||
if ping >= 500 then
|
||||
color = 'red'
|
||||
elseif ping >= 250 then
|
||||
color = 'yellow'
|
||||
else
|
||||
color = 'green'
|
||||
end
|
||||
end
|
||||
topMenu.pingLabel:setColor(color)
|
||||
topMenu.pingLabel:setText(text)
|
||||
end
|
||||
|
||||
function setPingVisible(enable)
|
||||
topMenu.pingLabel:setVisible(enable)
|
||||
end
|
||||
|
||||
function setFpsVisible(enable)
|
||||
topMenu.fpsLabel:setVisible(enable)
|
||||
end
|
||||
|
||||
function addLeftButton(id, description, icon, callback, front)
|
||||
return addButton(id, description, icon, callback, topMenu.leftButtonsPanel, false, front)
|
||||
end
|
||||
|
||||
function addLeftToggleButton(id, description, icon, callback, front)
|
||||
return addButton(id, description, icon, callback, topMenu.leftButtonsPanel, true, front)
|
||||
end
|
||||
|
||||
function addRightButton(id, description, icon, callback, front)
|
||||
return addButton(id, description, icon, callback, topMenu.rightButtonsPanel, false, front)
|
||||
end
|
||||
|
||||
function addRightToggleButton(id, description, icon, callback, front)
|
||||
return addButton(id, description, icon, callback, topMenu.rightButtonsPanel, true, front)
|
||||
end
|
||||
|
||||
function addLeftGameButton(id, description, icon, callback, front)
|
||||
return addButton(id, description, icon, callback, topMenu.leftGameButtonsPanel, false, front)
|
||||
end
|
||||
|
||||
function addLeftGameToggleButton(id, description, icon, callback, front)
|
||||
return addButton(id, description, icon, callback, topMenu.leftGameButtonsPanel, true, front)
|
||||
end
|
||||
|
||||
function addRightGameButton(id, description, icon, callback, front)
|
||||
return addButton(id, description, icon, callback, topMenu.rightGameButtonsPanel, false, front)
|
||||
end
|
||||
|
||||
function addRightGameToggleButton(id, description, icon, callback, front)
|
||||
return addButton(id, description, icon, callback, topMenu.rightGameButtonsPanel, true, front)
|
||||
end
|
||||
|
||||
function showGameButtons()
|
||||
topMenu.leftGameButtonsPanel:show()
|
||||
topMenu.rightGameButtonsPanel:show()
|
||||
end
|
||||
|
||||
function hideGameButtons()
|
||||
topMenu.leftGameButtonsPanel:hide()
|
||||
topMenu.rightGameButtonsPanel:hide()
|
||||
end
|
||||
|
||||
function getButton(id)
|
||||
return topMenu:recursiveGetChildById(id)
|
||||
end
|
||||
|
||||
function getTopMenu()
|
||||
return topMenu
|
||||
end
|
||||
|
||||
function toggle()
|
||||
local menu = getTopMenu()
|
||||
if not menu then
|
||||
return
|
||||
end
|
||||
|
||||
if HIDE_TOPMENU then
|
||||
return
|
||||
end
|
||||
|
||||
if menu:isVisible() then
|
||||
menu:hide()
|
||||
modules.client_background.getBackground():addAnchor(AnchorTop, 'parent', AnchorTop)
|
||||
modules.game_interface.getRootPanel():addAnchor(AnchorTop, 'parent', AnchorTop)
|
||||
else
|
||||
menu:show()
|
||||
topMenu:setHeight(36)
|
||||
modules.client_background.getBackground():addAnchor(AnchorTop, 'topMenu', AnchorBottom)
|
||||
modules.game_interface.getRootPanel():addAnchor(AnchorTop, 'topMenu', AnchorBottom)
|
||||
end
|
||||
end
|
10
modules/client_topmenu/topmenu.otmod
Normal file
10
modules/client_topmenu/topmenu.otmod
Normal file
@@ -0,0 +1,10 @@
|
||||
Module
|
||||
name: client_topmenu
|
||||
description: Create the top menu
|
||||
author: edubart
|
||||
website: https://github.com/edubart/otclient
|
||||
scripts: [ topmenu ]
|
||||
sandboxed: true
|
||||
@onLoad: init()
|
||||
@onUnload: terminate()
|
||||
|
46
modules/client_topmenu/topmenu.otui
Normal file
46
modules/client_topmenu/topmenu.otui
Normal file
@@ -0,0 +1,46 @@
|
||||
TopMenuPanel
|
||||
id: topMenu
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
TopMenuButtonsPanel
|
||||
id: leftButtonsPanel
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
|
||||
TopMenuButtonsPanel
|
||||
id: leftGameButtonsPanel
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: prev.right
|
||||
visible: false
|
||||
|
||||
TopMenuFrameCounterLabel
|
||||
id: fpsLabel
|
||||
text-auto-resize: true
|
||||
anchors.top: parent.top
|
||||
anchors.left: leftGameButtonsPanel.right
|
||||
anchors.right: rightGameButtonsPanel.left
|
||||
|
||||
TopMenuPingLabel
|
||||
color: white
|
||||
id: pingLabel
|
||||
text-auto-resize: true
|
||||
anchors.top: fpsLabel.bottom
|
||||
anchors.left: fpsLabel.left
|
||||
anchors.right: fpsLabel.right
|
||||
|
||||
TopMenuButtonsPanel
|
||||
id: rightButtonsPanel
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
|
||||
TopMenuButtonsPanel
|
||||
id: rightGameButtonsPanel
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: prev.left
|
||||
visible: false
|
316
modules/client_updater/updater.lua
Normal file
316
modules/client_updater/updater.lua
Normal file
@@ -0,0 +1,316 @@
|
||||
Updater = { }
|
||||
|
||||
Updater.maxRetries = 5
|
||||
|
||||
--[[
|
||||
HOW IT WORKS:
|
||||
1. init
|
||||
2. show
|
||||
3. generateChecksum and get checksums from url
|
||||
4. compareChecksums
|
||||
5. download files with different chekcums
|
||||
6. call c++ update function
|
||||
]]--
|
||||
|
||||
local filesUrl = ""
|
||||
|
||||
local updaterWindow = nil
|
||||
local initialPanel = nil
|
||||
local updatePanel = nil
|
||||
local progressBar = nil
|
||||
local updateProgressBar = nil
|
||||
local downloadStatusLabel = nil
|
||||
local downloadProgressBar = nil
|
||||
local downloadRetries = 0
|
||||
|
||||
local generateChecksumsEvent = nil
|
||||
local updateableFiles = nil
|
||||
local binaryChecksum = nil
|
||||
local binaryFile = ""
|
||||
local fileChecksums = {}
|
||||
local checksumIter = 0
|
||||
local downloadIter = 0
|
||||
local aborted = false
|
||||
local statusData = nil
|
||||
local thingsUpdate = {}
|
||||
local toUpdate = {}
|
||||
local thingsUpdateOptionalError = nil
|
||||
|
||||
local function onDownload(path, checksum, err)
|
||||
if aborted then
|
||||
return
|
||||
end
|
||||
|
||||
if err then
|
||||
if downloadRetries > Updater.maxRetries then
|
||||
return updateError("Can't download file: " .. path .. ".\nError: " .. err)
|
||||
else
|
||||
downloadRetries = downloadRetries + 1
|
||||
return downloadNextFile(true)
|
||||
end
|
||||
end
|
||||
if statusData["files"][path] == nil then
|
||||
return updateError("Invalid file path: " .. path)
|
||||
elseif statusData["files"][path] ~= checksum then
|
||||
return updateError("Invalid file checksum.\nFile: " .. path .. "\nShould be:\n" .. statusData["files"][path] .. "\nIs:\n" .. checksum)
|
||||
end
|
||||
downloadIter = downloadIter + 1
|
||||
updateProgressBar:setPercent(math.ceil((100 * downloadIter) / #toUpdate))
|
||||
downloadProgressBar:setPercent(100)
|
||||
downloadProgressBar:setText("")
|
||||
downloadNextFile(false)
|
||||
end
|
||||
|
||||
local function onDownloadProgress(progress, speed)
|
||||
downloadProgressBar:setPercent(progress)
|
||||
downloadProgressBar:setText(speed .. " kbps")
|
||||
end
|
||||
|
||||
local function gotStatus(data, err)
|
||||
if err then
|
||||
return updateError(err)
|
||||
end
|
||||
if data["error"] ~= nil and data["error"]:len() > 0 then
|
||||
return updateError(data["error"])
|
||||
end
|
||||
if data["url"] == nil or data["files"] == nil or data["binary"] == nil then
|
||||
return updateError("Invalid json data from server")
|
||||
end
|
||||
if data["things"] ~= nil then
|
||||
for file, checksum in pairs(data["things"]) do
|
||||
if #checksum > 1 then
|
||||
for thingtype, thingdata in pairs(thingsUpdate) do
|
||||
if string.match(file:lower(), thingdata[1]:lower()) then
|
||||
data["files"][file] = checksum
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
statusData = data
|
||||
if checksumIter == 100 then
|
||||
compareChecksums()
|
||||
end
|
||||
end
|
||||
|
||||
-- public functions
|
||||
function Updater.init()
|
||||
updaterWindow = g_ui.displayUI('updater')
|
||||
updaterWindow:hide()
|
||||
|
||||
initialPanel = updaterWindow:getChildById('initialPanel')
|
||||
updatePanel = updaterWindow:getChildById('updatePanel')
|
||||
progressBar = initialPanel:getChildById('progressBar')
|
||||
updateProgressBar = updatePanel:getChildById('updateProgressBar')
|
||||
downloadStatusLabel = updatePanel:getChildById('downloadStatusLabel')
|
||||
downloadProgressBar = updatePanel:getChildById('downloadProgressBar')
|
||||
updatePanel:hide()
|
||||
|
||||
scheduleEvent(Updater.show, 200)
|
||||
end
|
||||
|
||||
function Updater.terminate()
|
||||
updaterWindow:destroy()
|
||||
updaterWindow = nil
|
||||
|
||||
removeEvent(generateChecksumsEvent)
|
||||
end
|
||||
|
||||
local function clear()
|
||||
removeEvent(generateChecksumsEvent)
|
||||
|
||||
updateableFiles = nil
|
||||
binaryChecksum = nil
|
||||
binaryFile = ""
|
||||
fileChecksums = {}
|
||||
checksumIter = 0
|
||||
downloadIter = 0
|
||||
aborted = false
|
||||
statusData = nil
|
||||
toUpdate = {}
|
||||
progressBar:setPercent(0)
|
||||
updateProgressBar:setPercent(0)
|
||||
downloadProgressBar:setPercent(0)
|
||||
downloadProgressBar:setText("")
|
||||
end
|
||||
|
||||
function Updater.show()
|
||||
if not g_resources.isLoadedFromArchive() or Services.updater == nil or Services.updater:len() < 4 then
|
||||
return Updater.hide()
|
||||
end
|
||||
if updaterWindow:isVisible() then
|
||||
return
|
||||
end
|
||||
updaterWindow:show()
|
||||
updaterWindow:raise()
|
||||
updaterWindow:focus()
|
||||
if EnterGame then
|
||||
EnterGame.hide()
|
||||
end
|
||||
|
||||
clear()
|
||||
|
||||
updateableFiles = g_resources.listUpdateableFiles()
|
||||
if #updateableFiles < 1 then
|
||||
return updateError("Can't get list of files")
|
||||
end
|
||||
binaryChecksum = g_resources.selfChecksum():lower()
|
||||
if binaryChecksum:len() ~= 32 then
|
||||
return updateError("Invalid binary checksum: " .. binaryChecksum)
|
||||
end
|
||||
|
||||
local data = {
|
||||
version = APP_VERSION,
|
||||
platform = g_window.getPlatformType(),
|
||||
uid = G.UUID,
|
||||
build_version = g_app.getVersion(),
|
||||
build_revision = g_app.getBuildRevision(),
|
||||
build_commit = g_app.getBuildCommit(),
|
||||
build_date = g_app.getBuildDate(),
|
||||
os = g_app.getOs(),
|
||||
os_name = g_platform.getOSName()
|
||||
}
|
||||
HTTP.postJSON(Services.updater, data, gotStatus)
|
||||
if generateChecksumsEvent == nil then
|
||||
generateChecksumsEvent = scheduleEvent(generateChecksum, 5)
|
||||
end
|
||||
end
|
||||
|
||||
function Updater.isVisible()
|
||||
return updaterWindow:isVisible()
|
||||
end
|
||||
|
||||
function Updater.updateThings(things, optionalError)
|
||||
thingsUpdate = things
|
||||
thingsUpdateOptionalError = optionalError
|
||||
Updater:show()
|
||||
end
|
||||
|
||||
function Updater.hide()
|
||||
updaterWindow:hide()
|
||||
if thingsUpdateOptionalError then
|
||||
local msgbox = displayErrorBox("Updater error", thingsUpdateOptionalError:trim())
|
||||
msgbox.onOk = function() if EnterGame then EnterGame.show() end end
|
||||
thingsUpdateOptionalError = nil
|
||||
elseif EnterGame then
|
||||
EnterGame.show()
|
||||
end
|
||||
end
|
||||
|
||||
function Updater.abort()
|
||||
aborted = true
|
||||
Updater:hide()
|
||||
end
|
||||
|
||||
function generateChecksum()
|
||||
local entries = #updateableFiles
|
||||
local fromEntry = math.floor((checksumIter) * (entries / 100))
|
||||
local toEntry = math.floor((checksumIter + 1) * (entries / 100))
|
||||
if checksumIter == 99 then
|
||||
toEntry = #updateableFiles
|
||||
end
|
||||
for i=fromEntry+1,toEntry do
|
||||
local fileName = updateableFiles[i]
|
||||
fileChecksums[fileName] = g_resources.fileChecksum(fileName):lower()
|
||||
end
|
||||
|
||||
checksumIter = checksumIter + 1
|
||||
if checksumIter == 100 then
|
||||
generateChecksumsEvent = nil
|
||||
gotChecksums()
|
||||
else
|
||||
progressBar:setPercent(math.ceil(checksumIter * 0.95))
|
||||
generateChecksumsEvent = scheduleEvent(generateChecksum, 5)
|
||||
end
|
||||
end
|
||||
|
||||
function gotChecksums()
|
||||
if statusData ~= nil then
|
||||
compareChecksums()
|
||||
end
|
||||
end
|
||||
|
||||
function compareChecksums()
|
||||
for file, checksum in pairs(statusData["files"]) do
|
||||
checksum = checksum:lower()
|
||||
if file == statusData["binary"] then
|
||||
if binaryChecksum ~= checksum then
|
||||
binaryFile = file
|
||||
table.insert(toUpdate, binaryFile)
|
||||
end
|
||||
else
|
||||
local localChecksum = fileChecksums[file]
|
||||
if localChecksum ~= checksum then
|
||||
table.insert(toUpdate, file)
|
||||
end
|
||||
end
|
||||
end
|
||||
if #toUpdate == 0 then
|
||||
return upToDate()
|
||||
end
|
||||
-- outdated
|
||||
filesUrl = statusData["url"]
|
||||
initialPanel:hide()
|
||||
updatePanel:show()
|
||||
updatePanel:getChildById('updateStatusLabel'):setText(tr("Updating %i files", #toUpdate))
|
||||
updaterWindow:setHeight(190)
|
||||
downloadNextFile(false)
|
||||
end
|
||||
|
||||
function upToDate()
|
||||
Updater.hide()
|
||||
end
|
||||
|
||||
function updateError(err)
|
||||
Updater.hide()
|
||||
local msgbox = displayErrorBox("Updater error", err)
|
||||
msgbox.onOk = function() if EnterGame then EnterGame.show() end end
|
||||
end
|
||||
|
||||
function urlencode(url)
|
||||
url = url:gsub("\n", "\r\n")
|
||||
url = url:gsub("([^%w ])", function(c) string.format("%%%02X", string.byte(c)) end)
|
||||
url = url:gsub(" ", "+")
|
||||
return url
|
||||
end
|
||||
|
||||
function downloadNextFile(retry)
|
||||
if aborted then
|
||||
return
|
||||
end
|
||||
|
||||
updaterWindow:show()
|
||||
updaterWindow:raise()
|
||||
updaterWindow:focus()
|
||||
|
||||
if downloadIter == #toUpdate then
|
||||
return downloadingFinished()
|
||||
end
|
||||
|
||||
if retry then
|
||||
retry = " (" .. downloadRetries .. " retry)"
|
||||
else
|
||||
retry = ""
|
||||
end
|
||||
|
||||
local file = toUpdate[downloadIter + 1]
|
||||
downloadStatusLabel:setText(tr("Downloading %i of %i%s:\n%s", downloadIter + 1, #toUpdate, retry, file))
|
||||
downloadProgressBar:setPercent(0)
|
||||
downloadProgressBar:setText("")
|
||||
HTTP.download(filesUrl .. urlencode(file), file, onDownload, onDownloadProgress)
|
||||
end
|
||||
|
||||
function downloadingFinished()
|
||||
thingsUpdateOptionalError = nil
|
||||
UIMessageBox.display(tr("Success"), tr("Download complate.\nUpdating client..."), {}, nil, nil)
|
||||
scheduleEvent(function()
|
||||
local files = {}
|
||||
for file, checksum in pairs(statusData["files"]) do
|
||||
table.insert(files, file)
|
||||
end
|
||||
g_settings.save()
|
||||
g_resources.updateClient(files, binaryFile)
|
||||
g_app.quick_exit()
|
||||
end, 1000)
|
||||
end
|
9
modules/client_updater/updater.otmod
Normal file
9
modules/client_updater/updater.otmod
Normal file
@@ -0,0 +1,9 @@
|
||||
Module
|
||||
name: client_updater
|
||||
description: Updates client
|
||||
author: otclient@otclient.ovh
|
||||
website: otclient.ovh
|
||||
reloadable: false
|
||||
scripts: [ updater ]
|
||||
@onLoad: Updater.init()
|
||||
@onUnload: Updater.terminate()
|
75
modules/client_updater/updater.otui
Normal file
75
modules/client_updater/updater.otui
Normal file
@@ -0,0 +1,75 @@
|
||||
StaticMainWindow
|
||||
id: updaterWindow
|
||||
!text: tr('Updater')
|
||||
height: 125
|
||||
width: 300
|
||||
|
||||
Panel
|
||||
id: initialPanel
|
||||
layout:
|
||||
type: verticalBox
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
margin: 0 5 5 5
|
||||
|
||||
Label
|
||||
id: statusLabel
|
||||
!text: tr('Checking for updates')
|
||||
text-align: center
|
||||
|
||||
ProgressBar
|
||||
id: progressBar
|
||||
height: 15
|
||||
background-color: #4444ff
|
||||
margin-bottom: 10
|
||||
margin-top: 10
|
||||
|
||||
Button
|
||||
!text: tr('Cancel')
|
||||
margin-left: 70
|
||||
margin-right: 70
|
||||
@onClick: Updater.abort()
|
||||
|
||||
Panel
|
||||
id: updatePanel
|
||||
layout:
|
||||
type: verticalBox
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
margin: 0 5 5 5
|
||||
|
||||
Label
|
||||
id: updateStatusLabel
|
||||
!text: tr('Updating')
|
||||
text-align: center
|
||||
|
||||
ProgressBar
|
||||
id: updateProgressBar
|
||||
height: 15
|
||||
background-color: #4444ff
|
||||
margin-bottom: 10
|
||||
margin-top: 10
|
||||
|
||||
Label
|
||||
id: downloadStatusLabel
|
||||
!text: tr('Downloading:')
|
||||
text-align: center
|
||||
margin-top: 5
|
||||
height: 25
|
||||
|
||||
ProgressBar
|
||||
id: downloadProgressBar
|
||||
height: 15
|
||||
background-color: #4444ff
|
||||
margin-bottom: 10
|
||||
margin-top: 10
|
||||
|
||||
Button
|
||||
!text: tr('Cancel')
|
||||
margin-left: 70
|
||||
margin-right: 70
|
||||
@onClick: Updater.abort()
|
17
modules/corelib/bitwise.lua
Normal file
17
modules/corelib/bitwise.lua
Normal file
@@ -0,0 +1,17 @@
|
||||
Bit = {}
|
||||
|
||||
function Bit.bit(p)
|
||||
return 2 ^ p
|
||||
end
|
||||
|
||||
function Bit.hasBit(x, p)
|
||||
return x % (p + p) >= p
|
||||
end
|
||||
|
||||
function Bit.setbit(x, p)
|
||||
return Bit.hasBit(x, p) and x or x + p
|
||||
end
|
||||
|
||||
function Bit.clearbit(x, p)
|
||||
return Bit.hasBit(x, p) and x - p or x
|
||||
end
|
73
modules/corelib/config.lua
Normal file
73
modules/corelib/config.lua
Normal file
@@ -0,0 +1,73 @@
|
||||
-- @docclass
|
||||
|
||||
local function convertSettingValue(value)
|
||||
if type(value) == 'table' then
|
||||
if value.x and value.width then
|
||||
return recttostring(value)
|
||||
elseif value.x then
|
||||
return pointtostring(value)
|
||||
elseif value.width then
|
||||
return sizetostring(value)
|
||||
elseif value.r then
|
||||
return colortostring(value)
|
||||
else
|
||||
return value
|
||||
end
|
||||
elseif value == nil then
|
||||
return ''
|
||||
else
|
||||
return tostring(value)
|
||||
end
|
||||
end
|
||||
|
||||
function Config:set(key, value)
|
||||
self:setValue(key, convertSettingValue(value))
|
||||
end
|
||||
|
||||
function Config:setDefault(key, value)
|
||||
if self:exists(key) then return false end
|
||||
self:set(key, value)
|
||||
return true
|
||||
end
|
||||
|
||||
function Config:get(key, default)
|
||||
if not self:exists(key) and default ~= nil then
|
||||
self:set(key, default)
|
||||
end
|
||||
return self:getValue(key)
|
||||
end
|
||||
|
||||
function Config:getString(key, default)
|
||||
return self:get(key, default)
|
||||
end
|
||||
|
||||
function Config:getInteger(key, default)
|
||||
local v = tonumber(self:get(key, default)) or 0
|
||||
return v
|
||||
end
|
||||
|
||||
function Config:getNumber(key, default)
|
||||
local v = tonumber(self:get(key, default)) or 0
|
||||
return v
|
||||
end
|
||||
|
||||
function Config:getBoolean(key, default)
|
||||
return toboolean(self:get(key, default))
|
||||
end
|
||||
|
||||
function Config:getPoint(key, default)
|
||||
return topoint(self:get(key, default))
|
||||
end
|
||||
|
||||
function Config:getRect(key, default)
|
||||
return torect(self:get(key, default))
|
||||
end
|
||||
|
||||
function Config:getSize(key, default)
|
||||
return tosize(self:get(key, default))
|
||||
end
|
||||
|
||||
function Config:getColor(key, default)
|
||||
return tocolor(self:get(key, default))
|
||||
end
|
||||
|
321
modules/corelib/const.lua
Normal file
321
modules/corelib/const.lua
Normal file
@@ -0,0 +1,321 @@
|
||||
-- @docconsts @{
|
||||
|
||||
AnchorNone = 0
|
||||
AnchorTop = 1
|
||||
AnchorBottom = 2
|
||||
AnchorLeft = 3
|
||||
AnchorRight = 4
|
||||
AnchorVerticalCenter = 5
|
||||
AnchorHorizontalCenter = 6
|
||||
|
||||
LogDebug = 0
|
||||
LogInfo = 1
|
||||
LogWarning = 2
|
||||
LogError = 3
|
||||
LogFatal = 4
|
||||
|
||||
MouseFocusReason = 0
|
||||
KeyboardFocusReason = 1
|
||||
ActiveFocusReason = 2
|
||||
OtherFocusReason = 3
|
||||
|
||||
AutoFocusNone = 0
|
||||
AutoFocusFirst = 1
|
||||
AutoFocusLast = 2
|
||||
|
||||
KeyboardNoModifier = 0
|
||||
KeyboardCtrlModifier = 1
|
||||
KeyboardAltModifier = 2
|
||||
KeyboardCtrlAltModifier = 3
|
||||
KeyboardShiftModifier = 4
|
||||
KeyboardCtrlShiftModifier = 5
|
||||
KeyboardAltShiftModifier = 6
|
||||
KeyboardCtrlAltShiftModifier = 7
|
||||
|
||||
MouseNoButton = 0
|
||||
MouseLeftButton = 1
|
||||
MouseRightButton = 2
|
||||
MouseMidButton = 3
|
||||
|
||||
MouseNoWheel = 0
|
||||
MouseWheelUp = 1
|
||||
MouseWheelDown = 2
|
||||
|
||||
AlignNone = 0
|
||||
AlignLeft = 1
|
||||
AlignRight = 2
|
||||
AlignTop = 4
|
||||
AlignBottom = 8
|
||||
AlignHorizontalCenter = 16
|
||||
AlignVerticalCenter = 32
|
||||
AlignTopLeft = 5
|
||||
AlignTopRight = 6
|
||||
AlignBottomLeft = 9
|
||||
AlignBottomRight = 10
|
||||
AlignLeftCenter = 33
|
||||
AlignRightCenter = 34
|
||||
AlignTopCenter = 20
|
||||
AlignBottomCenter = 24
|
||||
AlignCenter = 48
|
||||
|
||||
KeyUnknown = 0
|
||||
KeyEscape = 1
|
||||
KeyTab = 2
|
||||
KeyBackspace = 3
|
||||
KeyEnter = 5
|
||||
KeyInsert = 6
|
||||
KeyDelete = 7
|
||||
KeyPause = 8
|
||||
KeyPrintScreen = 9
|
||||
KeyHome = 10
|
||||
KeyEnd = 11
|
||||
KeyPageUp = 12
|
||||
KeyPageDown = 13
|
||||
KeyUp = 14
|
||||
KeyDown = 15
|
||||
KeyLeft = 16
|
||||
KeyRight = 17
|
||||
KeyNumLock = 18
|
||||
KeyScrollLock = 19
|
||||
KeyCapsLock = 20
|
||||
KeyCtrl = 21
|
||||
KeyShift = 22
|
||||
KeyAlt = 23
|
||||
KeyMeta = 25
|
||||
KeyMenu = 26
|
||||
KeySpace = 32 -- ' '
|
||||
KeyExclamation = 33 -- !
|
||||
KeyQuote = 34 -- "
|
||||
KeyNumberSign = 35 -- #
|
||||
KeyDollar = 36 -- $
|
||||
KeyPercent = 37 -- %
|
||||
KeyAmpersand = 38 -- &
|
||||
KeyApostrophe = 39 -- '
|
||||
KeyLeftParen = 40 -- (
|
||||
KeyRightParen = 41 -- )
|
||||
KeyAsterisk = 42 -- *
|
||||
KeyPlus = 43 -- +
|
||||
KeyComma = 44 -- ,
|
||||
KeyMinus = 45 -- -
|
||||
KeyPeriod = 46 -- .
|
||||
KeySlash = 47 -- /
|
||||
Key0 = 48 -- 0
|
||||
Key1 = 49 -- 1
|
||||
Key2 = 50 -- 2
|
||||
Key3 = 51 -- 3
|
||||
Key4 = 52 -- 4
|
||||
Key5 = 53 -- 5
|
||||
Key6 = 54 -- 6
|
||||
Key7 = 55 -- 7
|
||||
Key8 = 56 -- 8
|
||||
Key9 = 57 -- 9
|
||||
KeyColon = 58 -- :
|
||||
KeySemicolon = 59 -- ;
|
||||
KeyLess = 60 -- <
|
||||
KeyEqual = 61 -- =
|
||||
KeyGreater = 62 -- >
|
||||
KeyQuestion = 63 -- ?
|
||||
KeyAtSign = 64 -- @
|
||||
KeyA = 65 -- a
|
||||
KeyB = 66 -- b
|
||||
KeyC = 67 -- c
|
||||
KeyD = 68 -- d
|
||||
KeyE = 69 -- e
|
||||
KeyF = 70 -- f
|
||||
KeyG = 71 -- g
|
||||
KeyH = 72 -- h
|
||||
KeyI = 73 -- i
|
||||
KeyJ = 74 -- j
|
||||
KeyK = 75 -- k
|
||||
KeyL = 76 -- l
|
||||
KeyM = 77 -- m
|
||||
KeyN = 78 -- n
|
||||
KeyO = 79 -- o
|
||||
KeyP = 80 -- p
|
||||
KeyQ = 81 -- q
|
||||
KeyR = 82 -- r
|
||||
KeyS = 83 -- s
|
||||
KeyT = 84 -- t
|
||||
KeyU = 85 -- u
|
||||
KeyV = 86 -- v
|
||||
KeyW = 87 -- w
|
||||
KeyX = 88 -- x
|
||||
KeyY = 89 -- y
|
||||
KeyZ = 90 -- z
|
||||
KeyLeftBracket = 91 -- [
|
||||
KeyBackslash = 92 -- '\'
|
||||
KeyRightBracket = 93 -- ]
|
||||
KeyCaret = 94 -- ^
|
||||
KeyUnderscore = 95 -- _
|
||||
KeyGrave = 96 -- `
|
||||
KeyLeftCurly = 123 -- {
|
||||
KeyBar = 124 -- |
|
||||
KeyRightCurly = 125 -- }
|
||||
KeyTilde = 126 -- ~
|
||||
KeyF1 = 128
|
||||
KeyF2 = 129
|
||||
KeyF3 = 130
|
||||
KeyF4 = 131
|
||||
KeyF5 = 132
|
||||
KeyF6 = 134
|
||||
KeyF7 = 135
|
||||
KeyF8 = 136
|
||||
KeyF9 = 137
|
||||
KeyF10 = 138
|
||||
KeyF11 = 139
|
||||
KeyF12 = 140
|
||||
KeyNumpad0 = 141
|
||||
KeyNumpad1 = 142
|
||||
KeyNumpad2 = 143
|
||||
KeyNumpad3 = 144
|
||||
KeyNumpad4 = 145
|
||||
KeyNumpad5 = 146
|
||||
KeyNumpad6 = 147
|
||||
KeyNumpad7 = 148
|
||||
KeyNumpad8 = 149
|
||||
KeyNumpad9 = 150
|
||||
|
||||
FirstKey = KeyUnknown
|
||||
LastKey = KeyNumpad9
|
||||
|
||||
ExtendedActivate = 0
|
||||
ExtendedLocales = 1
|
||||
ExtendedParticles = 2
|
||||
|
||||
-- @}
|
||||
|
||||
KeyCodeDescs = {
|
||||
[KeyUnknown] = 'Unknown',
|
||||
[KeyEscape] = 'Escape',
|
||||
[KeyTab] = 'Tab',
|
||||
[KeyBackspace] = 'Backspace',
|
||||
[KeyEnter] = 'Enter',
|
||||
[KeyInsert] = 'Insert',
|
||||
[KeyDelete] = 'Delete',
|
||||
[KeyPause] = 'Pause',
|
||||
[KeyPrintScreen] = 'PrintScreen',
|
||||
[KeyHome] = 'Home',
|
||||
[KeyEnd] = 'End',
|
||||
[KeyPageUp] = 'PageUp',
|
||||
[KeyPageDown] = 'PageDown',
|
||||
[KeyUp] = 'Up',
|
||||
[KeyDown] = 'Down',
|
||||
[KeyLeft] = 'Left',
|
||||
[KeyRight] = 'Right',
|
||||
[KeyNumLock] = 'NumLock',
|
||||
[KeyScrollLock] = 'ScrollLock',
|
||||
[KeyCapsLock] = 'CapsLock',
|
||||
[KeyCtrl] = 'Ctrl',
|
||||
[KeyShift] = 'Shift',
|
||||
[KeyAlt] = 'Alt',
|
||||
[KeyMeta] = 'Meta',
|
||||
[KeyMenu] = 'Menu',
|
||||
[KeySpace] = 'Space',
|
||||
[KeyExclamation] = '!',
|
||||
[KeyQuote] = '\"',
|
||||
[KeyNumberSign] = '#',
|
||||
[KeyDollar] = '$',
|
||||
[KeyPercent] = '%',
|
||||
[KeyAmpersand] = '&',
|
||||
[KeyApostrophe] = '\'',
|
||||
[KeyLeftParen] = '(',
|
||||
[KeyRightParen] = ')',
|
||||
[KeyAsterisk] = '*',
|
||||
[KeyPlus] = 'Plus',
|
||||
[KeyComma] = ',',
|
||||
[KeyMinus] = '-',
|
||||
[KeyPeriod] = '.',
|
||||
[KeySlash] = '/',
|
||||
[Key0] = '0',
|
||||
[Key1] = '1',
|
||||
[Key2] = '2',
|
||||
[Key3] = '3',
|
||||
[Key4] = '4',
|
||||
[Key5] = '5',
|
||||
[Key6] = '6',
|
||||
[Key7] = '7',
|
||||
[Key8] = '8',
|
||||
[Key9] = '9',
|
||||
[KeyColon] = ':',
|
||||
[KeySemicolon] = ';',
|
||||
[KeyLess] = '<',
|
||||
[KeyEqual] = '=',
|
||||
[KeyGreater] = '>',
|
||||
[KeyQuestion] = '?',
|
||||
[KeyAtSign] = '@',
|
||||
[KeyA] = 'A',
|
||||
[KeyB] = 'B',
|
||||
[KeyC] = 'C',
|
||||
[KeyD] = 'D',
|
||||
[KeyE] = 'E',
|
||||
[KeyF] = 'F',
|
||||
[KeyG] = 'G',
|
||||
[KeyH] = 'H',
|
||||
[KeyI] = 'I',
|
||||
[KeyJ] = 'J',
|
||||
[KeyK] = 'K',
|
||||
[KeyL] = 'L',
|
||||
[KeyM] = 'M',
|
||||
[KeyN] = 'N',
|
||||
[KeyO] = 'O',
|
||||
[KeyP] = 'P',
|
||||
[KeyQ] = 'Q',
|
||||
[KeyR] = 'R',
|
||||
[KeyS] = 'S',
|
||||
[KeyT] = 'T',
|
||||
[KeyU] = 'U',
|
||||
[KeyV] = 'V',
|
||||
[KeyW] = 'W',
|
||||
[KeyX] = 'X',
|
||||
[KeyY] = 'Y',
|
||||
[KeyZ] = 'Z',
|
||||
[KeyLeftBracket] = '[',
|
||||
[KeyBackslash] = '\\',
|
||||
[KeyRightBracket] = ']',
|
||||
[KeyCaret] = '^',
|
||||
[KeyUnderscore] = '_',
|
||||
[KeyGrave] = '`',
|
||||
[KeyLeftCurly] = '{',
|
||||
[KeyBar] = '|',
|
||||
[KeyRightCurly] = '}',
|
||||
[KeyTilde] = '~',
|
||||
[KeyF1] = 'F1',
|
||||
[KeyF2] = 'F2',
|
||||
[KeyF3] = 'F3',
|
||||
[KeyF4] = 'F4',
|
||||
[KeyF5] = 'F5',
|
||||
[KeyF6] = 'F6',
|
||||
[KeyF7] = 'F7',
|
||||
[KeyF8] = 'F8',
|
||||
[KeyF9] = 'F9',
|
||||
[KeyF10] = 'F10',
|
||||
[KeyF11] = 'F11',
|
||||
[KeyF12] = 'F12',
|
||||
[KeyNumpad0] = 'Numpad0',
|
||||
[KeyNumpad1] = 'Numpad1',
|
||||
[KeyNumpad2] = 'Numpad2',
|
||||
[KeyNumpad3] = 'Numpad3',
|
||||
[KeyNumpad4] = 'Numpad4',
|
||||
[KeyNumpad5] = 'Numpad5',
|
||||
[KeyNumpad6] = 'Numpad6',
|
||||
[KeyNumpad7] = 'Numpad7',
|
||||
[KeyNumpad8] = 'Numpad8',
|
||||
[KeyNumpad9] = 'Numpad9',
|
||||
}
|
||||
|
||||
NetworkMessageTypes = {
|
||||
Boolean = 1,
|
||||
U8 = 2,
|
||||
U16 = 3,
|
||||
U32 = 4,
|
||||
U64 = 5,
|
||||
NumberString = 6,
|
||||
String = 7,
|
||||
Table = 8,
|
||||
}
|
||||
|
||||
SoundChannels = {
|
||||
Music = 1,
|
||||
Ambient = 2,
|
||||
Effect = 3,
|
||||
}
|
33
modules/corelib/corelib.otmod
Normal file
33
modules/corelib/corelib.otmod
Normal file
@@ -0,0 +1,33 @@
|
||||
Module
|
||||
name: corelib
|
||||
description: Contains core lua classes, functions and constants used by other modules
|
||||
author: OTClient team
|
||||
website: https://github.com/edubart/otclient
|
||||
reloadable: false
|
||||
|
||||
@onLoad: |
|
||||
dofile 'math'
|
||||
dofile 'string'
|
||||
dofile 'table'
|
||||
dofile 'bitwise'
|
||||
dofile 'struct'
|
||||
|
||||
dofile 'const'
|
||||
dofile 'util'
|
||||
dofile 'globals'
|
||||
dofile 'config'
|
||||
dofile 'settings'
|
||||
dofile 'keyboard'
|
||||
dofile 'mouse'
|
||||
dofile 'net'
|
||||
|
||||
dofiles 'classes'
|
||||
dofiles 'ui'
|
||||
|
||||
dofile 'inputmessage'
|
||||
dofile 'outputmessage'
|
||||
dofile 'orderedtable'
|
||||
|
||||
dofile 'json'
|
||||
dofile 'http'
|
||||
|
76
modules/corelib/globals.lua
Normal file
76
modules/corelib/globals.lua
Normal file
@@ -0,0 +1,76 @@
|
||||
-- @docvars @{
|
||||
|
||||
-- root widget
|
||||
rootWidget = g_ui.getRootWidget()
|
||||
modules = package.loaded
|
||||
|
||||
-- G is used as a global table to save variables in memory between reloads
|
||||
G = G or {}
|
||||
|
||||
-- @}
|
||||
|
||||
-- @docfuncs @{
|
||||
|
||||
function scheduleEvent(callback, delay)
|
||||
local desc = "lua"
|
||||
local info = debug.getinfo(2, "Sl")
|
||||
if info then
|
||||
desc = info.short_src .. ":" .. info.currentline
|
||||
end
|
||||
local event = g_dispatcher.scheduleEvent(desc, callback, delay)
|
||||
-- must hold a reference to the callback, otherwise it would be collected
|
||||
event._callback = callback
|
||||
return event
|
||||
end
|
||||
|
||||
function addEvent(callback, front)
|
||||
local desc = "lua"
|
||||
local info = debug.getinfo(2, "Sl")
|
||||
if info then
|
||||
desc = info.short_src .. ":" .. info.currentline
|
||||
end
|
||||
local event = g_dispatcher.addEvent(desc, callback, front)
|
||||
-- must hold a reference to the callback, otherwise it would be collected
|
||||
event._callback = callback
|
||||
return event
|
||||
end
|
||||
|
||||
function cycleEvent(callback, interval)
|
||||
local desc = "lua"
|
||||
local info = debug.getinfo(2, "Sl")
|
||||
if info then
|
||||
desc = info.short_src .. ":" .. info.currentline
|
||||
end
|
||||
local event = g_dispatcher.cycleEvent(desc, callback, interval)
|
||||
-- must hold a reference to the callback, otherwise it would be collected
|
||||
event._callback = callback
|
||||
return event
|
||||
end
|
||||
|
||||
function periodicalEvent(eventFunc, conditionFunc, delay, autoRepeatDelay)
|
||||
delay = delay or 30
|
||||
autoRepeatDelay = autoRepeatDelay or delay
|
||||
|
||||
local func
|
||||
func = function()
|
||||
if conditionFunc and not conditionFunc() then
|
||||
func = nil
|
||||
return
|
||||
end
|
||||
eventFunc()
|
||||
scheduleEvent(func, delay)
|
||||
end
|
||||
|
||||
scheduleEvent(function()
|
||||
func()
|
||||
end, autoRepeatDelay)
|
||||
end
|
||||
|
||||
function removeEvent(event)
|
||||
if event then
|
||||
event:cancel()
|
||||
event._callback = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- @}
|
157
modules/corelib/http.lua
Normal file
157
modules/corelib/http.lua
Normal file
@@ -0,0 +1,157 @@
|
||||
HTTP = {
|
||||
timeout=5,
|
||||
imageId=1000,
|
||||
images={},
|
||||
operations={}
|
||||
}
|
||||
|
||||
function HTTP.get(url, callback)
|
||||
local operation = g_http.get(url, HTTP.timeout)
|
||||
HTTP.operations[operation] = {type="get", url=url, callback=callback}
|
||||
return opreation
|
||||
end
|
||||
|
||||
function HTTP.getJSON(url, callback)
|
||||
local operation = g_http.get(url, HTTP.timeout)
|
||||
HTTP.operations[operation] = {type="get", json=true, url=url, callback=callback}
|
||||
return opreation
|
||||
end
|
||||
|
||||
function HTTP.post(url, data, callback)
|
||||
if type(data) == "table" then
|
||||
data = json.encode(data)
|
||||
end
|
||||
local operation = g_http.post(url, data, HTTP.timeout)
|
||||
HTTP.operations[operation] = {type="post", url=url, callback=callback}
|
||||
return opreation
|
||||
end
|
||||
|
||||
function HTTP.postJSON(url, data, callback)
|
||||
if type(data) == "table" then
|
||||
data = json.encode(data)
|
||||
end
|
||||
local operation = g_http.post(url, data, HTTP.timeout)
|
||||
HTTP.operations[operation] = {type="post", json=true, url=url, callback=callback}
|
||||
return opreation
|
||||
end
|
||||
|
||||
function HTTP.download(url, file, callback, progressCallback)
|
||||
local operation = g_http.download(url, file, HTTP.timeout)
|
||||
HTTP.operations[operation] = {type="download", url=url, file=file, callback=callback, progressCallback=progressCallback}
|
||||
return opreation
|
||||
end
|
||||
|
||||
function HTTP.downloadImage(url, callback)
|
||||
if HTTP.images[url] ~= nil then
|
||||
if callback then
|
||||
callback('/downloads/' .. HTTP.images[url], nil)
|
||||
end
|
||||
return
|
||||
end
|
||||
local file = "autoimage_" .. HTTP.imageId .. ".png"
|
||||
HTTP.imageId = HTTP.imageId + 1
|
||||
local operation = g_http.download(url, file, HTTP.timeout)
|
||||
HTTP.operations[operation] = {type="image", url=url, file=file, callback=callback}
|
||||
return opreation
|
||||
end
|
||||
|
||||
function HTTP.progress(operationId)
|
||||
return g_http.getProgress(operationId)
|
||||
end
|
||||
|
||||
function HTTP.cancel(operationId)
|
||||
return g_http.cancel(operationId)
|
||||
end
|
||||
|
||||
function HTTP.onGet(operationId, url, err, data)
|
||||
local operation = HTTP.operations[operationId]
|
||||
if operation == nil then
|
||||
return
|
||||
end
|
||||
if err and err:len() == 0 then
|
||||
err = nil
|
||||
end
|
||||
if not err and operation.json then
|
||||
local status, result = pcall(function() return json.decode(data) end)
|
||||
if not status then
|
||||
err = "JSON ERROR: " .. result
|
||||
end
|
||||
data = result
|
||||
end
|
||||
if operation.callback then
|
||||
operation.callback(data, err)
|
||||
end
|
||||
end
|
||||
|
||||
function HTTP.onGetProgress(operationId, url, progress)
|
||||
local operation = HTTP.operations[operationId]
|
||||
if operation == nil then
|
||||
return
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
function HTTP.onPost(operationId, url, err, data)
|
||||
local operation = HTTP.operations[operationId]
|
||||
if operation == nil then
|
||||
return
|
||||
end
|
||||
if err and err:len() == 0 then
|
||||
err = nil
|
||||
end
|
||||
if not err and operation.json then
|
||||
local status, result = pcall(function() return json.decode(data) end)
|
||||
if not status then
|
||||
err = "JSON ERROR: " .. result
|
||||
end
|
||||
data = result
|
||||
end
|
||||
if operation.callback then
|
||||
operation.callback(data, err)
|
||||
end
|
||||
end
|
||||
|
||||
function HTTP.onPostProgress(operationId, url, progress)
|
||||
local operation = HTTP.operations[operationId]
|
||||
if operation == nil then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
function HTTP.onDownload(operationId, url, err, path, checksum)
|
||||
local operation = HTTP.operations[operationId]
|
||||
if operation == nil then
|
||||
return
|
||||
end
|
||||
if err and err:len() == 0 then
|
||||
err = nil
|
||||
end
|
||||
if operation.callback then
|
||||
if operation["type"] == "image" then
|
||||
HTTP.images[url] = path
|
||||
operation.callback('/downloads/' .. path, err)
|
||||
else
|
||||
operation.callback(path, checksum, err)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function HTTP.onDownloadProgress(operationId, url, progress, speed)
|
||||
local operation = HTTP.operations[operationId]
|
||||
if operation == nil then
|
||||
return
|
||||
end
|
||||
if operation.progressCallback then
|
||||
operation.progressCallback(progress, speed)
|
||||
end
|
||||
end
|
||||
|
||||
connect(g_http,
|
||||
{
|
||||
onGet = HTTP.onGet,
|
||||
onGetProgress = HTTP.onGetProgress,
|
||||
onPost = HTTP.onPost,
|
||||
onPostProgress = HTTP.onPostProgress,
|
||||
onDownload = HTTP.onDownload,
|
||||
onDownloadProgress = HTTP.onDownloadProgress
|
||||
})
|
51
modules/corelib/inputmessage.lua
Normal file
51
modules/corelib/inputmessage.lua
Normal file
@@ -0,0 +1,51 @@
|
||||
function InputMessage:getData()
|
||||
local dataType = self:getU8()
|
||||
if dataType == NetworkMessageTypes.Boolean then
|
||||
return numbertoboolean(self:getU8())
|
||||
elseif dataType == NetworkMessageTypes.U8 then
|
||||
return self:getU8()
|
||||
elseif dataType == NetworkMessageTypes.U16 then
|
||||
return self:getU16()
|
||||
elseif dataType == NetworkMessageTypes.U32 then
|
||||
return self:getU32()
|
||||
elseif dataType == NetworkMessageTypes.U64 then
|
||||
return self:getU64()
|
||||
elseif dataType == NetworkMessageTypes.NumberString then
|
||||
return tonumber(self:getString())
|
||||
elseif dataType == NetworkMessageTypes.String then
|
||||
return self:getString()
|
||||
elseif dataType == NetworkMessageTypes.Table then
|
||||
return self:getTable()
|
||||
else
|
||||
perror('Unknown data type ' .. dataType)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function InputMessage:getTable()
|
||||
local ret = {}
|
||||
local size = self:getU16()
|
||||
for i=1,size do
|
||||
local index = self:getData()
|
||||
local value = self:getData()
|
||||
ret[index] = value
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
function InputMessage:getColor()
|
||||
local color = {}
|
||||
color.r = self:getU8()
|
||||
color.g = self:getU8()
|
||||
color.b = self:getU8()
|
||||
color.a = self:getU8()
|
||||
return color
|
||||
end
|
||||
|
||||
function InputMessage:getPosition()
|
||||
local position = {}
|
||||
position.x = self:getU16()
|
||||
position.y = self:getU16()
|
||||
position.z = self:getU8()
|
||||
return position
|
||||
end
|
397
modules/corelib/json.lua
Normal file
397
modules/corelib/json.lua
Normal file
@@ -0,0 +1,397 @@
|
||||
--
|
||||
-- json.lua
|
||||
--
|
||||
-- Copyright (c) 2018 rxi
|
||||
--
|
||||
-- Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
-- this software and associated documentation files (the "Software"), to deal in
|
||||
-- the Software without restriction, including without limitation the rights to
|
||||
-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||
-- of the Software, and to permit persons to whom the Software is furnished to do
|
||||
-- so, subject to the following conditions:
|
||||
--
|
||||
-- The above copyright notice and this permission notice shall be included in all
|
||||
-- copies or substantial portions of the Software.
|
||||
--
|
||||
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
-- SOFTWARE.
|
||||
--
|
||||
|
||||
json = { _version = "0.1.1" }
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Encode
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
local encode
|
||||
|
||||
local escape_char_map = {
|
||||
[ "\\" ] = "\\\\",
|
||||
[ "\"" ] = "\\\"",
|
||||
[ "\b" ] = "\\b",
|
||||
[ "\f" ] = "\\f",
|
||||
[ "\n" ] = "\\n",
|
||||
[ "\r" ] = "\\r",
|
||||
[ "\t" ] = "\\t",
|
||||
}
|
||||
|
||||
local escape_char_map_inv = { [ "\\/" ] = "/" }
|
||||
for k, v in pairs(escape_char_map) do
|
||||
escape_char_map_inv[v] = k
|
||||
end
|
||||
|
||||
|
||||
local function escape_char(c)
|
||||
return escape_char_map[c] or string.format("\\u%04x", c:byte())
|
||||
end
|
||||
|
||||
|
||||
local function encode_nil(val)
|
||||
return "null"
|
||||
end
|
||||
|
||||
|
||||
local function encode_table(val, stack)
|
||||
local res = {}
|
||||
stack = stack or {}
|
||||
|
||||
-- Circular reference?
|
||||
if stack[val] then error("circular reference") end
|
||||
|
||||
stack[val] = true
|
||||
|
||||
if val[1] ~= nil or next(val) == nil then
|
||||
-- Treat as array -- check keys are valid and it is not sparse
|
||||
local n = 0
|
||||
for k in pairs(val) do
|
||||
if type(k) ~= "number" then
|
||||
error("invalid table: mixed or invalid key types")
|
||||
end
|
||||
n = n + 1
|
||||
end
|
||||
if n ~= #val then
|
||||
error("invalid table: sparse array")
|
||||
end
|
||||
-- Encode
|
||||
for i, v in ipairs(val) do
|
||||
table.insert(res, encode(v, stack))
|
||||
end
|
||||
stack[val] = nil
|
||||
return "[" .. table.concat(res, ",") .. "]"
|
||||
|
||||
else
|
||||
-- Treat as an object
|
||||
for k, v in pairs(val) do
|
||||
if type(k) ~= "string" then
|
||||
error("invalid table: mixed or invalid key types")
|
||||
end
|
||||
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
|
||||
end
|
||||
stack[val] = nil
|
||||
return "{" .. table.concat(res, ",") .. "}"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function encode_string(val)
|
||||
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
|
||||
end
|
||||
|
||||
|
||||
local function encode_number(val)
|
||||
-- Check for NaN, -inf and inf
|
||||
if val ~= val or val <= -math.huge or val >= math.huge then
|
||||
error("unexpected number value '" .. tostring(val) .. "'")
|
||||
end
|
||||
return string.format("%.14g", val)
|
||||
end
|
||||
|
||||
|
||||
local type_func_map = {
|
||||
[ "nil" ] = encode_nil,
|
||||
[ "table" ] = encode_table,
|
||||
[ "string" ] = encode_string,
|
||||
[ "number" ] = encode_number,
|
||||
[ "boolean" ] = tostring,
|
||||
}
|
||||
|
||||
|
||||
encode = function(val, stack)
|
||||
local t = type(val)
|
||||
local f = type_func_map[t]
|
||||
if f then
|
||||
return f(val, stack)
|
||||
end
|
||||
error("unexpected type '" .. t .. "'")
|
||||
end
|
||||
|
||||
|
||||
function json.encode(val)
|
||||
return ( encode(val) )
|
||||
end
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Decode
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
local parse
|
||||
|
||||
local function create_set(...)
|
||||
local res = {}
|
||||
for i = 1, select("#", ...) do
|
||||
res[ select(i, ...) ] = true
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
local space_chars = create_set(" ", "\t", "\r", "\n")
|
||||
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
|
||||
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
|
||||
local literals = create_set("true", "false", "null")
|
||||
|
||||
local literal_map = {
|
||||
[ "true" ] = true,
|
||||
[ "false" ] = false,
|
||||
[ "null" ] = nil,
|
||||
}
|
||||
|
||||
|
||||
local function next_char(str, idx, set, negate)
|
||||
for i = idx, #str do
|
||||
if set[str:sub(i, i)] ~= negate then
|
||||
return i
|
||||
end
|
||||
end
|
||||
return #str + 1
|
||||
end
|
||||
|
||||
|
||||
local function decode_error(str, idx, msg)
|
||||
local line_count = 1
|
||||
local col_count = 1
|
||||
for i = 1, idx - 1 do
|
||||
col_count = col_count + 1
|
||||
if str:sub(i, i) == "\n" then
|
||||
line_count = line_count + 1
|
||||
col_count = 1
|
||||
end
|
||||
end
|
||||
error( string.format("%s at line %d col %d", msg, line_count, col_count) )
|
||||
end
|
||||
|
||||
|
||||
local function codepoint_to_utf8(n)
|
||||
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
|
||||
local f = math.floor
|
||||
if n <= 0x7f then
|
||||
return string.char(n)
|
||||
elseif n <= 0x7ff then
|
||||
return string.char(f(n / 64) + 192, n % 64 + 128)
|
||||
elseif n <= 0xffff then
|
||||
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
|
||||
elseif n <= 0x10ffff then
|
||||
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
|
||||
f(n % 4096 / 64) + 128, n % 64 + 128)
|
||||
end
|
||||
error( string.format("invalid unicode codepoint '%x'", n) )
|
||||
end
|
||||
|
||||
|
||||
local function parse_unicode_escape(s)
|
||||
local n1 = tonumber( s:sub(3, 6), 16 )
|
||||
local n2 = tonumber( s:sub(9, 12), 16 )
|
||||
-- Surrogate pair?
|
||||
if n2 then
|
||||
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
|
||||
else
|
||||
return codepoint_to_utf8(n1)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function parse_string(str, i)
|
||||
local has_unicode_escape = false
|
||||
local has_surrogate_escape = false
|
||||
local has_escape = false
|
||||
local last
|
||||
for j = i + 1, #str do
|
||||
local x = str:byte(j)
|
||||
|
||||
if x < 32 then
|
||||
decode_error(str, j, "control character in string")
|
||||
end
|
||||
|
||||
if last == 92 then -- "\\" (escape char)
|
||||
if x == 117 then -- "u" (unicode escape sequence)
|
||||
local hex = str:sub(j + 1, j + 5)
|
||||
if not hex:find("%x%x%x%x") then
|
||||
decode_error(str, j, "invalid unicode escape in string")
|
||||
end
|
||||
if hex:find("^[dD][89aAbB]") then
|
||||
has_surrogate_escape = true
|
||||
else
|
||||
has_unicode_escape = true
|
||||
end
|
||||
else
|
||||
local c = string.char(x)
|
||||
if not escape_chars[c] then
|
||||
decode_error(str, j, "invalid escape char '" .. c .. "' in string")
|
||||
end
|
||||
has_escape = true
|
||||
end
|
||||
last = nil
|
||||
|
||||
elseif x == 34 then -- '"' (end of string)
|
||||
local s = str:sub(i + 1, j - 1)
|
||||
if has_surrogate_escape then
|
||||
s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape)
|
||||
end
|
||||
if has_unicode_escape then
|
||||
s = s:gsub("\\u....", parse_unicode_escape)
|
||||
end
|
||||
if has_escape then
|
||||
s = s:gsub("\\.", escape_char_map_inv)
|
||||
end
|
||||
return s, j + 1
|
||||
|
||||
else
|
||||
last = x
|
||||
end
|
||||
end
|
||||
decode_error(str, i, "expected closing quote for string")
|
||||
end
|
||||
|
||||
|
||||
local function parse_number(str, i)
|
||||
local x = next_char(str, i, delim_chars)
|
||||
local s = str:sub(i, x - 1)
|
||||
local n = tonumber(s)
|
||||
if not n then
|
||||
decode_error(str, i, "invalid number '" .. s .. "'")
|
||||
end
|
||||
return n, x
|
||||
end
|
||||
|
||||
|
||||
local function parse_literal(str, i)
|
||||
local x = next_char(str, i, delim_chars)
|
||||
local word = str:sub(i, x - 1)
|
||||
if not literals[word] then
|
||||
decode_error(str, i, "invalid literal '" .. word .. "'")
|
||||
end
|
||||
return literal_map[word], x
|
||||
end
|
||||
|
||||
|
||||
local function parse_array(str, i)
|
||||
local res = {}
|
||||
local n = 1
|
||||
i = i + 1
|
||||
while 1 do
|
||||
local x
|
||||
i = next_char(str, i, space_chars, true)
|
||||
-- Empty / end of array?
|
||||
if str:sub(i, i) == "]" then
|
||||
i = i + 1
|
||||
break
|
||||
end
|
||||
-- Read token
|
||||
x, i = parse(str, i)
|
||||
res[n] = x
|
||||
n = n + 1
|
||||
-- Next token
|
||||
i = next_char(str, i, space_chars, true)
|
||||
local chr = str:sub(i, i)
|
||||
i = i + 1
|
||||
if chr == "]" then break end
|
||||
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
|
||||
end
|
||||
return res, i
|
||||
end
|
||||
|
||||
|
||||
local function parse_object(str, i)
|
||||
local res = {}
|
||||
i = i + 1
|
||||
while 1 do
|
||||
local key, val
|
||||
i = next_char(str, i, space_chars, true)
|
||||
-- Empty / end of object?
|
||||
if str:sub(i, i) == "}" then
|
||||
i = i + 1
|
||||
break
|
||||
end
|
||||
-- Read key
|
||||
if str:sub(i, i) ~= '"' then
|
||||
decode_error(str, i, "expected string for key")
|
||||
end
|
||||
key, i = parse(str, i)
|
||||
-- Read ':' delimiter
|
||||
i = next_char(str, i, space_chars, true)
|
||||
if str:sub(i, i) ~= ":" then
|
||||
decode_error(str, i, "expected ':' after key")
|
||||
end
|
||||
i = next_char(str, i + 1, space_chars, true)
|
||||
-- Read value
|
||||
val, i = parse(str, i)
|
||||
-- Set
|
||||
res[key] = val
|
||||
-- Next token
|
||||
i = next_char(str, i, space_chars, true)
|
||||
local chr = str:sub(i, i)
|
||||
i = i + 1
|
||||
if chr == "}" then break end
|
||||
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
|
||||
end
|
||||
return res, i
|
||||
end
|
||||
|
||||
|
||||
local char_func_map = {
|
||||
[ '"' ] = parse_string,
|
||||
[ "0" ] = parse_number,
|
||||
[ "1" ] = parse_number,
|
||||
[ "2" ] = parse_number,
|
||||
[ "3" ] = parse_number,
|
||||
[ "4" ] = parse_number,
|
||||
[ "5" ] = parse_number,
|
||||
[ "6" ] = parse_number,
|
||||
[ "7" ] = parse_number,
|
||||
[ "8" ] = parse_number,
|
||||
[ "9" ] = parse_number,
|
||||
[ "-" ] = parse_number,
|
||||
[ "t" ] = parse_literal,
|
||||
[ "f" ] = parse_literal,
|
||||
[ "n" ] = parse_literal,
|
||||
[ "[" ] = parse_array,
|
||||
[ "{" ] = parse_object,
|
||||
}
|
||||
|
||||
|
||||
parse = function(str, idx)
|
||||
local chr = str:sub(idx, idx)
|
||||
local f = char_func_map[chr]
|
||||
if f then
|
||||
return f(str, idx)
|
||||
end
|
||||
decode_error(str, idx, "unexpected character '" .. chr .. "'")
|
||||
end
|
||||
|
||||
|
||||
function json.decode(str)
|
||||
if type(str) ~= "string" then
|
||||
error("expected argument of type string, got " .. type(str))
|
||||
end
|
||||
local res, idx = parse(str, next_char(str, 1, space_chars, true))
|
||||
idx = next_char(str, idx, space_chars, true)
|
||||
if idx <= #str then
|
||||
decode_error(str, idx, "trailing garbage")
|
||||
end
|
||||
return res
|
||||
end
|
226
modules/corelib/keyboard.lua
Normal file
226
modules/corelib/keyboard.lua
Normal file
@@ -0,0 +1,226 @@
|
||||
-- @docclass
|
||||
g_keyboard = {}
|
||||
|
||||
-- private functions
|
||||
function translateKeyCombo(keyCombo)
|
||||
if not keyCombo or #keyCombo == 0 then return nil end
|
||||
local keyComboDesc = ''
|
||||
for k,v in pairs(keyCombo) do
|
||||
local keyDesc = KeyCodeDescs[v]
|
||||
if keyDesc == nil then return nil end
|
||||
keyComboDesc = keyComboDesc .. '+' .. keyDesc
|
||||
end
|
||||
keyComboDesc = keyComboDesc:sub(2)
|
||||
return keyComboDesc
|
||||
end
|
||||
|
||||
local function getKeyCode(key)
|
||||
for keyCode, keyDesc in pairs(KeyCodeDescs) do
|
||||
if keyDesc:lower() == key:trim():lower() then
|
||||
return keyCode
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function retranslateKeyComboDesc(keyComboDesc)
|
||||
if keyComboDesc == nil then
|
||||
error('Unable to translate key combo \'' .. keyComboDesc .. '\'')
|
||||
end
|
||||
|
||||
if type(keyComboDesc) == 'number' then
|
||||
keyComboDesc = tostring(keyComboDesc)
|
||||
end
|
||||
|
||||
local keyCombo = {}
|
||||
for i,currentKeyDesc in ipairs(keyComboDesc:split('+')) do
|
||||
for keyCode, keyDesc in pairs(KeyCodeDescs) do
|
||||
if keyDesc:lower() == currentKeyDesc:trim():lower() then
|
||||
table.insert(keyCombo, keyCode)
|
||||
end
|
||||
end
|
||||
end
|
||||
return translateKeyCombo(keyCombo)
|
||||
end
|
||||
|
||||
function determineKeyComboDesc(keyCode, keyboardModifiers)
|
||||
local keyCombo = {}
|
||||
if keyCode == KeyCtrl or keyCode == KeyShift or keyCode == KeyAlt then
|
||||
table.insert(keyCombo, keyCode)
|
||||
elseif KeyCodeDescs[keyCode] ~= nil then
|
||||
if keyboardModifiers == KeyboardCtrlModifier then
|
||||
table.insert(keyCombo, KeyCtrl)
|
||||
elseif keyboardModifiers == KeyboardAltModifier then
|
||||
table.insert(keyCombo, KeyAlt)
|
||||
elseif keyboardModifiers == KeyboardCtrlAltModifier then
|
||||
table.insert(keyCombo, KeyCtrl)
|
||||
table.insert(keyCombo, KeyAlt)
|
||||
elseif keyboardModifiers == KeyboardShiftModifier then
|
||||
table.insert(keyCombo, KeyShift)
|
||||
elseif keyboardModifiers == KeyboardCtrlShiftModifier then
|
||||
table.insert(keyCombo, KeyCtrl)
|
||||
table.insert(keyCombo, KeyShift)
|
||||
elseif keyboardModifiers == KeyboardAltShiftModifier then
|
||||
table.insert(keyCombo, KeyAlt)
|
||||
table.insert(keyCombo, KeyShift)
|
||||
elseif keyboardModifiers == KeyboardCtrlAltShiftModifier then
|
||||
table.insert(keyCombo, KeyCtrl)
|
||||
table.insert(keyCombo, KeyAlt)
|
||||
table.insert(keyCombo, KeyShift)
|
||||
end
|
||||
table.insert(keyCombo, keyCode)
|
||||
end
|
||||
return translateKeyCombo(keyCombo)
|
||||
end
|
||||
|
||||
local function onWidgetKeyDown(widget, keyCode, keyboardModifiers)
|
||||
if keyCode == KeyUnknown then return false end
|
||||
local callback = widget.boundAloneKeyDownCombos[determineKeyComboDesc(keyCode, KeyboardNoModifier)]
|
||||
signalcall(callback, widget, keyCode)
|
||||
callback = widget.boundKeyDownCombos[determineKeyComboDesc(keyCode, keyboardModifiers)]
|
||||
return signalcall(callback, widget, keyCode)
|
||||
end
|
||||
|
||||
local function onWidgetKeyUp(widget, keyCode, keyboardModifiers)
|
||||
if keyCode == KeyUnknown then return false end
|
||||
local callback = widget.boundAloneKeyUpCombos[determineKeyComboDesc(keyCode, KeyboardNoModifier)]
|
||||
signalcall(callback, widget, keyCode)
|
||||
callback = widget.boundKeyUpCombos[determineKeyComboDesc(keyCode, keyboardModifiers)]
|
||||
return signalcall(callback, widget, keyCode)
|
||||
end
|
||||
|
||||
local function onWidgetKeyPress(widget, keyCode, keyboardModifiers, autoRepeatTicks)
|
||||
if keyCode == KeyUnknown then return false end
|
||||
local callback = widget.boundKeyPressCombos[determineKeyComboDesc(keyCode, keyboardModifiers)]
|
||||
return signalcall(callback, widget, keyCode, autoRepeatTicks)
|
||||
end
|
||||
|
||||
local function connectKeyDownEvent(widget)
|
||||
if widget.boundKeyDownCombos then return end
|
||||
connect(widget, { onKeyDown = onWidgetKeyDown })
|
||||
widget.boundKeyDownCombos = {}
|
||||
widget.boundAloneKeyDownCombos = {}
|
||||
end
|
||||
|
||||
local function connectKeyUpEvent(widget)
|
||||
if widget.boundKeyUpCombos then return end
|
||||
connect(widget, { onKeyUp = onWidgetKeyUp })
|
||||
widget.boundKeyUpCombos = {}
|
||||
widget.boundAloneKeyUpCombos = {}
|
||||
end
|
||||
|
||||
local function connectKeyPressEvent(widget)
|
||||
if widget.boundKeyPressCombos then return end
|
||||
connect(widget, { onKeyPress = onWidgetKeyPress })
|
||||
widget.boundKeyPressCombos = {}
|
||||
end
|
||||
|
||||
-- public functions
|
||||
function g_keyboard.bindKeyDown(keyComboDesc, callback, widget, alone)
|
||||
widget = widget or rootWidget
|
||||
connectKeyDownEvent(widget)
|
||||
local keyComboDesc = retranslateKeyComboDesc(keyComboDesc)
|
||||
if alone then
|
||||
connect(widget.boundAloneKeyDownCombos, keyComboDesc, callback)
|
||||
else
|
||||
connect(widget.boundKeyDownCombos, keyComboDesc, callback)
|
||||
end
|
||||
end
|
||||
|
||||
function g_keyboard.bindKeyUp(keyComboDesc, callback, widget, alone)
|
||||
widget = widget or rootWidget
|
||||
connectKeyUpEvent(widget)
|
||||
local keyComboDesc = retranslateKeyComboDesc(keyComboDesc)
|
||||
if alone then
|
||||
connect(widget.boundAloneKeyUpCombos, keyComboDesc, callback)
|
||||
else
|
||||
connect(widget.boundKeyUpCombos, keyComboDesc, callback)
|
||||
end
|
||||
end
|
||||
|
||||
function g_keyboard.bindKeyPress(keyComboDesc, callback, widget)
|
||||
widget = widget or rootWidget
|
||||
connectKeyPressEvent(widget)
|
||||
local keyComboDesc = retranslateKeyComboDesc(keyComboDesc)
|
||||
connect(widget.boundKeyPressCombos, keyComboDesc, callback)
|
||||
end
|
||||
|
||||
local function getUnbindArgs(arg1, arg2)
|
||||
local callback
|
||||
local widget
|
||||
if type(arg1) == 'function' then callback = arg1
|
||||
elseif type(arg2) == 'function' then callback = arg2 end
|
||||
if type(arg1) == 'userdata' then widget = arg1
|
||||
elseif type(arg2) == 'userdata' then widget = arg2 end
|
||||
widget = widget or rootWidget
|
||||
return callback, widget
|
||||
end
|
||||
|
||||
function g_keyboard.unbindKeyDown(keyComboDesc, arg1, arg2)
|
||||
local callback, widget = getUnbindArgs(arg1, arg2)
|
||||
if widget.boundKeyDownCombos == nil then return end
|
||||
local keyComboDesc = retranslateKeyComboDesc(keyComboDesc)
|
||||
disconnect(widget.boundKeyDownCombos, keyComboDesc, callback)
|
||||
end
|
||||
|
||||
function g_keyboard.unbindKeyUp(keyComboDesc, arg1, arg2)
|
||||
local callback, widget = getUnbindArgs(arg1, arg2)
|
||||
if widget.boundKeyUpCombos == nil then return end
|
||||
local keyComboDesc = retranslateKeyComboDesc(keyComboDesc)
|
||||
disconnect(widget.boundKeyUpCombos, keyComboDesc, callback)
|
||||
end
|
||||
|
||||
function g_keyboard.unbindKeyPress(keyComboDesc, arg1, arg2)
|
||||
local callback, widget = getUnbindArgs(arg1, arg2)
|
||||
if widget.boundKeyPressCombos == nil then return end
|
||||
local keyComboDesc = retranslateKeyComboDesc(keyComboDesc)
|
||||
disconnect(widget.boundKeyPressCombos, keyComboDesc, callback)
|
||||
end
|
||||
|
||||
function g_keyboard.getModifiers()
|
||||
return g_window.getKeyboardModifiers()
|
||||
end
|
||||
|
||||
function g_keyboard.isKeyPressed(key)
|
||||
if type(key) == 'string' then
|
||||
key = getKeyCode(key)
|
||||
end
|
||||
return g_window.isKeyPressed(key)
|
||||
end
|
||||
|
||||
function g_keyboard.isKeySetPressed(keys, all)
|
||||
all = all or false
|
||||
local result = {}
|
||||
for k,v in pairs(keys) do
|
||||
if type(v) == 'string' then
|
||||
v = getKeyCode(v)
|
||||
end
|
||||
if g_window.isKeyPressed(v) then
|
||||
if not all then
|
||||
return true
|
||||
end
|
||||
table.insert(result, true)
|
||||
end
|
||||
end
|
||||
return #result == #keys
|
||||
end
|
||||
|
||||
function g_keyboard.isInUse()
|
||||
for i = FirstKey, LastKey do
|
||||
if g_window.isKeyPressed(key) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function g_keyboard.isCtrlPressed()
|
||||
return bit32.band(g_window.getKeyboardModifiers(), KeyboardCtrlModifier) ~= 0
|
||||
end
|
||||
|
||||
function g_keyboard.isAltPressed()
|
||||
return bit32.band(g_window.getKeyboardModifiers(), KeyboardAltModifier) ~= 0
|
||||
end
|
||||
|
||||
function g_keyboard.isShiftPressed()
|
||||
return bit32.band(g_window.getKeyboardModifiers(), KeyboardShiftModifier) ~= 0
|
||||
end
|
35
modules/corelib/math.lua
Normal file
35
modules/corelib/math.lua
Normal file
@@ -0,0 +1,35 @@
|
||||
-- @docclass math
|
||||
|
||||
local U8 = 2^8
|
||||
local U16 = 2^16
|
||||
local U32 = 2^32
|
||||
local U64 = 2^64
|
||||
|
||||
function math.round(num, idp)
|
||||
local mult = 10^(idp or 0)
|
||||
if num >= 0 then
|
||||
return math.floor(num * mult + 0.5) / mult
|
||||
else
|
||||
return math.ceil(num * mult - 0.5) / mult
|
||||
end
|
||||
end
|
||||
|
||||
function math.isu8(num)
|
||||
return math.isinteger(num) and num >= 0 and num < U8
|
||||
end
|
||||
|
||||
function math.isu16(num)
|
||||
return math.isinteger(num) and num >= U8 and num < U16
|
||||
end
|
||||
|
||||
function math.isu32(num)
|
||||
return math.isinteger(num) and num >= U16 and num < U32
|
||||
end
|
||||
|
||||
function math.isu64(num)
|
||||
return math.isinteger(num) and num >= U32 and num < U64
|
||||
end
|
||||
|
||||
function math.isinteger(num)
|
||||
return ((type(num) == 'number') and (num == math.floor(num)))
|
||||
end
|
36
modules/corelib/mouse.lua
Normal file
36
modules/corelib/mouse.lua
Normal file
@@ -0,0 +1,36 @@
|
||||
-- @docclass
|
||||
function g_mouse.bindAutoPress(widget, callback, delay, button)
|
||||
local button = button or MouseLeftButton
|
||||
connect(widget, { onMousePress = function(widget, mousePos, mouseButton)
|
||||
if mouseButton ~= button then
|
||||
return false
|
||||
end
|
||||
local startTime = g_clock.millis()
|
||||
callback(widget, mousePos, mouseButton, 0)
|
||||
periodicalEvent(function()
|
||||
callback(widget, g_window.getMousePosition(), mouseButton, g_clock.millis() - startTime)
|
||||
end, function()
|
||||
return g_mouse.isPressed(mouseButton)
|
||||
end, 30, delay)
|
||||
return true
|
||||
end })
|
||||
end
|
||||
|
||||
function g_mouse.bindPressMove(widget, callback)
|
||||
connect(widget, { onMouseMove = function(widget, mousePos, mouseMoved)
|
||||
if widget:isPressed() then
|
||||
callback(mousePos, mouseMoved)
|
||||
return true
|
||||
end
|
||||
end })
|
||||
end
|
||||
|
||||
function g_mouse.bindPress(widget, callback, button)
|
||||
connect(widget, { onMousePress = function(widget, mousePos, mouseButton)
|
||||
if not button or button == mouseButton then
|
||||
callback(mousePos, mouseButton)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end })
|
||||
end
|
16
modules/corelib/net.lua
Normal file
16
modules/corelib/net.lua
Normal file
@@ -0,0 +1,16 @@
|
||||
function translateNetworkError(errcode, connecting, errdesc)
|
||||
local text
|
||||
if errcode == 111 then
|
||||
text = tr('Connection refused, the server might be offline or restarting.\nPlease try again later.')
|
||||
elseif errcode == 110 then
|
||||
text = tr('Connection timed out. Either your network is failing or the server is offline.')
|
||||
elseif errcode == 1 then
|
||||
text = tr('Connection failed, the server address does not exist.')
|
||||
elseif connecting then
|
||||
text = tr('Connection failed.')
|
||||
else
|
||||
text = tr('Your connection has been lost.\nEither your network or the server went down.')
|
||||
end
|
||||
text = text .. ' ' .. tr('(ERROR %d)', errcode)
|
||||
return text
|
||||
end
|
43
modules/corelib/orderedtable.lua
Normal file
43
modules/corelib/orderedtable.lua
Normal file
@@ -0,0 +1,43 @@
|
||||
function __genOrderedIndex( t )
|
||||
local orderedIndex = {}
|
||||
for key in pairs(t) do
|
||||
table.insert( orderedIndex, key )
|
||||
end
|
||||
table.sort( orderedIndex )
|
||||
return orderedIndex
|
||||
end
|
||||
|
||||
function orderedNext(t, state)
|
||||
-- Equivalent of the next function, but returns the keys in the alphabetic
|
||||
-- order. We use a temporary ordered key table that is stored in the
|
||||
-- table being iterated.
|
||||
|
||||
local key = nil
|
||||
--print("orderedNext: state = "..tostring(state) )
|
||||
if state == nil then
|
||||
-- the first time, generate the index
|
||||
t.__orderedIndex = __genOrderedIndex( t )
|
||||
key = t.__orderedIndex[1]
|
||||
else
|
||||
-- fetch the next value
|
||||
for i = 1,table.getn(t.__orderedIndex) do
|
||||
if t.__orderedIndex[i] == state then
|
||||
key = t.__orderedIndex[i+1]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if key then
|
||||
return key, t[key]
|
||||
end
|
||||
|
||||
-- no more value to return, cleanup
|
||||
t.__orderedIndex = nil
|
||||
return
|
||||
end
|
||||
|
||||
function orderedPairs(t)
|
||||
-- Equivalent of the pairs() function on tables. Allows to iterate
|
||||
-- in order
|
||||
return orderedNext, t, nil
|
||||
end
|
69
modules/corelib/outputmessage.lua
Normal file
69
modules/corelib/outputmessage.lua
Normal file
@@ -0,0 +1,69 @@
|
||||
function OutputMessage:addData(data)
|
||||
if type(data) == 'boolean' then
|
||||
self:addU8(NetworkMessageTypes.Boolean)
|
||||
self:addU8(booleantonumber(data))
|
||||
elseif type(data) == 'number' then
|
||||
if math.isu8(data) then
|
||||
self:addU8(NetworkMessageTypes.U8)
|
||||
self:addU8(data)
|
||||
elseif math.isu16(data) then
|
||||
self:addU8(NetworkMessageTypes.U16)
|
||||
self:addU16(data)
|
||||
elseif math.isu32(data) then
|
||||
self:addU8(NetworkMessageTypes.U32)
|
||||
self:addU32(data)
|
||||
elseif math.isu64(data) then
|
||||
self:addU8(NetworkMessageTypes.U64)
|
||||
self:addU64(data)
|
||||
else -- negative or non integer numbers
|
||||
self:addU8(NetworkMessageTypes.NumberString)
|
||||
self:addString(tostring(data))
|
||||
end
|
||||
elseif type(data) == 'string' then
|
||||
self:addU8(NetworkMessageTypes.String)
|
||||
self:addString(data)
|
||||
elseif type(data) == 'table' then
|
||||
self:addU8(NetworkMessageTypes.Table)
|
||||
self:addTable(data)
|
||||
else
|
||||
perror('Invalid data type ' .. type(data))
|
||||
end
|
||||
end
|
||||
|
||||
function OutputMessage:addTable(data)
|
||||
local size = 0
|
||||
|
||||
-- reserve for size (should be addData, find a way to use it further)
|
||||
local sizePos = self:getWritePos()
|
||||
self:addU16(size)
|
||||
local sizeSize = self:getWritePos() - sizePos
|
||||
|
||||
-- add values
|
||||
for key,value in pairs(data) do
|
||||
self:addData(key)
|
||||
self:addData(value)
|
||||
size = size + 1
|
||||
end
|
||||
|
||||
-- write size
|
||||
local currentPos = self:getWritePos()
|
||||
self:setWritePos(sizePos)
|
||||
self:addU16(size)
|
||||
|
||||
-- fix msg size and go back to end
|
||||
self:setMessageSize(self:getMessageSize() - sizeSize)
|
||||
self:setWritePos(currentPos)
|
||||
end
|
||||
|
||||
function OutputMessage:addColor(color)
|
||||
self:addU8(color.r)
|
||||
self:addU8(color.g)
|
||||
self:addU8(color.b)
|
||||
self:addU8(color.a)
|
||||
end
|
||||
|
||||
function OutputMessage:addPosition(position)
|
||||
self:addU16(position.x)
|
||||
self:addU16(position.y)
|
||||
self:addU8(position.z)
|
||||
end
|
3
modules/corelib/settings.lua
Normal file
3
modules/corelib/settings.lua
Normal file
@@ -0,0 +1,3 @@
|
||||
g_settings = makesingleton(g_configs.getSettings())
|
||||
|
||||
-- Reserved for future functionality
|
59
modules/corelib/string.lua
Normal file
59
modules/corelib/string.lua
Normal file
@@ -0,0 +1,59 @@
|
||||
-- @docclass string
|
||||
|
||||
function string:split(delim)
|
||||
local start = 1
|
||||
local results = {}
|
||||
while true do
|
||||
local pos = string.find(self, delim, start, true)
|
||||
if not pos then
|
||||
break
|
||||
end
|
||||
table.insert(results, string.sub(self, start, pos-1))
|
||||
start = pos + string.len(delim)
|
||||
end
|
||||
table.insert(results, string.sub(self, start))
|
||||
table.removevalue(results, '')
|
||||
return results
|
||||
end
|
||||
|
||||
function string:starts(start)
|
||||
return string.sub(self, 1, #start) == start
|
||||
end
|
||||
|
||||
function string:ends(test)
|
||||
return test =='' or string.sub(self,-string.len(test)) == test
|
||||
end
|
||||
|
||||
function string:trim()
|
||||
return string.match(self, '^%s*(.*%S)') or ''
|
||||
end
|
||||
|
||||
function string:explode(sep, limit)
|
||||
if type(sep) ~= 'string' or tostring(self):len() == 0 or sep:len() == 0 then
|
||||
return {}
|
||||
end
|
||||
|
||||
local i, pos, tmp, t = 0, 1, "", {}
|
||||
for s, e in function() return string.find(self, sep, pos) end do
|
||||
tmp = self:sub(pos, s - 1):trim()
|
||||
table.insert(t, tmp)
|
||||
pos = e + 1
|
||||
|
||||
i = i + 1
|
||||
if limit ~= nil and i == limit then
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
tmp = self:sub(pos):trim()
|
||||
table.insert(t, tmp)
|
||||
return t
|
||||
end
|
||||
|
||||
function string:contains(str, checkCase, start, plain)
|
||||
if(not checkCase) then
|
||||
self = self:lower()
|
||||
str = str:lower()
|
||||
end
|
||||
return string.find(self, str, start and start or 1, plain == nil and true or false)
|
||||
end
|
173
modules/corelib/struct.lua
Normal file
173
modules/corelib/struct.lua
Normal file
@@ -0,0 +1,173 @@
|
||||
Struct = {}
|
||||
|
||||
function Struct.pack(format, ...)
|
||||
local stream = {}
|
||||
local vars = {...}
|
||||
local endianness = true
|
||||
|
||||
for i = 1, format:len() do
|
||||
local opt = format:sub(i, i)
|
||||
|
||||
if opt == '>' then
|
||||
endianness = false
|
||||
elseif opt:find('[bBhHiIlL]') then
|
||||
local n = opt:find('[hH]') and 2 or opt:find('[iI]') and 4 or opt:find('[lL]') and 8 or 1
|
||||
local val = tonumber(table.remove(vars, 1))
|
||||
|
||||
if val < 0 then
|
||||
val = val + 2 ^ (n * 8 - 1)
|
||||
end
|
||||
|
||||
local bytes = {}
|
||||
for j = 1, n do
|
||||
table.insert(bytes, string.char(val % (2 ^ 8)))
|
||||
val = math.floor(val / (2 ^ 8))
|
||||
end
|
||||
|
||||
if not endianness then
|
||||
table.insert(stream, string.reverse(table.concat(bytes)))
|
||||
else
|
||||
table.insert(stream, table.concat(bytes))
|
||||
end
|
||||
elseif opt:find('[fd]') then
|
||||
local val = tonumber(table.remove(vars, 1))
|
||||
local sign = 0
|
||||
|
||||
if val < 0 then
|
||||
sign = 1
|
||||
val = -val
|
||||
end
|
||||
|
||||
local mantissa, exponent = math.frexp(val)
|
||||
if val == 0 then
|
||||
mantissa = 0
|
||||
exponent = 0
|
||||
else
|
||||
mantissa = (mantissa * 2 - 1) * math.ldexp(0.5, (opt == 'd') and 53 or 24)
|
||||
exponent = exponent + ((opt == 'd') and 1022 or 126)
|
||||
end
|
||||
|
||||
local bytes = {}
|
||||
if opt == 'd' then
|
||||
val = mantissa
|
||||
for i = 1, 6 do
|
||||
table.insert(bytes, string.char(math.floor(val) % (2 ^ 8)))
|
||||
val = math.floor(val / (2 ^ 8))
|
||||
end
|
||||
else
|
||||
table.insert(bytes, string.char(math.floor(mantissa) % (2 ^ 8)))
|
||||
val = math.floor(mantissa / (2 ^ 8))
|
||||
table.insert(bytes, string.char(math.floor(val) % (2 ^ 8)))
|
||||
val = math.floor(val / (2 ^ 8))
|
||||
end
|
||||
|
||||
table.insert(bytes, string.char(math.floor(exponent * ((opt == 'd') and 16 or 128) + val) % (2 ^ 8)))
|
||||
val = math.floor((exponent * ((opt == 'd') and 16 or 128) + val) / (2 ^ 8))
|
||||
table.insert(bytes, string.char(math.floor(sign * 128 + val) % (2 ^ 8)))
|
||||
val = math.floor((sign * 128 + val) / (2 ^ 8))
|
||||
|
||||
if not endianness then
|
||||
table.insert(stream, string.reverse(table.concat(bytes)))
|
||||
else
|
||||
table.insert(stream, table.concat(bytes))
|
||||
end
|
||||
elseif opt == 's' then
|
||||
table.insert(stream, tostring(table.remove(vars, 1)))
|
||||
table.insert(stream, string.char(0))
|
||||
elseif opt == 'c' then
|
||||
local n = format:sub(i + 1):match('%d+')
|
||||
local length = tonumber(n)
|
||||
|
||||
if length > 0 then
|
||||
local str = tostring(table.remove(vars, 1))
|
||||
if length - str:len() > 0 then
|
||||
str = str .. string.rep(' ', length - str:len())
|
||||
end
|
||||
table.insert(stream, str:sub(1, length))
|
||||
end
|
||||
i = i + n:len()
|
||||
end
|
||||
end
|
||||
|
||||
return table.concat(stream)
|
||||
end
|
||||
|
||||
function Struct.unpack(format, stream)
|
||||
local vars = {}
|
||||
local iterator = 1
|
||||
local endianness = true
|
||||
|
||||
for i = 1, format:len() do
|
||||
local opt = format:sub(i, i)
|
||||
|
||||
if opt == '>' then
|
||||
endianness = false
|
||||
elseif opt:find('[bBhHiIlL]') then
|
||||
local n = opt:find('[hH]') and 2 or opt:find('[iI]') and 4 or opt:find('[lL]') and 8 or 1
|
||||
local signed = opt:lower() == opt
|
||||
|
||||
local val = 0
|
||||
for j = 1, n do
|
||||
local byte = string.byte(stream:sub(iterator, iterator))
|
||||
if endianness then
|
||||
val = val + byte * (2 ^ ((j - 1) * 8))
|
||||
else
|
||||
val = val + byte * (2 ^ ((n - j) * 8))
|
||||
end
|
||||
iterator = iterator + 1
|
||||
end
|
||||
|
||||
if signed then
|
||||
val = val - 2 ^ (n * 8 - 1)
|
||||
end
|
||||
|
||||
table.insert(vars, val)
|
||||
elseif opt:find('[fd]') then
|
||||
local n = (opt == 'd') and 8 or 4
|
||||
local x = stream:sub(iterator, iterator + n - 1)
|
||||
iterator = iterator + n
|
||||
|
||||
if not endianness then
|
||||
x = string.reverse(x)
|
||||
end
|
||||
|
||||
local sign = 1
|
||||
local mantissa = string.byte(x, (opt == 'd') and 7 or 3) % ((opt == 'd') and 16 or 128)
|
||||
for i = n - 2, 1, -1 do
|
||||
mantissa = mantissa * (2 ^ 8) + string.byte(x, i)
|
||||
end
|
||||
|
||||
if string.byte(x, n) > 127 then
|
||||
sign = -1
|
||||
end
|
||||
|
||||
local exponent = (string.byte(x, n) % 128) * ((opt == 'd') and 16 or 2) + math.floor(string.byte(x, n - 1) / ((opt == 'd') and 16 or 128))
|
||||
if exponent == 0 then
|
||||
table.insert(vars, 0.0)
|
||||
else
|
||||
mantissa = (math.ldexp(mantissa, (opt == 'd') and -52 or -23) + 1) * sign
|
||||
table.insert(vars, math.ldexp(mantissa, exponent - ((opt == 'd') and 1023 or 127)))
|
||||
end
|
||||
elseif opt == 's' then
|
||||
local bytes = {}
|
||||
for j = iterator, stream:len() do
|
||||
if stream:sub(j, j) == string.char(0) then
|
||||
break
|
||||
end
|
||||
|
||||
table.insert(bytes, stream:sub(j, j))
|
||||
end
|
||||
|
||||
local str = table.concat(bytes)
|
||||
iterator = iterator + str:len() + 1
|
||||
table.insert(vars, str)
|
||||
elseif opt == 'c' then
|
||||
local n = format:sub(i + 1):match('%d+')
|
||||
table.insert(vars, stream:sub(iterator, iterator + tonumber(n)))
|
||||
iterator = iterator + tonumber(n)
|
||||
i = i + n:len()
|
||||
end
|
||||
end
|
||||
|
||||
return unpack(vars)
|
||||
end
|
212
modules/corelib/table.lua
Normal file
212
modules/corelib/table.lua
Normal file
@@ -0,0 +1,212 @@
|
||||
-- @docclass table
|
||||
|
||||
function table.dump(t, depth)
|
||||
if not depth then depth = 0 end
|
||||
for k,v in pairs(t) do
|
||||
str = (' '):rep(depth * 2) .. k .. ': '
|
||||
if type(v) ~= "table" then
|
||||
print(str .. tostring(v))
|
||||
else
|
||||
print(str)
|
||||
table.dump(v, depth+1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function table.clear(t)
|
||||
for k,v in pairs(t) do
|
||||
t[k] = nil
|
||||
end
|
||||
end
|
||||
|
||||
function table.copy(t)
|
||||
local res = {}
|
||||
for k,v in pairs(t) do
|
||||
res[k] = v
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
function table.recursivecopy(t)
|
||||
local res = {}
|
||||
for k,v in pairs(t) do
|
||||
if type(v) == "table" then
|
||||
res[k] = table.recursivecopy(v)
|
||||
else
|
||||
res[k] = v
|
||||
end
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
function table.selectivecopy(t, keys)
|
||||
local res = { }
|
||||
for i,v in ipairs(keys) do
|
||||
res[v] = t[v]
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
function table.merge(t, src)
|
||||
for k,v in pairs(src) do
|
||||
t[k] = v
|
||||
end
|
||||
end
|
||||
|
||||
function table.find(t, value, lowercase)
|
||||
for k,v in pairs(t) do
|
||||
if lowercase and type(value) == 'string' and type(v) == 'string' then
|
||||
if v:lower() == value:lower() then return k end
|
||||
end
|
||||
if v == value then return k end
|
||||
end
|
||||
end
|
||||
|
||||
function table.findbykey(t, key, lowercase)
|
||||
for k,v in pairs(t) do
|
||||
if lowercase and type(key) == 'string' and type(k) == 'string' then
|
||||
if k:lower() == key:lower() then return v end
|
||||
end
|
||||
if k == key then return v end
|
||||
end
|
||||
end
|
||||
|
||||
function table.contains(t, value, lowercase)
|
||||
return table.find(t, value, lowercase) ~= nil
|
||||
end
|
||||
|
||||
function table.findkey(t, key)
|
||||
if t and type(t) == 'table' then
|
||||
for k,v in pairs(t) do
|
||||
if k == key then return k end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function table.haskey(t, key)
|
||||
return table.findkey(t, key) ~= nil
|
||||
end
|
||||
|
||||
function table.removevalue(t, value)
|
||||
for k,v in pairs(t) do
|
||||
if v == value then
|
||||
table.remove(t, k)
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function table.popvalue(value)
|
||||
local index = nil
|
||||
for k,v in pairs(t) do
|
||||
if v == value or not value then
|
||||
index = k
|
||||
end
|
||||
end
|
||||
if index then
|
||||
table.remove(t, index)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function table.compare(t, other)
|
||||
if #t ~= #other then return false end
|
||||
for k,v in pairs(t) do
|
||||
if v ~= other[k] then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function table.empty(t)
|
||||
if t and type(t) == 'table' then
|
||||
return next(t) == nil
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function table.permute(t, n, count)
|
||||
n = n or #t
|
||||
for i=1,count or n do
|
||||
local j = math.random(i, n)
|
||||
t[i], t[j] = t[j], t[i]
|
||||
end
|
||||
return t
|
||||
end
|
||||
|
||||
function table.findbyfield(t, fieldname, fieldvalue)
|
||||
for _i,subt in pairs(t) do
|
||||
if subt[fieldname] == fieldvalue then
|
||||
return subt
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function table.size(t)
|
||||
local size = 0
|
||||
for i, n in pairs(t) do
|
||||
size = size + 1
|
||||
end
|
||||
|
||||
return size
|
||||
end
|
||||
|
||||
function table.tostring(t)
|
||||
local maxn = #t
|
||||
local str = ""
|
||||
for k,v in pairs(t) do
|
||||
v = tostring(v)
|
||||
if k == maxn and k ~= 1 then
|
||||
str = str .. " and " .. v
|
||||
elseif maxn > 1 and k ~= 1 then
|
||||
str = str .. ", " .. v
|
||||
else
|
||||
str = str .. " " .. v
|
||||
end
|
||||
end
|
||||
return str
|
||||
end
|
||||
|
||||
function table.collect(t, func)
|
||||
local res = {}
|
||||
for k,v in pairs(t) do
|
||||
local a,b = func(k,v)
|
||||
if a and b then
|
||||
res[a] = b
|
||||
elseif a ~= nil then
|
||||
table.insert(res,a)
|
||||
end
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
function table.equals(t, comp)
|
||||
if type(t) == "table" and type(comp) == "table" then
|
||||
for k,v in pairs(t) do
|
||||
if v ~= comp[k] then return false end
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function table.equal(t1,t2,ignore_mt)
|
||||
local ty1 = type(t1)
|
||||
local ty2 = type(t2)
|
||||
if ty1 ~= ty2 then return false end
|
||||
-- non-table types can be directly compared
|
||||
if ty1 ~= 'table' and ty2 ~= 'table' then return t1 == t2 end
|
||||
-- as well as tables which have the metamethod __eq
|
||||
local mt = getmetatable(t1)
|
||||
if not ignore_mt and mt and mt.__eq then return t1 == t2 end
|
||||
for k1,v1 in pairs(t1) do
|
||||
local v2 = t2[k1]
|
||||
if v2 == nil or not table.equal(v1,v2) then return false end
|
||||
end
|
||||
for k2,v2 in pairs(t2) do
|
||||
local v1 = t1[k2]
|
||||
if v1 == nil or not table.equal(v1,v2) then return false end
|
||||
end
|
||||
return true
|
||||
end
|
67
modules/corelib/ui/effects.lua
Normal file
67
modules/corelib/ui/effects.lua
Normal file
@@ -0,0 +1,67 @@
|
||||
-- @docclass
|
||||
g_effects = {}
|
||||
|
||||
function g_effects.fadeIn(widget, time, elapsed)
|
||||
if not elapsed then elapsed = 0 end
|
||||
if not time then time = 300 end
|
||||
widget:setOpacity(math.min(elapsed/time, 1))
|
||||
removeEvent(widget.fadeEvent)
|
||||
if elapsed < time then
|
||||
removeEvent(widget.fadeEvent)
|
||||
widget.fadeEvent = scheduleEvent(function()
|
||||
g_effects.fadeIn(widget, time, elapsed + 30)
|
||||
end, 30)
|
||||
else
|
||||
widget.fadeEvent = nil
|
||||
end
|
||||
end
|
||||
|
||||
function g_effects.fadeOut(widget, time, elapsed)
|
||||
if not elapsed then elapsed = 0 end
|
||||
if not time then time = 300 end
|
||||
elapsed = math.max((1 - widget:getOpacity()) * time, elapsed)
|
||||
removeEvent(widget.fadeEvent)
|
||||
widget:setOpacity(math.max((time - elapsed)/time, 0))
|
||||
if elapsed < time then
|
||||
widget.fadeEvent = scheduleEvent(function()
|
||||
g_effects.fadeOut(widget, time, elapsed + 30)
|
||||
end, 30)
|
||||
else
|
||||
widget.fadeEvent = nil
|
||||
end
|
||||
end
|
||||
|
||||
function g_effects.cancelFade(widget)
|
||||
removeEvent(widget.fadeEvent)
|
||||
widget.fadeEvent = nil
|
||||
end
|
||||
|
||||
function g_effects.startBlink(widget, duration, interval, clickCancel)
|
||||
duration = duration or 0 -- until stop is called
|
||||
interval = interval or 500
|
||||
clickCancel = clickCancel or true
|
||||
|
||||
removeEvent(widget.blinkEvent)
|
||||
removeEvent(widget.blinkStopEvent)
|
||||
|
||||
widget.blinkEvent = cycleEvent(function()
|
||||
widget:setOn(not widget:isOn())
|
||||
end, interval)
|
||||
|
||||
if duration > 0 then
|
||||
widget.blinkStopEvent = scheduleEvent(function()
|
||||
g_effects.stopBlink(widget)
|
||||
end, duration)
|
||||
end
|
||||
|
||||
connect(widget, { onClick = g_effects.stopBlink })
|
||||
end
|
||||
|
||||
function g_effects.stopBlink(widget)
|
||||
disconnect(widget, { onClick = g_effects.stopBlink })
|
||||
removeEvent(widget.blinkEvent)
|
||||
removeEvent(widget.blinkStopEvent)
|
||||
widget.blinkEvent = nil
|
||||
widget.blinkStopEvent = nil
|
||||
widget:setOn(false)
|
||||
end
|
117
modules/corelib/ui/tooltip.lua
Normal file
117
modules/corelib/ui/tooltip.lua
Normal file
@@ -0,0 +1,117 @@
|
||||
-- @docclass
|
||||
g_tooltip = {}
|
||||
|
||||
-- private variables
|
||||
local toolTipLabel
|
||||
local currentHoveredWidget
|
||||
|
||||
-- private functions
|
||||
local function moveToolTip(first)
|
||||
if not first and (not toolTipLabel:isVisible() or toolTipLabel:getOpacity() < 0.1) then return end
|
||||
|
||||
local pos = g_window.getMousePosition()
|
||||
local windowSize = g_window.getSize()
|
||||
local labelSize = toolTipLabel:getSize()
|
||||
|
||||
pos.x = pos.x + 1
|
||||
pos.y = pos.y + 1
|
||||
|
||||
if windowSize.width - (pos.x + labelSize.width) < 10 then
|
||||
pos.x = pos.x - labelSize.width - 3
|
||||
else
|
||||
pos.x = pos.x + 10
|
||||
end
|
||||
|
||||
if windowSize.height - (pos.y + labelSize.height) < 10 then
|
||||
pos.y = pos.y - labelSize.height - 3
|
||||
else
|
||||
pos.y = pos.y + 10
|
||||
end
|
||||
|
||||
toolTipLabel:setPosition(pos)
|
||||
end
|
||||
|
||||
local function onWidgetHoverChange(widget, hovered)
|
||||
if hovered then
|
||||
if widget.tooltip and not g_mouse.isPressed() then
|
||||
g_tooltip.display(widget.tooltip)
|
||||
currentHoveredWidget = widget
|
||||
end
|
||||
else
|
||||
if widget == currentHoveredWidget then
|
||||
g_tooltip.hide()
|
||||
currentHoveredWidget = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function onWidgetStyleApply(widget, styleName, styleNode)
|
||||
if styleNode.tooltip then
|
||||
widget.tooltip = styleNode.tooltip
|
||||
end
|
||||
end
|
||||
|
||||
-- public functions
|
||||
function g_tooltip.init()
|
||||
connect(UIWidget, { onStyleApply = onWidgetStyleApply,
|
||||
onHoverChange = onWidgetHoverChange})
|
||||
|
||||
addEvent(function()
|
||||
toolTipLabel = g_ui.createWidget('UILabel', rootWidget)
|
||||
toolTipLabel:setId('toolTip')
|
||||
toolTipLabel:setBackgroundColor('#111111cc')
|
||||
toolTipLabel:setTextAlign(AlignCenter)
|
||||
toolTipLabel:hide()
|
||||
toolTipLabel.onMouseMove = function() moveToolTip() end
|
||||
end)
|
||||
end
|
||||
|
||||
function g_tooltip.terminate()
|
||||
disconnect(UIWidget, { onStyleApply = onWidgetStyleApply,
|
||||
onHoverChange = onWidgetHoverChange })
|
||||
|
||||
currentHoveredWidget = nil
|
||||
toolTipLabel:destroy()
|
||||
toolTipLabel = nil
|
||||
|
||||
g_tooltip = nil
|
||||
end
|
||||
|
||||
function g_tooltip.display(text)
|
||||
if text == nil or text:len() == 0 then return end
|
||||
if not toolTipLabel then return end
|
||||
|
||||
toolTipLabel:setText(text)
|
||||
toolTipLabel:resizeToText()
|
||||
toolTipLabel:resize(toolTipLabel:getWidth() + 4, toolTipLabel:getHeight() + 4)
|
||||
toolTipLabel:show()
|
||||
toolTipLabel:raise()
|
||||
toolTipLabel:enable()
|
||||
g_effects.fadeIn(toolTipLabel, 100)
|
||||
moveToolTip(true)
|
||||
end
|
||||
|
||||
function g_tooltip.hide()
|
||||
g_effects.fadeOut(toolTipLabel, 100)
|
||||
end
|
||||
|
||||
|
||||
-- @docclass UIWidget @{
|
||||
|
||||
-- UIWidget extensions
|
||||
function UIWidget:setTooltip(text)
|
||||
self.tooltip = text
|
||||
end
|
||||
|
||||
function UIWidget:removeTooltip()
|
||||
self.tooltip = nil
|
||||
end
|
||||
|
||||
function UIWidget:getTooltip()
|
||||
return self.tooltip
|
||||
end
|
||||
|
||||
-- @}
|
||||
|
||||
g_tooltip.init()
|
||||
connect(g_app, { onTerminate = g_tooltip.terminate })
|
12
modules/corelib/ui/uibutton.lua
Normal file
12
modules/corelib/ui/uibutton.lua
Normal file
@@ -0,0 +1,12 @@
|
||||
-- @docclass
|
||||
UIButton = extends(UIWidget, "UIButton")
|
||||
|
||||
function UIButton.create()
|
||||
local button = UIButton.internalCreate()
|
||||
button:setFocusable(false)
|
||||
return button
|
||||
end
|
||||
|
||||
function UIButton:onMouseRelease(pos, button)
|
||||
return self:isPressed()
|
||||
end
|
13
modules/corelib/ui/uicheckbox.lua
Normal file
13
modules/corelib/ui/uicheckbox.lua
Normal file
@@ -0,0 +1,13 @@
|
||||
-- @docclass
|
||||
UICheckBox = extends(UIWidget, "UICheckBox")
|
||||
|
||||
function UICheckBox.create()
|
||||
local checkbox = UICheckBox.internalCreate()
|
||||
checkbox:setFocusable(false)
|
||||
checkbox:setTextAlign(AlignLeft)
|
||||
return checkbox
|
||||
end
|
||||
|
||||
function UICheckBox:onClick()
|
||||
self:setChecked(not self:isChecked())
|
||||
end
|
180
modules/corelib/ui/uicombobox.lua
Normal file
180
modules/corelib/ui/uicombobox.lua
Normal file
@@ -0,0 +1,180 @@
|
||||
-- @docclass
|
||||
UIComboBox = extends(UIWidget, "UIComboBox")
|
||||
|
||||
function UIComboBox.create()
|
||||
local combobox = UIComboBox.internalCreate()
|
||||
combobox:setFocusable(false)
|
||||
combobox.options = {}
|
||||
combobox.currentIndex = -1
|
||||
combobox.mouseScroll = true
|
||||
combobox.menuScroll = false
|
||||
combobox.menuHeight = 100
|
||||
combobox.menuScrollStep = 0
|
||||
return combobox
|
||||
end
|
||||
|
||||
function UIComboBox:clearOptions()
|
||||
self.options = {}
|
||||
self.currentIndex = -1
|
||||
self:clearText()
|
||||
end
|
||||
|
||||
function UIComboBox:getOptionsCount()
|
||||
return #self.options
|
||||
end
|
||||
|
||||
function UIComboBox:isOption(text)
|
||||
if not self.options then return false end
|
||||
for i,v in ipairs(self.options) do
|
||||
if v.text == text then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function UIComboBox:setOption(text, dontSignal)
|
||||
self:setCurrentOption(text, dontSignal)
|
||||
end
|
||||
|
||||
function UIComboBox:setCurrentOption(text, dontSignal)
|
||||
if not self.options then return end
|
||||
for i,v in ipairs(self.options) do
|
||||
if v.text == text and self.currentIndex ~= i then
|
||||
self.currentIndex = i
|
||||
self:setText(text)
|
||||
if not dontSignal then
|
||||
signalcall(self.onOptionChange, self, text, v.data)
|
||||
end
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function UIComboBox:updateCurrentOption(newText)
|
||||
self.options[self.currentIndex].text = newText
|
||||
self:setText(newText)
|
||||
end
|
||||
|
||||
function UIComboBox:setCurrentOptionByData(data, dontSignal)
|
||||
if not self.options then return end
|
||||
for i,v in ipairs(self.options) do
|
||||
if v.data == data and self.currentIndex ~= i then
|
||||
self.currentIndex = i
|
||||
self:setText(v.text)
|
||||
if not dontSignal then
|
||||
signalcall(self.onOptionChange, self, v.text, v.data)
|
||||
end
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function UIComboBox:setCurrentIndex(index, dontSignal)
|
||||
if index >= 1 and index <= #self.options then
|
||||
local v = self.options[index]
|
||||
self.currentIndex = index
|
||||
self:setText(v.text)
|
||||
if not dontSignal then
|
||||
signalcall(self.onOptionChange, self, v.text, v.data)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function UIComboBox:getCurrentOption()
|
||||
if table.haskey(self.options, self.currentIndex) then
|
||||
return self.options[self.currentIndex]
|
||||
end
|
||||
end
|
||||
|
||||
function UIComboBox:addOption(text, data)
|
||||
table.insert(self.options, { text = text, data = data })
|
||||
local index = #self.options
|
||||
if index == 1 then self:setCurrentOption(text) end
|
||||
return index
|
||||
end
|
||||
|
||||
function UIComboBox:removeOption(text)
|
||||
for i,v in ipairs(self.options) do
|
||||
if v.text == text then
|
||||
table.remove(self.options, i)
|
||||
if self.currentIndex == i then
|
||||
self:setCurrentIndex(1)
|
||||
elseif self.currentIndex > i then
|
||||
self.currentIndex = self.currentIndex - 1
|
||||
end
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function UIComboBox:onMousePress(mousePos, mouseButton)
|
||||
local menu
|
||||
if self.menuScroll then
|
||||
menu = g_ui.createWidget(self:getStyleName() .. 'PopupScrollMenu')
|
||||
menu:setHeight(self.menuHeight)
|
||||
if self.menuScrollStep > 0 then
|
||||
menu:setScrollbarStep(self.menuScrollStep)
|
||||
end
|
||||
else
|
||||
menu = g_ui.createWidget(self:getStyleName() .. 'PopupMenu')
|
||||
end
|
||||
menu:setId(self:getId() .. 'PopupMenu')
|
||||
for i,v in ipairs(self.options) do
|
||||
menu:addOption(v.text, function() self:setCurrentOption(v.text) end)
|
||||
end
|
||||
menu:setWidth(self:getWidth())
|
||||
menu:display({ x = self:getX(), y = self:getY() + self:getHeight() })
|
||||
connect(menu, { onDestroy = function() self:setOn(false) end })
|
||||
self:setOn(true)
|
||||
return true
|
||||
end
|
||||
|
||||
function UIComboBox:onMouseWheel(mousePos, direction)
|
||||
if not self.mouseScroll then
|
||||
return false
|
||||
end
|
||||
if direction == MouseWheelUp and self.currentIndex > 1 then
|
||||
self:setCurrentIndex(self.currentIndex - 1)
|
||||
elseif direction == MouseWheelDown and self.currentIndex < #self.options then
|
||||
self:setCurrentIndex(self.currentIndex + 1)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function UIComboBox:onStyleApply(styleName, styleNode)
|
||||
if styleNode.options then
|
||||
for k,option in pairs(styleNode.options) do
|
||||
self:addOption(option)
|
||||
end
|
||||
end
|
||||
|
||||
if styleNode.data then
|
||||
for k,data in pairs(styleNode.data) do
|
||||
local option = self.options[k]
|
||||
if option then
|
||||
option.data = data
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for name,value in pairs(styleNode) do
|
||||
if name == 'mouse-scroll' then
|
||||
self.mouseScroll = value
|
||||
elseif name == 'menu-scroll' then
|
||||
self.menuScroll = value
|
||||
elseif name == 'menu-height' then
|
||||
self.menuHeight = value
|
||||
elseif name == 'menu-scroll-step' then
|
||||
self.menuScrollStep = value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function UIComboBox:setMouseScroll(scroll)
|
||||
self.mouseScroll = scroll
|
||||
end
|
||||
|
||||
function UIComboBox:canMouseScroll()
|
||||
return self.mouseScroll
|
||||
end
|
99
modules/corelib/ui/uiimageview.lua
Normal file
99
modules/corelib/ui/uiimageview.lua
Normal file
@@ -0,0 +1,99 @@
|
||||
-- @docclass
|
||||
UIImageView = extends(UIWidget, "UIImageView")
|
||||
|
||||
function UIImageView.create()
|
||||
local imageView = UIImageView.internalCreate()
|
||||
imageView.zoom = 1
|
||||
imageView.minZoom = math.pow(10, -2)
|
||||
imageView.maxZoom = math.pow(10, 2)
|
||||
imageView:setClipping(true)
|
||||
return imageView
|
||||
end
|
||||
|
||||
function UIImageView:getDefaultZoom()
|
||||
local width = self:getWidth()
|
||||
local height = self:getHeight()
|
||||
local textureWidth = self:getImageTextureWidth()
|
||||
local textureHeight = self:getImageTextureHeight()
|
||||
local zoomX = width / textureWidth
|
||||
local zoomY = height / textureHeight
|
||||
return math.min(zoomX, zoomY)
|
||||
end
|
||||
|
||||
function UIImageView:getImagePosition(x, y)
|
||||
x = x or self:getWidth() / 2
|
||||
y = y or self:getHeight() / 2
|
||||
local offsetX = self:getImageOffsetX()
|
||||
local offsetY = self:getImageOffsetY()
|
||||
local posX = (x - offsetX) / self.zoom
|
||||
local posY = (y - offsetY) / self.zoom
|
||||
return posX, posY
|
||||
end
|
||||
|
||||
function UIImageView:setImage(image)
|
||||
self:setImageSource(image)
|
||||
local zoom = self:getDefaultZoom()
|
||||
self:setZoom(zoom)
|
||||
self:center()
|
||||
end
|
||||
|
||||
function UIImageView:setZoom(zoom, x, y)
|
||||
zoom = math.max(math.min(zoom, self.maxZoom), self.minZoom)
|
||||
local posX, posY = self:getImagePosition(x, y)
|
||||
local textureWidth = self:getImageTextureWidth()
|
||||
local textureHeight = self:getImageTextureHeight()
|
||||
local imageWidth = textureWidth * zoom
|
||||
local imageHeight = textureHeight * zoom
|
||||
self:setImageWidth(imageWidth)
|
||||
self:setImageHeight(imageHeight)
|
||||
self.zoom = zoom
|
||||
self:move(posX, posY, x, y)
|
||||
end
|
||||
|
||||
function UIImageView:zoomIn(x, y)
|
||||
local zoom = self.zoom * 1.1
|
||||
self:setZoom(zoom, x, y)
|
||||
end
|
||||
|
||||
function UIImageView:zoomOut(x, y)
|
||||
local zoom = self.zoom / 1.1
|
||||
self:setZoom(zoom, x, y)
|
||||
end
|
||||
|
||||
function UIImageView:center()
|
||||
self:move(self:getImageTextureWidth() / 2, self:getImageTextureHeight() / 2)
|
||||
end
|
||||
|
||||
function UIImageView:move(x, y, centerX, centerY)
|
||||
x = math.max(math.min(x, self:getImageTextureWidth()), 0)
|
||||
y = math.max(math.min(y, self:getImageTextureHeight()), 0)
|
||||
local centerX = centerX or self:getWidth() / 2
|
||||
local centerY = centerY or self:getHeight() / 2
|
||||
local offsetX = centerX - x * self.zoom
|
||||
local offsetY = centerY - y * self.zoom
|
||||
self:setImageOffset({x=offsetX, y=offsetY})
|
||||
end
|
||||
|
||||
function UIImageView:onDragEnter(pos)
|
||||
return true
|
||||
end
|
||||
|
||||
function UIImageView:onDragMove(pos, moved)
|
||||
local posX, posY = self:getImagePosition()
|
||||
self:move(posX - moved.x / self.zoom, posY - moved.y / self.zoom)
|
||||
return true
|
||||
end
|
||||
|
||||
function UIImageView:onDragLeave(widget, pos)
|
||||
return true
|
||||
end
|
||||
|
||||
function UIImageView:onMouseWheel(mousePos, direction)
|
||||
local x = mousePos.x - self:getX()
|
||||
local y = mousePos.y - self:getY()
|
||||
if direction == MouseWheelUp then
|
||||
self:zoomIn(x, y)
|
||||
elseif direction == MouseWheelDown then
|
||||
self:zoomOut(x, y)
|
||||
end
|
||||
end
|
114
modules/corelib/ui/uiinputbox.lua
Normal file
114
modules/corelib/ui/uiinputbox.lua
Normal file
@@ -0,0 +1,114 @@
|
||||
if not UIWindow then dofile 'uiwindow' end
|
||||
|
||||
-- @docclass
|
||||
UIInputBox = extends(UIWindow, "UIInputBox")
|
||||
|
||||
function UIInputBox.create(title, okCallback, cancelCallback)
|
||||
local inputBox = UIInputBox.internalCreate()
|
||||
|
||||
inputBox:setText(title)
|
||||
inputBox.inputs = {}
|
||||
inputBox.onEnter = function()
|
||||
local results = {}
|
||||
for _,func in pairs(inputBox.inputs) do
|
||||
table.insert(results, func())
|
||||
end
|
||||
okCallback(unpack(results))
|
||||
inputBox:destroy()
|
||||
end
|
||||
inputBox.onEscape = function()
|
||||
if cancelCallback then
|
||||
cancelCallback()
|
||||
end
|
||||
inputBox:destroy()
|
||||
end
|
||||
|
||||
return inputBox
|
||||
end
|
||||
|
||||
function UIInputBox:addLabel(text)
|
||||
local label = g_ui.createWidget('InputBoxLabel', self)
|
||||
label:setText(text)
|
||||
return label
|
||||
end
|
||||
|
||||
function UIInputBox:addLineEdit(labelText, defaultText, maxLength)
|
||||
if labelText then self:addLabel(labelText) end
|
||||
local lineEdit = g_ui.createWidget('InputBoxLineEdit', self)
|
||||
if defaultText then lineEdit:setText(defaultText) end
|
||||
if maxLength then lineEdit:setMaxLength(maxLength) end
|
||||
table.insert(self.inputs, function() return lineEdit:getText() end)
|
||||
return lineEdit
|
||||
end
|
||||
|
||||
function UIInputBox:addTextEdit(labelText, defaultText, maxLength, visibleLines)
|
||||
if labelText then self:addLabel(labelText) end
|
||||
local textEdit = g_ui.createWidget('InputBoxTextEdit', self)
|
||||
if defaultText then textEdit:setText(defaultText) end
|
||||
if maxLength then textEdit:setMaxLength(maxLength) end
|
||||
visibleLines = visibleLines or 1
|
||||
textEdit:setHeight(textEdit:getHeight() * visibleLines)
|
||||
table.insert(self.inputs, function() return textEdit:getText() end)
|
||||
return textEdit
|
||||
end
|
||||
|
||||
function UIInputBox:addCheckBox(text, checked)
|
||||
local checkBox = g_ui.createWidget('InputBoxCheckBox', self)
|
||||
checkBox:setText(text)
|
||||
checkBox:setChecked(checked)
|
||||
table.insert(self.inputs, function() return checkBox:isChecked() end)
|
||||
return checkBox
|
||||
end
|
||||
|
||||
function UIInputBox:addComboBox(labelText, ...)
|
||||
if labelText then self:addLabel(labelText) end
|
||||
local comboBox = g_ui.createWidget('InputBoxComboBox', self)
|
||||
local options = {...}
|
||||
for i=1,#options do
|
||||
comboBox:addOption(options[i])
|
||||
end
|
||||
table.insert(self.inputs, function() return comboBox:getCurrentOption() end)
|
||||
return comboBox
|
||||
end
|
||||
|
||||
function UIInputBox:addSpinBox(labelText, minimum, maximum, value, step)
|
||||
if labelText then self:addLabel(labelText) end
|
||||
local spinBox = g_ui.createWidget('InputBoxSpinBox', self)
|
||||
spinBox:setMinimum(minimum)
|
||||
spinBox:setMaximum(maximum)
|
||||
spinBox:setValue(value)
|
||||
spinBox:setStep(step)
|
||||
table.insert(self.inputs, function() return spinBox:getValue() end)
|
||||
return spinBox
|
||||
end
|
||||
|
||||
function UIInputBox:display(okButtonText, cancelButtonText)
|
||||
okButtonText = okButtonText or tr('Ok')
|
||||
cancelButtonText = cancelButtonText or tr('Cancel')
|
||||
|
||||
local buttonsWidget = g_ui.createWidget('InputBoxButtonsPanel', self)
|
||||
local okButton = g_ui.createWidget('InputBoxButton', buttonsWidget)
|
||||
okButton:setText(okButtonText)
|
||||
okButton.onClick = self.onEnter
|
||||
|
||||
local cancelButton = g_ui.createWidget('InputBoxButton', buttonsWidget)
|
||||
cancelButton:setText(cancelButtonText)
|
||||
cancelButton.onClick = self.onEscape
|
||||
|
||||
buttonsWidget:setHeight(okButton:getHeight())
|
||||
|
||||
rootWidget:addChild(self)
|
||||
self:setStyle('InputBoxWindow')
|
||||
end
|
||||
|
||||
function displayTextInputBox(title, label, okCallback, cancelCallback)
|
||||
local inputBox = UIInputBox.create(title, okCallback, cancelCallback)
|
||||
inputBox:addLineEdit(label)
|
||||
inputBox:display()
|
||||
end
|
||||
|
||||
function displayNumberInputBox(title, label, okCallback, cancelCallback, min, max, value, step)
|
||||
local inputBox = UIInputBox.create(title, okCallback, cancelCallback)
|
||||
inputBox:addSpinBox(label, min, max, value, step)
|
||||
inputBox:display()
|
||||
end
|
10
modules/corelib/ui/uilabel.lua
Normal file
10
modules/corelib/ui/uilabel.lua
Normal file
@@ -0,0 +1,10 @@
|
||||
-- @docclass
|
||||
UILabel = extends(UIWidget, "UILabel")
|
||||
|
||||
function UILabel.create()
|
||||
local label = UILabel.internalCreate()
|
||||
label:setPhantom(true)
|
||||
label:setFocusable(false)
|
||||
label:setTextAlign(AlignLeft)
|
||||
return label
|
||||
end
|
96
modules/corelib/ui/uimessagebox.lua
Normal file
96
modules/corelib/ui/uimessagebox.lua
Normal file
@@ -0,0 +1,96 @@
|
||||
if not UIWindow then dofile 'uiwindow' end
|
||||
|
||||
-- @docclass
|
||||
UIMessageBox = extends(UIWindow, "UIMessageBox")
|
||||
|
||||
-- messagebox cannot be created from otui files
|
||||
UIMessageBox.create = nil
|
||||
|
||||
function UIMessageBox.display(title, message, buttons, onEnterCallback, onEscapeCallback)
|
||||
local messageBox = UIMessageBox.internalCreate()
|
||||
rootWidget:addChild(messageBox)
|
||||
|
||||
messageBox:setStyle('MainWindow')
|
||||
messageBox:setText(title)
|
||||
|
||||
local messageLabel = g_ui.createWidget('MessageBoxLabel', messageBox)
|
||||
messageLabel:setText(message)
|
||||
|
||||
local buttonsWidth = 0
|
||||
local buttonsHeight = 0
|
||||
|
||||
local anchor = AnchorRight
|
||||
if buttons.anchor then anchor = buttons.anchor end
|
||||
|
||||
local buttonHolder = g_ui.createWidget('MessageBoxButtonHolder', messageBox)
|
||||
buttonHolder:addAnchor(anchor, 'parent', anchor)
|
||||
|
||||
for i=1,#buttons do
|
||||
local button = messageBox:addButton(buttons[i].text, buttons[i].callback)
|
||||
if i == 1 then
|
||||
button:setMarginLeft(0)
|
||||
button:addAnchor(AnchorBottom, 'parent', AnchorBottom)
|
||||
button:addAnchor(AnchorLeft, 'parent', AnchorLeft)
|
||||
buttonsHeight = button:getHeight()
|
||||
else
|
||||
button:addAnchor(AnchorBottom, 'prev', AnchorBottom)
|
||||
button:addAnchor(AnchorLeft, 'prev', AnchorRight)
|
||||
end
|
||||
buttonsWidth = buttonsWidth + button:getWidth() + button:getMarginLeft()
|
||||
end
|
||||
|
||||
buttonHolder:setWidth(buttonsWidth)
|
||||
buttonHolder:setHeight(buttonsHeight)
|
||||
|
||||
if onEnterCallback then connect(messageBox, { onEnter = onEnterCallback }) end
|
||||
if onEscapeCallback then connect(messageBox, { onEscape = onEscapeCallback }) end
|
||||
|
||||
messageBox:setWidth(math.max(messageLabel:getWidth(), messageBox:getTextSize().width, buttonHolder:getWidth()) + messageBox:getPaddingLeft() + messageBox:getPaddingRight())
|
||||
messageBox:setHeight(messageLabel:getHeight() + messageBox:getPaddingTop() + messageBox:getPaddingBottom() + buttonHolder:getHeight() + buttonHolder:getMarginTop())
|
||||
return messageBox
|
||||
end
|
||||
|
||||
function displayInfoBox(title, message)
|
||||
local messageBox
|
||||
local defaultCallback = function() messageBox:ok() end
|
||||
messageBox = UIMessageBox.display(title, message, {{text='Ok', callback=defaultCallback}}, defaultCallback, defaultCallback)
|
||||
return messageBox
|
||||
end
|
||||
|
||||
function displayErrorBox(title, message)
|
||||
local messageBox
|
||||
local defaultCallback = function() messageBox:ok() end
|
||||
messageBox = UIMessageBox.display(title, message, {{text='Ok', callback=defaultCallback}}, defaultCallback, defaultCallback)
|
||||
return messageBox
|
||||
end
|
||||
|
||||
function displayCancelBox(title, message)
|
||||
local messageBox
|
||||
local defaultCallback = function() messageBox:cancel() end
|
||||
messageBox = UIMessageBox.display(title, message, {{text='Cancel', callback=defaultCallback}}, defaultCallback, defaultCallback)
|
||||
return messageBox
|
||||
end
|
||||
|
||||
function displayGeneralBox(title, message, buttons, onEnterCallback, onEscapeCallback)
|
||||
return UIMessageBox.display(title, message, buttons, onEnterCallback, onEscapeCallback)
|
||||
end
|
||||
|
||||
function UIMessageBox:addButton(text, callback)
|
||||
local buttonHolder = self:getChildById('buttonHolder')
|
||||
local button = g_ui.createWidget('MessageBoxButton', buttonHolder)
|
||||
button:setText(text)
|
||||
connect(button, { onClick = callback })
|
||||
return button
|
||||
end
|
||||
|
||||
function UIMessageBox:ok()
|
||||
signalcall(self.onOk, self)
|
||||
self.onOk = nil
|
||||
self:destroy()
|
||||
end
|
||||
|
||||
function UIMessageBox:cancel()
|
||||
signalcall(self.onCancel, self)
|
||||
self.onCancel = nil
|
||||
self:destroy()
|
||||
end
|
433
modules/corelib/ui/uiminiwindow.lua
Normal file
433
modules/corelib/ui/uiminiwindow.lua
Normal file
@@ -0,0 +1,433 @@
|
||||
-- @docclass
|
||||
UIMiniWindow = extends(UIWindow, "UIMiniWindow")
|
||||
|
||||
function UIMiniWindow.create()
|
||||
local miniwindow = UIMiniWindow.internalCreate()
|
||||
miniwindow.UIMiniWindowContainer = true
|
||||
return miniwindow
|
||||
end
|
||||
|
||||
function UIMiniWindow:open(dontSave)
|
||||
self:setVisible(true)
|
||||
|
||||
if not dontSave then
|
||||
self:setSettings({closed = false})
|
||||
end
|
||||
|
||||
signalcall(self.onOpen, self)
|
||||
end
|
||||
|
||||
function UIMiniWindow:close(dontSave)
|
||||
if not self:isExplicitlyVisible() then return end
|
||||
if self.forceOpen then return end
|
||||
self:setVisible(false)
|
||||
|
||||
if not dontSave then
|
||||
self:setSettings({closed = true})
|
||||
end
|
||||
|
||||
signalcall(self.onClose, self)
|
||||
end
|
||||
|
||||
function UIMiniWindow:minimize(dontSave)
|
||||
self:setOn(true)
|
||||
self:getChildById('contentsPanel'):hide()
|
||||
self:getChildById('miniwindowScrollBar'):hide()
|
||||
self:getChildById('bottomResizeBorder'):hide()
|
||||
self:getChildById('minimizeButton'):setOn(true)
|
||||
self.maximizedHeight = self:getHeight()
|
||||
self:setHeight(self.minimizedHeight)
|
||||
|
||||
if not dontSave then
|
||||
self:setSettings({minimized = true})
|
||||
end
|
||||
|
||||
signalcall(self.onMinimize, self)
|
||||
end
|
||||
|
||||
function UIMiniWindow:maximize(dontSave)
|
||||
self:setOn(false)
|
||||
self:getChildById('contentsPanel'):show()
|
||||
self:getChildById('miniwindowScrollBar'):show()
|
||||
self:getChildById('bottomResizeBorder'):show()
|
||||
self:getChildById('minimizeButton'):setOn(false)
|
||||
self:setHeight(self:getSettings('height') or self.maximizedHeight)
|
||||
|
||||
if not dontSave then
|
||||
self:setSettings({minimized = false})
|
||||
end
|
||||
|
||||
local parent = self:getParent()
|
||||
if parent and parent:getClassName() == 'UIMiniWindowContainer' then
|
||||
parent:fitAll(self)
|
||||
end
|
||||
|
||||
signalcall(self.onMaximize, self)
|
||||
end
|
||||
|
||||
function UIMiniWindow:lock(dontSave)
|
||||
local lockButton = self:getChildById('lockButton')
|
||||
if lockButton then
|
||||
lockButton:setOn(true)
|
||||
end
|
||||
self:setDraggable(false)
|
||||
if not dontsave then
|
||||
self:setSettings({locked = true})
|
||||
end
|
||||
|
||||
signalcall(self.onLockChange, self)
|
||||
end
|
||||
|
||||
function UIMiniWindow:unlock(dontSave)
|
||||
local lockButton = self:getChildById('lockButton')
|
||||
if lockButton then
|
||||
lockButton:setOn(false)
|
||||
end
|
||||
self:setDraggable(true)
|
||||
if not dontsave then
|
||||
self:setSettings({locked = false})
|
||||
end
|
||||
signalcall(self.onLockChange, self)
|
||||
end
|
||||
|
||||
function UIMiniWindow:setup()
|
||||
self:getChildById('closeButton').onClick =
|
||||
function()
|
||||
self:close()
|
||||
end
|
||||
if self.forceOpen then
|
||||
self:getChildById('closeButton'):hide()
|
||||
self:getChildById('minimizeButton'):addAnchor(AnchorRight, 'parent', AnchorRight)
|
||||
end
|
||||
|
||||
self:getChildById('minimizeButton').onClick =
|
||||
function()
|
||||
if self:isOn() then
|
||||
self:maximize()
|
||||
else
|
||||
self:minimize()
|
||||
end
|
||||
end
|
||||
|
||||
local lockButton = self:getChildById('lockButton')
|
||||
if lockButton then
|
||||
lockButton.onClick =
|
||||
function ()
|
||||
if self:isDraggable() then
|
||||
self:lock()
|
||||
else
|
||||
self:unlock()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self:getChildById('miniwindowTopBar').onDoubleClick =
|
||||
function()
|
||||
if self:isOn() then
|
||||
self:maximize()
|
||||
else
|
||||
self:minimize()
|
||||
end
|
||||
end
|
||||
|
||||
local oldParent = self:getParent()
|
||||
|
||||
local settings = g_settings.getNode('MiniWindows')
|
||||
if settings then
|
||||
local selfSettings = settings[self:getId()]
|
||||
if selfSettings then
|
||||
if selfSettings.parentId then
|
||||
local parent = rootWidget:recursiveGetChildById(selfSettings.parentId)
|
||||
if parent then
|
||||
if parent:getClassName() == 'UIMiniWindowContainer' and selfSettings.index and parent:isOn() then
|
||||
self.miniIndex = selfSettings.index
|
||||
parent:scheduleInsert(self, selfSettings.index)
|
||||
elseif selfSettings.position then
|
||||
self:setParent(parent, true)
|
||||
self:setPosition(topoint(selfSettings.position))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if selfSettings.minimized then
|
||||
self:minimize(true)
|
||||
else
|
||||
if selfSettings.height and self:isResizeable() then
|
||||
self:setHeight(selfSettings.height)
|
||||
elseif selfSettings.height and not self:isResizeable() then
|
||||
self:eraseSettings({height = true})
|
||||
end
|
||||
end
|
||||
|
||||
if selfSettings.closed and not self.forceOpen then
|
||||
self:close(true)
|
||||
end
|
||||
|
||||
if selfSettings.locked then
|
||||
self:lock(true)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local newParent = self:getParent()
|
||||
|
||||
self.miniLoaded = true
|
||||
|
||||
if self.save then
|
||||
if oldParent and oldParent:getClassName() == 'UIMiniWindowContainer' then
|
||||
addEvent(function() oldParent:order() end)
|
||||
end
|
||||
if newParent and newParent:getClassName() == 'UIMiniWindowContainer' and newParent ~= oldParent then
|
||||
addEvent(function() newParent:order() end)
|
||||
end
|
||||
end
|
||||
|
||||
self:fitOnParent()
|
||||
end
|
||||
|
||||
function UIMiniWindow:onVisibilityChange(visible)
|
||||
self:fitOnParent()
|
||||
end
|
||||
|
||||
function UIMiniWindow:onDragEnter(mousePos)
|
||||
local parent = self:getParent()
|
||||
if not parent then return false end
|
||||
|
||||
if parent:getClassName() == 'UIMiniWindowContainer' then
|
||||
local containerParent = parent:getParent():getParent()
|
||||
parent:removeChild(self)
|
||||
containerParent:addChild(self)
|
||||
parent:saveChildren()
|
||||
end
|
||||
|
||||
local oldPos = self:getPosition()
|
||||
self.movingReference = { x = mousePos.x - oldPos.x, y = mousePos.y - oldPos.y }
|
||||
self:setPosition(oldPos)
|
||||
self.free = true
|
||||
return true
|
||||
end
|
||||
|
||||
function UIMiniWindow:onDragLeave(droppedWidget, mousePos)
|
||||
if self.movedWidget then
|
||||
self.setMovedChildMargin(self.movedOldMargin or 0)
|
||||
self.movedWidget = nil
|
||||
self.setMovedChildMargin = nil
|
||||
self.movedOldMargin = nil
|
||||
self.movedIndex = nil
|
||||
end
|
||||
|
||||
UIWindow:onDragLeave(self, droppedWidget, mousePos)
|
||||
self:saveParent(self:getParent())
|
||||
end
|
||||
|
||||
function UIMiniWindow:onDragMove(mousePos, mouseMoved)
|
||||
local oldMousePosY = mousePos.y - mouseMoved.y
|
||||
local children = rootWidget:recursiveGetChildrenByMarginPos(mousePos)
|
||||
local overAnyWidget = false
|
||||
for i=1,#children do
|
||||
local child = children[i]
|
||||
if child:getParent():getClassName() == 'UIMiniWindowContainer' then
|
||||
overAnyWidget = true
|
||||
|
||||
local childCenterY = child:getY() + child:getHeight() / 2
|
||||
if child == self.movedWidget and mousePos.y < childCenterY and oldMousePosY < childCenterY then
|
||||
break
|
||||
end
|
||||
|
||||
if self.movedWidget then
|
||||
self.setMovedChildMargin(self.movedOldMargin or 0)
|
||||
self.setMovedChildMargin = nil
|
||||
end
|
||||
|
||||
if mousePos.y < childCenterY then
|
||||
self.movedOldMargin = child:getMarginTop()
|
||||
self.setMovedChildMargin = function(v) child:setMarginTop(v) end
|
||||
self.movedIndex = 0
|
||||
else
|
||||
self.movedOldMargin = child:getMarginBottom()
|
||||
self.setMovedChildMargin = function(v) child:setMarginBottom(v) end
|
||||
self.movedIndex = 1
|
||||
end
|
||||
|
||||
self.movedWidget = child
|
||||
self.setMovedChildMargin(self:getHeight())
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not overAnyWidget and self.movedWidget then
|
||||
self.setMovedChildMargin(self.movedOldMargin or 0)
|
||||
self.movedWidget = nil
|
||||
end
|
||||
|
||||
return UIWindow.onDragMove(self, mousePos, mouseMoved)
|
||||
end
|
||||
|
||||
function UIMiniWindow:onMousePress()
|
||||
local parent = self:getParent()
|
||||
if not parent then return false end
|
||||
if parent:getClassName() ~= 'UIMiniWindowContainer' then
|
||||
self:raise()
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function UIMiniWindow:onFocusChange(focused)
|
||||
if not focused then return end
|
||||
local parent = self:getParent()
|
||||
if parent and parent:getClassName() ~= 'UIMiniWindowContainer' then
|
||||
self:raise()
|
||||
end
|
||||
end
|
||||
|
||||
function UIMiniWindow:onHeightChange(height)
|
||||
if not self:isOn() then
|
||||
self:setSettings({height = height})
|
||||
end
|
||||
self:fitOnParent()
|
||||
end
|
||||
|
||||
function UIMiniWindow:getSettings(name)
|
||||
if not self.save then return nil end
|
||||
local settings = g_settings.getNode('MiniWindows')
|
||||
if settings then
|
||||
local selfSettings = settings[self:getId()]
|
||||
if selfSettings then
|
||||
return selfSettings[name]
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function UIMiniWindow:setSettings(data)
|
||||
if not self.save then return end
|
||||
|
||||
local settings = g_settings.getNode('MiniWindows')
|
||||
if not settings then
|
||||
settings = {}
|
||||
end
|
||||
|
||||
local id = self:getId()
|
||||
if not settings[id] then
|
||||
settings[id] = {}
|
||||
end
|
||||
|
||||
for key,value in pairs(data) do
|
||||
settings[id][key] = value
|
||||
end
|
||||
|
||||
g_settings.setNode('MiniWindows', settings)
|
||||
end
|
||||
|
||||
function UIMiniWindow:eraseSettings(data)
|
||||
if not self.save then return end
|
||||
|
||||
local settings = g_settings.getNode('MiniWindows')
|
||||
if not settings then
|
||||
settings = {}
|
||||
end
|
||||
|
||||
local id = self:getId()
|
||||
if not settings[id] then
|
||||
settings[id] = {}
|
||||
end
|
||||
|
||||
for key,value in pairs(data) do
|
||||
settings[id][key] = nil
|
||||
end
|
||||
|
||||
g_settings.setNode('MiniWindows', settings)
|
||||
end
|
||||
|
||||
function UIMiniWindow:saveParent(parent)
|
||||
local parent = self:getParent()
|
||||
if parent then
|
||||
if parent:getClassName() == 'UIMiniWindowContainer' then
|
||||
parent:saveChildren()
|
||||
else
|
||||
self:saveParentPosition(parent:getId(), self:getPosition())
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function UIMiniWindow:saveParentPosition(parentId, position)
|
||||
local selfSettings = {}
|
||||
selfSettings.parentId = parentId
|
||||
selfSettings.position = pointtostring(position)
|
||||
self:setSettings(selfSettings)
|
||||
end
|
||||
|
||||
function UIMiniWindow:saveParentIndex(parentId, index)
|
||||
local selfSettings = {}
|
||||
selfSettings.parentId = parentId
|
||||
selfSettings.index = index
|
||||
self:setSettings(selfSettings)
|
||||
self.miniIndex = index
|
||||
end
|
||||
|
||||
function UIMiniWindow:disableResize()
|
||||
self:getChildById('bottomResizeBorder'):disable()
|
||||
end
|
||||
|
||||
function UIMiniWindow:enableResize()
|
||||
self:getChildById('bottomResizeBorder'):enable()
|
||||
end
|
||||
|
||||
function UIMiniWindow:fitOnParent()
|
||||
local parent = self:getParent()
|
||||
if self:isVisible() and parent and parent:getClassName() == 'UIMiniWindowContainer' then
|
||||
parent:fitAll(self)
|
||||
end
|
||||
end
|
||||
|
||||
function UIMiniWindow:setParent(parent, dontsave)
|
||||
UIWidget.setParent(self, parent)
|
||||
if not dontsave then
|
||||
self:saveParent(parent)
|
||||
end
|
||||
self:fitOnParent()
|
||||
end
|
||||
|
||||
function UIMiniWindow:setHeight(height)
|
||||
UIWidget.setHeight(self, height)
|
||||
signalcall(self.onHeightChange, self, height)
|
||||
end
|
||||
|
||||
function UIMiniWindow:setContentHeight(height)
|
||||
local contentsPanel = self:getChildById('contentsPanel')
|
||||
local minHeight = contentsPanel:getMarginTop() + contentsPanel:getMarginBottom() + contentsPanel:getPaddingTop() + contentsPanel:getPaddingBottom()
|
||||
|
||||
local resizeBorder = self:getChildById('bottomResizeBorder')
|
||||
resizeBorder:setParentSize(minHeight + height)
|
||||
end
|
||||
|
||||
function UIMiniWindow:setContentMinimumHeight(height)
|
||||
local contentsPanel = self:getChildById('contentsPanel')
|
||||
local minHeight = contentsPanel:getMarginTop() + contentsPanel:getMarginBottom() + contentsPanel:getPaddingTop() + contentsPanel:getPaddingBottom()
|
||||
|
||||
local resizeBorder = self:getChildById('bottomResizeBorder')
|
||||
resizeBorder:setMinimum(minHeight + height)
|
||||
end
|
||||
|
||||
function UIMiniWindow:setContentMaximumHeight(height)
|
||||
local contentsPanel = self:getChildById('contentsPanel')
|
||||
local minHeight = contentsPanel:getMarginTop() + contentsPanel:getMarginBottom() + contentsPanel:getPaddingTop() + contentsPanel:getPaddingBottom()
|
||||
|
||||
local resizeBorder = self:getChildById('bottomResizeBorder')
|
||||
resizeBorder:setMaximum(minHeight + height)
|
||||
end
|
||||
|
||||
function UIMiniWindow:getMinimumHeight()
|
||||
local resizeBorder = self:getChildById('bottomResizeBorder')
|
||||
return resizeBorder:getMinimum()
|
||||
end
|
||||
|
||||
function UIMiniWindow:getMaximumHeight()
|
||||
local resizeBorder = self:getChildById('bottomResizeBorder')
|
||||
return resizeBorder:getMaximum()
|
||||
end
|
||||
|
||||
function UIMiniWindow:isResizeable()
|
||||
local resizeBorder = self:getChildById('bottomResizeBorder')
|
||||
return resizeBorder:isExplicitlyVisible() and resizeBorder:isEnabled()
|
||||
end
|
211
modules/corelib/ui/uiminiwindowcontainer.lua
Normal file
211
modules/corelib/ui/uiminiwindowcontainer.lua
Normal file
@@ -0,0 +1,211 @@
|
||||
-- @docclass
|
||||
UIMiniWindowContainer = extends(UIWidget, "UIMiniWindowContainer")
|
||||
|
||||
function UIMiniWindowContainer.create()
|
||||
local container = UIMiniWindowContainer.internalCreate()
|
||||
container.scheduledWidgets = {}
|
||||
container:setFocusable(false)
|
||||
container:setPhantom(true)
|
||||
return container
|
||||
end
|
||||
|
||||
-- TODO: connect to window onResize event
|
||||
-- TODO: try to resize another widget?
|
||||
-- TODO: try to find another panel?
|
||||
function UIMiniWindowContainer:fitAll(noRemoveChild)
|
||||
if not self:isVisible() then
|
||||
return
|
||||
end
|
||||
|
||||
if not noRemoveChild then
|
||||
local children = self:getChildren()
|
||||
if #children > 0 then
|
||||
noRemoveChild = children[#children]
|
||||
else
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local sumHeight = 0
|
||||
local children = self:getChildren()
|
||||
for i=1,#children do
|
||||
if children[i]:isVisible() then
|
||||
sumHeight = sumHeight + children[i]:getHeight()
|
||||
end
|
||||
end
|
||||
|
||||
local selfHeight = self:getHeight() - (self:getPaddingTop() + self:getPaddingBottom())
|
||||
if sumHeight <= selfHeight then
|
||||
return
|
||||
end
|
||||
|
||||
local removeChildren = {}
|
||||
|
||||
-- try to resize noRemoveChild
|
||||
local maximumHeight = selfHeight - (sumHeight - noRemoveChild:getHeight())
|
||||
if noRemoveChild:isResizeable() and noRemoveChild:getMinimumHeight() <= maximumHeight then
|
||||
sumHeight = sumHeight - noRemoveChild:getHeight() + maximumHeight
|
||||
addEvent(function() noRemoveChild:setHeight(maximumHeight) end)
|
||||
end
|
||||
|
||||
-- try to remove no-save widget
|
||||
for i=#children,1,-1 do
|
||||
if sumHeight <= selfHeight then
|
||||
break
|
||||
end
|
||||
|
||||
local child = children[i]
|
||||
if child ~= noRemoveChild and not child.save then
|
||||
local childHeight = child:getHeight()
|
||||
sumHeight = sumHeight - childHeight
|
||||
table.insert(removeChildren, child)
|
||||
end
|
||||
end
|
||||
|
||||
-- try to remove save widget, not forceOpen
|
||||
for i=#children,1,-1 do
|
||||
if sumHeight <= selfHeight then
|
||||
break
|
||||
end
|
||||
|
||||
local child = children[i]
|
||||
if child ~= noRemoveChild and child:isVisible() and not child.forceOpen then
|
||||
local childHeight = child:getHeight()
|
||||
sumHeight = sumHeight - childHeight
|
||||
table.insert(removeChildren, child)
|
||||
end
|
||||
end
|
||||
|
||||
-- try to remove save widget
|
||||
for i=#children,1,-1 do
|
||||
if sumHeight <= selfHeight then
|
||||
break
|
||||
end
|
||||
|
||||
local child = children[i]
|
||||
if child ~= noRemoveChild and child:isVisible() then
|
||||
local childHeight = child:getHeight() - 50
|
||||
sumHeight = sumHeight - childHeight
|
||||
table.insert(removeChildren, child)
|
||||
end
|
||||
end
|
||||
|
||||
-- close widgets
|
||||
for i=1,#removeChildren do
|
||||
if removeChildren[i].forceOpen then
|
||||
removeChildren[i]:minimize(true)
|
||||
else
|
||||
removeChildren[i]:close()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function UIMiniWindowContainer:onDrop(widget, mousePos)
|
||||
if widget.UIMiniWindowContainer then
|
||||
local oldParent = widget:getParent()
|
||||
if oldParent == self then
|
||||
return true
|
||||
end
|
||||
|
||||
if oldParent then
|
||||
oldParent:removeChild(widget)
|
||||
end
|
||||
|
||||
if widget.movedWidget then
|
||||
local index = self:getChildIndex(widget.movedWidget)
|
||||
self:insertChild(index + widget.movedIndex, widget)
|
||||
else
|
||||
self:addChild(widget)
|
||||
end
|
||||
|
||||
self:fitAll(widget)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function UIMiniWindowContainer:moveTo(newPanel)
|
||||
if not newPanel or newPanel == self then
|
||||
return
|
||||
end
|
||||
local children = self:getChildByIndex(1)
|
||||
while children do
|
||||
newPanel:addChild(children)
|
||||
children = self:getChildByIndex(1)
|
||||
end
|
||||
newPanel:fitAll()
|
||||
end
|
||||
|
||||
function UIMiniWindowContainer:swapInsert(widget, index)
|
||||
local oldParent = widget:getParent()
|
||||
local oldIndex = self:getChildIndex(widget)
|
||||
|
||||
if oldParent == self and oldIndex ~= index then
|
||||
local oldWidget = self:getChildByIndex(index)
|
||||
if oldWidget then
|
||||
self:removeChild(oldWidget)
|
||||
self:insertChild(oldIndex, oldWidget)
|
||||
end
|
||||
self:removeChild(widget)
|
||||
self:insertChild(index, widget)
|
||||
end
|
||||
end
|
||||
|
||||
function UIMiniWindowContainer:scheduleInsert(widget, index)
|
||||
if index - 1 > self:getChildCount() then
|
||||
if self.scheduledWidgets[index] then
|
||||
pdebug('replacing scheduled widget id ' .. widget:getId())
|
||||
end
|
||||
self.scheduledWidgets[index] = widget
|
||||
else
|
||||
local oldParent = widget:getParent()
|
||||
if oldParent ~= self then
|
||||
if oldParent then
|
||||
oldParent:removeChild(widget)
|
||||
end
|
||||
self:insertChild(index, widget)
|
||||
|
||||
while true do
|
||||
local placed = false
|
||||
for nIndex,nWidget in pairs(self.scheduledWidgets) do
|
||||
if nIndex - 1 <= self:getChildCount() then
|
||||
self:insertChild(nIndex, nWidget)
|
||||
self.scheduledWidgets[nIndex] = nil
|
||||
placed = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not placed then break end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function UIMiniWindowContainer:order()
|
||||
local children = self:getChildren()
|
||||
for i=1,#children do
|
||||
if not children[i].miniLoaded then return end
|
||||
end
|
||||
|
||||
for i=1,#children do
|
||||
if children[i].miniIndex then
|
||||
self:swapInsert(children[i], children[i].miniIndex)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function UIMiniWindowContainer:saveChildren()
|
||||
local children = self:getChildren()
|
||||
local ignoreIndex = 0
|
||||
for i=1,#children do
|
||||
if children[i].save then
|
||||
children[i]:saveParentIndex(self:getId(), i - ignoreIndex)
|
||||
else
|
||||
ignoreIndex = ignoreIndex + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function UIMiniWindowContainer:onGeometryChange()
|
||||
self:fitAll()
|
||||
end
|
501
modules/corelib/ui/uimovabletabbar.lua
Normal file
501
modules/corelib/ui/uimovabletabbar.lua
Normal file
@@ -0,0 +1,501 @@
|
||||
-- @docclass
|
||||
UIMoveableTabBar = extends(UIWidget, "UIMoveableTabBar")
|
||||
|
||||
-- private functions
|
||||
local function onTabClick(tab)
|
||||
tab.tabBar:selectTab(tab)
|
||||
end
|
||||
|
||||
local function updateMargins(tabBar)
|
||||
if #tabBar.tabs == 0 then return end
|
||||
|
||||
local currentMargin = 0
|
||||
for i = 1, #tabBar.tabs do
|
||||
tabBar.tabs[i]:setMarginLeft(currentMargin)
|
||||
currentMargin = currentMargin + tabBar.tabSpacing + tabBar.tabs[i]:getWidth()
|
||||
end
|
||||
end
|
||||
|
||||
local function updateNavigation(tabBar)
|
||||
if tabBar.prevNavigation then
|
||||
if #tabBar.preTabs > 0 or table.find(tabBar.tabs, tabBar.currentTab) ~= 1 then
|
||||
tabBar.prevNavigation:enable()
|
||||
else
|
||||
tabBar.prevNavigation:disable()
|
||||
end
|
||||
end
|
||||
|
||||
if tabBar.nextNavigation then
|
||||
if #tabBar.postTabs > 0 or table.find(tabBar.tabs, tabBar.currentTab) ~= #tabBar.tabs then
|
||||
tabBar.nextNavigation:enable()
|
||||
else
|
||||
tabBar.nextNavigation:disable()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function updateIndexes(tabBar, tab, xoff)
|
||||
local tabs = tabBar.tabs
|
||||
local currentMargin = 0
|
||||
local prevIndex = table.find(tabs, tab)
|
||||
local newIndex = prevIndex
|
||||
local xmid = xoff + tab:getWidth()/2
|
||||
for i = 1, #tabs do
|
||||
local nextTab = tabs[i]
|
||||
if xmid >= currentMargin + nextTab:getWidth()/2 then
|
||||
newIndex = table.find(tabs, nextTab)
|
||||
end
|
||||
currentMargin = currentMargin + tabBar.tabSpacing * (i - 1) + tabBar.tabs[i]:getWidth()
|
||||
end
|
||||
if newIndex ~= prevIndex then
|
||||
table.remove(tabs, table.find(tabs, tab))
|
||||
table.insert(tabs, newIndex, tab)
|
||||
end
|
||||
updateNavigation(tabBar)
|
||||
end
|
||||
|
||||
local function getMaxMargin(tabBar, tab)
|
||||
if #tabBar.tabs == 0 then return 0 end
|
||||
|
||||
local maxMargin = 0
|
||||
for i = 1, #tabBar.tabs do
|
||||
if tabBar.tabs[i] ~= tab then
|
||||
maxMargin = maxMargin + tabBar.tabs[i]:getWidth()
|
||||
end
|
||||
end
|
||||
return maxMargin + tabBar.tabSpacing * (#tabBar.tabs - 1)
|
||||
end
|
||||
|
||||
local function updateTabs(tabBar)
|
||||
if #tabBar.postTabs > 0 then
|
||||
local i = 1
|
||||
while i <= #tabBar.postTabs do
|
||||
local tab = tabBar.postTabs[i]
|
||||
if getMaxMargin(tabBar) + tab:getWidth() > tabBar:getWidth() then
|
||||
break
|
||||
end
|
||||
|
||||
table.remove(tabBar.postTabs, i)
|
||||
table.insert(tabBar.tabs, tab)
|
||||
tab:setVisible(true)
|
||||
end
|
||||
end
|
||||
if #tabBar.preTabs > 0 then
|
||||
for i = #tabBar.preTabs, 1, -1 do
|
||||
local tab = tabBar.preTabs[i]
|
||||
if getMaxMargin(tabBar) + tab:getWidth() > tabBar:getWidth() then
|
||||
break
|
||||
end
|
||||
|
||||
table.remove(tabBar.preTabs, i)
|
||||
table.insert(tabBar.tabs, 1, tab)
|
||||
tab:setVisible(true)
|
||||
end
|
||||
end
|
||||
updateNavigation(tabBar)
|
||||
updateMargins(tabBar)
|
||||
|
||||
if not tabBar.currentTab and #tabBar.tabs > 0 then
|
||||
tabBar:selectTab(tabBar.tabs[1])
|
||||
end
|
||||
end
|
||||
|
||||
local function hideTabs(tabBar, fromBack, toArray, width)
|
||||
while #tabBar.tabs > 0 and getMaxMargin(tabBar) + width > tabBar:getWidth() do
|
||||
local index = fromBack and #tabBar.tabs or 1
|
||||
local tab = tabBar.tabs[index]
|
||||
table.remove(tabBar.tabs, index)
|
||||
if fromBack then
|
||||
table.insert(toArray, 1, tab)
|
||||
else
|
||||
table.insert(toArray, tab)
|
||||
end
|
||||
if tabBar.currentTab == tab then
|
||||
if #tabBar.tabs > 0 then
|
||||
tabBar:selectTab(tabBar.tabs[#tabBar.tabs])
|
||||
else
|
||||
tabBar.currentTab:setChecked(false)
|
||||
tabBar.currentTab = nil
|
||||
end
|
||||
end
|
||||
tab:setVisible(false)
|
||||
end
|
||||
end
|
||||
|
||||
local function showPreTab(tabBar)
|
||||
if #tabBar.preTabs == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
local tmpTab = tabBar.preTabs[#tabBar.preTabs]
|
||||
hideTabs(tabBar, true, tabBar.postTabs, tmpTab:getWidth())
|
||||
|
||||
table.remove(tabBar.preTabs, #tabBar.preTabs)
|
||||
table.insert(tabBar.tabs, 1, tmpTab)
|
||||
tmpTab:setVisible(true)
|
||||
return tmpTab
|
||||
end
|
||||
|
||||
local function showPostTab(tabBar)
|
||||
if #tabBar.postTabs == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
local tmpTab = tabBar.postTabs[1]
|
||||
hideTabs(tabBar, false, tabBar.preTabs, tmpTab:getWidth())
|
||||
|
||||
table.remove(tabBar.postTabs, 1)
|
||||
table.insert(tabBar.tabs, tmpTab)
|
||||
tmpTab:setVisible(true)
|
||||
return tmpTab
|
||||
end
|
||||
|
||||
local function onTabMousePress(tab, mousePos, mouseButton)
|
||||
if mouseButton == MouseRightButton then
|
||||
if tab.menuCallback then tab.menuCallback(tab, mousePos, mouseButton) end
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
local function onTabDragEnter(tab, mousePos)
|
||||
tab:raise()
|
||||
tab.hotSpot = mousePos.x - tab:getMarginLeft()
|
||||
tab.tabBar.selected = tab
|
||||
return true
|
||||
end
|
||||
|
||||
local function onTabDragLeave(tab)
|
||||
updateMargins(tab.tabBar)
|
||||
tab.tabBar.selected = nil
|
||||
return true
|
||||
end
|
||||
|
||||
local function onTabDragMove(tab, mousePos, mouseMoved)
|
||||
if tab == tab.tabBar.selected then
|
||||
local xoff = mousePos.x - tab.hotSpot
|
||||
|
||||
-- update indexes
|
||||
updateIndexes(tab.tabBar, tab, xoff)
|
||||
updateIndexes(tab.tabBar, tab, xoff)
|
||||
|
||||
-- update margins
|
||||
updateMargins(tab.tabBar)
|
||||
xoff = math.max(xoff, 0)
|
||||
xoff = math.min(xoff, getMaxMargin(tab.tabBar, tab))
|
||||
tab:setMarginLeft(xoff)
|
||||
end
|
||||
end
|
||||
|
||||
local function tabBlink(tab, step)
|
||||
local step = step or 0
|
||||
tab:setOn(not tab:isOn())
|
||||
|
||||
removeEvent(tab.blinkEvent)
|
||||
if step < 4 then
|
||||
tab.blinkEvent = scheduleEvent(function() tabBlink(tab, step+1) end, 500)
|
||||
else
|
||||
tab:setOn(true)
|
||||
tab.blinkEvent = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- public functions
|
||||
function UIMoveableTabBar.create()
|
||||
local tabbar = UIMoveableTabBar.internalCreate()
|
||||
tabbar:setFocusable(false)
|
||||
tabbar.tabs = {}
|
||||
tabbar.selected = nil -- dragged tab
|
||||
tabbar.tabSpacing = 0
|
||||
tabbar.tabsMoveable = false
|
||||
tabbar.preTabs = {}
|
||||
tabbar.postTabs = {}
|
||||
tabbar.prevNavigation = nil
|
||||
tabbar.nextNavigation = nil
|
||||
tabbar.onGeometryChange = function()
|
||||
hideTabs(tabbar, true, tabbar.postTabs, 0)
|
||||
updateTabs(tabbar)
|
||||
end
|
||||
return tabbar
|
||||
end
|
||||
|
||||
function UIMoveableTabBar:onDestroy()
|
||||
if self.prevNavigation then
|
||||
self.prevNavigation:disable()
|
||||
end
|
||||
|
||||
if self.nextNavigation then
|
||||
self.nextNavigation:disable()
|
||||
end
|
||||
|
||||
self.nextNavigation = nil
|
||||
self.prevNavigation = nil
|
||||
end
|
||||
|
||||
function UIMoveableTabBar:setContentWidget(widget)
|
||||
self.contentWidget = widget
|
||||
if #self.tabs > 0 then
|
||||
self.contentWidget:addChild(self.tabs[1].tabPanel)
|
||||
end
|
||||
end
|
||||
|
||||
function UIMoveableTabBar:setTabSpacing(tabSpacing)
|
||||
self.tabSpacing = tabSpacing
|
||||
updateMargins(self)
|
||||
end
|
||||
|
||||
function UIMoveableTabBar:addTab(text, panel, menuCallback)
|
||||
if panel == nil then
|
||||
panel = g_ui.createWidget(self:getStyleName() .. 'Panel')
|
||||
panel:setId('tabPanel')
|
||||
end
|
||||
|
||||
local tab = g_ui.createWidget(self:getStyleName() .. 'Button', self)
|
||||
panel.isTab = true
|
||||
tab.tabPanel = panel
|
||||
tab.tabBar = self
|
||||
tab:setId('tab')
|
||||
tab:setDraggable(self.tabsMoveable)
|
||||
tab:setText(text)
|
||||
tab:setWidth(tab:getTextSize().width + tab:getPaddingLeft() + tab:getPaddingRight())
|
||||
tab.menuCallback = menuCallback or nil
|
||||
tab.onClick = onTabClick
|
||||
tab.onMousePress = onTabMousePress
|
||||
tab.onDragEnter = onTabDragEnter
|
||||
tab.onDragLeave = onTabDragLeave
|
||||
tab.onDragMove = onTabDragMove
|
||||
tab.onDestroy = function() tab.tabPanel:destroy() end
|
||||
|
||||
if #self.tabs == 0 then
|
||||
self:selectTab(tab)
|
||||
tab:setMarginLeft(0)
|
||||
table.insert(self.tabs, tab)
|
||||
else
|
||||
local newMargin = self.tabSpacing * #self.tabs
|
||||
for i = 1, #self.tabs do
|
||||
newMargin = newMargin + self.tabs[i]:getWidth()
|
||||
end
|
||||
tab:setMarginLeft(newMargin)
|
||||
|
||||
hideTabs(self, true, self.postTabs, tab:getWidth())
|
||||
table.insert(self.tabs, tab)
|
||||
if #self.tabs == 1 then
|
||||
self:selectTab(tab)
|
||||
end
|
||||
updateMargins(self)
|
||||
end
|
||||
|
||||
updateNavigation(self)
|
||||
return tab
|
||||
end
|
||||
|
||||
-- Additional function to move the tab by lua
|
||||
function UIMoveableTabBar:moveTab(tab, units)
|
||||
local index = table.find(self.tabs, tab)
|
||||
if index == nil then return end
|
||||
|
||||
local focus = false
|
||||
if self.currentTab == tab then
|
||||
self:selectPrevTab()
|
||||
focus = true
|
||||
end
|
||||
|
||||
table.remove(self.tabs, index)
|
||||
|
||||
local newIndex = math.min(#self.tabs+1, math.max(index + units, 1))
|
||||
table.insert(self.tabs, newIndex, tab)
|
||||
if focus then self:selectTab(tab) end
|
||||
updateMargins(self)
|
||||
return newIndex
|
||||
end
|
||||
|
||||
function UIMoveableTabBar:onStyleApply(styleName, styleNode)
|
||||
if styleNode['movable'] then
|
||||
self.tabsMoveable = styleNode['movable']
|
||||
end
|
||||
if styleNode['tab-spacing'] then
|
||||
self:setTabSpacing(styleNode['tab-spacing'])
|
||||
end
|
||||
end
|
||||
|
||||
function UIMoveableTabBar:removeTab(tab)
|
||||
local tabTables = {self.tabs, self.preTabs, self.postTabs}
|
||||
local index = nil
|
||||
local tabTable = nil
|
||||
for i = 1, #tabTables do
|
||||
index = table.find(tabTables[i], tab)
|
||||
if index ~= nil then
|
||||
tabTable = tabTables[i]
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if tabTable == nil then
|
||||
return
|
||||
end
|
||||
|
||||
if self.currentTab == tab then
|
||||
self:selectPrevTab()
|
||||
if #self.tabs == 1 then
|
||||
self.currentTab = nil
|
||||
end
|
||||
end
|
||||
table.remove(tabTable, index)
|
||||
if tab.blinkEvent then
|
||||
removeEvent(tab.blinkEvent)
|
||||
end
|
||||
tab:destroy()
|
||||
updateTabs(self)
|
||||
end
|
||||
|
||||
function UIMoveableTabBar:getTab(text)
|
||||
for k,tab in pairs(self.tabs) do
|
||||
if tab:getText():lower() == text:lower() then
|
||||
return tab
|
||||
end
|
||||
end
|
||||
for k,tab in pairs(self.preTabs) do
|
||||
if tab:getText():lower() == text:lower() then
|
||||
return tab
|
||||
end
|
||||
end
|
||||
for k,tab in pairs(self.postTabs) do
|
||||
if tab:getText():lower() == text:lower() then
|
||||
return tab
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function UIMoveableTabBar:selectTab(tab)
|
||||
if self.currentTab == tab then return end
|
||||
if self.contentWidget then
|
||||
local selectedWidget = self.contentWidget:getLastChild()
|
||||
if selectedWidget and selectedWidget.isTab then
|
||||
self.contentWidget:removeChild(selectedWidget)
|
||||
end
|
||||
self.contentWidget:addChild(tab.tabPanel)
|
||||
tab.tabPanel:fill('parent')
|
||||
end
|
||||
|
||||
if self.currentTab then
|
||||
self.currentTab:setChecked(false)
|
||||
end
|
||||
signalcall(self.onTabChange, self, tab)
|
||||
self.currentTab = tab
|
||||
tab:setChecked(true)
|
||||
tab:setOn(false)
|
||||
tab.blinking = false
|
||||
|
||||
if tab.blinkEvent then
|
||||
removeEvent(tab.blinkEvent)
|
||||
tab.blinkEvent = nil
|
||||
end
|
||||
|
||||
local parent = tab:getParent()
|
||||
parent:focusChild(tab, MouseFocusReason)
|
||||
updateNavigation(self)
|
||||
end
|
||||
|
||||
function UIMoveableTabBar:selectNextTab()
|
||||
if self.currentTab == nil then
|
||||
return
|
||||
end
|
||||
|
||||
local index = table.find(self.tabs, self.currentTab)
|
||||
if index == nil then
|
||||
return
|
||||
end
|
||||
|
||||
local newIndex = index + 1
|
||||
if newIndex > #self.tabs then
|
||||
if #self.postTabs > 0 then
|
||||
local widget = showPostTab(self)
|
||||
self:selectTab(widget)
|
||||
else
|
||||
if #self.preTabs > 0 then
|
||||
for i = 1, #self.preTabs do
|
||||
showPreTab(self)
|
||||
end
|
||||
end
|
||||
|
||||
self:selectTab(self.tabs[1])
|
||||
end
|
||||
updateTabs(self)
|
||||
return
|
||||
end
|
||||
|
||||
local nextTab = self.tabs[newIndex]
|
||||
if not nextTab then
|
||||
return
|
||||
end
|
||||
|
||||
self:selectTab(nextTab)
|
||||
end
|
||||
|
||||
function UIMoveableTabBar:selectPrevTab()
|
||||
if self.currentTab == nil then
|
||||
return
|
||||
end
|
||||
|
||||
local index = table.find(self.tabs, self.currentTab)
|
||||
if index == nil then
|
||||
return
|
||||
end
|
||||
|
||||
local newIndex = index - 1
|
||||
if newIndex <= 0 then
|
||||
if #self.preTabs > 0 then
|
||||
local widget = showPreTab(self)
|
||||
self:selectTab(widget)
|
||||
else
|
||||
if #self.postTabs > 0 then
|
||||
for i = 1, #self.postTabs do
|
||||
showPostTab(self)
|
||||
end
|
||||
end
|
||||
|
||||
self:selectTab(self.tabs[#self.tabs])
|
||||
end
|
||||
updateTabs(self)
|
||||
return
|
||||
end
|
||||
|
||||
local prevTab = self.tabs[newIndex]
|
||||
if not prevTab then
|
||||
return
|
||||
end
|
||||
|
||||
self:selectTab(prevTab)
|
||||
end
|
||||
|
||||
function UIMoveableTabBar:blinkTab(tab)
|
||||
if tab:isChecked() then return end
|
||||
tab.blinking = true
|
||||
tabBlink(tab)
|
||||
end
|
||||
|
||||
function UIMoveableTabBar:getTabPanel(tab)
|
||||
return tab.tabPanel
|
||||
end
|
||||
|
||||
function UIMoveableTabBar:getCurrentTabPanel()
|
||||
if self.currentTab then
|
||||
return self.currentTab.tabPanel
|
||||
end
|
||||
end
|
||||
|
||||
function UIMoveableTabBar:getCurrentTab()
|
||||
return self.currentTab
|
||||
end
|
||||
|
||||
function UIMoveableTabBar:setNavigation(prevButton, nextButton)
|
||||
self.prevNavigation = prevButton
|
||||
self.nextNavigation = nextButton
|
||||
|
||||
if self.prevNavigation then
|
||||
self.prevNavigation.onClick = function() self:selectPrevTab() end
|
||||
end
|
||||
if self.nextNavigation then
|
||||
self.nextNavigation.onClick = function() self:selectNextTab() end
|
||||
end
|
||||
updateNavigation(self)
|
||||
end
|
122
modules/corelib/ui/uipopupmenu.lua
Normal file
122
modules/corelib/ui/uipopupmenu.lua
Normal file
@@ -0,0 +1,122 @@
|
||||
-- @docclass
|
||||
UIPopupMenu = extends(UIWidget, "UIPopupMenu")
|
||||
|
||||
local currentMenu
|
||||
|
||||
function UIPopupMenu.create()
|
||||
local menu = UIPopupMenu.internalCreate()
|
||||
local layout = UIVerticalLayout.create(menu)
|
||||
layout:setFitChildren(true)
|
||||
menu:setLayout(layout)
|
||||
menu.isGameMenu = false
|
||||
return menu
|
||||
end
|
||||
|
||||
function UIPopupMenu:display(pos)
|
||||
-- don't display if not options was added
|
||||
if self:getChildCount() == 0 then
|
||||
self:destroy()
|
||||
return
|
||||
end
|
||||
|
||||
if g_ui.isMouseGrabbed() then
|
||||
self:destroy()
|
||||
return
|
||||
end
|
||||
|
||||
if currentMenu then
|
||||
currentMenu:destroy()
|
||||
end
|
||||
|
||||
if pos == nil then
|
||||
pos = g_window.getMousePosition()
|
||||
end
|
||||
|
||||
rootWidget:addChild(self)
|
||||
self:setPosition(pos)
|
||||
self:grabMouse()
|
||||
self:focus()
|
||||
--self:grabKeyboard()
|
||||
currentMenu = self
|
||||
end
|
||||
|
||||
function UIPopupMenu:onGeometryChange(oldRect, newRect)
|
||||
local parent = self:getParent()
|
||||
if not parent then return end
|
||||
local ymax = parent:getY() + parent:getHeight()
|
||||
local xmax = parent:getX() + parent:getWidth()
|
||||
if newRect.y + newRect.height > ymax then
|
||||
local newy = newRect.y - newRect.height
|
||||
if newy > 0 and newy + newRect.height < ymax then self:setY(newy) end
|
||||
end
|
||||
if newRect.x + newRect.width > xmax then
|
||||
local newx = newRect.x - newRect.width
|
||||
if newx > 0 and newx + newRect.width < xmax then self:setX(newx) end
|
||||
end
|
||||
self:bindRectToParent()
|
||||
end
|
||||
|
||||
function UIPopupMenu:addOption(optionName, optionCallback, shortcut)
|
||||
local optionWidget = g_ui.createWidget(self:getStyleName() .. 'Button', self)
|
||||
optionWidget.onClick = function(widget)
|
||||
self:destroy()
|
||||
optionCallback()
|
||||
end
|
||||
optionWidget:setText(optionName)
|
||||
local width = optionWidget:getTextSize().width + optionWidget:getMarginLeft() + optionWidget:getMarginRight() + 15
|
||||
|
||||
if shortcut then
|
||||
local shortcutLabel = g_ui.createWidget(self:getStyleName() .. 'ShortcutLabel', optionWidget)
|
||||
shortcutLabel:setText(shortcut)
|
||||
width = width + shortcutLabel:getTextSize().width + shortcutLabel:getMarginLeft() + shortcutLabel:getMarginRight()
|
||||
end
|
||||
|
||||
self:setWidth(math.max(self:getWidth(), width))
|
||||
end
|
||||
|
||||
function UIPopupMenu:addSeparator()
|
||||
g_ui.createWidget(self:getStyleName() .. 'Separator', self)
|
||||
end
|
||||
|
||||
function UIPopupMenu:setGameMenu(state)
|
||||
self.isGameMenu = state
|
||||
end
|
||||
|
||||
function UIPopupMenu:onDestroy()
|
||||
if currentMenu == self then
|
||||
currentMenu = nil
|
||||
end
|
||||
self:ungrabMouse()
|
||||
end
|
||||
|
||||
function UIPopupMenu:onMousePress(mousePos, mouseButton)
|
||||
-- clicks outside menu area destroys the menu
|
||||
if not self:containsPoint(mousePos) then
|
||||
self:destroy()
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function UIPopupMenu:onKeyPress(keyCode, keyboardModifiers)
|
||||
if keyCode == KeyEscape then
|
||||
self:destroy()
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- close all menus when the window is resized
|
||||
local function onRootGeometryUpdate()
|
||||
if currentMenu then
|
||||
currentMenu:destroy()
|
||||
end
|
||||
end
|
||||
|
||||
local function onGameEnd()
|
||||
if currentMenu and currentMenu.isGameMenu then
|
||||
currentMenu:destroy()
|
||||
end
|
||||
end
|
||||
|
||||
connect(rootWidget, { onGeometryChange = onRootGeometryUpdate })
|
||||
connect(g_game, { onGameEnd = onGameEnd } )
|
129
modules/corelib/ui/uipopupscrollmenu.lua
Normal file
129
modules/corelib/ui/uipopupscrollmenu.lua
Normal file
@@ -0,0 +1,129 @@
|
||||
-- @docclass
|
||||
UIPopupScrollMenu = extends(UIWidget, "UIPopupScrollMenu")
|
||||
|
||||
local currentMenu
|
||||
|
||||
function UIPopupScrollMenu.create()
|
||||
local menu = UIPopupScrollMenu.internalCreate()
|
||||
|
||||
local scrollArea = g_ui.createWidget('UIScrollArea', menu)
|
||||
scrollArea:setLayout(UIVerticalLayout.create(menu))
|
||||
scrollArea:setId('scrollArea')
|
||||
|
||||
local scrollBar = g_ui.createWidget('VerticalScrollBar', menu)
|
||||
scrollBar:setId('scrollBar')
|
||||
scrollBar.pixelsScroll = false
|
||||
|
||||
scrollBar:addAnchor(AnchorRight, 'parent', AnchorRight)
|
||||
scrollBar:addAnchor(AnchorTop, 'parent', AnchorTop)
|
||||
scrollBar:addAnchor(AnchorBottom, 'parent', AnchorBottom)
|
||||
|
||||
scrollArea:addAnchor(AnchorLeft, 'parent', AnchorLeft)
|
||||
scrollArea:addAnchor(AnchorTop, 'parent', AnchorTop)
|
||||
scrollArea:addAnchor(AnchorBottom, 'parent', AnchorBottom)
|
||||
scrollArea:addAnchor(AnchorRight, 'next', AnchorLeft)
|
||||
scrollArea:setVerticalScrollBar(scrollBar)
|
||||
|
||||
menu.scrollArea = scrollArea
|
||||
menu.scrollBar = scrollBar
|
||||
return menu
|
||||
end
|
||||
|
||||
function UIPopupScrollMenu:setScrollbarStep(step)
|
||||
self.scrollBar:setStep(step)
|
||||
end
|
||||
|
||||
function UIPopupScrollMenu:display(pos)
|
||||
-- don't display if not options was added
|
||||
if self.scrollArea:getChildCount() == 0 then
|
||||
self:destroy()
|
||||
return
|
||||
end
|
||||
|
||||
if g_ui.isMouseGrabbed() then
|
||||
self:destroy()
|
||||
return
|
||||
end
|
||||
|
||||
if currentMenu then
|
||||
currentMenu:destroy()
|
||||
end
|
||||
|
||||
if pos == nil then
|
||||
pos = g_window.getMousePosition()
|
||||
end
|
||||
|
||||
rootWidget:addChild(self)
|
||||
self:setPosition(pos)
|
||||
self:grabMouse()
|
||||
currentMenu = self
|
||||
end
|
||||
|
||||
function UIPopupScrollMenu:onGeometryChange(oldRect, newRect)
|
||||
local parent = self:getParent()
|
||||
if not parent then return end
|
||||
local ymax = parent:getY() + parent:getHeight()
|
||||
local xmax = parent:getX() + parent:getWidth()
|
||||
if newRect.y + newRect.height > ymax then
|
||||
local newy = newRect.y - newRect.height
|
||||
if newy > 0 and newy + newRect.height < ymax then self:setY(newy) end
|
||||
end
|
||||
if newRect.x + newRect.width > xmax then
|
||||
local newx = newRect.x - newRect.width
|
||||
if newx > 0 and newx + newRect.width < xmax then self:setX(newx) end
|
||||
end
|
||||
self:bindRectToParent()
|
||||
end
|
||||
|
||||
function UIPopupScrollMenu:addOption(optionName, optionCallback, shortcut)
|
||||
local optionWidget = g_ui.createWidget(self:getStyleName() .. 'Button', self.scrollArea)
|
||||
optionWidget.onClick = function(widget)
|
||||
self:destroy()
|
||||
optionCallback()
|
||||
end
|
||||
optionWidget:setText(optionName)
|
||||
local width = optionWidget:getTextSize().width + optionWidget:getMarginLeft() + optionWidget:getMarginRight() + 15
|
||||
|
||||
if shortcut then
|
||||
local shortcutLabel = g_ui.createWidget(self:getStyleName() .. 'ShortcutLabel', optionWidget)
|
||||
shortcutLabel:setText(shortcut)
|
||||
width = width + shortcutLabel:getTextSize().width + shortcutLabel:getMarginLeft() + shortcutLabel:getMarginRight()
|
||||
end
|
||||
|
||||
self:setWidth(math.max(self:getWidth(), width))
|
||||
end
|
||||
|
||||
function UIPopupScrollMenu:addSeparator()
|
||||
g_ui.createWidget(self:getStyleName() .. 'Separator', self.scrollArea)
|
||||
end
|
||||
|
||||
function UIPopupScrollMenu:onDestroy()
|
||||
if currentMenu == self then
|
||||
currentMenu = nil
|
||||
end
|
||||
self:ungrabMouse()
|
||||
end
|
||||
|
||||
function UIPopupScrollMenu:onMousePress(mousePos, mouseButton)
|
||||
-- clicks outside menu area destroys the menu
|
||||
if not self:containsPoint(mousePos) then
|
||||
self:destroy()
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function UIPopupScrollMenu:onKeyPress(keyCode, keyboardModifiers)
|
||||
if keyCode == KeyEscape then
|
||||
self:destroy()
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- close all menus when the window is resized
|
||||
local function onRootGeometryUpdate()
|
||||
if currentMenu then
|
||||
currentMenu:destroy()
|
||||
end
|
||||
end
|
||||
connect(rootWidget, { onGeometryChange = onRootGeometryUpdate} )
|
99
modules/corelib/ui/uiprogressbar.lua
Normal file
99
modules/corelib/ui/uiprogressbar.lua
Normal file
@@ -0,0 +1,99 @@
|
||||
-- @docclass
|
||||
UIProgressBar = extends(UIWidget, "UIProgressBar")
|
||||
|
||||
function UIProgressBar.create()
|
||||
local progressbar = UIProgressBar.internalCreate()
|
||||
progressbar:setFocusable(false)
|
||||
progressbar:setOn(true)
|
||||
progressbar.min = 0
|
||||
progressbar.max = 100
|
||||
progressbar.value = 0
|
||||
progressbar.bgBorderLeft = 0
|
||||
progressbar.bgBorderRight = 0
|
||||
progressbar.bgBorderTop = 0
|
||||
progressbar.bgBorderBottom = 0
|
||||
return progressbar
|
||||
end
|
||||
|
||||
function UIProgressBar:setMinimum(minimum)
|
||||
self.minimum = minimum
|
||||
if self.value < minimum then
|
||||
self:setValue(minimum)
|
||||
end
|
||||
end
|
||||
|
||||
function UIProgressBar:setMaximum(maximum)
|
||||
self.maximum = maximum
|
||||
if self.value > maximum then
|
||||
self:setValue(maximum)
|
||||
end
|
||||
end
|
||||
|
||||
function UIProgressBar:setValue(value, minimum, maximum)
|
||||
if minimum then
|
||||
self:setMinimum(minimum)
|
||||
end
|
||||
|
||||
if maximum then
|
||||
self:setMaximum(maximum)
|
||||
end
|
||||
|
||||
self.value = math.max(math.min(value, self.maximum), self.minimum)
|
||||
self:updateBackground()
|
||||
end
|
||||
|
||||
function UIProgressBar:setPercent(percent)
|
||||
self:setValue(percent, 0, 100)
|
||||
end
|
||||
|
||||
function UIProgressBar:getPercent()
|
||||
return self.value
|
||||
end
|
||||
|
||||
function UIProgressBar:getPercentPixels()
|
||||
return (self.maximum - self.minimum) / self:getWidth()
|
||||
end
|
||||
|
||||
function UIProgressBar:getProgress()
|
||||
if self.minimum == self.maximum then return 1 end
|
||||
return (self.value - self.minimum) / (self.maximum - self.minimum)
|
||||
end
|
||||
|
||||
function UIProgressBar:updateBackground()
|
||||
if self:isOn() then
|
||||
local width = math.round(math.max((self:getProgress() * (self:getWidth() - self.bgBorderLeft - self.bgBorderRight)), 1))
|
||||
local height = self:getHeight() - self.bgBorderTop - self.bgBorderBottom
|
||||
local rect = { x = self.bgBorderLeft, y = self.bgBorderTop, width = width, height = height }
|
||||
self:setBackgroundRect(rect)
|
||||
end
|
||||
end
|
||||
|
||||
function UIProgressBar:onSetup()
|
||||
self:updateBackground()
|
||||
end
|
||||
|
||||
function UIProgressBar:onStyleApply(name, node)
|
||||
for name,value in pairs(node) do
|
||||
if name == 'background-border-left' then
|
||||
self.bgBorderLeft = tonumber(value)
|
||||
elseif name == 'background-border-right' then
|
||||
self.bgBorderRight = tonumber(value)
|
||||
elseif name == 'background-border-top' then
|
||||
self.bgBorderTop = tonumber(value)
|
||||
elseif name == 'background-border-bottom' then
|
||||
self.bgBorderBottom = tonumber(value)
|
||||
elseif name == 'background-border' then
|
||||
self.bgBorderLeft = tonumber(value)
|
||||
self.bgBorderRight = tonumber(value)
|
||||
self.bgBorderTop = tonumber(value)
|
||||
self.bgBorderBottom = tonumber(value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function UIProgressBar:onGeometryChange(oldRect, newRect)
|
||||
if not self:isOn() then
|
||||
self:setHeight(0)
|
||||
end
|
||||
self:updateBackground()
|
||||
end
|
66
modules/corelib/ui/uiradiogroup.lua
Normal file
66
modules/corelib/ui/uiradiogroup.lua
Normal file
@@ -0,0 +1,66 @@
|
||||
-- @docclass
|
||||
UIRadioGroup = newclass("UIRadioGroup")
|
||||
|
||||
function UIRadioGroup.create()
|
||||
local radiogroup = UIRadioGroup.internalCreate()
|
||||
radiogroup.widgets = {}
|
||||
radiogroup.selectedWidget = nil
|
||||
return radiogroup
|
||||
end
|
||||
|
||||
function UIRadioGroup:destroy()
|
||||
for k,widget in pairs(self.widgets) do
|
||||
widget.onClick = nil
|
||||
end
|
||||
self.widgets = {}
|
||||
end
|
||||
|
||||
function UIRadioGroup:addWidget(widget)
|
||||
table.insert(self.widgets, widget)
|
||||
widget.onClick = function(widget) self:selectWidget(widget) end
|
||||
end
|
||||
|
||||
function UIRadioGroup:removeWidget(widget)
|
||||
if self.selectedWidget == widget then
|
||||
self:selectWidget(nil)
|
||||
end
|
||||
widget.onClick = nil
|
||||
table.removevalue(self.widgets, widget)
|
||||
end
|
||||
|
||||
function UIRadioGroup:selectWidget(selectedWidget, dontSignal)
|
||||
if selectedWidget == self.selectedWidget then return end
|
||||
|
||||
local previousSelectedWidget = self.selectedWidget
|
||||
self.selectedWidget = selectedWidget
|
||||
|
||||
if previousSelectedWidget then
|
||||
previousSelectedWidget:setChecked(false)
|
||||
end
|
||||
|
||||
if selectedWidget then
|
||||
selectedWidget:setChecked(true)
|
||||
end
|
||||
|
||||
if not dontSignal then
|
||||
signalcall(self.onSelectionChange, self, selectedWidget, previousSelectedWidget)
|
||||
end
|
||||
end
|
||||
|
||||
function UIRadioGroup:clearSelected()
|
||||
if not self.selectedWidget then return end
|
||||
|
||||
local previousSelectedWidget = self.selectedWidget
|
||||
self.selectedWidget:setChecked(false)
|
||||
self.selectedWidget = nil
|
||||
|
||||
signalcall(self.onSelectionChange, self, nil, previousSelectedWidget)
|
||||
end
|
||||
|
||||
function UIRadioGroup:getSelectedWidget()
|
||||
return self.selectedWidget
|
||||
end
|
||||
|
||||
function UIRadioGroup:getFirstWidget()
|
||||
return self.widgets[1]
|
||||
end
|
132
modules/corelib/ui/uiresizeborder.lua
Normal file
132
modules/corelib/ui/uiresizeborder.lua
Normal file
@@ -0,0 +1,132 @@
|
||||
-- @docclass
|
||||
UIResizeBorder = extends(UIWidget, "UIResizeBorder")
|
||||
|
||||
function UIResizeBorder.create()
|
||||
local resizeborder = UIResizeBorder.internalCreate()
|
||||
resizeborder:setFocusable(false)
|
||||
resizeborder.minimum = 0
|
||||
resizeborder.maximum = 1000
|
||||
return resizeborder
|
||||
end
|
||||
|
||||
function UIResizeBorder:onSetup()
|
||||
if self:getWidth() > self:getHeight() then
|
||||
self.vertical = true
|
||||
else
|
||||
self.vertical = false
|
||||
end
|
||||
end
|
||||
|
||||
function UIResizeBorder:onDestroy()
|
||||
if self.hovering then
|
||||
g_mouse.popCursor(self.cursortype)
|
||||
end
|
||||
end
|
||||
|
||||
function UIResizeBorder:onHoverChange(hovered)
|
||||
if hovered then
|
||||
if g_mouse.isCursorChanged() or g_mouse.isPressed() then return end
|
||||
if self:getWidth() > self:getHeight() then
|
||||
self.vertical = true
|
||||
self.cursortype = 'vertical'
|
||||
else
|
||||
self.vertical = false
|
||||
self.cursortype = 'horizontal'
|
||||
end
|
||||
g_mouse.pushCursor(self.cursortype)
|
||||
self.hovering = true
|
||||
if not self:isPressed() then
|
||||
g_effects.fadeIn(self)
|
||||
end
|
||||
else
|
||||
if not self:isPressed() and self.hovering then
|
||||
g_mouse.popCursor(self.cursortype)
|
||||
g_effects.fadeOut(self)
|
||||
self.hovering = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function UIResizeBorder:onMouseMove(mousePos, mouseMoved)
|
||||
if self:isPressed() then
|
||||
local parent = self:getParent()
|
||||
local newSize = 0
|
||||
if self.vertical then
|
||||
local delta = mousePos.y - self:getY() - self:getHeight()/2
|
||||
newSize = math.min(math.max(parent:getHeight() + delta, self.minimum), self.maximum)
|
||||
parent:setHeight(newSize)
|
||||
else
|
||||
local delta = mousePos.x - self:getX() - self:getWidth()/2
|
||||
newSize = math.min(math.max(parent:getWidth() + delta, self.minimum), self.maximum)
|
||||
parent:setWidth(newSize)
|
||||
end
|
||||
|
||||
self:checkBoundary(newSize)
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function UIResizeBorder:onMouseRelease(mousePos, mouseButton)
|
||||
if not self:isHovered() then
|
||||
g_mouse.popCursor(self.cursortype)
|
||||
g_effects.fadeOut(self)
|
||||
self.hovering = false
|
||||
end
|
||||
end
|
||||
|
||||
function UIResizeBorder:onStyleApply(styleName, styleNode)
|
||||
for name,value in pairs(styleNode) do
|
||||
if name == 'maximum' then
|
||||
self:setMaximum(tonumber(value))
|
||||
elseif name == 'minimum' then
|
||||
self:setMinimum(tonumber(value))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function UIResizeBorder:onVisibilityChange(visible)
|
||||
if visible and self.maximum == self.minimum then
|
||||
self:hide()
|
||||
end
|
||||
end
|
||||
|
||||
function UIResizeBorder:setMaximum(maximum)
|
||||
self.maximum = maximum
|
||||
self:checkBoundary()
|
||||
end
|
||||
|
||||
function UIResizeBorder:setMinimum(minimum)
|
||||
self.minimum = minimum
|
||||
self:checkBoundary()
|
||||
end
|
||||
|
||||
function UIResizeBorder:getMaximum() return self.maximum end
|
||||
function UIResizeBorder:getMinimum() return self.minimum end
|
||||
|
||||
function UIResizeBorder:setParentSize(size)
|
||||
local parent = self:getParent()
|
||||
if self.vertical then
|
||||
parent:setHeight(size)
|
||||
else
|
||||
parent:setWidth(size)
|
||||
end
|
||||
self:checkBoundary(size)
|
||||
end
|
||||
|
||||
function UIResizeBorder:getParentSize()
|
||||
local parent = self:getParent()
|
||||
if self.vertical then
|
||||
return parent:getHeight()
|
||||
else
|
||||
return parent:getWidth()
|
||||
end
|
||||
end
|
||||
|
||||
function UIResizeBorder:checkBoundary(size)
|
||||
size = size or self:getParentSize()
|
||||
if self.maximum == self.minimum and size == self.maximum then
|
||||
self:hide()
|
||||
else
|
||||
self:show()
|
||||
end
|
||||
end
|
190
modules/corelib/ui/uiscrollarea.lua
Normal file
190
modules/corelib/ui/uiscrollarea.lua
Normal file
@@ -0,0 +1,190 @@
|
||||
-- @docclass
|
||||
UIScrollArea = extends(UIWidget, "UIScrollArea")
|
||||
|
||||
-- public functions
|
||||
function UIScrollArea.create()
|
||||
local scrollarea = UIScrollArea.internalCreate()
|
||||
scrollarea:setClipping(true)
|
||||
scrollarea.inverted = false
|
||||
scrollarea.alwaysScrollMaximum = false
|
||||
return scrollarea
|
||||
end
|
||||
|
||||
function UIScrollArea:onStyleApply(styleName, styleNode)
|
||||
for name,value in pairs(styleNode) do
|
||||
if name == 'vertical-scrollbar' then
|
||||
addEvent(function()
|
||||
local parent = self:getParent()
|
||||
if parent then
|
||||
self:setVerticalScrollBar(parent:getChildById(value))
|
||||
end
|
||||
end)
|
||||
elseif name == 'horizontal-scrollbar' then
|
||||
addEvent(function()
|
||||
local parent = self:getParent()
|
||||
if parent then
|
||||
self:setHorizontalScrollBar(self:getParent():getChildById(value))
|
||||
end
|
||||
end)
|
||||
elseif name == 'inverted-scroll' then
|
||||
self:setInverted(value)
|
||||
elseif name == 'always-scroll-maximum' then
|
||||
self:setAlwaysScrollMaximum(value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function UIScrollArea:updateScrollBars()
|
||||
local scrollWidth = math.max(self:getChildrenRect().width - self:getPaddingRect().width, 0)
|
||||
local scrollHeight = math.max(self:getChildrenRect().height - self:getPaddingRect().height, 0)
|
||||
|
||||
local scrollbar = self.verticalScrollBar
|
||||
if scrollbar then
|
||||
if self.inverted then
|
||||
scrollbar:setMinimum(-scrollHeight)
|
||||
scrollbar:setMaximum(0)
|
||||
else
|
||||
scrollbar:setMinimum(0)
|
||||
scrollbar:setMaximum(scrollHeight)
|
||||
end
|
||||
end
|
||||
|
||||
local scrollbar = self.horizontalScrollBar
|
||||
if scrollbar then
|
||||
if self.inverted then
|
||||
scrollbar:setMinimum(-scrollWidth)
|
||||
scrollbar:setMaximum(0)
|
||||
else
|
||||
scrollbar:setMinimum(0)
|
||||
scrollbar:setMaximum(scrollWidth)
|
||||
end
|
||||
end
|
||||
|
||||
if self.lastScrollWidth ~= scrollWidth then
|
||||
self:onScrollWidthChange()
|
||||
end
|
||||
if self.lastScrollHeight ~= scrollHeight then
|
||||
self:onScrollHeightChange()
|
||||
end
|
||||
|
||||
self.lastScrollWidth = scrollWidth
|
||||
self.lastScrollHeight = scrollHeight
|
||||
end
|
||||
|
||||
function UIScrollArea:setVerticalScrollBar(scrollbar)
|
||||
self.verticalScrollBar = scrollbar
|
||||
connect(self.verticalScrollBar, 'onValueChange', function(scrollbar, value)
|
||||
local virtualOffset = self:getVirtualOffset()
|
||||
virtualOffset.y = value
|
||||
self:setVirtualOffset(virtualOffset)
|
||||
signalcall(self.onScrollChange, self, virtualOffset)
|
||||
end)
|
||||
self:updateScrollBars()
|
||||
end
|
||||
|
||||
function UIScrollArea:setHorizontalScrollBar(scrollbar)
|
||||
self.horizontalScrollBar = scrollbar
|
||||
connect(self.horizontalScrollBar, 'onValueChange', function(scrollbar, value)
|
||||
local virtualOffset = self:getVirtualOffset()
|
||||
virtualOffset.x = value
|
||||
self:setVirtualOffset(virtualOffset)
|
||||
signalcall(self.onScrollChange, self, virtualOffset)
|
||||
end)
|
||||
self:updateScrollBars()
|
||||
end
|
||||
|
||||
function UIScrollArea:setInverted(inverted)
|
||||
self.inverted = inverted
|
||||
end
|
||||
|
||||
function UIScrollArea:setAlwaysScrollMaximum(value)
|
||||
self.alwaysScrollMaximum = value
|
||||
end
|
||||
|
||||
function UIScrollArea:onLayoutUpdate()
|
||||
self:updateScrollBars()
|
||||
end
|
||||
|
||||
function UIScrollArea:onMouseWheel(mousePos, mouseWheel)
|
||||
if self.verticalScrollBar then
|
||||
if not self.verticalScrollBar:isOn() then
|
||||
return false
|
||||
end
|
||||
if mouseWheel == MouseWheelUp then
|
||||
local minimum = self.verticalScrollBar:getMinimum()
|
||||
if self.verticalScrollBar:getValue() <= minimum then
|
||||
return false
|
||||
end
|
||||
self.verticalScrollBar:decrement()
|
||||
else
|
||||
local maximum = self.verticalScrollBar:getMaximum()
|
||||
if self.verticalScrollBar:getValue() >= maximum then
|
||||
return false
|
||||
end
|
||||
self.verticalScrollBar:increment()
|
||||
end
|
||||
elseif self.horizontalScrollBar then
|
||||
if not self.horizontalScrollBar:isOn() then
|
||||
return false
|
||||
end
|
||||
if mouseWheel == MouseWheelUp then
|
||||
local maximum = self.horizontalScrollBar:getMaximum()
|
||||
if self.horizontalScrollBar:getValue() >= maximum then
|
||||
return false
|
||||
end
|
||||
self.horizontalScrollBar:increment()
|
||||
else
|
||||
local minimum = self.horizontalScrollBar:getMinimum()
|
||||
if self.horizontalScrollBar:getValue() <= minimum then
|
||||
return false
|
||||
end
|
||||
self.horizontalScrollBar:decrement()
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function UIScrollArea:ensureChildVisible(child)
|
||||
if child then
|
||||
local paddingRect = self:getPaddingRect()
|
||||
if self.verticalScrollBar then
|
||||
local deltaY = paddingRect.y - child:getY()
|
||||
if deltaY > 0 then
|
||||
self.verticalScrollBar:decrement(deltaY)
|
||||
end
|
||||
|
||||
deltaY = (child:getY() + child:getHeight()) - (paddingRect.y + paddingRect.height)
|
||||
if deltaY > 0 then
|
||||
self.verticalScrollBar:increment(deltaY)
|
||||
end
|
||||
elseif self.horizontalScrollBar then
|
||||
local deltaX = paddingRect.x - child:getX()
|
||||
if deltaX > 0 then
|
||||
self.horizontalScrollBar:decrement(deltaX)
|
||||
end
|
||||
|
||||
deltaX = (child:getX() + child:getWidth()) - (paddingRect.x + paddingRect.width)
|
||||
if deltaX > 0 then
|
||||
self.horizontalScrollBar:increment(deltaX)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function UIScrollArea:onChildFocusChange(focusedChild, oldFocused, reason)
|
||||
if focusedChild and (reason == MouseFocusReason or reason == KeyboardFocusReason) then
|
||||
self:ensureChildVisible(focusedChild)
|
||||
end
|
||||
end
|
||||
|
||||
function UIScrollArea:onScrollWidthChange()
|
||||
if self.alwaysScrollMaximum and self.horizontalScrollBar then
|
||||
self.horizontalScrollBar:setValue(self.horizontalScrollBar:getMaximum())
|
||||
end
|
||||
end
|
||||
|
||||
function UIScrollArea:onScrollHeightChange()
|
||||
if self.alwaysScrollMaximum and self.verticalScrollBar then
|
||||
self.verticalScrollBar:setValue(self.verticalScrollBar:getMaximum())
|
||||
end
|
||||
end
|
287
modules/corelib/ui/uiscrollbar.lua
Normal file
287
modules/corelib/ui/uiscrollbar.lua
Normal file
@@ -0,0 +1,287 @@
|
||||
-- @docclass
|
||||
UIScrollBar = extends(UIWidget, "UIScrollBar")
|
||||
|
||||
-- private functions
|
||||
local function calcValues(self)
|
||||
local slider = self:getChildById('sliderButton')
|
||||
local decrementButton = self:getChildById('decrementButton')
|
||||
local incrementButton = self:getChildById('incrementButton')
|
||||
|
||||
local pxrange, center
|
||||
if self.orientation == 'vertical' then
|
||||
pxrange = (self:getHeight() - decrementButton:getHeight() - decrementButton:getMarginTop() - decrementButton:getMarginBottom()
|
||||
- incrementButton:getHeight() - incrementButton:getMarginTop() - incrementButton:getMarginBottom())
|
||||
center = self:getY() + math.floor(self:getHeight() / 2)
|
||||
else -- horizontal
|
||||
pxrange = (self:getWidth() - decrementButton:getWidth() - decrementButton:getMarginLeft() - decrementButton:getMarginRight()
|
||||
- incrementButton:getWidth() - incrementButton:getMarginLeft() - incrementButton:getMarginRight())
|
||||
center = self:getX() + math.floor(self:getWidth() / 2)
|
||||
end
|
||||
|
||||
local range = self.maximum - self.minimum + 1
|
||||
|
||||
local proportion
|
||||
|
||||
if self.pixelsScroll then
|
||||
proportion = pxrange/(range+pxrange)
|
||||
else
|
||||
proportion = math.min(math.max(self.step, 1), range)/range
|
||||
end
|
||||
|
||||
local px = math.max(proportion * pxrange, 6)
|
||||
px = px - px % 2 + 1
|
||||
|
||||
local offset = 0
|
||||
if range == 0 or self.value == self.minimum then
|
||||
if self.orientation == 'vertical' then
|
||||
offset = -math.floor((self:getHeight() - px) / 2) + decrementButton:getMarginRect().height
|
||||
else
|
||||
offset = -math.floor((self:getWidth() - px) / 2) + decrementButton:getMarginRect().width
|
||||
end
|
||||
elseif range > 1 and self.value == self.maximum then
|
||||
if self.orientation == 'vertical' then
|
||||
offset = math.ceil((self:getHeight() - px) / 2) - incrementButton:getMarginRect().height
|
||||
else
|
||||
offset = math.ceil((self:getWidth() - px) / 2) - incrementButton:getMarginRect().width
|
||||
end
|
||||
elseif range > 1 then
|
||||
offset = (((self.value - self.minimum) / (range - 1)) - 0.5) * (pxrange - px)
|
||||
end
|
||||
|
||||
return range, pxrange, px, offset, center
|
||||
end
|
||||
|
||||
local function updateValueDisplay(widget)
|
||||
if widget == nil then return end
|
||||
|
||||
if widget:getShowValue() then
|
||||
widget:setText(widget:getValue() .. (widget:getSymbol() or ''))
|
||||
end
|
||||
end
|
||||
|
||||
local function updateSlider(self)
|
||||
local slider = self:getChildById('sliderButton')
|
||||
if slider == nil then return end
|
||||
|
||||
local range, pxrange, px, offset, center = calcValues(self)
|
||||
if self.orientation == 'vertical' then
|
||||
slider:setHeight(px)
|
||||
slider:setMarginTop(offset)
|
||||
else -- horizontal
|
||||
slider:setWidth(px)
|
||||
slider:setMarginLeft(offset)
|
||||
end
|
||||
updateValueDisplay(self)
|
||||
|
||||
local status = (self.maximum ~= self.minimum)
|
||||
|
||||
self:setOn(status)
|
||||
for _i,child in pairs(self:getChildren()) do
|
||||
child:setEnabled(status)
|
||||
end
|
||||
end
|
||||
|
||||
local function parseSliderPos(self, slider, pos, move)
|
||||
local delta, hotDistance
|
||||
if self.orientation == 'vertical' then
|
||||
delta = move.y
|
||||
hotDistance = pos.y - slider:getY()
|
||||
else
|
||||
delta = move.x
|
||||
hotDistance = pos.x - slider:getX()
|
||||
end
|
||||
|
||||
if (delta > 0 and hotDistance + delta > self.hotDistance) or
|
||||
(delta < 0 and hotDistance + delta < self.hotDistance) then
|
||||
local range, pxrange, px, offset, center = calcValues(self)
|
||||
local newvalue = self.value + delta * (range / (pxrange - px))
|
||||
self:setValue(newvalue)
|
||||
end
|
||||
end
|
||||
|
||||
local function parseSliderPress(self, slider, pos, button)
|
||||
if self.orientation == 'vertical' then
|
||||
self.hotDistance = pos.y - slider:getY()
|
||||
else
|
||||
self.hotDistance = pos.x - slider:getX()
|
||||
end
|
||||
end
|
||||
|
||||
-- public functions
|
||||
function UIScrollBar.create()
|
||||
local scrollbar = UIScrollBar.internalCreate()
|
||||
scrollbar:setFocusable(false)
|
||||
scrollbar.value = 0
|
||||
scrollbar.minimum = -999999
|
||||
scrollbar.maximum = 999999
|
||||
scrollbar.step = 1
|
||||
scrollbar.orientation = 'vertical'
|
||||
scrollbar.pixelsScroll = false
|
||||
scrollbar.showValue = false
|
||||
scrollbar.symbol = nil
|
||||
scrollbar.mouseScroll = true
|
||||
return scrollbar
|
||||
end
|
||||
|
||||
function UIScrollBar:onSetup()
|
||||
self.setupDone = true
|
||||
local sliderButton = self:getChildById('sliderButton')
|
||||
g_mouse.bindAutoPress(self:getChildById('decrementButton'), function() self:onDecrement() end, 300)
|
||||
g_mouse.bindAutoPress(self:getChildById('incrementButton'), function() self:onIncrement() end, 300)
|
||||
g_mouse.bindPressMove(sliderButton, function(mousePos, mouseMoved) parseSliderPos(self, sliderButton, mousePos, mouseMoved) end)
|
||||
g_mouse.bindPress(sliderButton, function(mousePos, mouseButton) parseSliderPress(self, sliderButton, mousePos, mouseButton) end)
|
||||
|
||||
updateSlider(self)
|
||||
end
|
||||
|
||||
function UIScrollBar:onStyleApply(styleName, styleNode)
|
||||
for name,value in pairs(styleNode) do
|
||||
if name == 'maximum' then
|
||||
self:setMaximum(tonumber(value))
|
||||
elseif name == 'minimum' then
|
||||
self:setMinimum(tonumber(value))
|
||||
elseif name == 'step' then
|
||||
self:setStep(tonumber(value))
|
||||
elseif name == 'orientation' then
|
||||
self:setOrientation(value)
|
||||
elseif name == 'value' then
|
||||
self:setValue(value)
|
||||
elseif name == 'pixels-scroll' then
|
||||
self.pixelsScroll = true
|
||||
elseif name == 'show-value' then
|
||||
self.showValue = true
|
||||
elseif name == 'symbol' then
|
||||
self.symbol = value
|
||||
elseif name == 'mouse-scroll' then
|
||||
self.mouseScroll = value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function UIScrollBar:onDecrement()
|
||||
if g_keyboard.isCtrlPressed() then
|
||||
self:decrement(self.value)
|
||||
elseif g_keyboard.isShiftPressed() then
|
||||
self:decrement(10)
|
||||
else
|
||||
self:decrement()
|
||||
end
|
||||
end
|
||||
|
||||
function UIScrollBar:onIncrement()
|
||||
if g_keyboard.isCtrlPressed() then
|
||||
self:increment(self.maximum)
|
||||
elseif g_keyboard.isShiftPressed() then
|
||||
self:increment(10)
|
||||
else
|
||||
self:increment()
|
||||
end
|
||||
end
|
||||
|
||||
function UIScrollBar:decrement(count)
|
||||
count = count or self.step
|
||||
self:setValue(self.value - count)
|
||||
end
|
||||
|
||||
function UIScrollBar:increment(count)
|
||||
count = count or self.step
|
||||
self:setValue(self.value + count)
|
||||
end
|
||||
|
||||
function UIScrollBar:setMaximum(maximum)
|
||||
if maximum == self.maximum then return end
|
||||
self.maximum = maximum
|
||||
if self.minimum > maximum then
|
||||
self:setMinimum(maximum)
|
||||
end
|
||||
if self.value > maximum then
|
||||
self:setValue(maximum)
|
||||
else
|
||||
updateSlider(self)
|
||||
end
|
||||
end
|
||||
|
||||
function UIScrollBar:setMinimum(minimum)
|
||||
if minimum == self.minimum then return end
|
||||
self.minimum = minimum
|
||||
if self.maximum < minimum then
|
||||
self:setMaximum(minimum)
|
||||
end
|
||||
if self.value < minimum then
|
||||
self:setValue(minimum)
|
||||
else
|
||||
updateSlider(self)
|
||||
end
|
||||
end
|
||||
|
||||
function UIScrollBar:setRange(minimum, maximum)
|
||||
self:setMinimum(minimum)
|
||||
self:setMaximum(maximum)
|
||||
end
|
||||
|
||||
function UIScrollBar:setValue(value)
|
||||
value = math.max(math.min(value, self.maximum), self.minimum)
|
||||
if self.value == value then return end
|
||||
local delta = value - self.value
|
||||
self.value = value
|
||||
updateSlider(self)
|
||||
if self.setupDone then
|
||||
signalcall(self.onValueChange, self, math.round(value), delta)
|
||||
end
|
||||
end
|
||||
|
||||
function UIScrollBar:setMouseScroll(scroll)
|
||||
self.mouseScroll = scroll
|
||||
end
|
||||
|
||||
function UIScrollBar:setStep(step)
|
||||
self.step = step
|
||||
end
|
||||
|
||||
function UIScrollBar:setOrientation(orientation)
|
||||
self.orientation = orientation
|
||||
end
|
||||
|
||||
function UIScrollBar:setText(text)
|
||||
local valueLabel = self:getChildById('valueLabel')
|
||||
if valueLabel then
|
||||
valueLabel:setText(text)
|
||||
end
|
||||
end
|
||||
|
||||
function UIScrollBar:onGeometryChange()
|
||||
updateSlider(self)
|
||||
end
|
||||
|
||||
function UIScrollBar:onMouseWheel(mousePos, mouseWheel)
|
||||
if not self.mouseScroll or not self:isOn() then
|
||||
return false
|
||||
end
|
||||
if mouseWheel == MouseWheelUp then
|
||||
if self.orientation == 'vertical' then
|
||||
if self.value <= self.minimum then return false end
|
||||
self:decrement()
|
||||
else
|
||||
if self.value >= self.maximum then return false end
|
||||
self:increment()
|
||||
end
|
||||
else
|
||||
if self.orientation == 'vertical' then
|
||||
if self.value >= self.maximum then return false end
|
||||
self:increment()
|
||||
else
|
||||
if self.value <= self.minimum then return false end
|
||||
self:decrement()
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function UIScrollBar:getMaximum() return self.maximum end
|
||||
function UIScrollBar:getMinimum() return self.minimum end
|
||||
function UIScrollBar:getValue() return math.round(self.value) end
|
||||
function UIScrollBar:getStep() return self.step end
|
||||
function UIScrollBar:getOrientation() return self.orientation end
|
||||
function UIScrollBar:getShowValue() return self.showValue end
|
||||
function UIScrollBar:getSymbol() return self.symbol end
|
||||
function UIScrollBar:getMouseScroll() return self.mouseScroll end
|
186
modules/corelib/ui/uispinbox.lua
Normal file
186
modules/corelib/ui/uispinbox.lua
Normal file
@@ -0,0 +1,186 @@
|
||||
-- @docclass
|
||||
UISpinBox = extends(UITextEdit, "UISpinBox")
|
||||
|
||||
function UISpinBox.create()
|
||||
local spinbox = UISpinBox.internalCreate()
|
||||
spinbox:setFocusable(false)
|
||||
spinbox:setValidCharacters('0123456789')
|
||||
spinbox.displayButtons = true
|
||||
spinbox.minimum = 0
|
||||
spinbox.maximum = 1
|
||||
spinbox.value = 0
|
||||
spinbox.step = 1
|
||||
spinbox.firstchange = true
|
||||
spinbox.mouseScroll = true
|
||||
spinbox:setText("1")
|
||||
spinbox:setValue(1)
|
||||
return spinbox
|
||||
end
|
||||
|
||||
function UISpinBox:onSetup()
|
||||
g_mouse.bindAutoPress(self:getChildById('up'), function() self:up() end, 300)
|
||||
g_mouse.bindAutoPress(self:getChildById('down'), function() self:down() end, 300)
|
||||
end
|
||||
|
||||
function UISpinBox:onMouseWheel(mousePos, direction)
|
||||
if not self.mouseScroll then
|
||||
return false
|
||||
end
|
||||
if direction == MouseWheelUp then
|
||||
self:up()
|
||||
elseif direction == MouseWheelDown then
|
||||
self:down()
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function UISpinBox:onKeyPress()
|
||||
if self.firstchange then
|
||||
self.firstchange = false
|
||||
self:setText('')
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function UISpinBox:onTextChange(text, oldText)
|
||||
if text:len() == 0 then
|
||||
self:setValue(self.minimum)
|
||||
return
|
||||
end
|
||||
|
||||
local number = tonumber(text)
|
||||
if not number then
|
||||
self:setText(number)
|
||||
return
|
||||
else
|
||||
if number < self.minimum then
|
||||
self:setText(self.minimum)
|
||||
return
|
||||
elseif number > self.maximum then
|
||||
self:setText(self.maximum)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
self:setValue(number)
|
||||
end
|
||||
|
||||
function UISpinBox:onValueChange(value)
|
||||
-- nothing to do
|
||||
end
|
||||
|
||||
function UISpinBox:onFocusChange(focused)
|
||||
if not focused then
|
||||
if self:getText():len() == 0 then
|
||||
self:setText(self.minimum)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function UISpinBox:onStyleApply(styleName, styleNode)
|
||||
for name, value in pairs(styleNode) do
|
||||
if name == 'maximum' then
|
||||
addEvent(function() self:setMaximum(value) end)
|
||||
elseif name == 'minimum' then
|
||||
addEvent(function() self:setMinimum(value) end)
|
||||
elseif name == 'mouse-scroll' then
|
||||
addEvent(function() self:setMouseScroll(value) end)
|
||||
elseif name == 'buttons' then
|
||||
addEvent(function()
|
||||
if value then
|
||||
self:showButtons()
|
||||
else
|
||||
self:hideButtons()
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function UISpinBox:showButtons()
|
||||
self:getChildById('up'):show()
|
||||
self:getChildById('down'):show()
|
||||
self.displayButtons = true
|
||||
end
|
||||
|
||||
function UISpinBox:hideButtons()
|
||||
self:getChildById('up'):hide()
|
||||
self:getChildById('down'):hide()
|
||||
self.displayButtons = false
|
||||
end
|
||||
|
||||
function UISpinBox:up()
|
||||
self:setValue(self.value + self.step)
|
||||
end
|
||||
|
||||
function UISpinBox:down()
|
||||
self:setValue(self.value - self.step)
|
||||
end
|
||||
|
||||
function UISpinBox:setValue(value, dontSignal)
|
||||
value = value or 0
|
||||
value = math.max(math.min(self.maximum, value), self.minimum)
|
||||
|
||||
if value == self.value then return end
|
||||
|
||||
self.value = value
|
||||
if self:getText():len() > 0 then
|
||||
self:setText(value)
|
||||
end
|
||||
|
||||
local upButton = self:getChildById('up')
|
||||
local downButton = self:getChildById('down')
|
||||
if upButton then
|
||||
upButton:setEnabled(self.maximum ~= self.minimum and self.value ~= self.maximum)
|
||||
end
|
||||
if downButton then
|
||||
downButton:setEnabled(self.maximum ~= self.minimum and self.value ~= self.minimum)
|
||||
end
|
||||
|
||||
if not dontSignal then
|
||||
signalcall(self.onValueChange, self, value)
|
||||
end
|
||||
end
|
||||
|
||||
function UISpinBox:getValue()
|
||||
return self.value
|
||||
end
|
||||
|
||||
function UISpinBox:setMinimum(minimum)
|
||||
minimum = minimum or -9223372036854775808
|
||||
self.minimum = minimum
|
||||
if self.minimum > self.maximum then
|
||||
self.maximum = self.minimum
|
||||
end
|
||||
if self.value < minimum then
|
||||
self:setValue(minimum)
|
||||
end
|
||||
end
|
||||
|
||||
function UISpinBox:getMinimum()
|
||||
return self.minimum
|
||||
end
|
||||
|
||||
function UISpinBox:setMaximum(maximum)
|
||||
maximum = maximum or 9223372036854775807
|
||||
self.maximum = maximum
|
||||
if self.value > maximum then
|
||||
self:setValue(maximum)
|
||||
end
|
||||
end
|
||||
|
||||
function UISpinBox:getMaximum()
|
||||
return self.maximum
|
||||
end
|
||||
|
||||
function UISpinBox:setStep(step)
|
||||
self.step = step or 1
|
||||
end
|
||||
|
||||
function UISpinBox:setMouseScroll(mouseScroll)
|
||||
self.mouseScroll = mouseScroll
|
||||
end
|
||||
|
||||
function UISpinBox:getMouseScroll()
|
||||
return self.mouseScroll
|
||||
end
|
85
modules/corelib/ui/uisplitter.lua
Normal file
85
modules/corelib/ui/uisplitter.lua
Normal file
@@ -0,0 +1,85 @@
|
||||
-- @docclass
|
||||
UISplitter = extends(UIWidget, "UISplitter")
|
||||
|
||||
function UISplitter.create()
|
||||
local splitter = UISplitter.internalCreate()
|
||||
splitter:setFocusable(false)
|
||||
splitter.relativeMargin = 'bottom'
|
||||
return splitter
|
||||
end
|
||||
|
||||
function UISplitter:onHoverChange(hovered)
|
||||
-- Check if margin can be changed
|
||||
local margin = (self.vertical and self:getMarginBottom() or self:getMarginRight())
|
||||
if hovered and (self:canUpdateMargin(margin + 1) ~= margin or self:canUpdateMargin(margin - 1) ~= margin) then
|
||||
if g_mouse.isCursorChanged() or g_mouse.isPressed() then return end
|
||||
if self:getWidth() > self:getHeight() then
|
||||
self.vertical = true
|
||||
self.cursortype = 'vertical'
|
||||
else
|
||||
self.vertical = false
|
||||
self.cursortype = 'horizontal'
|
||||
end
|
||||
self.hovering = true
|
||||
g_mouse.pushCursor(self.cursortype)
|
||||
if not self:isPressed() then
|
||||
g_effects.fadeIn(self)
|
||||
end
|
||||
else
|
||||
if not self:isPressed() and self.hovering then
|
||||
g_mouse.popCursor(self.cursortype)
|
||||
g_effects.fadeOut(self)
|
||||
self.hovering = false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function UISplitter:onMouseMove(mousePos, mouseMoved)
|
||||
if self:isPressed() then
|
||||
--local currentmargin, newmargin, delta
|
||||
if self.vertical then
|
||||
local delta = mousePos.y - self:getY() - self:getHeight()/2
|
||||
local newMargin = self:canUpdateMargin(self:getMarginBottom() - delta)
|
||||
local currentMargin = self:getMarginBottom()
|
||||
if newMargin ~= currentMargin then
|
||||
self.newMargin = newMargin
|
||||
if not self.event or self.event:isExecuted() then
|
||||
self.event = addEvent(function()
|
||||
self:setMarginBottom(self.newMargin)
|
||||
end)
|
||||
end
|
||||
end
|
||||
else
|
||||
local delta = mousePos.x - self:getX() - self:getWidth()/2
|
||||
local newMargin = self:canUpdateMargin(self:getMarginRight() - delta)
|
||||
local currentMargin = self:getMarginRight()
|
||||
if newMargin ~= currentMargin then
|
||||
self.newMargin = newMargin
|
||||
if not self.event or self.event:isExecuted() then
|
||||
self.event = addEvent(function()
|
||||
self:setMarginRight(self.newMargin)
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function UISplitter:onMouseRelease(mousePos, mouseButton)
|
||||
if not self:isHovered() then
|
||||
g_mouse.popCursor(self.cursortype)
|
||||
g_effects.fadeOut(self)
|
||||
self.hovering = false
|
||||
end
|
||||
end
|
||||
|
||||
function UISplitter:onStyleApply(styleName, styleNode)
|
||||
if styleNode['relative-margin'] then
|
||||
self.relativeMargin = styleNode['relative-margin']
|
||||
end
|
||||
end
|
||||
|
||||
function UISplitter:canUpdateMargin(newMargin)
|
||||
return newMargin
|
||||
end
|
157
modules/corelib/ui/uitabbar.lua
Normal file
157
modules/corelib/ui/uitabbar.lua
Normal file
@@ -0,0 +1,157 @@
|
||||
-- @docclass
|
||||
UITabBar = extends(UIWidget, "UITabBar")
|
||||
|
||||
-- private functions
|
||||
local function onTabClick(tab)
|
||||
tab.tabBar:selectTab(tab)
|
||||
end
|
||||
|
||||
local function onTabMouseRelease(tab, mousePos, mouseButton)
|
||||
if mouseButton == MouseRightButton and tab:containsPoint(mousePos) then
|
||||
signalcall(tab.tabBar.onTabLeftClick, tab.tabBar, tab)
|
||||
end
|
||||
end
|
||||
|
||||
-- public functions
|
||||
function UITabBar.create()
|
||||
local tabbar = UITabBar.internalCreate()
|
||||
tabbar:setFocusable(false)
|
||||
tabbar.tabs = {}
|
||||
return tabbar
|
||||
end
|
||||
|
||||
function UITabBar:onSetup()
|
||||
self.buttonsPanel = self:getChildById('buttonsPanel')
|
||||
end
|
||||
|
||||
function UITabBar:setContentWidget(widget)
|
||||
self.contentWidget = widget
|
||||
if #self.tabs > 0 then
|
||||
self.contentWidget:addChild(self.tabs[1].tabPanel)
|
||||
end
|
||||
end
|
||||
|
||||
function UITabBar:addTab(text, panel, icon)
|
||||
if panel == nil then
|
||||
panel = g_ui.createWidget(self:getStyleName() .. 'Panel')
|
||||
panel:setId('tabPanel')
|
||||
end
|
||||
|
||||
local tab = g_ui.createWidget(self:getStyleName() .. 'Button', self.buttonsPanel)
|
||||
|
||||
panel.isTab = true
|
||||
tab.tabPanel = panel
|
||||
tab.tabBar = self
|
||||
tab:setId('tab')
|
||||
tab:setText(text)
|
||||
tab:setWidth(tab:getTextSize().width + tab:getPaddingLeft() + tab:getPaddingRight())
|
||||
tab.onClick = onTabClick
|
||||
tab.onMouseRelease = onTabMouseRelease
|
||||
tab.onDestroy = function() tab.tabPanel:destroy() end
|
||||
|
||||
table.insert(self.tabs, tab)
|
||||
if #self.tabs == 1 then
|
||||
self:selectTab(tab)
|
||||
end
|
||||
|
||||
local tabStyle = {}
|
||||
tabStyle['icon-source'] = icon
|
||||
tab:mergeStyle(tabStyle)
|
||||
|
||||
return tab
|
||||
end
|
||||
|
||||
function UITabBar:addButton(text, func, icon)
|
||||
local button = g_ui.createWidget(self:getStyleName() .. 'Button', self.buttonsPanel)
|
||||
button:setText(text)
|
||||
|
||||
local style = {}
|
||||
style['icon-source'] = icon
|
||||
button:mergeStyle(style)
|
||||
|
||||
button.onClick = func
|
||||
return button
|
||||
end
|
||||
|
||||
function UITabBar:removeTab(tab)
|
||||
local index = table.find(self.tabs, tab)
|
||||
if index == nil then return end
|
||||
if self.currentTab == tab then
|
||||
self:selectPrevTab()
|
||||
end
|
||||
table.remove(self.tabs, index)
|
||||
tab:destroy()
|
||||
end
|
||||
|
||||
function UITabBar:getTab(text)
|
||||
for k,tab in pairs(self.tabs) do
|
||||
if tab:getText():lower() == text:lower() then
|
||||
return tab
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function UITabBar:selectTab(tab)
|
||||
if self.currentTab == tab then return end
|
||||
if self.contentWidget then
|
||||
local selectedWidget = self.contentWidget:getLastChild()
|
||||
if selectedWidget and selectedWidget.isTab then
|
||||
self.contentWidget:removeChild(selectedWidget)
|
||||
end
|
||||
self.contentWidget:addChild(tab.tabPanel)
|
||||
tab.tabPanel:fill('parent')
|
||||
end
|
||||
|
||||
if self.currentTab then
|
||||
self.currentTab:setChecked(false)
|
||||
end
|
||||
signalcall(self.onTabChange, self, tab)
|
||||
self.currentTab = tab
|
||||
tab:setChecked(true)
|
||||
tab:setOn(false)
|
||||
|
||||
local parent = tab:getParent()
|
||||
if parent then
|
||||
parent:focusChild(tab, MouseFocusReason)
|
||||
end
|
||||
end
|
||||
|
||||
function UITabBar:selectNextTab()
|
||||
if self.currentTab == nil then return end
|
||||
local index = table.find(self.tabs, self.currentTab)
|
||||
if index == nil then return end
|
||||
local nextTab = self.tabs[index + 1] or self.tabs[1]
|
||||
if not nextTab then return end
|
||||
self:selectTab(nextTab)
|
||||
end
|
||||
|
||||
function UITabBar:selectPrevTab()
|
||||
if self.currentTab == nil then return end
|
||||
local index = table.find(self.tabs, self.currentTab)
|
||||
if index == nil then return end
|
||||
local prevTab = self.tabs[index - 1] or self.tabs[#self.tabs]
|
||||
if not prevTab then return end
|
||||
self:selectTab(prevTab)
|
||||
end
|
||||
|
||||
function UITabBar:getTabPanel(tab)
|
||||
return tab.tabPanel
|
||||
end
|
||||
|
||||
function UITabBar:getCurrentTabPanel()
|
||||
if self.currentTab then
|
||||
return self.currentTab.tabPanel
|
||||
end
|
||||
end
|
||||
|
||||
function UITabBar:getCurrentTab()
|
||||
return self.currentTab
|
||||
end
|
||||
|
||||
function UITabBar:getTabs()
|
||||
return self.tabs
|
||||
end
|
||||
|
||||
function UITabBar:getTabsPanel()
|
||||
return table.collect(self.tabs, function(_,tab) return tab.tabPanel end)
|
||||
end
|
432
modules/corelib/ui/uitable.lua
Normal file
432
modules/corelib/ui/uitable.lua
Normal file
@@ -0,0 +1,432 @@
|
||||
-- @docclass
|
||||
--[[
|
||||
TODO:
|
||||
* Make table headers more robust.
|
||||
* Get dynamic row heights working with text wrapping.
|
||||
]]
|
||||
|
||||
TABLE_SORTING_ASC = 0
|
||||
TABLE_SORTING_DESC = 1
|
||||
|
||||
UITable = extends(UIWidget, "UITable")
|
||||
|
||||
-- Initialize default values
|
||||
function UITable.create()
|
||||
local table = UITable.internalCreate()
|
||||
table.headerRow = nil
|
||||
table.headerColumns = {}
|
||||
table.dataSpace = nil
|
||||
table.rows = {}
|
||||
table.rowBaseStyle = nil
|
||||
table.columns = {}
|
||||
table.columnWidth = {}
|
||||
table.columBaseStyle = nil
|
||||
table.headerRowBaseStyle = nil
|
||||
table.headerColumnBaseStyle = nil
|
||||
table.selectedRow = nil
|
||||
table.defaultColumnWidth = 80
|
||||
table.sortColumn = -1
|
||||
table.sortType = TABLE_SORTING_ASC
|
||||
table.autoSort = false
|
||||
|
||||
return table
|
||||
end
|
||||
|
||||
-- Clear table values
|
||||
function UITable:onDestroy()
|
||||
for _,row in pairs(self.rows) do
|
||||
row.onClick = nil
|
||||
end
|
||||
self.rows = {}
|
||||
self.columns = {}
|
||||
self.headerRow = nil
|
||||
self.headerColumns = {}
|
||||
self.columnWidth = {}
|
||||
self.selectedRow = nil
|
||||
|
||||
if self.dataSpace then
|
||||
self.dataSpace:destroyChildren()
|
||||
self.dataSpace = nil
|
||||
end
|
||||
end
|
||||
|
||||
-- Detect if a header is already defined
|
||||
function UITable:onSetup()
|
||||
local header = self:getChildById('header')
|
||||
if header then
|
||||
self:setHeader(header)
|
||||
end
|
||||
end
|
||||
|
||||
-- Parse table related styles
|
||||
function UITable:onStyleApply(styleName, styleNode)
|
||||
for name, value in pairs(styleNode) do
|
||||
if value ~= false then
|
||||
if name == 'table-data' then
|
||||
addEvent(function()
|
||||
self:setTableData(self:getParent():getChildById(value))
|
||||
end)
|
||||
elseif name == 'column-style' then
|
||||
addEvent(function()
|
||||
self:setColumnStyle(value)
|
||||
end)
|
||||
elseif name == 'row-style' then
|
||||
addEvent(function()
|
||||
self:setRowStyle(value)
|
||||
end)
|
||||
elseif name == 'header-column-style' then
|
||||
addEvent(function()
|
||||
self:setHeaderColumnStyle(value)
|
||||
end)
|
||||
elseif name == 'header-row-style' then
|
||||
addEvent(function()
|
||||
self:setHeaderRowStyle(value)
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function UITable:setColumnWidth(width)
|
||||
if self:hasHeader() then return end
|
||||
self.columnWidth = width
|
||||
end
|
||||
|
||||
function UITable:setDefaultColumnWidth(width)
|
||||
self.defaultColumnWidth = width
|
||||
end
|
||||
|
||||
-- Check if the table has a header
|
||||
function UITable:hasHeader()
|
||||
return self.headerRow ~= nil
|
||||
end
|
||||
|
||||
-- Clear all rows
|
||||
function UITable:clearData()
|
||||
if not self.dataSpace then
|
||||
return
|
||||
end
|
||||
self.dataSpace:destroyChildren()
|
||||
self.selectedRow = nil
|
||||
self.columns = {}
|
||||
self.rows = {}
|
||||
end
|
||||
|
||||
-- Set existing child as header
|
||||
function UITable:setHeader(headerWidget)
|
||||
self:removeHeader()
|
||||
|
||||
if self.dataSpace then
|
||||
local newHeight = self.dataSpace:getHeight()-headerRow:getHeight()-self.dataSpace:getMarginTop()
|
||||
self.dataSpace:applyStyle({ height = newHeight })
|
||||
end
|
||||
|
||||
self.headerColumns = {}
|
||||
self.columnWidth = {}
|
||||
for colId, column in pairs(headerWidget:getChildren()) do
|
||||
column.colId = colId
|
||||
column.table = self
|
||||
table.insert(self.columnWidth, column:getWidth())
|
||||
table.insert(self.headerColumns, column)
|
||||
end
|
||||
|
||||
self.headerRow = headerWidget
|
||||
end
|
||||
|
||||
-- Create and add header from table data
|
||||
function UITable:addHeader(data)
|
||||
if not data or type(data) ~= 'table' then
|
||||
g_logger.error('UITable:addHeaderRow - table columns must be provided in a table')
|
||||
return
|
||||
end
|
||||
|
||||
self:removeHeader()
|
||||
|
||||
-- build header columns
|
||||
local columns = {}
|
||||
for colId, column in pairs(data) do
|
||||
local col = g_ui.createWidget(self.headerColumnBaseStyle)
|
||||
col.colId = colId
|
||||
col.table = self
|
||||
for type, value in pairs(column) do
|
||||
if type == 'width' then
|
||||
col:setWidth(value)
|
||||
elseif type == 'height' then
|
||||
col:setHeight(value)
|
||||
elseif type == 'text' then
|
||||
col:setText(value)
|
||||
elseif type == 'onClick' then
|
||||
col.onClick = value
|
||||
end
|
||||
end
|
||||
table.insert(columns, col)
|
||||
end
|
||||
|
||||
-- create a new header
|
||||
local headerRow = g_ui.createWidget(self.headerRowBaseStyle, self)
|
||||
local newHeight = self.dataSpace:getHeight()-headerRow:getHeight()-self.dataSpace:getMarginTop()
|
||||
self.dataSpace:applyStyle({ height = newHeight })
|
||||
|
||||
headerRow:setId('header')
|
||||
self.headerColumns = {}
|
||||
self.columnWidth = {}
|
||||
for _, column in pairs(columns) do
|
||||
headerRow:addChild(column)
|
||||
table.insert(self.columnWidth, column:getWidth())
|
||||
table.insert(self.headerColumns, column)
|
||||
end
|
||||
|
||||
self.headerRow = headerRow
|
||||
return headerRow
|
||||
end
|
||||
|
||||
-- Remove header
|
||||
function UITable:removeHeader()
|
||||
if self:hasHeader() then
|
||||
if self.dataSpace then
|
||||
local newHeight = self.dataSpace:getHeight()+self.headerRow:getHeight()+self.dataSpace:getMarginTop()
|
||||
self.dataSpace:applyStyle({ height = newHeight })
|
||||
end
|
||||
self.headerColumns = {}
|
||||
self.columnWidth = {}
|
||||
self.headerRow:destroy()
|
||||
self.headerRow = nil
|
||||
end
|
||||
end
|
||||
|
||||
function UITable:addRow(data, height)
|
||||
if not self.dataSpace then
|
||||
g_logger.error('UITable:addRow - table data space has not been set, cannot add rows.')
|
||||
return
|
||||
end
|
||||
if not data or type(data) ~= 'table' then
|
||||
g_logger.error('UITable:addRow - table columns must be provided in a table.')
|
||||
return
|
||||
end
|
||||
|
||||
local row = g_ui.createWidget(self.rowBaseStyle)
|
||||
row.table = self
|
||||
if height then row:setHeight(height) end
|
||||
|
||||
local rowId = #self.rows + 1
|
||||
row.rowId = rowId
|
||||
row:setId('row'..rowId)
|
||||
row:updateBackgroundColor()
|
||||
|
||||
self.columns[rowId] = {}
|
||||
for colId, column in pairs(data) do
|
||||
local col = g_ui.createWidget(self.columBaseStyle, row)
|
||||
if column.width then
|
||||
col:setWidth(column.width)
|
||||
else
|
||||
col:setWidth(self.columnWidth[colId] or self.defaultColumnWidth)
|
||||
end
|
||||
if column.height then
|
||||
col:setHeight(column.height)
|
||||
end
|
||||
if column.text then
|
||||
col:setText(column.text)
|
||||
end
|
||||
if column.sortvalue then
|
||||
col.sortvalue = column.sortvalue
|
||||
else
|
||||
col.sortvalue = column.text or 0
|
||||
end
|
||||
table.insert(self.columns[rowId], col)
|
||||
end
|
||||
|
||||
self.dataSpace:addChild(row)
|
||||
table.insert(self.rows, row)
|
||||
|
||||
if self.autoSort then
|
||||
self:sort()
|
||||
end
|
||||
|
||||
return row
|
||||
end
|
||||
|
||||
-- Update row indices and background color
|
||||
function UITable:updateRows()
|
||||
for rowId = 1, #self.rows do
|
||||
local row = self.rows[rowId]
|
||||
row.rowId = rowId
|
||||
row:setId('row'..rowId)
|
||||
row:updateBackgroundColor()
|
||||
end
|
||||
end
|
||||
|
||||
-- Removes the given row widget from the table
|
||||
function UITable:removeRow(row)
|
||||
if self.selectedRow == row then
|
||||
self:selectRow(nil)
|
||||
end
|
||||
row.onClick = nil
|
||||
row.table = nil
|
||||
table.remove(self.columns, row.rowId)
|
||||
table.remove(self.rows, row.rowId)
|
||||
self.dataSpace:removeChild(row)
|
||||
self:updateRows()
|
||||
end
|
||||
|
||||
function UITable:toggleSorting(enabled)
|
||||
self.autoSort = enabled
|
||||
end
|
||||
|
||||
function UITable:setSorting(colId, sortType)
|
||||
self.headerColumns[colId]:focus()
|
||||
|
||||
if sortType then
|
||||
self.sortType = sortType
|
||||
elseif self.sortColumn == colId then
|
||||
if self.sortType == TABLE_SORTING_ASC then
|
||||
self.sortType = TABLE_SORTING_DESC
|
||||
else
|
||||
self.sortType = TABLE_SORTING_ASC
|
||||
end
|
||||
else
|
||||
self.sortType = TABLE_SORTING_ASC
|
||||
end
|
||||
self.sortColumn = colId
|
||||
end
|
||||
|
||||
function UITable:sort()
|
||||
if self.sortColumn <= 0 then
|
||||
return
|
||||
end
|
||||
|
||||
if self.sortType == TABLE_SORTING_ASC then
|
||||
table.sort(self.rows, function(rowA, b)
|
||||
return rowA:getChildByIndex(self.sortColumn).sortvalue < b:getChildByIndex(self.sortColumn).sortvalue
|
||||
end)
|
||||
else
|
||||
table.sort(self.rows, function(rowA, b)
|
||||
return rowA:getChildByIndex(self.sortColumn).sortvalue > b:getChildByIndex(self.sortColumn).sortvalue
|
||||
end)
|
||||
end
|
||||
|
||||
if self.dataSpace then
|
||||
for _, child in pairs(self.dataSpace:getChildren()) do
|
||||
self.dataSpace:removeChild(child)
|
||||
end
|
||||
end
|
||||
|
||||
self:updateRows()
|
||||
self.columns = {}
|
||||
for _, row in pairs(self.rows) do
|
||||
if self.dataSpace then
|
||||
self.dataSpace:addChild(row)
|
||||
end
|
||||
|
||||
self.columns[row.rowId] = {}
|
||||
for _, column in pairs(row:getChildren()) do
|
||||
table.insert(self.columns[row.rowId], column)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function UITable:selectRow(selectedRow)
|
||||
if selectedRow == self.selectedRow then return end
|
||||
|
||||
local previousSelectedRow = self.selectedRow
|
||||
self.selectedRow = selectedRow
|
||||
|
||||
if previousSelectedRow then
|
||||
previousSelectedRow:setChecked(false)
|
||||
end
|
||||
|
||||
if selectedRow then
|
||||
selectedRow:setChecked(true)
|
||||
end
|
||||
|
||||
signalcall(self.onSelectionChange, self, selectedRow, previousSelectedRow)
|
||||
end
|
||||
|
||||
function UITable:setTableData(tableData)
|
||||
local headerHeight = 0
|
||||
if self.headerRow then
|
||||
headerHeight = self.headerRow:getHeight()
|
||||
end
|
||||
|
||||
self.dataSpace = tableData
|
||||
self.dataSpace:applyStyle({ height = self:getHeight()-headerHeight-self:getMarginTop() })
|
||||
end
|
||||
|
||||
function UITable:setRowStyle(style, dontUpdate)
|
||||
self.rowBaseStyle = style
|
||||
|
||||
if not dontUpdate then
|
||||
for _, row in pairs(self.rows) do
|
||||
row:setStyle(style)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function UITable:setColumnStyle(style, dontUpdate)
|
||||
self.columBaseStyle = style
|
||||
|
||||
if not dontUpdate then
|
||||
for _, columns in pairs(self.columns) do
|
||||
for _, col in pairs(columns) do
|
||||
col:setStyle(style)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function UITable:setHeaderRowStyle(style)
|
||||
self.headerRowBaseStyle = style
|
||||
if self.headerRow then
|
||||
self.headerRow:setStyle(style)
|
||||
end
|
||||
end
|
||||
|
||||
function UITable:setHeaderColumnStyle(style)
|
||||
self.headerColumnBaseStyle = style
|
||||
for _, col in pairs(self.headerColumns) do
|
||||
col:setStyle(style)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
UITableRow = extends(UIWidget, "UITableRow")
|
||||
|
||||
function UITableRow:onFocusChange(focused)
|
||||
if focused then
|
||||
if self.table then self.table:selectRow(self) end
|
||||
end
|
||||
end
|
||||
|
||||
function UITableRow:onStyleApply(styleName, styleNode)
|
||||
for name,value in pairs(styleNode) do
|
||||
if name == 'even-background-color' then
|
||||
self.evenBackgroundColor = value
|
||||
elseif name == 'odd-background-color' then
|
||||
self.oddBackgroundColor = value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function UITableRow:updateBackgroundColor()
|
||||
self.backgroundColor = nil
|
||||
|
||||
local isEven = (self.rowId % 2 == 0)
|
||||
if isEven and self.evenBackgroundColor then
|
||||
self.backgroundColor = self.evenBackgroundColor
|
||||
elseif not isEven and self.oddBackgroundColor then
|
||||
self.backgroundColor = self.oddBackgroundColor
|
||||
end
|
||||
|
||||
if self.backgroundColor then
|
||||
self:mergeStyle({ ['background-color'] = self.backgroundColor })
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
UITableHeaderColumn = extends(UIButton, "UITableHeaderColumn")
|
||||
|
||||
function UITableHeaderColumn:onClick()
|
||||
if self.table then
|
||||
self.table:setSorting(self.colId)
|
||||
self.table:sort()
|
||||
end
|
||||
end
|
78
modules/corelib/ui/uitextedit.lua
Normal file
78
modules/corelib/ui/uitextedit.lua
Normal file
@@ -0,0 +1,78 @@
|
||||
function UITextEdit:onStyleApply(styleName, styleNode)
|
||||
for name,value in pairs(styleNode) do
|
||||
if name == 'vertical-scrollbar' then
|
||||
addEvent(function()
|
||||
self:setVerticalScrollBar(self:getParent():getChildById(value))
|
||||
end)
|
||||
elseif name == 'horizontal-scrollbar' then
|
||||
addEvent(function()
|
||||
self:setHorizontalScrollBar(self:getParent():getChildById(value))
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function UITextEdit:onMouseWheel(mousePos, mouseWheel)
|
||||
if self.verticalScrollBar and self:isMultiline() then
|
||||
if mouseWheel == MouseWheelUp then
|
||||
self.verticalScrollBar:decrement()
|
||||
else
|
||||
self.verticalScrollBar:increment()
|
||||
end
|
||||
return true
|
||||
elseif self.horizontalScrollBar then
|
||||
if mouseWheel == MouseWheelUp then
|
||||
self.horizontalScrollBar:increment()
|
||||
else
|
||||
self.horizontalScrollBar:decrement()
|
||||
end
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
function UITextEdit:onTextAreaUpdate(virtualOffset, virtualSize, totalSize)
|
||||
self:updateScrollBars()
|
||||
end
|
||||
|
||||
function UITextEdit:setVerticalScrollBar(scrollbar)
|
||||
self.verticalScrollBar = scrollbar
|
||||
self.verticalScrollBar.onValueChange = function(scrollbar, value)
|
||||
local virtualOffset = self:getTextVirtualOffset()
|
||||
virtualOffset.y = value
|
||||
self:setTextVirtualOffset(virtualOffset)
|
||||
end
|
||||
self:updateScrollBars()
|
||||
end
|
||||
|
||||
function UITextEdit:setHorizontalScrollBar(scrollbar)
|
||||
self.horizontalScrollBar = scrollbar
|
||||
self.horizontalScrollBar.onValueChange = function(scrollbar, value)
|
||||
local virtualOffset = self:getTextVirtualOffset()
|
||||
virtualOffset.x = value
|
||||
self:setTextVirtualOffset(virtualOffset)
|
||||
end
|
||||
self:updateScrollBars()
|
||||
end
|
||||
|
||||
function UITextEdit:updateScrollBars()
|
||||
local scrollSize = self:getTextTotalSize()
|
||||
local scrollWidth = math.max(scrollSize.width - self:getTextVirtualSize().width, 0)
|
||||
local scrollHeight = math.max(scrollSize.height - self:getTextVirtualSize().height, 0)
|
||||
|
||||
local scrollbar = self.verticalScrollBar
|
||||
if scrollbar then
|
||||
scrollbar:setMinimum(0)
|
||||
scrollbar:setMaximum(scrollHeight)
|
||||
scrollbar:setValue(self:getTextVirtualOffset().y)
|
||||
end
|
||||
|
||||
local scrollbar = self.horizontalScrollBar
|
||||
if scrollbar then
|
||||
scrollbar:setMinimum(0)
|
||||
scrollbar:setMaximum(scrollWidth)
|
||||
scrollbar:setValue(self:getTextVirtualOffset().x)
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
-- todo: ontext change, focus to cursor
|
21
modules/corelib/ui/uiwidget.lua
Normal file
21
modules/corelib/ui/uiwidget.lua
Normal file
@@ -0,0 +1,21 @@
|
||||
-- @docclass UIWidget
|
||||
|
||||
function UIWidget:setMargin(...)
|
||||
local params = {...}
|
||||
if #params == 1 then
|
||||
self:setMarginTop(params[1])
|
||||
self:setMarginRight(params[1])
|
||||
self:setMarginBottom(params[1])
|
||||
self:setMarginLeft(params[1])
|
||||
elseif #params == 2 then
|
||||
self:setMarginTop(params[1])
|
||||
self:setMarginRight(params[2])
|
||||
self:setMarginBottom(params[1])
|
||||
self:setMarginLeft(params[2])
|
||||
elseif #params == 4 then
|
||||
self:setMarginTop(params[1])
|
||||
self:setMarginRight(params[2])
|
||||
self:setMarginBottom(params[3])
|
||||
self:setMarginLeft(params[4])
|
||||
end
|
||||
end
|
46
modules/corelib/ui/uiwindow.lua
Normal file
46
modules/corelib/ui/uiwindow.lua
Normal file
@@ -0,0 +1,46 @@
|
||||
-- @docclass
|
||||
UIWindow = extends(UIWidget, "UIWindow")
|
||||
|
||||
function UIWindow.create()
|
||||
local window = UIWindow.internalCreate()
|
||||
window:setTextAlign(AlignTopCenter)
|
||||
window:setDraggable(true)
|
||||
window:setAutoFocusPolicy(AutoFocusFirst)
|
||||
return window
|
||||
end
|
||||
|
||||
function UIWindow:onKeyDown(keyCode, keyboardModifiers)
|
||||
if keyboardModifiers == KeyboardNoModifier then
|
||||
if keyCode == KeyEnter then
|
||||
signalcall(self.onEnter, self)
|
||||
elseif keyCode == KeyEscape then
|
||||
signalcall(self.onEscape, self)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function UIWindow:onFocusChange(focused)
|
||||
if focused then self:raise() end
|
||||
end
|
||||
|
||||
function UIWindow:onDragEnter(mousePos)
|
||||
if self.static then
|
||||
return
|
||||
end
|
||||
self:breakAnchors()
|
||||
self.movingReference = { x = mousePos.x - self:getX(), y = mousePos.y - self:getY() }
|
||||
return true
|
||||
end
|
||||
|
||||
function UIWindow:onDragLeave(droppedWidget, mousePos)
|
||||
-- TODO: auto detect and reconnect anchors
|
||||
end
|
||||
|
||||
function UIWindow:onDragMove(mousePos, mouseMoved)
|
||||
if self.static then
|
||||
return
|
||||
end
|
||||
local pos = { x = mousePos.x - self.movingReference.x, y = mousePos.y - self.movingReference.y }
|
||||
self:setPosition(pos)
|
||||
self:bindRectToParent()
|
||||
end
|
365
modules/corelib/util.lua
Normal file
365
modules/corelib/util.lua
Normal file
@@ -0,0 +1,365 @@
|
||||
-- @docfuncs @{
|
||||
|
||||
function print(...)
|
||||
local msg = ""
|
||||
local args = {...}
|
||||
local appendSpace = #args > 1
|
||||
for i,v in ipairs(args) do
|
||||
msg = msg .. tostring(v)
|
||||
if appendSpace and i < #args then
|
||||
msg = msg .. ' '
|
||||
end
|
||||
end
|
||||
g_logger.log(LogInfo, msg)
|
||||
end
|
||||
|
||||
function pinfo(msg)
|
||||
g_logger.log(LogInfo, msg)
|
||||
end
|
||||
|
||||
function perror(msg)
|
||||
g_logger.log(LogError, msg)
|
||||
end
|
||||
|
||||
function pwarning(msg)
|
||||
g_logger.log(LogWarning, msg)
|
||||
end
|
||||
|
||||
function pdebug(msg)
|
||||
g_logger.log(LogDebug, msg)
|
||||
end
|
||||
|
||||
function fatal(msg)
|
||||
g_logger.log(LogFatal, msg)
|
||||
end
|
||||
|
||||
function exit()
|
||||
g_app.exit()
|
||||
end
|
||||
|
||||
function quit()
|
||||
g_app.quit()
|
||||
end
|
||||
|
||||
function connect(object, arg1, arg2, arg3)
|
||||
local signalsAndSlots
|
||||
local pushFront
|
||||
if type(arg1) == 'string' then
|
||||
signalsAndSlots = { [arg1] = arg2 }
|
||||
pushFront = arg3
|
||||
else
|
||||
signalsAndSlots = arg1
|
||||
pushFront = arg2
|
||||
end
|
||||
|
||||
for signal,slot in pairs(signalsAndSlots) do
|
||||
if not object[signal] then
|
||||
local mt = getmetatable(object)
|
||||
if mt and type(object) == 'userdata' then
|
||||
object[signal] = function(...)
|
||||
return signalcall(mt[signal], ...)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if not object[signal] then
|
||||
object[signal] = slot
|
||||
elseif type(object[signal]) == 'function' then
|
||||
object[signal] = { object[signal] }
|
||||
end
|
||||
|
||||
if type(slot) ~= 'function' then
|
||||
perror(debug.traceback('unable to connect a non function value'))
|
||||
end
|
||||
|
||||
if type(object[signal]) == 'table' then
|
||||
if pushFront then
|
||||
table.insert(object[signal], 1, slot)
|
||||
else
|
||||
table.insert(object[signal], #object[signal]+1, slot)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function disconnect(object, arg1, arg2)
|
||||
local signalsAndSlots
|
||||
if type(arg1) == 'string' then
|
||||
if arg2 == nil then
|
||||
object[arg1] = nil
|
||||
return
|
||||
end
|
||||
signalsAndSlots = { [arg1] = arg2 }
|
||||
elseif type(arg1) == 'table' then
|
||||
signalsAndSlots = arg1
|
||||
else
|
||||
perror(debug.traceback('unable to disconnect'))
|
||||
end
|
||||
|
||||
for signal,slot in pairs(signalsAndSlots) do
|
||||
if not object[signal] then
|
||||
elseif type(object[signal]) == 'function' then
|
||||
if object[signal] == slot then
|
||||
object[signal] = nil
|
||||
end
|
||||
elseif type(object[signal]) == 'table' then
|
||||
for k,func in pairs(object[signal]) do
|
||||
if func == slot then
|
||||
table.remove(object[signal], k)
|
||||
|
||||
if #object[signal] == 1 then
|
||||
object[signal] = object[signal][1]
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function newclass(name)
|
||||
if not name then
|
||||
perror(debug.traceback('new class has no name.'))
|
||||
end
|
||||
|
||||
local class = {}
|
||||
function class.internalCreate()
|
||||
local instance = {}
|
||||
for k,v in pairs(class) do
|
||||
instance[k] = v
|
||||
end
|
||||
return instance
|
||||
end
|
||||
class.create = class.internalCreate
|
||||
class.__class = name
|
||||
class.getClassName = function() return name end
|
||||
return class
|
||||
end
|
||||
|
||||
function extends(base, name)
|
||||
if not name then
|
||||
perror(debug.traceback('extended class has no name.'))
|
||||
end
|
||||
|
||||
local derived = {}
|
||||
function derived.internalCreate()
|
||||
local instance = base.create()
|
||||
for k,v in pairs(derived) do
|
||||
instance[k] = v
|
||||
end
|
||||
return instance
|
||||
end
|
||||
derived.create = derived.internalCreate
|
||||
derived.__class = name
|
||||
derived.getClassName = function() return name end
|
||||
return derived
|
||||
end
|
||||
|
||||
function runinsandbox(func, ...)
|
||||
if type(func) == 'string' then
|
||||
func, err = loadfile(resolvepath(func, 2))
|
||||
if not func then
|
||||
error(err)
|
||||
end
|
||||
end
|
||||
local env = { }
|
||||
local oldenv = getfenv(0)
|
||||
setmetatable(env, { __index = oldenv } )
|
||||
setfenv(0, env)
|
||||
func(...)
|
||||
setfenv(0, oldenv)
|
||||
return env
|
||||
end
|
||||
|
||||
function loadasmodule(name, file)
|
||||
file = file or resolvepath(name, 2)
|
||||
if package.loaded[name] then
|
||||
return package.loaded[name]
|
||||
end
|
||||
local env = runinsandbox(file)
|
||||
package.loaded[name] = env
|
||||
return env
|
||||
end
|
||||
|
||||
local function module_loader(modname)
|
||||
local module = g_modules.getModule(modname)
|
||||
if not module then
|
||||
return '\n\tno module \'' .. modname .. '\''
|
||||
end
|
||||
return function()
|
||||
if not module:load() then
|
||||
error('unable to load required module ' .. modname)
|
||||
end
|
||||
return module:getSandbox()
|
||||
end
|
||||
end
|
||||
table.insert(package.loaders, 1, module_loader)
|
||||
|
||||
function import(table)
|
||||
assert(type(table) == 'table')
|
||||
local env = getfenv(2)
|
||||
for k,v in pairs(table) do
|
||||
env[k] = v
|
||||
end
|
||||
end
|
||||
|
||||
function export(what, key)
|
||||
if key ~= nil then
|
||||
_G[key] = what
|
||||
else
|
||||
for k,v in pairs(what) do
|
||||
_G[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function unexport(key)
|
||||
if type(key) == 'table' then
|
||||
for _k,v in pairs(key) do
|
||||
_G[v] = nil
|
||||
end
|
||||
else
|
||||
_G[key] = nil
|
||||
end
|
||||
end
|
||||
|
||||
function getfsrcpath(depth)
|
||||
depth = depth or 2
|
||||
local info = debug.getinfo(1+depth, "Sn")
|
||||
local path
|
||||
if info.short_src then
|
||||
path = info.short_src:match("(.*)/.*")
|
||||
end
|
||||
if not path then
|
||||
path = '/'
|
||||
elseif path:sub(0, 1) ~= '/' then
|
||||
path = '/' .. path
|
||||
end
|
||||
return path
|
||||
end
|
||||
|
||||
function resolvepath(filePath, depth)
|
||||
if not filePath then return nil end
|
||||
depth = depth or 1
|
||||
if filePath then
|
||||
if filePath:sub(0, 1) ~= '/' then
|
||||
local basepath = getfsrcpath(depth+1)
|
||||
if basepath:sub(#basepath) ~= '/' then basepath = basepath .. '/' end
|
||||
return basepath .. filePath
|
||||
else
|
||||
return filePath
|
||||
end
|
||||
else
|
||||
local basepath = getfsrcpath(depth+1)
|
||||
if basepath:sub(#basepath) ~= '/' then basepath = basepath .. '/' end
|
||||
return basepath
|
||||
end
|
||||
end
|
||||
|
||||
function toboolean(v)
|
||||
if type(v) == 'string' then
|
||||
v = v:trim():lower()
|
||||
if v == '1' or v == 'true' then
|
||||
return true
|
||||
end
|
||||
elseif type(v) == 'number' then
|
||||
if v == 1 then
|
||||
return true
|
||||
end
|
||||
elseif type(v) == 'boolean' then
|
||||
return v
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function fromboolean(boolean)
|
||||
if boolean then
|
||||
return 'true'
|
||||
else
|
||||
return 'false'
|
||||
end
|
||||
end
|
||||
|
||||
function booleantonumber(boolean)
|
||||
if boolean then
|
||||
return 1
|
||||
else
|
||||
return 0
|
||||
end
|
||||
end
|
||||
|
||||
function numbertoboolean(number)
|
||||
if number ~= 0 then
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
function protectedcall(func, ...)
|
||||
local status, ret = pcall(func, ...)
|
||||
if status then
|
||||
return ret
|
||||
end
|
||||
|
||||
perror(ret)
|
||||
return false
|
||||
end
|
||||
|
||||
function signalcall(param, ...)
|
||||
if type(param) == 'function' then
|
||||
local status, ret = pcall(param, ...)
|
||||
if status then
|
||||
return ret
|
||||
else
|
||||
perror(ret)
|
||||
end
|
||||
elseif type(param) == 'table' then
|
||||
for k,v in pairs(param) do
|
||||
local status, ret = pcall(v, ...)
|
||||
if status then
|
||||
if ret then return true end
|
||||
else
|
||||
perror(ret)
|
||||
end
|
||||
end
|
||||
elseif param ~= nil then
|
||||
error('attempt to call a non function value')
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function tr(s, ...)
|
||||
return string.format(s, ...)
|
||||
end
|
||||
|
||||
function getOppositeAnchor(anchor)
|
||||
if anchor == AnchorLeft then
|
||||
return AnchorRight
|
||||
elseif anchor == AnchorRight then
|
||||
return AnchorLeft
|
||||
elseif anchor == AnchorTop then
|
||||
return AnchorBottom
|
||||
elseif anchor == AnchorBottom then
|
||||
return AnchorTop
|
||||
elseif anchor == AnchorVerticalCenter then
|
||||
return AnchorHorizontalCenter
|
||||
elseif anchor == AnchorHorizontalCenter then
|
||||
return AnchorVerticalCenter
|
||||
end
|
||||
return anchor
|
||||
end
|
||||
|
||||
function makesingleton(obj)
|
||||
local singleton = {}
|
||||
if obj.getClassName then
|
||||
for key,value in pairs(_G[obj:getClassName()]) do
|
||||
if type(value) == 'function' then
|
||||
singleton[key] = function(...) return value(obj, ...) end
|
||||
end
|
||||
end
|
||||
end
|
||||
return singleton
|
||||
end
|
||||
|
||||
-- @}
|
449
modules/game_battle/battle.lua
Normal file
449
modules/game_battle/battle.lua
Normal file
@@ -0,0 +1,449 @@
|
||||
battleWindow = nil
|
||||
battleButton = nil
|
||||
battlePanel = nil
|
||||
filterPanel = nil
|
||||
toggleFilterButton = nil
|
||||
creatureAgeList = {}
|
||||
battleButtonsList = {}
|
||||
|
||||
mouseWidget = nil
|
||||
|
||||
sortTypeBox = nil
|
||||
sortOrderBox = nil
|
||||
hidePlayersButton = nil
|
||||
hideNPCsButton = nil
|
||||
hideMonstersButton = nil
|
||||
hideSkullsButton = nil
|
||||
hidePartyButton = nil
|
||||
|
||||
updateEvent = nil
|
||||
|
||||
hoveredCreature = nil
|
||||
newHoveredCreature = nil
|
||||
prevCreature = nil
|
||||
|
||||
local creatureAgeCounter = 1
|
||||
|
||||
function init()
|
||||
g_ui.importStyle('battlebutton')
|
||||
battleButton = modules.client_topmenu.addRightGameToggleButton('battleButton', tr('Battle') .. ' (Ctrl+B)', '/images/topbuttons/battle', toggle)
|
||||
battleButton:setOn(true)
|
||||
battleWindow = g_ui.loadUI('battle', modules.game_interface.getRightPanel())
|
||||
g_keyboard.bindKeyDown('Ctrl+B', toggle)
|
||||
|
||||
-- this disables scrollbar auto hiding
|
||||
local scrollbar = battleWindow:getChildById('miniwindowScrollBar')
|
||||
scrollbar:mergeStyle({ ['$!on'] = { }})
|
||||
|
||||
battlePanel = battleWindow:recursiveGetChildById('battlePanel')
|
||||
|
||||
filterPanel = battleWindow:recursiveGetChildById('filterPanel')
|
||||
toggleFilterButton = battleWindow:recursiveGetChildById('toggleFilterButton')
|
||||
|
||||
if isHidingFilters() then
|
||||
hideFilterPanel()
|
||||
end
|
||||
|
||||
sortTypeBox = battleWindow:recursiveGetChildById('sortTypeBox')
|
||||
sortOrderBox = battleWindow:recursiveGetChildById('sortOrderBox')
|
||||
hidePlayersButton = battleWindow:recursiveGetChildById('hidePlayers')
|
||||
hideNPCsButton = battleWindow:recursiveGetChildById('hideNPCs')
|
||||
hideMonstersButton = battleWindow:recursiveGetChildById('hideMonsters')
|
||||
hideSkullsButton = battleWindow:recursiveGetChildById('hideSkulls')
|
||||
hidePartyButton = battleWindow:recursiveGetChildById('hideParty')
|
||||
|
||||
mouseWidget = g_ui.createWidget('UIButton')
|
||||
mouseWidget:setVisible(false)
|
||||
mouseWidget:setFocusable(false)
|
||||
mouseWidget.cancelNextRelease = false
|
||||
|
||||
battleWindow:setContentMinimumHeight(80)
|
||||
|
||||
sortTypeBox:addOption('Name', 'name')
|
||||
sortTypeBox:addOption('Distance', 'distance')
|
||||
sortTypeBox:addOption('Age', 'age')
|
||||
sortTypeBox:addOption('Health', 'health')
|
||||
sortTypeBox:setCurrentOptionByData(getSortType())
|
||||
sortTypeBox.onOptionChange = onChangeSortType
|
||||
|
||||
sortOrderBox:addOption('Asc.', 'asc')
|
||||
sortOrderBox:addOption('Desc.', 'desc')
|
||||
sortOrderBox:setCurrentOptionByData(getSortOrder())
|
||||
sortOrderBox.onOptionChange = onChangeSortOrder
|
||||
|
||||
updateBattleList()
|
||||
battleWindow:setup()
|
||||
|
||||
connect(LocalPlayer, {
|
||||
onPositionChange = onCreaturePositionChange
|
||||
})
|
||||
connect(Creature, {
|
||||
onAppear = updateSquare,
|
||||
onDisappear = updateSquare
|
||||
})
|
||||
connect(g_game, {
|
||||
onAttackingCreatureChange = updateSquare,
|
||||
onFollowingCreatureChange = updateSquare
|
||||
})
|
||||
end
|
||||
|
||||
function terminate()
|
||||
if battleButton == nil then
|
||||
return
|
||||
end
|
||||
|
||||
g_keyboard.unbindKeyDown('Ctrl+B')
|
||||
battleButtonsByCreaturesList = {}
|
||||
battleButton:destroy()
|
||||
battleWindow:destroy()
|
||||
mouseWidget:destroy()
|
||||
|
||||
disconnect(LocalPlayer, {
|
||||
onPositionChange = onCreaturePositionChange
|
||||
})
|
||||
disconnect(Creature, {
|
||||
onAppear = onCreatureAppear,
|
||||
onDisappear = onCreatureDisappear
|
||||
})
|
||||
disconnect(g_game, {
|
||||
onAttackingCreatureChange = updateSquare,
|
||||
onFollowingCreatureChange = updateSquare
|
||||
})
|
||||
|
||||
removeEvent(updateEvent)
|
||||
end
|
||||
|
||||
function toggle()
|
||||
if battleButton:isOn() then
|
||||
battleWindow:close()
|
||||
battleButton:setOn(false)
|
||||
else
|
||||
battleWindow:open()
|
||||
battleButton:setOn(true)
|
||||
end
|
||||
end
|
||||
|
||||
function onMiniWindowClose()
|
||||
battleButton:setOn(false)
|
||||
end
|
||||
|
||||
function getSortType()
|
||||
local settings = g_settings.getNode('BattleList')
|
||||
if not settings then
|
||||
return 'name'
|
||||
end
|
||||
return settings['sortType']
|
||||
end
|
||||
|
||||
function setSortType(state)
|
||||
settings = {}
|
||||
settings['sortType'] = state
|
||||
g_settings.mergeNode('BattleList', settings)
|
||||
|
||||
checkCreatures()
|
||||
end
|
||||
|
||||
function getSortOrder()
|
||||
local settings = g_settings.getNode('BattleList')
|
||||
if not settings then
|
||||
return 'asc'
|
||||
end
|
||||
return settings['sortOrder']
|
||||
end
|
||||
|
||||
function setSortOrder(state)
|
||||
settings = {}
|
||||
settings['sortOrder'] = state
|
||||
g_settings.mergeNode('BattleList', settings)
|
||||
|
||||
checkCreatures()
|
||||
end
|
||||
|
||||
function isSortAsc()
|
||||
return getSortOrder() == 'asc'
|
||||
end
|
||||
|
||||
function isSortDesc()
|
||||
return getSortOrder() == 'desc'
|
||||
end
|
||||
|
||||
function isHidingFilters()
|
||||
local settings = g_settings.getNode('BattleList')
|
||||
if not settings then
|
||||
return false
|
||||
end
|
||||
return settings['hidingFilters']
|
||||
end
|
||||
|
||||
function setHidingFilters(state)
|
||||
settings = {}
|
||||
settings['hidingFilters'] = state
|
||||
g_settings.mergeNode('BattleList', settings)
|
||||
end
|
||||
|
||||
function hideFilterPanel()
|
||||
filterPanel.originalHeight = filterPanel:getHeight()
|
||||
filterPanel:setHeight(0)
|
||||
toggleFilterButton:getParent():setMarginTop(0)
|
||||
toggleFilterButton:setImageClip(torect("0 0 21 12"))
|
||||
setHidingFilters(true)
|
||||
filterPanel:setVisible(false)
|
||||
end
|
||||
|
||||
function showFilterPanel()
|
||||
toggleFilterButton:getParent():setMarginTop(5)
|
||||
filterPanel:setHeight(filterPanel.originalHeight)
|
||||
toggleFilterButton:setImageClip(torect("21 0 21 12"))
|
||||
setHidingFilters(false)
|
||||
filterPanel:setVisible(true)
|
||||
end
|
||||
|
||||
function toggleFilterPanel()
|
||||
if filterPanel:isVisible() then
|
||||
hideFilterPanel()
|
||||
else
|
||||
showFilterPanel()
|
||||
end
|
||||
end
|
||||
|
||||
function onChangeSortType(comboBox, option)
|
||||
setSortType(option:lower())
|
||||
end
|
||||
|
||||
function onChangeSortOrder(comboBox, option)
|
||||
-- Replace dot in option name
|
||||
setSortOrder(option:lower():gsub('[.]', ''))
|
||||
end
|
||||
|
||||
-- functions
|
||||
function updateBattleList()
|
||||
updateEvent = scheduleEvent(updateBattleList, 200)
|
||||
checkCreatures()
|
||||
end
|
||||
|
||||
function checkCreatures()
|
||||
if not g_game.isOnline() then
|
||||
return
|
||||
end
|
||||
|
||||
local player = g_game.getLocalPlayer()
|
||||
local dimension = modules.game_interface.getMapPanel():getVisibleDimension()
|
||||
local spectators = g_map.getSpectatorsInRangeEx(player:getPosition(), false, math.floor(dimension.width / 2), math.floor(dimension.width / 2), math.floor(dimension.height / 2), math.floor(dimension.height / 2))
|
||||
|
||||
creatures = {}
|
||||
for _, creature in ipairs(spectators) do
|
||||
if creatureAgeList[creature] == nil then
|
||||
creatureAgeList[creature] = creatureAgeCounter
|
||||
creatureAgeCounter = creatureAgeCounter + 1
|
||||
end
|
||||
if doCreatureFitFilters(creature) then
|
||||
table.insert(creatures, creature)
|
||||
end
|
||||
end
|
||||
|
||||
updateSquare()
|
||||
|
||||
-- sorting
|
||||
local creature_i = 1
|
||||
sortCreatures(creatures)
|
||||
for i=1, #creatures do
|
||||
if creature_i > 30 then
|
||||
break
|
||||
end
|
||||
|
||||
local creature = creatures[i]
|
||||
if isSortAsc() then
|
||||
creature = creatures[#creatures - i + 1]
|
||||
end
|
||||
|
||||
if creature:getHealthPercent() > 0 then
|
||||
local battleButton = battleButtonsList[creature_i]
|
||||
|
||||
if battleButton == nil then
|
||||
battleButton = g_ui.createWidget('BattleButton')
|
||||
battleButton.onHoverChange = onBattleButtonHoverChange
|
||||
battleButton.onMouseRelease = onBattleButtonMouseRelease
|
||||
battleButton:setup(creature, creature_i)
|
||||
table.insert(battleButtonsList, battleButton)
|
||||
battlePanel:addChild(battleButton)
|
||||
end
|
||||
|
||||
battleButton:creatureSetup(creature)
|
||||
creature_i = creature_i + 1
|
||||
end
|
||||
end
|
||||
|
||||
local height = 0
|
||||
if creature_i > 1 then
|
||||
height = 25 * (creature_i - 1)
|
||||
end
|
||||
if battlePanel:getHeight() ~= height then
|
||||
battlePanel:setHeight(height)
|
||||
end
|
||||
end
|
||||
|
||||
function doCreatureFitFilters(creature)
|
||||
if creature:isLocalPlayer() then
|
||||
return false
|
||||
end
|
||||
|
||||
local pos = creature:getPosition()
|
||||
if not pos then return false end
|
||||
|
||||
local localPlayer = g_game.getLocalPlayer()
|
||||
if pos.z ~= localPlayer:getPosition().z or not creature:canBeSeen() then return false end
|
||||
|
||||
local hidePlayers = hidePlayersButton:isChecked()
|
||||
local hideNPCs = hideNPCsButton:isChecked()
|
||||
local hideMonsters = hideMonstersButton:isChecked()
|
||||
local hideSkulls = hideSkullsButton:isChecked()
|
||||
local hideParty = hidePartyButton:isChecked()
|
||||
|
||||
if hidePlayers and creature:isPlayer() then
|
||||
return false
|
||||
elseif hideNPCs and creature:isNpc() then
|
||||
return false
|
||||
elseif hideMonsters and creature:isMonster() then
|
||||
return false
|
||||
elseif hideSkulls and creature:isPlayer() and creature:getSkull() == SkullNone then
|
||||
return false
|
||||
elseif hideParty and creature:getShield() > ShieldWhiteBlue then
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local function getDistanceBetween(p1, p2)
|
||||
return math.max(math.abs(p1.x - p2.x), math.abs(p1.y - p2.y))
|
||||
end
|
||||
|
||||
function sortCreatures(creatures)
|
||||
local player = g_game.getLocalPlayer()
|
||||
|
||||
if getSortType() == 'distance' then
|
||||
local playerPos = player:getPosition()
|
||||
table.sort(creatures, function(a, b)
|
||||
if getDistanceBetween(playerPos, a:getPosition()) == getDistanceBetween(playerPos, b:getPosition()) then
|
||||
return creatureAgeList[a] > creatureAgeList[b]
|
||||
end
|
||||
return getDistanceBetween(playerPos, a:getPosition()) > getDistanceBetween(playerPos, b:getPosition())
|
||||
end)
|
||||
elseif getSortType() == 'health' then
|
||||
table.sort(creatures, function(a, b)
|
||||
if a:getHealthPercent() == b:getHealthPercent() then
|
||||
return creatureAgeList[a] > creatureAgeList[b]
|
||||
end
|
||||
return a:getHealthPercent() > b:getHealthPercent()
|
||||
end)
|
||||
elseif getSortType() == 'age' then
|
||||
table.sort(creatures, function(a, b) return creatureAgeList[a] > creatureAgeList[b] end)
|
||||
else -- name
|
||||
table.sort(creatures, function(a, b)
|
||||
if a:getName():lower() == b:getName():lower() then
|
||||
return creatureAgeList[a] > creatureAgeList[b]
|
||||
end
|
||||
return a:getName():lower() > b:getName():lower()
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
-- other functions
|
||||
function onBattleButtonMouseRelease(self, mousePosition, mouseButton)
|
||||
if mouseWidget.cancelNextRelease then
|
||||
mouseWidget.cancelNextRelease = false
|
||||
return false
|
||||
end
|
||||
if not self.creature then
|
||||
return false
|
||||
end
|
||||
if ((g_mouse.isPressed(MouseLeftButton) and mouseButton == MouseRightButton)
|
||||
or (g_mouse.isPressed(MouseRightButton) and mouseButton == MouseLeftButton)) then
|
||||
mouseWidget.cancelNextRelease = true
|
||||
g_game.look(self.creature, true)
|
||||
return true
|
||||
elseif mouseButton == MouseLeftButton and g_keyboard.isShiftPressed() then
|
||||
g_game.look(self.creature, true)
|
||||
return true
|
||||
elseif mouseButton == MouseRightButton and not g_mouse.isPressed(MouseLeftButton) then
|
||||
modules.game_interface.createThingMenu(mousePosition, nil, nil, self.creature)
|
||||
return true
|
||||
elseif mouseButton == MouseLeftButton and not g_mouse.isPressed(MouseRightButton) then
|
||||
if self.isTarget then
|
||||
g_game.cancelAttack()
|
||||
else
|
||||
g_game.attack(self.creature)
|
||||
end
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function onBattleButtonHoverChange(battleButton, hovered)
|
||||
if not hovered then
|
||||
newHoveredCreature = nil
|
||||
else
|
||||
newHoveredCreature = battleButton.creature
|
||||
end
|
||||
if battleButton.isHovered ~= hovered then
|
||||
battleButton.isHovered = hovered
|
||||
battleButton:update()
|
||||
end
|
||||
updateSquare()
|
||||
end
|
||||
|
||||
function onCreaturePositionChange(creature, newPos, oldPos)
|
||||
if creature:isLocalPlayer() then
|
||||
if oldPos and newPos and newPos.z ~= oldPos.z then
|
||||
checkCreatures()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local CreatureButtonColors = {
|
||||
onIdle = {notHovered = '#888888', hovered = '#FFFFFF' },
|
||||
onTargeted = {notHovered = '#FF0000', hovered = '#FF8888' },
|
||||
onFollowed = {notHovered = '#00FF00', hovered = '#88FF88' }
|
||||
}
|
||||
|
||||
function updateSquare()
|
||||
local following = g_game.getFollowingCreature()
|
||||
local attacking = g_game.getAttackingCreature()
|
||||
|
||||
if newHoveredCreature == nil then
|
||||
if hoveredCreature ~= nil then
|
||||
hoveredCreature:hideStaticSquare()
|
||||
hoveredCreature = nil
|
||||
end
|
||||
else
|
||||
if hoveredCreature ~= nil then
|
||||
hoveredCreature:hideStaticSquare()
|
||||
end
|
||||
hoveredCreature = newHoveredCreature
|
||||
hoveredCreature:showStaticSquare(CreatureButtonColors.onIdle.hovered)
|
||||
end
|
||||
|
||||
local color = CreatureButtonColors.onIdle
|
||||
local creature = nil
|
||||
if attacking then
|
||||
color = CreatureButtonColors.onTargeted
|
||||
creature = attacking
|
||||
elseif following then
|
||||
color = CreatureButtonColors.onFollowed
|
||||
creature = following
|
||||
end
|
||||
|
||||
if prevCreature ~= creature then
|
||||
if prevCreature ~= nil then
|
||||
prevCreature:hideStaticSquare()
|
||||
end
|
||||
prevCreature = creature
|
||||
end
|
||||
|
||||
if not creature then
|
||||
return
|
||||
end
|
||||
|
||||
color = creature == hoveredCreature and color.hovered or color.notHovered
|
||||
creature:showStaticSquare(color)
|
||||
end
|
9
modules/game_battle/battle.otmod
Normal file
9
modules/game_battle/battle.otmod
Normal file
@@ -0,0 +1,9 @@
|
||||
Module
|
||||
name: game_battle
|
||||
description: Manage battle window (new)
|
||||
author: otclient@otclient.ovh
|
||||
website: otclient.ovh
|
||||
sandboxed: true
|
||||
scripts: [ battle ]
|
||||
@onLoad: init()
|
||||
@onUnload: terminate()
|
148
modules/game_battle/battle.otui
Normal file
148
modules/game_battle/battle.otui
Normal file
@@ -0,0 +1,148 @@
|
||||
BattleIcon < UICheckBox
|
||||
size: 20 20
|
||||
image-color: white
|
||||
image-rect: 0 0 20 20
|
||||
|
||||
$hover !disabled:
|
||||
color: #cccccc
|
||||
|
||||
$!checked:
|
||||
image-clip: 0 0 20 20
|
||||
|
||||
$hover !checked:
|
||||
image-clip: 0 40 20 20
|
||||
|
||||
$checked:
|
||||
image-clip: 0 20 20 20
|
||||
|
||||
$hover checked:
|
||||
image-clip: 0 60 20 20
|
||||
|
||||
$disabled:
|
||||
image-color: #ffffff88
|
||||
|
||||
BattlePlayers < BattleIcon
|
||||
image-source: /images/game/battle/battle_players
|
||||
|
||||
BattleNPCs < BattleIcon
|
||||
image-source: /images/game/battle/battle_npcs
|
||||
|
||||
BattleMonsters < BattleIcon
|
||||
image-source: /images/game/battle/battle_monsters
|
||||
|
||||
BattleSkulls < BattleIcon
|
||||
image-source: /images/game/battle/battle_skulls
|
||||
|
||||
BattleParty < BattleIcon
|
||||
image-source: /images/game/battle/battle_party
|
||||
|
||||
MiniWindow
|
||||
id: battleWindow
|
||||
!text: tr('Battle')
|
||||
height: 166
|
||||
icon: /images/topbuttons/battle
|
||||
@onClose: modules.game_battle.onMiniWindowClose()
|
||||
&save: true
|
||||
|
||||
Panel
|
||||
id: filterPanel
|
||||
margin-top: 26
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: miniwindowScrollBar.left
|
||||
height: 45
|
||||
|
||||
Panel
|
||||
anchors.top: parent.top
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
height: 20
|
||||
width: 120
|
||||
layout:
|
||||
type: horizontalBox
|
||||
spacing: 5
|
||||
|
||||
BattlePlayers
|
||||
id: hidePlayers
|
||||
!tooltip: tr('Hide players')
|
||||
@onCheckChange: modules.game_battle.checkCreatures()
|
||||
|
||||
BattleNPCs
|
||||
id: hideNPCs
|
||||
!tooltip: tr('Hide Npcs')
|
||||
@onCheckChange: modules.game_battle.checkCreatures()
|
||||
|
||||
BattleMonsters
|
||||
id: hideMonsters
|
||||
!tooltip: tr('Hide monsters')
|
||||
@onCheckChange: modules.game_battle.checkCreatures()
|
||||
|
||||
BattleSkulls
|
||||
id: hideSkulls
|
||||
!tooltip: tr('Hide non-skull players')
|
||||
@onCheckChange: modules.game_battle.checkCreatures()
|
||||
|
||||
BattleParty
|
||||
id: hideParty
|
||||
!tooltip: tr('Hide party members')
|
||||
@onCheckChange: modules.game_battle.checkCreatures()
|
||||
|
||||
Panel
|
||||
anchors.top: prev.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 20
|
||||
margin-top: 6
|
||||
|
||||
ComboBox
|
||||
id: sortTypeBox
|
||||
width: 90
|
||||
anchors.top: parent.top
|
||||
anchors.left: prev.right
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
margin-left: -31
|
||||
|
||||
ComboBox
|
||||
id: sortOrderBox
|
||||
width: 60
|
||||
anchors.top: parent.top
|
||||
anchors.left: prev.right
|
||||
margin-left: 4
|
||||
|
||||
Panel
|
||||
height: 18
|
||||
anchors.top: prev.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: miniwindowScrollBar.left
|
||||
margin-top: 4
|
||||
|
||||
UIWidget
|
||||
id: toggleFilterButton
|
||||
anchors.top: prev.top
|
||||
width: 21
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
image-source: /images/ui/arrow_vertical
|
||||
image-rect: 0 0 21 12
|
||||
image-clip: 21 0 21 12
|
||||
@onClick: modules.game_battle.toggleFilterPanel()
|
||||
phantom: false
|
||||
|
||||
HorizontalSeparator
|
||||
anchors.top: prev.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: miniwindowScrollBar.left
|
||||
margin-right: 1
|
||||
margin-top: 11
|
||||
|
||||
MiniWindowContents
|
||||
anchors.top: prev.bottom
|
||||
margin-top: 6
|
||||
|
||||
Panel
|
||||
id: battlePanel
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
margin-top: 5
|
||||
padding-right: 5
|
||||
layout:
|
||||
type: verticalBox
|
2
modules/game_battle/battlebutton.otui
Normal file
2
modules/game_battle/battlebutton.otui
Normal file
@@ -0,0 +1,2 @@
|
||||
BattleButton < CreatureButton
|
||||
&isBattleButton: true
|
36
modules/game_bugreport/bugreport.lua
Normal file
36
modules/game_bugreport/bugreport.lua
Normal file
@@ -0,0 +1,36 @@
|
||||
-- TODO: find another hotkey for this. Ctrl+Z will be reserved to undo on textedits.
|
||||
HOTKEY = 'Ctrl+Z'
|
||||
|
||||
bugReportWindow = nil
|
||||
bugTextEdit = nil
|
||||
|
||||
function init()
|
||||
g_ui.importStyle('bugreport')
|
||||
|
||||
bugReportWindow = g_ui.createWidget('BugReportWindow', rootWidget)
|
||||
bugReportWindow:hide()
|
||||
|
||||
bugTextEdit = bugReportWindow:getChildById('bugTextEdit')
|
||||
|
||||
g_keyboard.bindKeyDown(HOTKEY, show)
|
||||
end
|
||||
|
||||
function terminate()
|
||||
g_keyboard.unbindKeyDown(HOTKEY)
|
||||
bugReportWindow:destroy()
|
||||
end
|
||||
|
||||
function doReport()
|
||||
g_game.reportBug(bugTextEdit:getText())
|
||||
bugReportWindow:hide()
|
||||
modules.game_textmessage.displayGameMessage(tr('Bug report sent.'))
|
||||
end
|
||||
|
||||
function show()
|
||||
if g_game.isOnline() then
|
||||
bugTextEdit:setText('')
|
||||
bugReportWindow:show()
|
||||
bugReportWindow:raise()
|
||||
bugReportWindow:focus()
|
||||
end
|
||||
end
|
9
modules/game_bugreport/bugreport.otmod
Normal file
9
modules/game_bugreport/bugreport.otmod
Normal file
@@ -0,0 +1,9 @@
|
||||
Module
|
||||
name: game_bugreport
|
||||
description: Bug report interface (Ctrl+Z)
|
||||
author: edubart
|
||||
website: https://github.com/edubart/otclient
|
||||
scripts: [ bugreport ]
|
||||
sandboxed: true
|
||||
@onLoad: init()
|
||||
@onUnload: terminate()
|
39
modules/game_bugreport/bugreport.otui
Normal file
39
modules/game_bugreport/bugreport.otui
Normal file
@@ -0,0 +1,39 @@
|
||||
BugReportWindow < MainWindow
|
||||
!text: tr('Report Bug')
|
||||
size: 280 250
|
||||
@onEscape: self:hide()
|
||||
|
||||
Label
|
||||
id: bugLabel
|
||||
!text: tr('Please use this dialog to only report bugs. Do not report rule violations here!')
|
||||
text-wrap: true
|
||||
text-auto-resize: true
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
MultilineTextEdit
|
||||
id: bugTextEdit
|
||||
anchors.top: bugLabel.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: sendButton.top
|
||||
margin-top: 4
|
||||
margin-bottom: 8
|
||||
|
||||
Button
|
||||
id: sendButton
|
||||
!text: tr('Send')
|
||||
anchors.bottom: cancelButton.bottom
|
||||
anchors.right: cancelButton.left
|
||||
margin-right: 10
|
||||
width: 80
|
||||
&onClick: doReport
|
||||
|
||||
Button
|
||||
id: cancelButton
|
||||
!text: tr('Cancel')
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
width: 80
|
||||
@onClick: self:getParent():hide()
|
65
modules/game_console/channelswindow.otui
Normal file
65
modules/game_console/channelswindow.otui
Normal file
@@ -0,0 +1,65 @@
|
||||
ChannelListLabel < Label
|
||||
font: verdana-11px-monochrome
|
||||
background-color: alpha
|
||||
text-offset: 2 0
|
||||
focusable: true
|
||||
|
||||
$focus:
|
||||
background-color: #ffffff22
|
||||
color: #ffffff
|
||||
|
||||
MainWindow
|
||||
id: channelsWindow
|
||||
!text: tr('Channels')
|
||||
size: 250 238
|
||||
@onEscape: self:destroy()
|
||||
|
||||
TextList
|
||||
id: channelList
|
||||
vertical-scrollbar: channelsScrollBar
|
||||
anchors.fill: parent
|
||||
anchors.bottom: next.top
|
||||
margin-bottom: 10
|
||||
padding: 1
|
||||
focusable: false
|
||||
|
||||
Label
|
||||
id: openPrivateChannelWithLabel
|
||||
!text: tr('Open a private message channel:')
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: next.top
|
||||
text-align: center
|
||||
margin-bottom: 2
|
||||
|
||||
TextEdit
|
||||
id: openPrivateChannelWith
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: next.top
|
||||
margin-bottom: 10
|
||||
|
||||
Button
|
||||
id: buttonOpen
|
||||
!text: tr('Open')
|
||||
width: 64
|
||||
anchors.right: next.left
|
||||
anchors.bottom: parent.bottom
|
||||
margin-right: 10
|
||||
@onClick: self:getParent():onEnter()
|
||||
|
||||
Button
|
||||
id: buttonCancel
|
||||
!text: tr('Cancel')
|
||||
width: 64
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
@onClick: self:getParent():destroy()
|
||||
|
||||
VerticalScrollBar
|
||||
id: channelsScrollBar
|
||||
anchors.top: channelList.top
|
||||
anchors.bottom: channelList.bottom
|
||||
anchors.right: channelList.right
|
||||
step: 14
|
||||
pixels-scroll: true
|
206
modules/game_console/communicationwindow.otui
Normal file
206
modules/game_console/communicationwindow.otui
Normal file
@@ -0,0 +1,206 @@
|
||||
IgnoreListLabel < Label
|
||||
font: verdana-11px-monochrome
|
||||
background-color: alpha
|
||||
text-offset: 2 0
|
||||
focusable: true
|
||||
phantom: false
|
||||
|
||||
$focus:
|
||||
background-color: #ffffff22
|
||||
color: #ffffff
|
||||
|
||||
WhiteListLabel < Label
|
||||
font: verdana-11px-monochrome
|
||||
background-color: alpha
|
||||
text-offset: 2 0
|
||||
focusable: true
|
||||
phantom: false
|
||||
|
||||
$focus:
|
||||
background-color: #ffffff22
|
||||
color: #ffffff
|
||||
|
||||
|
||||
MainWindow
|
||||
id: communicationWindow
|
||||
!text: tr('Ignore List')
|
||||
size: 515 410
|
||||
@onEscape: self:destroy()
|
||||
|
||||
CheckBox
|
||||
id: checkboxUseIgnoreList
|
||||
!text: tr('Activate ignorelist')
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
width: 180
|
||||
|
||||
Label
|
||||
!text: tr('Ignored Players:')
|
||||
anchors.left: parent.left
|
||||
anchors.top: prev.bottom
|
||||
margin-top: 10
|
||||
|
||||
TextList
|
||||
id: ignoreList
|
||||
vertical-scrollbar: ignoreListScrollBar
|
||||
anchors.left: parent.left
|
||||
anchors.top: prev.bottom
|
||||
height: 150
|
||||
width: 230
|
||||
margin-bottom: 10
|
||||
margin-top: 3
|
||||
padding: 1
|
||||
focusable: false
|
||||
|
||||
TextEdit
|
||||
id: ignoreNameEdit
|
||||
anchors.top: prev.bottom
|
||||
anchors.left: parent.left
|
||||
width: 110
|
||||
margin-top: 5
|
||||
|
||||
Button
|
||||
id: buttonIgnoreAdd
|
||||
!text: tr('Add')
|
||||
width: 48
|
||||
height: 20
|
||||
margin-left: 5
|
||||
anchors.top: prev.top
|
||||
anchors.left: prev.right
|
||||
|
||||
Button
|
||||
id: buttonIgnoreRemove
|
||||
!text: tr('Remove')
|
||||
width: 64
|
||||
height: 20
|
||||
margin-left: 5
|
||||
anchors.top: prev.top
|
||||
anchors.left: prev.right
|
||||
|
||||
Label
|
||||
!text: tr('Global ignore settings')
|
||||
anchors.left: parent.left
|
||||
anchors.top: prev.bottom
|
||||
margin-top: 20
|
||||
|
||||
CheckBox
|
||||
id: checkboxIgnorePrivateMessages
|
||||
!text: tr('Ignore Private Messages')
|
||||
anchors.left: parent.left
|
||||
anchors.top: prev.bottom
|
||||
width: 180
|
||||
margin-top: 5
|
||||
|
||||
CheckBox
|
||||
id: checkboxIgnoreYelling
|
||||
!text: tr('Ignore Yelling')
|
||||
anchors.left: parent.left
|
||||
anchors.top: prev.bottom
|
||||
width: 180
|
||||
margin-top: 5
|
||||
|
||||
CheckBox
|
||||
id: checkboxUseWhiteList
|
||||
!text: tr('Activate whitelist')
|
||||
anchors.top: parent.top
|
||||
anchors.left: ignoreList.right
|
||||
margin-left: 20
|
||||
width: 180
|
||||
|
||||
Label
|
||||
!text: tr('Allowed Players:')
|
||||
anchors.top: prev.bottom
|
||||
anchors.left: prev.left
|
||||
margin-top: 10
|
||||
|
||||
TextList
|
||||
id: whiteList
|
||||
vertical-scrollbar: whiteListScrollBar
|
||||
anchors.left: prev.left
|
||||
anchors.top: prev.bottom
|
||||
height: 150
|
||||
width: 230
|
||||
margin-bottom: 10
|
||||
margin-top: 3
|
||||
padding: 1
|
||||
focusable: false
|
||||
|
||||
TextEdit
|
||||
id: whitelistNameEdit
|
||||
anchors.top: prev.bottom
|
||||
anchors.left: prev.left
|
||||
width: 110
|
||||
margin-top: 5
|
||||
|
||||
Button
|
||||
id: buttonWhitelistAdd
|
||||
!text: tr('Add')
|
||||
width: 48
|
||||
height: 20
|
||||
margin-left: 5
|
||||
anchors.top: prev.top
|
||||
anchors.left: prev.right
|
||||
|
||||
Button
|
||||
id: buttonWhitelistRemove
|
||||
!text: tr('Remove')
|
||||
width: 64
|
||||
height: 20
|
||||
margin-left: 5
|
||||
anchors.top: prev.top
|
||||
anchors.left: prev.right
|
||||
|
||||
Label
|
||||
!text: tr('Global whitelist settings')
|
||||
anchors.left: whiteList.left
|
||||
anchors.top: prev.bottom
|
||||
margin-top: 20
|
||||
|
||||
CheckBox
|
||||
id: checkboxAllowVIPs
|
||||
!text: tr('Allow VIPs to message you')
|
||||
anchors.left: prev.left
|
||||
anchors.top: prev.bottom
|
||||
width: 180
|
||||
margin-top: 5
|
||||
|
||||
Panel
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
height: 30
|
||||
|
||||
Panel
|
||||
size: 160 30
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
Button
|
||||
id: buttonSave
|
||||
!text: tr('Save')
|
||||
width: 75
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
|
||||
Button
|
||||
id: buttonCancel
|
||||
!text: tr('Cancel')
|
||||
width: 75
|
||||
anchors.top: parent.top
|
||||
anchors.left: prev.right
|
||||
margin-left: 10
|
||||
|
||||
VerticalScrollBar
|
||||
id: ignoreListScrollBar
|
||||
anchors.top: ignoreList.top
|
||||
anchors.bottom: ignoreList.bottom
|
||||
anchors.right: ignoreList.right
|
||||
step: 14
|
||||
pixels-scroll: true
|
||||
|
||||
VerticalScrollBar
|
||||
id: whiteListScrollBar
|
||||
anchors.top: whiteList.top
|
||||
anchors.bottom: whiteList.bottom
|
||||
anchors.right: whiteList.right
|
||||
step: 14
|
||||
pixels-scroll: true
|
1538
modules/game_console/console.lua
Normal file
1538
modules/game_console/console.lua
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user