Resolve "Merge the best from 7.40 branch"

This commit is contained in:
Erikas Kontenis
2022-04-06 14:58:52 +00:00
parent 3bd1a6f07e
commit 155da3573c
928 changed files with 43723 additions and 1221 deletions

View File

@@ -0,0 +1,176 @@
--[[
base64 -- v1.5.1 public domain Lua base64 encoder/decoder
no warranty implied; use at your own risk
Needs bit32.extract function. If not present it's implemented using BitOp
or Lua 5.3 native bit operators. For Lua 5.1 fallbacks to pure Lua
implementation inspired by Rici Lake's post:
http://ricilake.blogspot.co.uk/2007/10/iterating-bits-in-lua.html
author: Ilya Kolbin (iskolbin@gmail.com)
url: github.com/iskolbin/lbase64
COMPATIBILITY
Lua 5.1, 5.2, 5.3, LuaJIT
LICENSE
See end of file for license information.
--]]
base64 = {}
local extract = _G.bit32 and _G.bit32.extract
if not extract then
if _G.bit then
local shl, shr, band = _G.bit.lshift, _G.bit.rshift, _G.bit.band
extract = function( v, from, width )
return band( shr( v, from ), shl( 1, width ) - 1 )
end
elseif _G._VERSION >= "Lua 5.3" then
extract = load[[return function( v, from, width )
return ( v >> from ) & ((1 << width) - 1)
end]]()
else
extract = function( v, from, width )
local w = 0
local flag = 2^from
for i = 0, width-1 do
local flag2 = flag + flag
if v % flag2 >= flag then
w = w + 2^i
end
flag = flag2
end
return w
end
end
end
function base64.makeencoder( s62, s63, spad )
local encoder = {}
for b64code, char in pairs{[0]='A','B','C','D','E','F','G','H','I','J',
'K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y',
'Z','a','b','c','d','e','f','g','h','i','j','k','l','m','n',
'o','p','q','r','s','t','u','v','w','x','y','z','0','1','2',
'3','4','5','6','7','8','9',s62 or '+',s63 or'/',spad or'='} do
encoder[b64code] = char:byte()
end
return encoder
end
function base64.makedecoder( s62, s63, spad )
local decoder = {}
for b64code, charcode in pairs( base64.makeencoder( s62, s63, spad )) do
decoder[charcode] = b64code
end
return decoder
end
local DEFAULT_ENCODER = base64.makeencoder()
local DEFAULT_DECODER = base64.makedecoder()
local char, concat = string.char, table.concat
function base64.encode( str, encoder, usecaching )
encoder = encoder or DEFAULT_ENCODER
local t, k, n = {}, 1, #str
local lastn = n % 3
local cache = {}
for i = 1, n-lastn, 3 do
local a, b, c = str:byte( i, i+2 )
local v = a*0x10000 + b*0x100 + c
local s
if usecaching then
s = cache[v]
if not s then
s = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[extract(v,0,6)])
cache[v] = s
end
else
s = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[extract(v,0,6)])
end
t[k] = s
k = k + 1
end
if lastn == 2 then
local a, b = str:byte( n-1, n )
local v = a*0x10000 + b*0x100
t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[64])
elseif lastn == 1 then
local v = str:byte( n )*0x10000
t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[64], encoder[64])
end
return concat( t )
end
function base64.decode( b64, decoder, usecaching )
decoder = decoder or DEFAULT_DECODER
local pattern = '[^%w%+%/%=]'
if decoder then
local s62, s63
for charcode, b64code in pairs( decoder ) do
if b64code == 62 then s62 = charcode
elseif b64code == 63 then s63 = charcode
end
end
pattern = ('[^%%w%%%s%%%s%%=]'):format( char(s62), char(s63) )
end
b64 = b64:gsub( pattern, '' )
local cache = usecaching and {}
local t, k = {}, 1
local n = #b64
local padding = b64:sub(-2) == '==' and 2 or b64:sub(-1) == '=' and 1 or 0
for i = 1, padding > 0 and n-4 or n, 4 do
local a, b, c, d = b64:byte( i, i+3 )
local s
if usecaching then
local v0 = a*0x1000000 + b*0x10000 + c*0x100 + d
s = cache[v0]
if not s then
local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + decoder[d]
s = char( extract(v,16,8), extract(v,8,8), extract(v,0,8))
cache[v0] = s
end
else
local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + decoder[d]
s = char( extract(v,16,8), extract(v,8,8), extract(v,0,8))
end
t[k] = s
k = k + 1
end
if padding == 1 then
local a, b, c = b64:byte( n-3, n-1 )
local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40
t[k] = char( extract(v,16,8), extract(v,8,8))
elseif padding == 2 then
local a, b = b64:byte( n-3, n-2 )
local v = decoder[a]*0x40000 + decoder[b]*0x1000
t[k] = char( extract(v,16,8))
end
return concat( t )
end
--[[
Copyright (c) 2018 Ilya Kolbin
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.
--]]

