From 39098cc206e49d225e13e3ae6a2ffcb36cd9a2e3 Mon Sep 17 00:00:00 2001 From: Ryan Ward Date: Sun, 11 Oct 2026 09:16:08 -0700 Subject: [PATCH] working on in app editor --- gui | 2 +- main.lua | 373 ++++++++++++++++++++++++++++++++++- menus/board.lua | 59 ++++-- templates/imageword.lua | 7 +- templates/multiplechoice.lua | 11 +- templates/one-image.lua | 5 +- templates/whatis.lua | 10 +- templates/whatsound.lua | 1 + templates/whatvideo.lua | 3 +- templates/zoom.lua | 21 +- 10 files changed, 436 insertions(+), 56 deletions(-) diff --git a/gui b/gui index 1a6ba8e..76abe31 160000 --- a/gui +++ b/gui @@ -1 +1 @@ -Subproject commit 1a6ba8e09b24b66560bb8d4202aecbb13c479a85 +Subproject commit 76abe314266c3ab32f1fb6ab85145fbc4b96de1b diff --git a/main.lua b/main.lua index d76b496..a791f18 100644 --- a/main.lua +++ b/main.lua @@ -71,11 +71,11 @@ end) local loadLog = log:child("load") local old = multi.error -multi.error = function(self, err) - loadLog:error("Error: %s", err) - loadLog:error(debug.traceback()) - love.quit() -end +-- multi.error = function(self, err) +-- loadLog:error("Error: %s", err) +-- loadLog:error(debug.traceback()) +-- love.quit() +-- end function readFileData(path) return io.open(path,"r"):read("*a") @@ -90,8 +90,358 @@ local picker = THREAD:newFunction(function() return nil, "No file selected." .. (err and (" (" .. err .. ")") or "") end end, true) +local nof = gui.noOf +function gui:newNumberPicker(x, y, w, h, sx, sy, sw, sh, opts) + opts = opts or {} + opts.value = opts.value or 0 + opts.inc = opts.inc or 1 + opts.decimals = opts.decimals or 0 -- 0 = integers only + -- opts.min / opts.max / opts.onChange are optional, leave nil for "no limit" + + local picker = self:newFrame(x, y, w, h, sx, sy, sw, sh) + picker:setRoundness(6, 6) + picker.active = false + + + local leftF = picker:newFrame(0, 0, 0, 0, 0, 0, .15, 1) + local left = leftF:newTextButton("-", 0, 0, 0, 0, 0, 0, 1, 1) + local rightF = picker:newFrame(0, 0, 0, 0, .85, 0, .15, 1) + local right = rightF:newTextButton("+", 0, 0, 0, 0, 0, 0, 1, 1) + local textbox = picker:newTextBox("", 0, 0, 0, 0, .15, 0, .7, 1) + + textbox.blink = false + + gui.apply({ + scaleFont = {.45}, + centerFont = {}, + run = {function(self) + if self.text == "-" or self.text == "+" then + self:centerX(true) + self:centerY(true) -- also center vertically, not just horizontally + end + end}, + align = gui.ALIGN_CENTER, + }, textbox, left, right) + + gui.apply({ + visibility = 0, + drawBorder = false, -- flat, no boxy outline around the arrows + }, left, right, leftF, rightF) + + -- ---- value handling ---------------------------------------------- + + local function clamp(n) + if opts.min and n < opts.min then n = opts.min end + if opts.max and n > opts.max then n = opts.max end + return n + end + + local function round(n) + if opts.decimals > 0 then + local mult = 10 ^ opts.decimals + return math.floor(n * mult + 0.5) / mult + end + return math.floor(n + 0.5) + end + + local function format(n) + if opts.decimals > 0 then + return string.format("%." .. opts.decimals .. "f", n) + end + return tostring(math.floor(n)) + end + + local function updateButtons() + local canDec = (opts.min == nil) or (opts.value > opts.min) + local canInc = (opts.max == nil) or (opts.value < opts.max) + left.active, right.active = canDec, canInc + left.textColor = canDec and {0, 0, 0, 1} or {.6, .6, .6, 1} + right.textColor = canInc and {0, 0, 0, 1} or {.6, .6, .6, 1} + end + + function picker:setValue(n, silent) + n = round(clamp(n)) + opts.value = n + textbox.text = format(n) + textbox.cur_pos = #textbox.text + updateButtons() + if not silent and opts.onChange then opts.onChange(n) end + end + + function picker:getValue() + return opts.value + end + + picker:setValue(opts.value, true) + + left:OnReleased(function() picker:setValue(opts.value - opts.inc) end) + right:OnReleased(function() picker:setValue(opts.value + opts.inc) end) + + textbox:OnWheelMoved(function(_,dx, dy) + if dy > 0 then picker:setValue(opts.value + opts.inc) + elseif dy < 0 then picker:setValue(opts.value - opts.inc) end + end) + + -- ---- restrict typing to numbers only ------------------------------- + + local allowNegative = opts.min == nil or opts.min < 0 + local allowDecimal = opts.decimals > 0 + + local function sanitize(s) + local out, seenDot = {}, false + for i = 1, #s do + local ch = s:sub(i, i) + if ch:match("%d") then + out[#out + 1] = ch + elseif ch == "-" and i == 1 and allowNegative then + out[#out + 1] = ch + elseif ch == "." and allowDecimal and not seenDot then + out[#out + 1] = ch + seenDot = true + end + end + return table.concat(out) + end + + local tiRef = gui.Events.OnTextInputed(function(input) + if gui:getObjectFocus() ~= textbox then return end + local clean = sanitize(textbox.text) + if clean ~= textbox.text then + textbox.text = clean + textbox.cur_pos = math.min(textbox.cur_pos, #clean) + end + end) + + local kpRef = gui.Events.OnKeyPressed(function(key) + if gui:getObjectFocus() ~= textbox then return end + if key == "up" then picker:setValue(opts.value + opts.inc) + elseif key == "down" then picker:setValue(opts.value - opts.inc) end + end) + + -- re-normalize (round/clamp/reformat) whatever was typed once they + -- hit enter or click away, so "3." or "-" don't linger as invalid text + local function commit() + picker:setValue(tonumber(textbox.text) or opts.value) + end + textbox:OnReturn(commit) + textbox:OnPressedOuter(commit) + + textbox:OnDestroy(function() + gui.Events.OnTextInputed:Unconnect(tiRef) + gui.Events.OnKeyPressed:Unconnect(kpRef) + end) + + return picker +end + +-- ---- HSV <-> RGB helpers ---- +local function hsvToRgb(h, s, v) + local c = v * s + local x = c * (1 - math.abs((h / 60) % 2 - 1)) + local m = v - c + local r, g, b + if h < 60 then r, g, b = c, x, 0 + elseif h < 120 then r, g, b = x, c, 0 + elseif h < 180 then r, g, b = 0, c, x + elseif h < 240 then r, g, b = 0, x, c + elseif h < 300 then r, g, b = x, 0, c + else r, g, b = c, 0, x end + return r + m, g + m, b + m +end + +local function rgbToHsv(r, g, b) + local max, min = math.max(r, g, b), math.min(r, g, b) + local d = max - min + local h = 0 + if d ~= 0 then + if max == r then h = ((g - b) / d) % 6 + elseif max == g then h = (b - r) / d + 2 + else h = (r - g) / d + 4 end + h = h * 60 + end + local s = (max == 0) and 0 or (d / max) + return h, s, max +end + +local function clampNum(n, lo, hi) return math.max(lo, math.min(hi, n)) end + +local function newCornerGradient(tl, tr, br, bl) + return love.graphics.newMesh({ + {0, 0, 0, 0, tl[1], tl[2], tl[3], tl[4]}, + {1, 0, 0, 0, tr[1], tr[2], tr[3], tr[4]}, + {1, 1, 0, 0, br[1], br[2], br[3], br[4]}, + {0, 1, 0, 0, bl[1], bl[2], bl[3], bl[4]}, + }, "fan") +end + +local _whiteFade, _blackFade +local function getFadeImages() + if not _whiteFade then + -- opaque white on the left fading to transparent on the right + _whiteFade = newCornerGradient( + {1,1,1,1}, {1,1,1,0}, {1,1,1,0}, {1,1,1,1} + ) + -- transparent at top fading to opaque black at bottom + _blackFade = newCornerGradient( + {0,0,0,0}, {0,0,0,0}, {0,0,0,1}, {0,0,0,1} + ) + end + return _whiteFade, _blackFade +end + +function gui:newColorPicker(x, y, w, h, sx, sy, sw, sh, opts) + opts = opts or {} + local r0 = (opts.r or 255) / 255 + local g0 = (opts.g or 0) / 255 + local b0 = (opts.b or 0) / 255 + + local picker = self:newFrame(x, y, w, h, sx, sy, sw, sh) + picker.active = false -- container only; don't compete for focus with its children + picker.drawBorder = false + picker.visibility = 0 + + local hue, sat, val = rgbToHsv(r0, g0, b0) + local hueR, hueG, hueB = hsvToRgb(hue, 1, 1) + + -- ---- Saturation/Value square ---- + local svFrame = picker:newFrame(6, 6, -12, -12, 0, 0, .55, 1) + svFrame.drawBorder = false + svFrame.visibility = 0 + + local whiteFade, blackFade = getFadeImages() + svFrame.post = function(self) + local sx2, sy2, sw2, sh2 = self:getAbsolutes() + love.graphics.setColor(hueR, hueG, hueB, 1) + love.graphics.rectangle("fill", sx2, sy2, sw2, sh2) + love.graphics.setColor(1, 1, 1, 1) + love.graphics.draw(whiteFade, sx2, sy2, 0, sw2, sh2) + love.graphics.setColor(1, 1, 1, .95) + love.graphics.draw(blackFade, sx2, sy2, 0, sw2, sh2) + love.graphics.setColor(0, 0, 0, .4) + love.graphics.rectangle("line", sx2, sy2, sw2, sh2) + love.graphics.setColor(1, 1, 1, 1) + end + + local svThumb = svFrame:newFrame(0, 0, 0, 0) + svThumb:makeCircle(0, 0, 6) + svThumb.color = {1, 1, 1} + svThumb.borderColor = {0, 0, 0} + svThumb.drawBorder = true + + -- ---- Hue strip ---- + local hueSlider = picker:newFrame(0, 6, 0, -12, .57, 0, .10, 1) + -- applyGradient's "horizontal"/"vertical" naming is backwards from what + -- you'd expect visually (it names how colors are laid out in the source + -- image, not the on-screen direction) -- if this renders sideways for + -- you, just flip the string to "vertical". + hueSlider:applyGradient("horizontal", + {1, 0, 0, 1}, {1, 1, 0, 1}, {0, 1, 0, 1}, + {0, 1, 1, 1}, {0, 0, 1, 1}, {1, 0, 1, 1}, {1, 0, 0, 1}) + hueSlider.drawBorder = false + + local hueThumb = hueSlider:newFrame(0, 0, 0, 4, 0, 0, 1, 0) + hueThumb.color = {1, 1, 1} + hueThumb.borderColor = {0, 0, 0} + hueThumb.drawBorder = true + + -- ---- Preview + numeric fields ---- + local preview = picker:newFrame(6, 6, -12, -6, .70, 0, .30, .20) + preview:setRoundness(4, 4) + + local rPicker, gPicker, bPicker + + local function refresh(fireChange) + local r, g, b = hsvToRgb(hue, sat, val) + hueR, hueG, hueB = hsvToRgb(hue, 1, 1) + + preview.color = {r, g, b} + + local _, _, sw2, sh2 = svFrame:getAbsolutes() + svThumb:rawSetDualDim(sat * sw2 - 12, (1 - val) * sh2 - 6) + + local _, _, _, hh2 = hueSlider:getAbsolutes() + hueThumb:rawSetDualDim(nil, (hue / 360) * hh2 - 2) + + local r255 = math.floor(r * 255 + .5) + local g255 = math.floor(g * 255 + .5) + local b255 = math.floor(b * 255 + .5) + if rPicker then + rPicker:setValue(r255, true) + gPicker:setValue(g255, true) + bPicker:setValue(b255, true) + end + + if fireChange ~= false and opts.onChange then + opts.onChange(r255, g255, b255) + end + end + + local function onRGBEdited() + local r = rPicker:getValue() / 255 + local g = gPicker:getValue() / 255 + local b = bPicker:getValue() / 255 + hue, sat, val = rgbToHsv(r, g, b) + refresh(true) + end + + rPicker = picker:newNumberPicker(6, 0, -12, -6, .70, .26, .30, .20, + {value = math.floor(r0 * 255 + .5), min = 0, max = 255, onChange = onRGBEdited}) + gPicker = picker:newNumberPicker(6, 0, -12, -6, .70, .52, .30, .20, + {value = math.floor(g0 * 255 + .5), min = 0, max = 255, onChange = onRGBEdited}) + bPicker = picker:newNumberPicker(6, 0, -12, -6, .70, .78, .30, .20, + {value = math.floor(b0 * 255 + .5), min = 0, max = 255, onChange = onRGBEdited}) + + gui.apply({ + scaleFont={.1} + },rPicker,gPicker,bPicker) + + -- ---- drag interaction (same press/move/release pattern the built-in + -- textbox uses for its own text-selection dragging) ---- + local function setSV(mx, my) + local sx2, sy2, sw2, sh2 = svFrame:getAbsolutes() + local lx = clampNum(mx - sx2, 0, sw2) + local ly = clampNum(my - sy2, 0, sh2) + sat = sw2 > 0 and (lx / sw2) or 0 + val = sh2 > 0 and (1 - ly / sh2) or 0 + refresh(true) + end + + local svDragging = false + svFrame:OnPressed(function(_, mx, my) svDragging = true; setSV(mx, my) end) + svFrame:OnMoved(function(_, mx, my) if svDragging then setSV(mx, my) end end) + svFrame:OnReleased(function() svDragging = false end) + svFrame:OnReleasedOuter(function() svDragging = false end) + + local function setHue(mx, my) + local _, sy2, _, sh2 = hueSlider:getAbsolutes() + local ly = clampNum(my - sy2, 0, sh2) + hue = sh2 > 0 and (ly / sh2) * 360 or 0 + refresh(true) + end + + local hueDragging = false + hueSlider:OnPressed(function(_, mx, my) hueDragging = true; setHue(mx, my) end) + hueSlider:OnMoved(function(_, mx, my) if hueDragging then setHue(mx, my) end end) + hueSlider:OnReleased(function() hueDragging = false end) + hueSlider:OnReleasedOuter(function() hueDragging = false end) + + -- ---- public API ---- + function picker:getColor() + local r, g, b = hsvToRgb(hue, sat, val) + return math.floor(r * 255 + .5), math.floor(g * 255 + .5), math.floor(b * 255 + .5) + end + + function picker:setColor(r255, g255, b255) + hue, sat, val = rgbToHsv(r255 / 255, g255 / 255, b255 / 255) + refresh(false) + end + + refresh(false) + return picker +end function love.load() + love.keyboard.setKeyRepeat(true) -- local z = zip.new() -- z:add("main.lua",readFileData("main.lua")) -- z:add("package.bat",readFileData("package.bat")) @@ -105,6 +455,18 @@ function love.load() -- print("File selected",file) -- end).OnError(print) + -- local numpicker = gui:newNumberPicker(0, 0, 200, 40, .5, .5, 0, 0, { + -- value = 1, inc = 1, min = 1, max = 4, + -- onChange = function(v) print("value is now", v) end, + -- }) + + local frame = gui:newFrame(0, 0, 500, 250, .5, .5, 0, 0) + local colorpicker = frame:newColorPicker(0, 0, 0, 0, 0, 0, 1, 1, { + r = 255, g = 100, b = 50, + onChange = function(r, g, b) print("color:", r, g, b) end, + }) + + --[[ -- bootstrap loadLog:info("Starting Jeopardy Game") loadLog:debug("Setting game identity to jeopardy") for i,v in ipairs(color.listColors(showAllColors)) do @@ -129,6 +491,7 @@ function love.load() menu.init(bg) loadLog:debug("Displaying title menu") title:visible(true) + ]] end function love.update(dt) diff --git a/menus/board.lua b/menus/board.lua index 76bb231..5d88e57 100644 --- a/menus/board.lua +++ b/menus/board.lua @@ -9,6 +9,14 @@ local menu = require("gui.core.menus") local transition = require("gui.core.transitions") local logger = require("utils/logger") +-- Constants +local IMAGE = "image" +local AUDIO = "audio" +local VIDEO = "video" +local TEXT = "text" +local LIST = "list" +local NUMBER = "number" + local timesup = love.audio.newSource("assets/sounds/timesup.mp3", "static") local double = love.audio.newSource("assets/sounds/double.mp3", "static") @@ -174,12 +182,12 @@ function LoadTemplate(name, path) ALIGN_LEFT = gui.ALIGN_LEFT, ALIGN_RIGHT = gui.ALIGN_RIGHT, ALIGN_JUSTIFY = gui.ALIGN_JUSTIFY, - IMAGE = "image", - AUDIO = "audio", - VIDEO = "video", - TEXT = "text", - LIST = "list", - NUMBER = "number", + IMAGE = IMAGE, + AUDIO = AUDIO, + VIDEO = VIDEO, + TEXT = TEXT, + LIST = LIST, + NUMBER = NUMBER, -- Global vars color = color, error = multi.error, @@ -265,13 +273,11 @@ function Menu(frame, path, deb) local scoreboard = ScoreBoard(frameRef, 0, 0, 0, 0, .06, .1, .170, .8) local properties = Properties(frameRef, 0, 0, 0, 0, .06, .1, .170, .8) - gui:setHotKey({"lctrl","q"})(function() - scoreboard.leaderboard.visible = not scoreboard.leaderboard.visible - end) - gui:setHotKey({"lctrl","e"})(function() - properties.visible = not properties.visible - end) - + local function showProperties(b) + properties.visible = b + scoreboard.leaderboard.visible = not b + end + showProperties(true) local qframe = frameRef:newFrame(0, 0, 0, 0, .24, .1, .70, .8) qframe.visibility = 0 @@ -542,6 +548,7 @@ function Menu(frame, path, deb) end self.textVisibility = 0 local template = LoadTemplate(q.template, path) + if not template then boardLog:fatal("template returned nil. Likely an invalid template format. Did you return?") end question.visible = true board.visible = false -- question.visibility = visibility @@ -569,7 +576,7 @@ function Menu(frame, path, deb) end) end local finished = false - template.index(question, q, function(ans, m) + local params = template.index(question, q, function(ans, m) local m = m or 1 if finished then return end player = GetActivePlayer() @@ -595,6 +602,7 @@ function Menu(frame, path, deb) self.text = "" end) boardUpdater:newThread("QuestionUpdater",function() + if not template.update then return end while true do template.update(dt) thread.sleep(.1) @@ -640,6 +648,9 @@ function love.filedropped(file) local data = file:read() print("Load file? " .. file:getFilename()) end +---------------- +--- Sideboard +--- local C_BG_PANEL = color.scoreboard_panel_bg local C_BG_HEADER = color.scoreboard_header_bg @@ -662,10 +673,28 @@ local PLAYER_HEIGHT = .05 function Properties(frame, x, y, w, h, sx, sy, sw, sh) local prop = frame:newFrame(x, y, w, h, sx, sy, sw, sh) prop.color = C_BORDER_NRM - + local header = prop:newTextLabel("PROPERTIES",0,0,0,0,0,0,1,LEADER_HEIGHT_SCALE) + header.borderColor = C_ACCENT + header.textColor = C_ACCENT + header.color = C_BG_HEADER + header.align = gui.ALIGN_CENTER + header:scaleFont(.7) + header:centerFont() + function prop:RenderAttributes(tab) + if tab == nil then + tab = {} + end + if not tab.title then -- make the first option + boardLog:fatal("Templates must define a title attribute") + end + if not tab.answer then -- append to the end + table.insert(tab,{answer=TEXT,description="The answer for the question"}) + end + end return prop end +-- :newTextLabel(index,0,0,0,0,.015,.1,1/8,.8) function ScoreBoard(frame, x, y, w, h, sx, sy, sw, sh) -- Config stuff diff --git a/templates/imageword.lua b/templates/imageword.lua index 7f5e333..ad4bce4 100644 --- a/templates/imageword.lua +++ b/templates/imageword.lua @@ -6,7 +6,7 @@ local plusLabel local function index(window, q, callback) frame = window:newFrame(0,0,0,-200,0,.2,1,.8) frame.visibility = 0 - label = window:newTextLabel(" " ..q.title.. " ",0,0,0,0,0,0,1,.2) + label = window:newTextLabel(q.title,0,0,0,0,0,0,1,.2) label.align = ALIGN_CENTER label.textColor = color.question_font label.color = color.question_bg @@ -46,8 +46,9 @@ local function index(window, q, callback) useDefaultHandler(callback) return { - {imageA=IMAGE, description="First image to display"}, - {imageB=IMAGE, description="Second image to display"}, + {title=label}, + {imageA=imageHolder, description="First image to display"}, + {imageB=imageHolder2, description="Second image to display"}, } end diff --git a/templates/multiplechoice.lua b/templates/multiplechoice.lua index 2d3e5a5..7fc817f 100644 --- a/templates/multiplechoice.lua +++ b/templates/multiplechoice.lua @@ -87,15 +87,12 @@ local function index(window, q, callback) -- Return the expected types return { - {choices=LIST, description="List of options"} + {title=label}, + {choices=LIST, description="List of options"}, + {answer=NUMBER, description="Answer to multiple choice as a number"} } end -local function update(dt) -- time in seconds that has passed since - -end - return { - index = index, - update = update + index = index } diff --git a/templates/one-image.lua b/templates/one-image.lua index 0eb0fc0..8f45d81 100644 --- a/templates/one-image.lua +++ b/templates/one-image.lua @@ -12,7 +12,7 @@ local imageHolder local function index(window, q, callback) frame = window:newFrame(0,0,0,0,0,.2,1,.8) frame.visibility = 0 - label = window:newTextLabel(" " ..q.title.. " ",0,0,0,0,0,0,1,.2) + label = window:newTextLabel(q.title,0,0,0,0,0,0,1,.2) label.align = ALIGN_CENTER label.textColor = color.question_font label.color = color.question_bg @@ -32,7 +32,8 @@ local function index(window, q, callback) -- Return the expected types return { - {image=IMGAE, description="Image the question is about"} + {title=label}, + {image=imageHolder, description="Image the question is about"} } end diff --git a/templates/whatis.lua b/templates/whatis.lua index ba1b9d0..2d999b2 100644 --- a/templates/whatis.lua +++ b/templates/whatis.lua @@ -6,10 +6,9 @@ callback true/false correct/wrong ]] -local label local function index(window, q, callback) - label = window:newTextLabel(q.title) + local label = window:newTextLabel(q.title) label.visibility = 0--q.cosmetics.visibility label.align = ALIGN_CENTER label:fullFrame() @@ -20,13 +19,10 @@ local function index(window, q, callback) label.drawBorder = false useDefaultHandler(callback) -end -local function update(dt) -- time in seconds that has passed since - + return {title=label} end return { - index = index, - update = update + index = index } diff --git a/templates/whatsound.lua b/templates/whatsound.lua index 7c1ed0f..f4e49ba 100644 --- a/templates/whatsound.lua +++ b/templates/whatsound.lua @@ -46,6 +46,7 @@ local function index(window, q, callback) -- Return the expected types return { + {title=label}, {sound=AUDIO, description="Sound that will be played"} } end diff --git a/templates/whatvideo.lua b/templates/whatvideo.lua index ee120a2..fe95538 100644 --- a/templates/whatvideo.lua +++ b/templates/whatvideo.lua @@ -15,7 +15,7 @@ local function index(window, q, callback) callback() return end - frame = window:newFrame(0,0,0,-200,0,.2,1,.8) + local frame = window:newFrame(0,0,0,-200,0,.2,1,.8) frame.visibility = 0 label = window:newTextLabel(" " ..q.title.. " ",0,0,0,0,0,0,1,.2) label.align = ALIGN_CENTER @@ -39,6 +39,7 @@ local function index(window, q, callback) -- Return the expected types return { + {title=label}, {video=VIDEO, description="Video that will be played"} } end diff --git a/templates/zoom.lua b/templates/zoom.lua index dc35212..f05e607 100644 --- a/templates/zoom.lua +++ b/templates/zoom.lua @@ -6,13 +6,12 @@ callback true/false correct/wrong ]] -local label local imageHolder local function index(window, q, callback) - frame = window:newFrame(0,0,0,0,0,.2,1,.8) + local frame = window:newFrame(0,0,0,0,0,.2,1,.8) frame.visibility = 0 - label = window:newTextLabel(" " ..q.title.. " ",0,0,0,0,0,0,1,.2) + local label = window:newTextLabel(" " ..q.title.. " ",0,0,0,0,0,0,1,.2) label.align = ALIGN_CENTER label.textColor = color.question_font label.color = color.question_bg @@ -76,18 +75,10 @@ local function index(window, q, callback) -- Return the expected types return { - { - zoom=IMAGE, - description = "Full zoom in on an image" - }, - { - minzoom=IMAGE, - description = "Minimum zoom before the full image is revealed" - }, - { - fullimage=IMAGE, - description = "Full image to show" - } + {title=label}, + {zoom=IMAGE,description = "Full zoom in on an image"}, + {minzoom=IMAGE,description = "Minimum zoom before the full image is revealed"}, + {fullimage=IMAGE,description = "Full image to show"} } end