saveProjectTab("Main",[[-- codea-scm supportedOrientations(LANDSCAPE_ANY) -- Use this function to perform your initial setup function setup() displayMode(STANDARD) saveG() saveProjectInfo("Author", "Anton Jouline") saveProjectInfo("Description", "Source control hub for your Codea projects") -- globals serializer = Serializer().engine apiBase = readLocalData("apiBase") or "https://codea-scm.aws.mapote.com" message = {} identity = readLocalData("identity") if identity then local pub user = string.match(identity, "([^:]+)") pub, private_key = readKeys(user) end setParameters() initProjects() if not identity then print("No identity. Please log in.") end VERSION = readLocalData("version") or "master" STATUS = user and string.format("%s @ %s", user, VERSION) or VERSION STATUS = STATUS .. "\n" .. apiBase sx,sy = nil,nil -- diff window diffW = Diff() end function hexDecode(s) return s and string.gsub(s, "..", function(v) return string.char(tonumber(v, 16)) end) or nil end function hexEncode(s) return s and string.gsub(s, ".", function(v) return string.format("%02x", string.byte(v)) end) or nil end function readProjectFile(project, name, warn) local path = os.getenv("HOME") .. "/Documents/" local file = io.open(path .. project .. ".codea/" .. name,"r") if file then local plist = file:read("*all") file:close() return plist elseif warn then print("WARNING: unable to read " .. name) end end function readProjectPlist(project) return readProjectFile(project, "Info.plist", true) end function saveProjectFile(project, name, plist) local path = os.getenv("HOME") .. "/Documents/" local file = io.open(path .. project .. ".codea/" .. name, "w") if file then file:write(plist) file:close() else print("WARNING: unable to save " .. name) end end function saveProjectPlist(project, plist) return saveProjectFile(project, "Info.plist", plist) end function saveG() if not g then local g = {} for k,v in pairs(_G) do g[k] = v end _G.g = g end end -- this function mimics some of the behaviour of Codea's restart. -- we don't really need a full restart, just some state reset. function reset() tween.delay(0.2, function() -- clear global vars local g = g local G = _G for k,v in pairs(G) do G[k] = nil end for k,v in g.pairs(g) do G[k] = v end _G.g = g -- run setup output.clear() parameter.clear() setup() end) end function urlEncode(str) if str then str = string.gsub(str, "\n", "\r\n") str = string.gsub(str, "([^%w %-%_%.%~])", function (c) return string.format("%%%02X", string.byte(c)) end) str = string.gsub(str, " ", "+") end return str end function readKeys(username) local pub = readGlobalData("codea-scm.key." .. username .. ".pub") local key = readGlobalData("codea-scm.key." .. username .. ".key") return pub, key end function saveKeys(username, pub, key) saveGlobalData("codea-scm.key." .. username .. ".pub", pub) saveGlobalData("codea-scm.key." .. username .. ".key", key) end function manageKeys(enteredName, backAction) if user then parameter.action("Show public key", function() local nick = tostring(user) local public_key = readKeys(nick) openURL(string.format("%s/whoami?nick=%s&public_key=%s", apiBase, urlEncode(nick), urlEncode(public_key or "") ), true) end) end if not user then parameter.action("New keypair", function() parameter.clear() local pub, pk = readKeys(enteredName) if pub and pk then message.warning = "This will overwrite your existing keypair. " .. "To proceed, click 'make keys'" parameter.watch("message.warning") else message.information = "This will create a new pair of SSH keys and store " .. "them on your device for user: " .. enteredName parameter.watch("message.information") end parameter.action("make keys", function() local name = enteredName if #name < 3 then print("PROBLEM: username too short. Minimum - 3 chars") setParameters() return end print("Downloading new keypair ...") http.request(apiBase .. "/keymaker", function(data, status, headers) if tonumber(status) == 200 then local t = serializer.decode(data) saveKeys(name, t.public_key, t.private_key) saveLocalData("identity", name .. ":nopass") print(string.format("Keypair stored for: %s", name)) tween.delay(1.0, reset) else print("ERROR: " .. data) end end, function(err) print("PROBLEM: " .. err) end, {method = "POST"}) setParameters() end) parameter.action("cancel", function() (backAction or setParameters)() end) end) else parameter.action("Delete keypair", function() parameter.clear() message.warning = "This will permanently delete your keypair. " .. "You will then need to create a new one, in order to log " .. "back in as: " .. user parameter.watch("message.warning") parameter.action("Ok, delete it", function() saveKeys(user, nil, nil) saveLocalData("identity", nil) reset() end) parameter.action("cancel", setParameters) end) end end function checkReturn(global, value, cb) if value:sub(#value,#value) == "\n" then showKeyboard() hideKeyboard() _G[global] = value:gsub("\n","") if cb then cb() end end end function loginMenu() parameter.clear() parameter.text("username", function(value) checkReturn("username", value, doLogin) end) parameter.action("login", doLogin) parameter.action("cancel", setParameters) end function doLogin() if #username < 3 then print("PROBLEM: username too short. Minimum - 3 chars") return end local pub, pk = readKeys(username) if not pub or not pk then parameter.clear() message.greeting = "Hello, " .. username .. ". You have no keys stored. " .. "Press 'New keypair' to create them." parameter.watch("message.greeting") manageKeys(username) parameter.action("cancel", setParameters) return end saveLocalData("identity", username .. ":nopass") username, password = nil, nil reset() end function setParameters() parameter.clear() if readLocalData("identity") then parameter.action(string.format("User (%s)", user), function() parameter.clear() parameter.action("Logout", function() saveLocalData("identity",nil) reset() end) manageKeys(user) parameter.action("cancel", setParameters) end) else parameter.action("Login", loginMenu) end parameter.action("link project", function() parameter.clear() parameter.text("project",function() if project and project ~= "" then project = project:gsub("\n","") remote = readLocalData("project." .. project .. ".remote") end end) parameter.text("remote") parameter.action("link", function() if project and project ~= "" then project = project:gsub("\n","") saveLocalData("project." .. project .. ".remote", remote) initProjects() end setParameters() end) parameter.action("cancel", function() setParameters() end) end) parameter.action("unlink project", function() parameter.clear() parameter.text("project") parameter.action("unlink", function() if project and project ~= "" then project = project:gsub("\n","") saveLocalData("project." .. project .. ".remote", nil) initProjects() end setParameters() end) parameter.action("cancel", function() setParameters() end) end) parameter.boolean("checkOnLoad", readLocalData("checkOnLoad"), function(value) saveLocalData("checkOnLoad", value or nil) end) parameter.action("Help", function() openURL("https://codea-scm.aws.mapote.com", true) end) end function initProjects() panel = Panel(12, 72, WIDTH-24, HEIGHT-84) -- clear global arrays buttons, statuses, commitButtons, resetButtons = {}, {}, {}, {} -- read project data from local storage local count = 0 for i,p in iterProjects() do local project = p.name print("Project: " .. p.name) local t = listProjectTabs(p.name) for _,k in ipairs(t) do local tab = readProjectTab(p.name .. ":" .. k) print("Tab: " .. k .. " (" .. #tab .. " chars)") end local b = Button(project, 80, HEIGHT-20-i*55, 300, 50) b.clicked = function(x) checkStatus(x, true, showDiff) end buttons[#buttons + 1] = b panel:add(b) local s = Status(b.x + 300 + 30, b.y + 25, 45, project) statuses[#statuses + 1] = s b.status = s b.project = p panel:add(s, 45) local cb = Button("push", s.x + 50, b.y, 80, 50) cb.clicked = commitAndPushProject cb.status = s cb.project = p commitButtons[#commitButtons + 1] = cb panel:add(cb) local rb = Button("pull", cb.x + 190, b.y, 70, 50) rb.clicked = pullProject rb.status = s rb.project = p resetButtons[#resetButtons + 1] = rb panel:add(rb) if user then -- schedule a status check if checkOnLoad then tween.delay(1.0, function() checkStatus(b) end) end end count = count + 1 end print("Num projects: " .. count) end function checkStatus(b, getDiff,f) if b.status.working then return end b.status:startAnimation() status(b.project, getDiff, function(...) local t = serializer.decode(...) b.status.c = ({ [256] = Status.blue, [0] = Status.green, })[t.status] or Status.grey if f then f(t) end b.status:stopAnimation() end, function(err) print("PROBLEM: " .. tostring(err)) b.status:stopAnimation() b.status.c = Status.grey end) end function commitAndPushProject(b) parameter.clear() parameter.text("comment") parameter.action("commit", function() setParameters() if b.status.working then return end b.status:startAnimation() push(b.project, comment, function(res) b.status:stopAnimation() local t = serializer.decode(res) if t.commit_status == 0 and t.push_status == 0 then b.status.c = Status.green end end, function(err) print("PROBLEM: " .. tostring(err)) b.status:stopAnimation() b.status.c = Status.grey end) comment = nil end) parameter.action("cancel", function() setParameters() end) end function getLog(b) b.status:startAnimation() log(b.project, function(res) b.status:stopAnimation() local t = serializer.decode(res) pullProject(b, t.log, true) end) end function pullProject(b, logt, showHistory) parameter.clear() parameter.text("version","latest") if showHistory and logt then parameter.number("history", 1, #logt, 1, function(value) value = math.floor(value+0.5) history = value local tags = logt[value].tags if tags then tags = tags:gsub("origin/[^%s]+",""):gsub(",+",","):gsub("%s+"," ") end version = logt[value].hash version = tags and (version .. " " .. tags) or version end) end parameter.action("pull", function() parameter.clear() message.warning = "This will replace the current code on device " .. "with the version from the remote repository" parameter.watch("message.warning") parameter.action("Yes, do it", function() if b.status.working then return end local ref = version ~= "latest" and version or "master" ref = ref:match("[^%s]+") b.status:startAnimation() pull(b.project, ref, function(res) b.status:stopAnimation() local t = serializer.decode(res) if t.clone_status == 0 and t.checkout_status == 0 then for name, content in pairs(t.tabs) do saveProjectTab(b.project.name .. ":" .. name, content) end local c = listProjectTabs(b.project.name) for i,name in ipairs(c) do if not t.tabs[name] and name ~= "Main" then print("deleting tab " .. name) saveProjectTab(b.project.name .. ":" .. name, nil) end end -- Info.plist if t.plist then saveProjectPlist(b.project.name, t.plist) end -- Icon if t.icon then saveProjectFile(b.project.name, "Icon.png", hexDecode(t.icon)) end b.status.c = (ref == "master" or ref == "HEAD" or (logt and logt[1].hash == ref)) and Status.green or Status.purple else b.status.c = Status.grey end end, function(err) print("PROBLEM: " .. tostring(err)) b.status:stopAnimation() b.status.c = Status.grey end) version = nil setParameters() end) parameter.action("No, keep what i have", function() version = nil setParameters() end) end) if not showHistory then parameter.action("recent history", function() getLog(b) end) end parameter.action("cancel", function() version = nil b.status:stopAnimation() setParameters() end) end function showDiff(t) if t.diff then --alert(t.diff, "diff") diffW:show(t.diff) end end function iterProjects() local t = {} for _,k in ipairs(listLocalData()) do local name = string.match(k, "project[.]([^.]+)[.]remote") if name then t[name] = {name = name, remote = readLocalData(k)} end end local names = {} for k,_ in pairs(t) do names[#names + 1] = k end table.sort(names, function(a,b) return tostring(a):lower() < tostring(b):lower() end) local j = 0 return function() j = j + 1 local k = names[j] return k and j,t[k] or nil end end -- This function gets called once every frame function draw() -- This sets a dark background color background(50, 50, 55, 255) -- Do your drawing here if panel then panel:draw() end font("GillSans") fontSize(22) if not sx then sx,sy = textSize(STATUS) end fill(141, 143, 157, 255) textMode(CORNER) textAlign(RIGHT) text(STATUS, WIDTH-sx-12, 12) diffW:draw() end function touched(touch) if diffW:touched(touch) then -- diff window is modal return end panel:touched(touch) end function prepData(project) local data = { remote = project.remote, ["plist"] = readProjectPlist(project.name), ["icon"] = hexEncode(readProjectFile(project.name, "Icon.png")), } local t = listProjectTabs(project.name) for i,name in ipairs(t) do data.tabs = data.tabs or {} data.tabs[name] = readProjectTab(project.name .. ":" .. name) end return data end function postTo(path, body, cb, eb) if not user then if eb then eb("No identity") end return end http.request( apiBase .. path, function(data, status, headers) local maxlen = 200 if #data > maxlen then print("response:", data:sub(1,maxlen), "...") else print("response:", data) end if cb then cb(data) end end, function(error) if eb then eb(error) end end, { method = "POST", data = body, headers = { Authorization = "CodeaScmUser " .. user}} ) end function status(project, get_diff, f, e) local data = prepData(project) if not data.tabs then return e("no tabs found for project: " .. project.name) end data.get_diff = get_diff data.pk = private_key postTo("/status.lua", serializer.encode(data), f, e) end function push(project, comment, f, e) local data = prepData(project) if not data.tabs then return e("no tabs found for project: " .. project.name) end data.comment = comment ~= "" and comment or nil data.pk = private_key postTo("/checkpoint.lua", serializer.encode(data), f, e) end function pull(project, ref, f, e) if #listProjectTabs(project.name) < 1 then return e(string.format("project '%s' does not seem to exist. " .. "Please create it first.", project.name)) end local data = {remote = project.remote, ref = ref, pk = private_key} postTo("/pull.lua", serializer.encode(data), f, e) end function log(project, f, e) local n = tonumber(readLocalData("logLength")) or 5 local data = {remote = project.remote, n = n, pk = private_key} postTo("/log.lua", serializer.encode(data), f, e) end ]]) saveProjectTab("Diff",[[Diff = class() function Diff:init() -- you can accept and set parameters here self.window = {20,20,WIDTH - 40,HEIGHT - 40} self.diffColors = { color(189, 189, 189, 255), color(192, 31, 18, 255), color(55, 166, 44, 255), color(81, 107, 206, 255) } self.markerX = WIDTH - 60 self.markerY = HEIGHT - 60 self.markerW = 35 self.markerH = 35 self.markerAlpha = 255 self.mnw = {x = self.markerX, y = self.markerY + self.markerH} self.msw = {x = self.markerX, y = self.markerY} self.mne = {x = self.markerX + self.markerW, y = self.markerY + self.markerH} self.mse = {x = self.markerX + self.markerW, y = self.markerY} end function Diff:draw() -- Codea does not automatically call this method if self.visible then pushStyle() fill(35, 35, 39, 245) rect(10,10,WIDTH-20,HEIGHT-20) clip(unpack(self.window)) -- text local chunks = self.chunks local colors = self.colors local sizes = self.sizes if chunks then local w, h textWrapWidth(WIDTH - 40) textAlign(LEFT) font("Inconsolata") fontSize(17) if not self.ypos then -- calculate initial position w, h = textSize(chunks[1]) sizes[1] = h self.ypos = HEIGHT - 20 - h self.miny = self.ypos self.ylimit = 20 end local y = self.ypos + sizes[1] local n = #chunks for j=1,n do local c = chunks[j] h = sizes[j] if not h then w, h = textSize(c) sizes[j] = h self.ylimit = self.ylimit + h if j == n and not self.maxy then self.maxy = math.max(self.ylimit, self.miny) end end y = y - h if y >= -HEIGHT and y <= HEIGHT then fill(colors[j]) text(c, 20, y) end end end -- check for scroll ends if self.scrollTween then local ypos = self.ypos if ypos + 150 < self.miny then self:stopScroll() self:bounceBack() elseif self.maxy and self.maxy < ypos - 150 then self:stopScroll() self:bounceBack() end end -- closer if self.markerAlpha > 0 then smooth() strokeWidth(6.0) stroke(59, 59, 59, self.markerAlpha) line(self.msw.x, self.msw.y, self.mne.x, self.mne.y) line(self.mnw.x, self.mnw.y, self.mse.x, self.mse.y) end popStyle() end end function Diff:setContent(t) -- break the text into chunks local linesInChunk = 20 local chunks = {} local colors = {} local diffColors = self.diffColors local pos, len = 1, #t local s, lc = pos, 0 local prevState, state = 1, 1 while pos <= len do local bb,be = t:find("[\r]?\n", pos) if not be then be = len end lc = lc + 1 local f = t:sub(pos,pos) if t:sub(pos,be):match("Binary files .* differ%s*$") then state = 4 elseif f == "+" then state = 3 elseif f == "-" then state = 2 else state = 1 end if lc >= linesInChunk or be == len or prevState ~= state then if prevState == state then chunks[#chunks + 1] = t:sub(s, be) colors[#colors + 1] = diffColors[state] pos = be + 1 s = pos lc = 0 else chunks[#chunks + 1] = t:sub(s, pos - 1) colors[#colors + 1] = diffColors[prevState] s = pos lc = 0 end else pos = be + 1 end prevState = state end self.chunks = chunks self.colors = colors self.sizes = {} end function Diff:show(content) self:setContent(content) self.visible = true self:showMarker() end function Diff:hide() self.visible = nil self.miny, self.maxy = nil, nil self.ypos, self.ylimit = nil, nil end function Diff:showMarker() if self.markerTween then tween.stop(self.markerTween) end self.markerAlpha = 255 tween.delay(1.0, function() self.markerTween = tween(1.0, self, {markerAlpha = 0}, tween.easing.linear, function() self.markerTween = nil end) end) end function Diff:stopScroll() if self.scrollTween then tween.stop(self.scrollTween) self.scrollTween = nil end end function Diff:scrollFinish(touch) self:stopScroll() -- inertia local ypos = self.ypos if self.miny <= ypos and (not self.maxy or ypos <= self.maxy) then ypos = self.ypos + touch.deltaY*30 self.scrollTween = tween(1.0, self, {ypos = ypos}, tween.easing.quadOut, function() self:bounceBack() end) else self:bounceBack() end end function Diff:bounceBack() -- bounce back if needed local ypos = math.max(self.miny, self.ypos) ypos = self.maxy and math.min(self.maxy, ypos) or ypos tween(0.3, self, {ypos = ypos}, tween.easing.quadOut, function() self.scrollTween = nil end) end function Diff:touched(touch) -- Codea does not automatically call this method if self.visible then if touch.state == BEGAN then self:stopScroll() elseif touch.state == ENDED then self.wasMoving, self.moving = self.moving, nil if self:insideMarker(touch) then self:hide() return true elseif not self.wasMoving then -- show closer marker self:showMarker() end -- smooth scrolling if self.wasMoving then self:scrollFinish(touch) end elseif touch.state == MOVING then self.moving = true self.ypos = self.ypos + touch.deltaY end return true end end function Diff:insideMarker(touch) return (self.msw.x <= touch.x and touch.x <= self.mne.x and self.msw.y <= touch.y and touch.y <= self.mne.y) end ]]) saveProjectTab("Serializer",[=[Serializer = class() local function assign(obj, name_spec, value) if not name_spec or name_spec == "" then return obj end local o = obj for w, d in name_spec:gmatch('([%w_]+)(%.?)') do w = tonumber(w) or w if d == "." then o[w] = o[w] or {} o = o[w] else o[w] = value break end end return obj end local function decode(data) local t = {} local pos, len = 1, #data local in_string, line local line_count = 0 while pos < len do local s, e = data:find('\n', pos) if not s then line = data:sub(pos) else line = data:sub(pos, s-1) end line_count = line_count + 1 local name, value = line:match('([%w_.]+)%s*=%s*([^\r]*)') if name and value then local marker = value:match('%[([=]*)%[') if marker then -- it's a string local sb, eb = data:find(string.format('[[]%s[[]', marker), pos) local sn, en = data:find(string.format(']%s]', marker), pos) if sn then assign(t, name, data:sub(eb + 1, sn - 1)) pos = en + 1 else return nil, string.format("unterminated string in line: #%s", line_count) end else local v = tonumber(value) if v then -- it's a number assign(t, name, v) else -- must be a boolean or nil v = string.lower(value:match('(%w+)') or '') if v == 'true' or v == 'false' then assign(t, name, v == 'true') end end pos = e and (e + 1) or len end else pos = e and (e + 1) or len end end return t end local function _encode(t, r, parent) for k,v in pairs(t) do k = tostring(k) local name = parent and (parent .. "." .. k) or k if type(v) == 'table' then _encode(v, r, name) elseif type(v) == 'string' then local marker = v:match('%[([=]*)%[') marker = marker or v:match(']([=]*)]') marker = marker and (marker .. "=") or "" r[#r + 1] = string.format('%s = [%s[%s]%s]', name, marker, v, marker) else r[#r + 1] = string.format('%s = %s', name, tostring(v)) end end end local function encode(t) local r = {} _encode(t, r) r[#r + 1] = "" return table.concat(r, '\r\n') end function Serializer:init() self.engine = { decode = decode, encode = encode, } end ]=]) saveProjectTab("Status",[[-- Color-coded indicator to show status of the project Status = class() Status.green = color(86, 185, 45, 255) Status.blue = color(77, 103, 179, 255) Status.yellow = color(192, 200, 41, 255) Status.orange = color(212, 154, 33, 255) Status.purple = color(147, 46, 155, 255) Status.grey = color(104, 104, 104, 255) Status.lightgrey = color(196, 196, 196, 255) Status.darkgrey = color(76, 76, 76, 255) Status.white = color(255, 255, 255, 255) local N = 12 function Status:init(x, y, d, project) -- you can accept and set parameters here self.x = x self.y = y self.d = d self.project = project self.c = Status.grey self.arrow = 0 end function Status:draw() -- Codea does not automatically call this method pushStyle() if self.working then local x,y,r = self.x,self.y,self.d/2 local n = 6 local w = 0.6 local j = self.arrow - n strokeWidth(4.0) smooth() for i=1,N do local alpha = math.pi*2/N*i local sin,cos = math.sin(alpha),math.cos(alpha) if (jN+j then -- lighter local k = self.arrow - i k = (k >= 0) and k or N+k stroke(Status.darkgrey:mix(Status.lightgrey, k/n)) else stroke(Status.darkgrey) end line(x+sin*r*w, y+cos*r*w, x+sin*r, y+cos*r) end else strokeWidth(1.0) stroke(19, 19, 19, 255) fill(self.c) ellipse(self.x, self.y, self.d) end popStyle() end function Status:startAnimation() if self.working then return end self.arrow = 0 self.working = true self._tween = tween(1.5, self, {arrow = N}, { easing = tween.easing.linear, loop = tween.loop.forever}, function() self.arrow = 0 end) end function Status:stopAnimation() if self._tween then tween.stop(self._tween) end self.working = false local d = self.d self.d = d/4 tween(0.5, self, {d = d}, tween.easing.cubicOut) end ]]) saveProjectTab("Button",[[Button = class() function Button:init(label, x, y, w, h, fillColor) -- you can accept and set parameters here self.label = label self.x = x self.y = y self.w = w self.h = h self.fillColor = fillColor or color(144, 144, 156, 255) local c = color(0, 0, 0, 255) self.strokeColor = self.fillColor:mix(c, 0.2) self._strokeColor = self.strokeColor self._fillColor = self.fillColor self._white = color(200, 200, 200, 255) self._black = color(60, 60, 70, 255) end function Button:draw() -- Codea does not automatically call this method strokeWidth(1.5) fill(self.fillColor) stroke(self.strokeColor) rectMode(CORNER) rect(self.x, self.y, self.w, self.h) textMode(CORNER) font("HelveticaNeue-CondensedBold") fill(self.pressed and self._white or self._black) fontSize(30) text(self.label, self.x+10, self.y+10) end function Button:touched(touch) -- Codea does not automatically call this method local x, y = self.x, self.y local w, h = self.w, self.h local clicked = false if (x <= touch.x and touch.x <= x+w) then if (y <= touch.y and touch.y <= y+h) then --print("button event" .. touch.state) if touch.state == BEGAN then self.pressed = true --self.fillColor = self.fillColor:mix(black, 0.8) --self.strokeColor = self.strokeColor:mix(black, 0.2) self.fillColor, self.strokeColor = self.strokeColor, self.fillColor sound(DATA, "ZgJANwAiQHM6QEBAAAAAADQfND7Cvvs+fwBAf0BAQEA8QEBA") elseif touch.state == ENDED and self.pressed then clicked = true end end end if self.pressed and touch.state == ENDED then -- reset state self.pressed = false self.strokeColor = self._strokeColor self.fillColor = self._fillColor end if clicked then self:clicked() end end function Button:clicked() -- redefine this in subclasses end]]) saveProjectTab("Readme",[[-------------------------------------- -- code-scm - is a source control hub -- for your Codea projects -------------------------------------- ]]) saveProjectTab("Panel",[[Panel = class() function Panel:init(x, y, w, h) -- you can accept and set parameters here self.x = x self.y = y self.w = w self.h = h self.ypos = 0 self.yposMax = 0 self.children = {} local th, bh = 130, 150 local bv = {vec2(x,y), vec2(x,y+bh), vec2(x+w,y+bh), vec2(x+w,y)} local tv = {vec2(x,y+h-th), vec2(x,y+h), vec2(x+w,y+h), vec2(x+w,y+h-th)} local c1 = color(0, 0, 0, 90) local c2 = color(0, 0, 0, 0) self.bottomShade = mesh() self.bottomShade.vertices = {bv[1],bv[2],bv[3],bv[4],bv[1],bv[3]} self.bottomShade.colors = {c1,c2,c2,c1,c1,c2} self.topShade = mesh() self.topShade.vertices = {tv[1],tv[2],tv[3],tv[4],tv[1],tv[3]} self.topShade.colors = {c2,c1,c1,c2,c2,c1} end function Panel:add(child, h) self.children[child] = true self.yposMax = math.max(self.yposMax, self.y - (child.y - 20)) end function Panel:remove(child) self.children[child] = nil end function Panel:draw() -- Codea does not automatically call this method pushMatrix() clip(self.x, self.y, self.w, self.h) translate(0, self.ypos) for c,_ in pairs(self.children) do c:draw() end translate(0, -self.ypos) self.topShade:draw() self.bottomShade:draw() clip() popMatrix() end function Panel:inside(touch) local x,y,w,h = self.x,self.y,self.w,self.h if y <= touch.y and touch.y <= y+h then if x <= touch.x and touch.x <= x+w then return true end end end function cancelPanelTween() if panelTween then tween.stop(panelTween) panelTween = nil end end function Panel:relayTouch(touch) childTouchOk = true -- translate touch to account for scrolling local t = {x = touch.x, y = touch.y - self.ypos, state = touch.state} -- let children react to touch for c,_ in pairs(self.children) do if c.touched then c:touched(t) end end end function Panel:touched(touch) if self:inside(touch) then if touch.state == BEGAN then -- delay the processing a bit to allow for scrolling childTouchOk = nil panelTween = tween.delay(0.1, function() self:relayTouch(touch) end) elseif touch.state == MOVING then -- cancel BEGAN touch event if we are scrolling the panel cancelPanelTween() self.ypos = math.min(math.max(0, self.ypos + touch.deltaY), self.yposMax) if childTouchOk then self:relayTouch(touch) end elseif touch.state == ENDED then if childTouchOk then self:relayTouch(touch) else cancelPanelTween() end end end end ]]) saveLocalData("version","v1.2")