View 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

View 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

View File

@@ -0,0 +1,325 @@
-- @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
MouseTouch = 4
MouseTouch2 = 5 -- multitouch, 2nd finger
MouseTouch3 = 6 -- multitouch, 3th finger
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,
Bot = 4
}

View File

@@ -0,0 +1,34 @@
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 'base64'
dofile 'json'
dofile 'http'
dofile 'test'

View 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
-- @}

View File

@@ -0,0 +1,278 @@
HTTP = {
timeout=5,
websocketTimeout=15,
agent="Mozilla/5.0",
imageId=1000,
images={},
operations={},
}
function HTTP.get(url, callback)
if not g_http or not g_http.get then
return error("HTTP.get is not supported")
end
local operation = g_http.get(url, HTTP.timeout)
HTTP.operations[operation] = {type="get", url=url, callback=callback}
return operation
end
function HTTP.getJSON(url, callback)
if not g_http or not g_http.get then
return error("HTTP.getJSON is not supported")
end
local operation = g_http.get(url, HTTP.timeout)
HTTP.operations[operation] = {type="get", json=true, url=url, callback=callback}
return operation
end
function HTTP.post(url, data, callback)
if not g_http or not g_http.post then
return error("HTTP.post is not supported")
end
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 operation
end
function HTTP.postJSON(url, data, callback)
if not g_http or not g_http.post then
return error("HTTP.postJSON is not supported")
end
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 operation
end
function HTTP.download(url, file, callback, progressCallback)
if not g_http or not g_http.download then
return error("HTTP.download is not supported")
end
local operation = g_http.download(url, file, HTTP.timeout)
HTTP.operations[operation] = {type="download", url=url, file=file, callback=callback, progressCallback=progressCallback}
return operation
end
function HTTP.downloadImage(url, callback)
if not g_http or not g_http.download then
return error("HTTP.downloadImage is not supported")
end
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 operation
end
function HTTP.webSocket(url, callbacks, timeout, jsonWebsocket)
if not g_http or not g_http.ws then
return error("WebSocket is not supported")
end
if not timeout or timeout < 1 then
timeout = HTTP.websocketTimeout
end
local operation = g_http.ws(url, timeout)
HTTP.operations[operation] = {type="ws", json=jsonWebsocket, url=url, callbacks=callbacks}
return {
id = operation,
url = url,
close = function()
g_http.wsClose(operation)
end,
send = function(message)
if type(message) == "table" then
message = json.encode(message)
end
g_http.wsSend(operation, message)
end
}
end
HTTP.WebSocket = HTTP.webSocket
function HTTP.webSocketJSON(url, callbacks, timeout)
return HTTP.webSocket(url, callbacks, timeout, true)
end
HTTP.WebSocketJSON = HTTP.webSocketJSON
function HTTP.cancel(operationId)
if not g_http or not g_http.cancel then
return
end
HTTP.operations[operationId] = nil
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
if data and data:len() > 0 then
err = err .. " (" .. data:sub(1, 100) .. ")"
end
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
if data and data:len() > 0 then
err = err .. " (" .. data:sub(1, 100) .. ")"
end
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
if not err then
HTTP.images[url] = path
end
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
function HTTP.onWsOpen(operationId, message)
local operation = HTTP.operations[operationId]
if operation == nil then
return
end
if operation.callbacks.onOpen then
operation.callbacks.onOpen(message, operationId)
end
end
function HTTP.onWsMessage(operationId, message)
local operation = HTTP.operations[operationId]
if operation == nil then
return
end
if operation.callbacks.onMessage then
if operation.json then
local status, result = pcall(function() return json.decode(message) end)
local err = nil
if not status then
err = "JSON ERROR: " .. result
if message and message:len() > 0 then
err = err .. " (" .. message:sub(1, 100) .. ")"
end
end
if err then
if operation.callbacks.onError then
operation.callbacks.onError(err, operationId)
end
else
operation.callbacks.onMessage(result, operationId)
end
else
operation.callbacks.onMessage(message, operationId)
end
end
end
function HTTP.onWsClose(operationId, message)
local operation = HTTP.operations[operationId]
if operation == nil then
return
end
if operation.callbacks.onClose then
operation.callbacks.onClose(message, operationId)
end
end
function HTTP.onWsError(operationId, message)
local operation = HTTP.operations[operationId]
if operation == nil then
return
end
if operation.callbacks.onError then
operation.callbacks.onError(message, operationId)
end
end
connect(g_http,
{
onGet = HTTP.onGet,
onGetProgress = HTTP.onGetProgress,
onPost = HTTP.onPost,
onPostProgress = HTTP.onPostProgress,
onDownload = HTTP.onDownload,
onDownloadProgress = HTTP.onDownloadProgress,
onWsOpen = HTTP.onWsOpen,
onWsMessage = HTTP.onWsMessage,
onWsClose = HTTP.onWsClose,
onWsError = HTTP.onWsError,
})
g_http.setUserAgent(HTTP.agent)

View 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

View File

@@ -0,0 +1,419 @@
--
-- json.lua
--
-- Copyright (c) 2019 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 make_indent(state)
return string.rep(" ", state.currentIndentLevel * state.indent)
end
local function escape_char(c)
return escape_char_map[c] or string.format("\\u%04x", c:byte())
end
local function encode_nil()
return "null"
end
local function encode_table(val, state)
local res = {}
local stack = state.stack
local pretty = state.indent > 0
local close_indent = make_indent(state)
local comma = pretty and ",\n" or ","
local colon = pretty and ": " or ":"
local open_brace = pretty and "{\n" or "{"
local close_brace = pretty and ("\n" .. close_indent .. "}") or "}"
local open_bracket = pretty and "[\n" or "["
local close_bracket = pretty and ("\n" .. close_indent .. "]") or "]"
-- Circular reference?
if stack[val] then error("circular reference") end
stack[val] = true
if rawget(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 _, v in ipairs(val) do
state.currentIndentLevel = state.currentIndentLevel + 1
table.insert(res, make_indent(state) .. encode(v, state))
state.currentIndentLevel = state.currentIndentLevel - 1
end
stack[val] = nil
return open_bracket .. table.concat(res, comma) .. close_bracket
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
state.currentIndentLevel = state.currentIndentLevel + 1
table.insert(res, make_indent(state) .. encode(k, state) .. colon .. encode(v, state))
state.currentIndentLevel = state.currentIndentLevel - 1
end
stack[val] = nil
return open_brace .. table.concat(res, comma) .. close_brace
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, state)
local t = type(val)
local f = type_func_map[t]
if f then
return f(val, state)
end
error("unexpected type '" .. t .. "'")
end
function json.encode(val, indent)
local state = {
indent = indent or 0,
currentIndentLevel = 0,
stack = {}
}
return encode(val, state)
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

View File

@@ -0,0 +1,251 @@
-- @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.areKeysPressed(keyComboDesc)
for i,currentKeyDesc in ipairs(keyComboDesc:split('+')) do
for keyCode, keyDesc in pairs(KeyCodeDescs) do
if keyDesc:lower() == currentKeyDesc:trim():lower() then
if keyDesc:lower() == "ctrl" then
if not g_keyboard.isCtrlPressed() then
return false
end
elseif keyDesc:lower() == "shift" then
if not g_keyboard.isShiftPressed() then
return false
end
elseif keyDesc:lower() == "alt" then
if not g_keyboard.isAltPressed() then
return false
end
elseif not g_window.isKeyPressed(keyCode) then
return false
end
end
end
end
return true
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

View 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

View 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

View 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

View 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

View 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

View File

@@ -0,0 +1,3 @@
g_settings = makesingleton(g_configs.getSettings())
-- Reserved for future functionality

View 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

View 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

View File

@@ -0,0 +1,287 @@
-- @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
function table.isList(t)
local size = #t
return table.size(t) == size and size > 0
end
function table.isStringList(t)
if not table.isList(t) then return false end
for k,v in ipairs(t) do
if type(v) ~= 'string' then
return false
end
end
return true
end
function table.isStringPairList(t)
if not table.isList(t) then return false end
for k,v in ipairs(t) do
if type(v) ~= 'table' or #v ~= 2 or type(v[1]) ~= 'string' or type(v[2]) ~= 'string' then
return false
end
end
return true
end
function table.encodeStringPairList(t)
local ret = ""
for k,v in ipairs(t) do
if v[2]:find("\n") then
ret = ret .. v[1] .. ":[[\n" .. v[2] .. "\n]]\n"
else
ret = ret .. v[1] .. ":" .. v[2] .. "\n"
end
end
return ret
end
function table.decodeStringPairList(l)
local ret = {}
local r = regexMatch(l, "(?:^|\\n)([^:^\n]{1,20}):?(.*)(?:$|\\n)")
local multiline = ""
local multilineKey = ""
local multilineActive = false
for k,v in ipairs(r) do
if multilineActive then
local endPos = v[1]:find("%]%]")
if endPos then
if endPos > 1 then
table.insert(ret, {multilineKey, multiline .. "\n" .. v[1]:sub(1, endPos - 1)})
else
table.insert(ret, {multilineKey, multiline})
end
multilineActive = false
multiline = ""
multilineKey = ""
else
if multiline:len() == 0 then
multiline = v[1]
else
multiline = multiline .. "\n" .. v[1]
end
end
else
local bracketPos = v[3]:find("%[%[")
if bracketPos == 1 then -- multiline begin
multiline = v[3]:sub(bracketPos + 2)
multilineActive = true
multilineKey = v[2]
elseif v[2]:len() > 0 and v[3]:len() > 0 then
table.insert(ret, {v[2], v[3]})
end
end
end
return ret

View File

@@ -0,0 +1,62 @@
Test = {
tests = {},
activeTest = 0,
screenShot = 1
}
Test.Test = function(name, func)
local testId = #Test.tests + 1
Test.tests[testId] = {
name = name,
actions = {},
delay = 0,
start = 0
}
local test = function(testFunc)
table.insert(Test.tests[testId].actions, {type = "test", value = testFunc})
end
local wait = function(millis)
Test.tests[testId].delay = Test.tests[testId].delay + millis
table.insert(Test.tests[testId].actions, {type = "wait", value = Test.tests[testId].delay})
end
local ss = function()
table.insert(Test.tests[testId].actions, {type = "screenshot"})
end
local fail = function(message)
g_logger.fatal("Test " .. name .. " failed: " .. message)
end
func(test, wait, ss, fail)
end
Test.run = function()
if Test.activeTest > #Test.tests then
g_logger.info("[TEST] Finished tests. Exiting...")
return g_app.exit()
end
local test = Test.tests[Test.activeTest]
if not test or #test.actions == 0 then
Test.activeTest = Test.activeTest + 1
local nextTest = Test.tests[Test.activeTest]
if nextTest then
nextTest.start = g_clock.millis()
g_logger.info("[TEST] Starting test: " .. nextTest.name)
end
return scheduleEvent(Test.run, 500)
end
local action = test.actions[1]
if action.type == "test" then
table.remove(test.actions, 1)
action.value()
elseif action.type == "screenshot" then
table.remove(test.actions, 1)
g_app.doScreenshot(Test.screenShot .. ".png")
Test.screenShot = Test.screenShot + 1
elseif action.type == "wait" then
if action.value + test.start < g_clock.millis() then
table.remove(test.actions, 1)
end
end
scheduleEvent(Test.run, 100)
end

View 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

View File

@@ -0,0 +1,124 @@
-- @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()
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)
connect(rootWidget, {
onMouseMove = moveToolTip,
})
end
function g_tooltip.hide()
g_effects.fadeOut(toolTipLabel, 100)
disconnect(rootWidget, {
onMouseMove = moveToolTip,
})
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 })

View 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

View 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

View File

@@ -0,0 +1,184 @@
-- @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:clear()
return self:clearOptions()
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 or self.disableScroll 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

View 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

View 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

View 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

View 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

View File

@@ -0,0 +1,516 @@
-- @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()
if self.minimizeButton then
self.minimizeButton:setOn(true)
end
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()
if self.minimizeButton then
self.minimizeButton:setOn(false)
end
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
if self.closeButton then
self.closeButton:hide()
end
end
if(self.minimizeButton) then
self.minimizeButton.onClick =
function()
if self:isOn() then
self:maximize()
else
self:minimize()
end
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 = {}
if g_settings.getNodeSize('MiniWindows') < 100 then
settings = g_settings.getNode('MiniWindows')
end
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 and not self.containerWindow then
self:close(true)
end
if selfSettings.locked then
self:lock(true)
end
else
if not self.forceOpen and self.autoOpen ~= nil and (self.autoOpen == 0 or self.autoOpen == false) and not self.containerWindow then
self:close(true)
end
end
end
local newParent = self:getParent()
self.miniLoaded = true
if self.save then
if oldParent and oldParent:getClassName() == 'UIMiniWindowContainer' and not self.containerWindow 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)
local children = rootWidget:recursiveGetChildrenByMarginPos(mousePos)
local dropInPanel = 0
for i=1,#children do
local child = children[i]
if child:getId():contains('gameLeftPanel') or child:getId():contains('gameRightPanel') then
dropInPanel = 1
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())
break
end
end
if dropInPanel == 0 then
tmpp = self
if(modules.game_interface.getLeftPanel():isVisible()) then
if modules.game_interface.getRootPanel():getWidth() / 2 < mousePos.x then
addEvent(function() tmpp:setParent(modules.game_interface.getRightPanel())
if tmpp.movedWidget then
tmpp.setMovedChildMargin(tmpp.movedOldMargin or 0)
tmpp.movedWidget = nil
tmpp.setMovedChildMargin = nil
tmpp.movedOldMargin = nil
tmpp.movedIndex = nil
end
UIWindow:onDragLeave(tmpp, droppedWidget, mousePos)
tmpp:saveParent(tmpp:getParent())
end)
else
addEvent(function() tmpp:setParent(modules.game_interface.getLeftPanel())
if tmpp.movedWidget then
tmpp.setMovedChildMargin(tmpp.movedOldMargin or 0)
tmpp.movedWidget = nil
tmpp.setMovedChildMargin = nil
tmpp.movedOldMargin = nil
tmpp.movedIndex = nil
end
UIWindow:onDragLeave(tmpp, droppedWidget, mousePos)
tmpp:saveParent(tmpp:getParent())
end)
end
else
addEvent(function() tmpp:setParent(modules.game_interface.getRightPanel())
if tmpp.movedWidget then
tmpp.setMovedChildMargin(tmpp.movedOldMargin or 0)
tmpp.movedWidget = nil
tmpp.setMovedChildMargin = nil
tmpp.movedOldMargin = nil
tmpp.movedIndex = nil
end
UIWindow:onDragLeave(tmpp, droppedWidget, mousePos)
tmpp:saveParent(tmpp:getParent())
end)
end
end
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:clearSettings()
if not self.save then return end
local settings = g_settings.getNode('MiniWindows')
if not settings then
settings = {}
end
local id = self:getId()
settings[id] = {}
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

View File

@@ -0,0 +1,228 @@
-- @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
local oldParent = nWidget:getParent()
if oldParent ~= self then
if oldParent then
oldParent:removeChild(nWidget)
end
self:insertChild(nIndex, nWidget)
else
self:moveChildToIndex(nWidget, nIndex)
end
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
table.sort(children, function(a, b)
local indexA = a.miniIndex or a.autoOpen or 999
local indexB = b.miniIndex or b.autoOpen or 999
return indexA < indexB
end)
self:reorderChildren(children)
local ignoreIndex = 0
for i=1,#children do
if children[i].save then
children[i].miniIndex = i - ignoreIndex
else
ignoreIndex = ignoreIndex + 1
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

View File

@@ -0,0 +1,505 @@
-- @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:clearTabs()
while #self.tabs > 0 do
self:removeTab(self.tabs[#self.tabs])
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
table.remove(tabTable, index)
if self.currentTab == tab then
self:selectPrevTab()
if #self.tabs == 1 then
self.currentTab = nil
end
end
if tab.blinkEvent then
removeEvent(tab.blinkEvent)
end
updateTabs(self)
tab:destroy()
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

View 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 = ymax - 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 = xmax - 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 } )

View 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} )

View 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

View 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

View 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

View 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

View File

@@ -0,0 +1,290 @@
-- @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)
if g_app.isMobile() then
px = math.max(proportion * pxrange, 24)
end
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() or self.disableScroll 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

View File

@@ -0,0 +1,191 @@
-- @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 or self.disableScroll 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
self.maximum = value
addEvent(function() self:setMaximum(value) end)
elseif name == 'minimum' then
self.minimum = value
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)
if type(value) == "string" then
value = tonumber(value)
end
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

View 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

View File

@@ -0,0 +1,163 @@
-- @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
function UITabBar:clearTabs()
while #self.tabs > 0 do
self:removeTab(self.tabs[#self.tabs])
end
end

View 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

View 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

View 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

View 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 false
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

View File

@@ -0,0 +1,376 @@
-- @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.exit()
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
function comma_value(amount)
local formatted = amount
while true do
formatted, k = string.gsub(formatted, "^(-?%d+)(%d%d%d)", '%1,%2')
if (k==0) then
break
end
end
return formatted
end
-- @}