1025 lines
31 KiB
Lua
1025 lines
31 KiB
Lua
-- Cubicle - A Minecraft ComputerCraft Window Compositor API
|
|
|
|
--- The @{Cubicle} API provides methods for compositing and window
|
|
-- management.
|
|
--
|
|
-- @module Cubicle
|
|
|
|
Cubicle = {}
|
|
|
|
--- Whether an object is an instance of a class
|
|
--
|
|
-- @tparam table o Object to compare
|
|
-- @tparam table class Class to compare
|
|
-- @treturn boolean Whether the object is an instance of class
|
|
local function instanceOf(o, class)
|
|
while o do
|
|
o = getmetatable(o)
|
|
if o == class then return true end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function min(a, b)
|
|
if a < b then return a
|
|
else return b end
|
|
end
|
|
|
|
--- Wrap text to a limited length
|
|
--
|
|
-- @tparam string text The text to wrap
|
|
-- @tparam number length The length limit of each line
|
|
-- @treturn table Array of lines
|
|
-- @usage wrapText("A very long string to wrap", 10)
|
|
local function wrapText(text, length)
|
|
_, text = assert(pcall(tostring, text))
|
|
assert(text,
|
|
"bad argument #1 for text (expected string got "..type(text)..")")
|
|
assert(length and type(length) == "number",
|
|
"bad argument #2 for length (expected number got "..type(length)..")")
|
|
local lineCount = #text/length
|
|
local wrappedText = {}
|
|
for i=0,lineCount do
|
|
table.insert(wrappedText, string.sub(text, i*length+1, (i+1)*length))
|
|
end
|
|
return wrappedText
|
|
end
|
|
|
|
--- Base class inherited by Button, Text, and Dialog
|
|
-- @class GraphicsElement
|
|
local GraphicsElement = {}
|
|
|
|
--- Instantiate a new GraphicsElement
|
|
--
|
|
-- While optional, creating a GraphicsElement without passing a definition is
|
|
-- not very useful.
|
|
--
|
|
-- @tparam[opt] table o Definition of the GraphicsElement
|
|
-- @treturn table Instance of class GraphicsElement
|
|
-- @usage GraphicsElement:new {
|
|
-- name = "graphicsElement",
|
|
-- title = "A GraphicsElement",
|
|
-- align = "center",
|
|
-- x = 1, y = 2,
|
|
-- width = 20, height = 3,
|
|
-- textColor = colors.black, bgColor = colors.lime,
|
|
-- visible = true
|
|
-- }
|
|
function GraphicsElement:new(o)
|
|
o = o or {
|
|
name = "graphicsElement",
|
|
title = "graphicsElement",
|
|
align = "center",
|
|
x = 1, y = 1,
|
|
width = 10, height = 1,
|
|
textColor = colors.black, bgColor = colors.white,
|
|
visible = true
|
|
}
|
|
|
|
setmetatable(o, self)
|
|
self.__index = self
|
|
return o
|
|
end
|
|
|
|
--- Set the x position of the GraphicsElement
|
|
--
|
|
-- @tparam number x New x position of the GraphicsElement
|
|
function GraphicsElement:setX(x) self.x = x end
|
|
|
|
--- Set the y position of the GraphicsElement
|
|
--
|
|
-- @tparam number y New y position of the GraphicsElement
|
|
function GraphicsElement:setY(y) self.y = y end
|
|
|
|
--- Set both the x and y positions of the GraphicsElement
|
|
--
|
|
-- @tparam number x New x position of the GraphicsElement
|
|
-- @tparam number y New x position of the GraphicsElement
|
|
function GraphicsElement:move(x, y) self.x, self.y = x, y end
|
|
|
|
--- Set width of the GraphicsElement
|
|
--
|
|
-- @tparam number width New width of the GraphicsElement
|
|
function GraphicsElement:setWidth(w) self.width = w end
|
|
|
|
--- Set height of the GraphicsElement
|
|
--
|
|
-- @tparam number height New height of the GraphicsElement
|
|
function GraphicsElement:setHeight(h) self.height = h end
|
|
|
|
--- Set visibility of the GraphicsElement
|
|
--
|
|
-- @tparam boolean visibility Whether the GraphicsElement should be rendered
|
|
function GraphicsElement:setVisible(visibility) self.visible = visibility end
|
|
|
|
--- Set the text color of the GraphicsElement
|
|
--
|
|
-- @tparam number color New color of the GraphicsElement text
|
|
function GraphicsElement:setTextColor(color) self.textColor = color end
|
|
|
|
--- Set the background color of the GraphicsElement
|
|
--
|
|
-- @tparam number color New color of the GraphicsElement background
|
|
function GraphicsElement:setBackgroundColor(color) self.bgColor = color end
|
|
|
|
--- Set the title of the GraphicsElement
|
|
--
|
|
-- @tparam string title New title of the GraphicsElement
|
|
function GraphicsElement:setTitle(title) self.title = tostring(title) end
|
|
|
|
--- Render the GraphicsElement
|
|
--
|
|
-- @tparam table terminal Terminal on which to render
|
|
-- @tparam number parentX X position of the parent Window
|
|
-- @tparam number parentY Y position of the parent Window
|
|
-- @usage Render the GraphicsElement in a Window on the current terminal
|
|
--
|
|
-- local window = Window:new()
|
|
-- GraphicsElement:render(term.current(), window.x, window.y)
|
|
-- @usage Render the GraphicsElement on a monitor connected to the right of the
|
|
-- computer
|
|
--
|
|
-- local mon = peripheral.wrap("right")
|
|
-- GraphicsElement:render(mon, 1, 1)
|
|
function GraphicsElement:render(terminal, parentX, parentY)
|
|
assert(terminal and type(terminal) == "table",
|
|
"bad argument #1 to terminal (expected table, got "..type(terminal))
|
|
assert(parentX and type(parentX) == "number",
|
|
"bad argument #2 to parentX (expected number, got "..type(parentX))
|
|
assert(parentX and type(parentX) == "number",
|
|
"bad argument #3 to parentY (expected number, got "..type(parentY))
|
|
|
|
if self.visible then
|
|
paintutils.drawFilledBox(
|
|
parentX + self.x - 1, parentY + self.y,
|
|
parentX + self.x + self.width - 2,
|
|
parentY + self.y + self.height - 1,
|
|
self.bgColor
|
|
)
|
|
|
|
terminal.setTextColor(self.textColor)
|
|
terminal.setBackgroundColor(self.bgColor)
|
|
|
|
local wrappedText = wrapText(self.title, self.width+1)
|
|
|
|
for k,v in ipairs(wrappedText) do
|
|
if self.align == "left" then
|
|
terminal.setCursorPos(
|
|
parentX + self.x - 1,
|
|
parentY + self.y + self.height/2 + k-1 -- - #wrappedText/2
|
|
)
|
|
elseif self.align == "center" then
|
|
terminal.setCursorPos(
|
|
parentX + self.x + self.width/2 - #v/2 - 1,
|
|
parentY + self.y
|
|
+ math.fmod(k-math.floor(#wrappedText/2), self.height/2)
|
|
)
|
|
elseif self.align == "right" then
|
|
terminal.setCursorPos(
|
|
parentX + self.x + self.width - #v - 1,
|
|
parentY + self.y + self.height/2 + k-1 -- - #wrappedText/2
|
|
)
|
|
end
|
|
|
|
terminal.write(v)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Button, Text, Input, and Window inherit base class GraphicsElement
|
|
-- @class Text
|
|
Text = GraphicsElement:new()
|
|
|
|
-- @class Button
|
|
Button = GraphicsElement:new()
|
|
|
|
--- Instantiate a new Button
|
|
--
|
|
-- Inherits @{GraphicsElement} with addition of action
|
|
--
|
|
-- While optional, creating a Button without passing a definition is not very
|
|
-- useful.
|
|
--
|
|
-- @tparam[opt] table o Definition of Button object
|
|
-- @treturn table Instance of class Button
|
|
-- @usage Button:new {
|
|
-- name = "buttonElement",
|
|
-- title = "A Button",
|
|
-- align = "center",
|
|
-- x = 1, y = 2,
|
|
-- width = 20, height = 3,
|
|
-- textColor = colors.black, bgColor = colors.lime,
|
|
-- visible = true,
|
|
-- action = function() return true end
|
|
-- }
|
|
function Button:new(o)
|
|
o = o or {
|
|
name = "buttonElement",
|
|
title = "buttonElement",
|
|
align = "center",
|
|
x = 1, y = 1,
|
|
width = 10, height = 1,
|
|
textColor = colors.black, bgColor = colors.white,
|
|
visible = true,
|
|
action = function() return true end
|
|
}
|
|
|
|
setmetatable(o, self)
|
|
self.__index = self
|
|
return o
|
|
end
|
|
|
|
-- @class Input
|
|
Input = GraphicsElement:new()
|
|
|
|
--- Instantiate a new Input
|
|
--
|
|
-- Inherits @{GraphicsElement} with addition of action and cursor rendering
|
|
--
|
|
-- While optional, creating an Input without passing a definition is not very
|
|
-- useful.
|
|
--
|
|
-- @tparam[opt] table o Definition of Input object
|
|
-- @treturn table Instance of class Input
|
|
-- @usage Input:new {
|
|
-- name = "inputElement",
|
|
-- value = "An Input",
|
|
-- x = 1, y = 2,
|
|
-- width = 20, height = 3,
|
|
-- textColor = colors.black, bgColor = colors.lime,
|
|
-- visible = true,
|
|
-- action = function() return true end
|
|
-- }
|
|
function Input:new(o)
|
|
o = o or {
|
|
name = "inputElement",
|
|
value = "inputElement",
|
|
x = 1, y = 1,
|
|
width = 10, height = 1,
|
|
textColor = colors.black, bgColor = colors.white,
|
|
visible = true,
|
|
action = function() return true end
|
|
}
|
|
|
|
setmetatable(o, self)
|
|
self.__index = self
|
|
return o
|
|
end
|
|
|
|
--- Get the value of the Input
|
|
--
|
|
-- @treturn string Current value of the Input
|
|
function Input:getValue(value)
|
|
return self.value
|
|
end
|
|
|
|
--- Set the value of the Input
|
|
--
|
|
-- @tparam string value New value of the Input
|
|
function Input:setValue(value)
|
|
self.value = tostring(value)
|
|
end
|
|
|
|
--- Render the Input
|
|
--
|
|
-- Overrides inherited @{GraphicsElement:render} method while scrolling title to
|
|
-- the left and constraining to bounds of Input width.
|
|
--
|
|
-- @tparam table terminal Terminal on which to render
|
|
-- @tparam number parentX X position of the parent Window
|
|
-- @tparam number parentY Y position of the parent Window
|
|
-- @usage Render the GraphicsElement in a Window on the current terminal
|
|
--
|
|
-- local window = Window:new()
|
|
-- Input:render(term.current(), window.x, window.y)
|
|
-- @usage Render the GraphicsElement on a monitor connected to the right of the computer
|
|
--
|
|
-- local mon = peripheral.wrap("right")
|
|
-- Input:render(mon, 1, 1)
|
|
function Input:render(terminal, parentX, parentY)
|
|
assert(terminal and type(terminal) == "table",
|
|
"bad argument #1 to terminal (expected table, got "..type(terminal))
|
|
assert(parentX and type(parentX) == "number",
|
|
"bad argument #2 to parentX (expected number, got "..type(parentX))
|
|
assert(parentX and type(parentX) == "number",
|
|
"bad argument #3 to parentY (expected number, got "..type(parentY))
|
|
|
|
if self.visible then
|
|
paintutils.drawFilledBox(
|
|
parentX + self.x - 1, parentY + self.y,
|
|
parentX + self.x + self.width - 2,
|
|
parentY + self.y + self.height - 1,
|
|
self.bgColor
|
|
)
|
|
|
|
terminal.setTextColor(self.textColor)
|
|
terminal.setBackgroundColor(self.bgColor)
|
|
|
|
terminal.setCursorPos(
|
|
parentX + self.x - 1,
|
|
parentY + self.y + self.height/2
|
|
)
|
|
|
|
if #self.value <= self.width - 1 then
|
|
terminal.write(self.value)
|
|
else
|
|
terminal.write(
|
|
self.value:sub(#self.value - self.width + 2, #self.value)
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- @class ProgressBar
|
|
ProgressBar = GraphicsElement:new()
|
|
|
|
--- Instantiate a new ProgressBar
|
|
--
|
|
-- Inherits @{GraphicsElement} with additon of progress and completeColor
|
|
--
|
|
-- While optional, creating a ProgressBar without passing a defition is not
|
|
-- very useful.
|
|
--
|
|
-- @tparam[opt] table o Definition of ProgressBar object
|
|
-- @treturn table Instance of class ProgressBar
|
|
function ProgressBar:new(o)
|
|
o = o or {
|
|
name = "progressBar",
|
|
x = 1, y = 1,
|
|
width = 20, height = 1,
|
|
textColor = colors.black, bgColor = colors.gray,
|
|
completeColor = colors.lime,
|
|
-- Progress as a number between 0 and 1
|
|
progress = 0,
|
|
visible = true,
|
|
-- By default, show a percentage of progress completion
|
|
progressText = true
|
|
}
|
|
|
|
setmetatable(o, self)
|
|
self.__index = self
|
|
return o
|
|
end
|
|
|
|
function ProgressBar:setProgress(progress)
|
|
assert(progress and type(progress) == "number",
|
|
"bad argument #1 to progress (expected number, got "..type(progress)..")")
|
|
assert(progress >= 0 and progress <= 1,
|
|
"bad argument #1 to progress: progress should be between 0 and 1")
|
|
|
|
self.progress = progress
|
|
end
|
|
|
|
function ProgressBar:render(terminal, parentX, parentY)
|
|
assert(terminal and type(terminal) == "table",
|
|
"bad argument #1 to terminal (expected table, got "..type(terminal)..")")
|
|
assert(parentX and type(parentX) == "number",
|
|
"bad argument #2 to parentX (expected number, got "..type(parentX)..")")
|
|
assert(parentX and type(parentX) == "number",
|
|
"bad argument #3 to parentY (expected number, got "..type(parentY)..")")
|
|
|
|
terminal.setCursorPos(
|
|
parentX + self.x + self.width - 3,
|
|
parentY + self.y
|
|
)
|
|
terminal.setTextColor(self.textColor)
|
|
terminal.write(tostring(self.progress*100).."%")
|
|
|
|
if self.visible then
|
|
if self.progressText then
|
|
-- Background
|
|
paintutils.drawFilledBox(
|
|
parentX + self.x - 1, parentY + self.y,
|
|
parentX + self.x + self.width - 5,
|
|
parentY + self.y + self.height - 1,
|
|
self.bgColor
|
|
)
|
|
if self.progress > 0 then
|
|
-- Progress
|
|
paintutils.drawFilledBox(
|
|
parentX + self.x - 1, parentY + self.y,
|
|
parentX + self.x + self.width*self.progress - 2 - 2*self.progress,
|
|
parentY + self.y + self.height - 1,
|
|
self.completeColor
|
|
)
|
|
end
|
|
else
|
|
-- Background
|
|
paintutils.drawFilledBox(
|
|
parentX + self.x - 1, parentY + self.y,
|
|
parentX + self.x + self.width - 2,
|
|
parentY + self.y + self.height - 1,
|
|
self.bgColor
|
|
)
|
|
if self.progress > 0 then
|
|
-- Progress
|
|
paintutils.drawFilledBox(
|
|
parentX + self.x - 1, parentY + self.y,
|
|
parentX + self.x + self.width*self.progress - 2,
|
|
parentY + self.y + self.height - 1,
|
|
self.completeColor
|
|
)
|
|
end
|
|
end
|
|
|
|
end
|
|
end
|
|
|
|
-- @class Dataview
|
|
Dataview = GraphicsElement:new()
|
|
|
|
--- Instantiate a new Dataview
|
|
--
|
|
-- Inherits @{GraphicsElement} with addition of columns and data
|
|
--
|
|
-- While optional, creating an Dataview without passing a definition is not
|
|
-- very useful.
|
|
--
|
|
-- @tparam[opt] table o Definition of Dataview object
|
|
-- @treturn table Instance of class Dataview
|
|
-- @usage Dataview:new {
|
|
-- name = "dataview",
|
|
-- title = "A Dataview",
|
|
-- x = 1, y = 1,
|
|
-- scrollX = 0, scrollY = 0,
|
|
-- width = 20, height = 10,
|
|
-- textColor = colors.black, headerColor = colors.white,
|
|
-- bgColor = colors.white,
|
|
-- visible = true,
|
|
-- columns = {
|
|
-- { name = "col1", title = "Column 1", width = 10 },
|
|
-- { name = "col2", title = "Column 2", width = 15 },
|
|
-- { name = "col3", title = "Column 3", width = 10 }
|
|
-- },
|
|
-- data = {
|
|
-- { col1 = "asdf", col2 = "1234", col3 = "hello, world!"}
|
|
-- }
|
|
-- }
|
|
function Dataview:new(o)
|
|
o = o or {
|
|
name = "dataview",
|
|
title = "Dataview",
|
|
x = 1, y = 1,
|
|
scrollX = 1, scrollY = 1,
|
|
width = 20, height = 10,
|
|
titleHeight = 1,
|
|
textColor = colors.white, bgColor = colors.black,
|
|
headerColor = colors.white, selectColor = colors.lightGray,
|
|
visible = true,
|
|
data = {},
|
|
columns = {},
|
|
select = 1
|
|
}
|
|
|
|
setmetatable(o, self)
|
|
self.__index = self
|
|
return o
|
|
end
|
|
|
|
function Dataview:getColumns() return self.columns end
|
|
|
|
function Dataview:getData() return self.data end
|
|
|
|
function Dataview:setData(data)
|
|
assert(data and type(data) == "table",
|
|
"bad argument #1 to data (expected table, got "..type(data)..")")
|
|
|
|
self.data = data
|
|
end
|
|
|
|
function Dataview:addRow(row)
|
|
assert(row and type(row) == "table",
|
|
"bad argument #1 to row (expected table, got "..type(row)..")")
|
|
|
|
table.insert(self.data, row)
|
|
end
|
|
|
|
function Dataview:getRow(id)
|
|
assert(id and type(id) == "number",
|
|
"bad argument #1 to id (expected number, got "..type(id)..")")
|
|
|
|
return self.data[id]
|
|
end
|
|
|
|
function Dataview:getSelection() return self.select end
|
|
|
|
function Dataview:setSelection(id)
|
|
assert(id and type(id) == "number",
|
|
"bad argument #1 to id (expected number, got "..type(id)..")")
|
|
|
|
self.select = id
|
|
end
|
|
|
|
function Dataview:getSelectedRow() return self.data[self.select] end
|
|
|
|
function Dataview:getScrollX(n) return self.scrollX end
|
|
|
|
function Dataview:setScrollX(n)
|
|
assert(n and type(n) == "number",
|
|
"bad argument #1 for n (expected number, got "..type(n)..")")
|
|
|
|
self.scrollX = n
|
|
end
|
|
|
|
function Dataview:getScrollY(n) return self.scrollY end
|
|
|
|
function Dataview:setScrollY(n)
|
|
assert(n and type(n) == "number",
|
|
"bad argument #1 for n (expected number, got "..type(n)..")")
|
|
|
|
self.scrollY = n
|
|
end
|
|
|
|
function Dataview:render(terminal, parentX, parentY)
|
|
assert(terminal and type(terminal) == "table",
|
|
"bad argument #1 to terminal (expected table, got "..type(terminal)..")")
|
|
assert(parentX and type(parentX) == "number",
|
|
"bad argument #2 to parentX (expected number, got "..type(parentX)..")")
|
|
assert(parentX and type(parentX) == "number",
|
|
"bad argument #3 to parentY (expected number, got "..type(parentY)..")")
|
|
|
|
if self.visible then
|
|
-- Background
|
|
paintutils.drawFilledBox(
|
|
parentX + self.x - 1, parentY + self.y,
|
|
parentX + self.x + self.width - 2,
|
|
parentY + self.y + self.height - 1,
|
|
self.bgColor
|
|
)
|
|
-- Header row
|
|
paintutils.drawLine(
|
|
parentX + self.x - 1, parentY + self.y,
|
|
parentX + self.x + self.width-1 + 2*#self.columns, parentY + self.y,
|
|
self.headerColor
|
|
)
|
|
|
|
terminal.setTextColor(self.textColor)
|
|
terminal.setBackgroundColor(self.headerColor)
|
|
|
|
local xPos = 0
|
|
for _,v in ipairs(self.columns) do
|
|
terminal.setCursorPos(parentX + self.x + xPos - 1, parentY + self.y)
|
|
terminal.write("| "..v.title)
|
|
xPos = xPos + v.width + 2
|
|
end
|
|
|
|
terminal.setBackgroundColor(self.bgColor)
|
|
|
|
-- Only display as many that will fit in the bounds of the dataview body
|
|
for i = self.scrollY, min(#self.data, self.scrollY + self.height - self.titleHeight - 1) do
|
|
if i == self.select then
|
|
paintutils.drawLine(
|
|
parentX + self.x,
|
|
parentY + self.y + self.titleHeight + i - self.scrollY,
|
|
parentX + self.x + self.width-1 + 2*#self.columns,
|
|
parentY + self.y + self.titleHeight + i - self.scrollY,
|
|
self.selectColor
|
|
)
|
|
else
|
|
terminal.setBackgroundColor(self.bgColor)
|
|
end
|
|
local xPos = 0
|
|
for _,v in ipairs(self.columns) do
|
|
terminal.setCursorPos(
|
|
parentX + self.x + xPos - 1,
|
|
parentY + self.y + self.titleHeight + i - self.scrollY
|
|
)
|
|
terminal.write("| "..self.data[i][v.name])
|
|
-- Set X position of the next column
|
|
xPos = xPos + v.width + 2
|
|
end
|
|
-- terminal.setCursorPos(
|
|
-- parentX + self.x - 1,
|
|
-- parentY + self.y + self.titleHeight + i - self.scrollY
|
|
-- )
|
|
-- terminal.write(i)
|
|
end
|
|
end
|
|
end
|
|
|
|
Window = GraphicsElement:new()
|
|
|
|
--- Instantiate a new Window
|
|
--
|
|
-- Inherits @{GraphicsElement} with addition of onLoad, onClose functions,
|
|
-- list of child elements, and title bar.
|
|
--
|
|
-- Windows can be dragged around the screen by clicking and dragging on the
|
|
-- title bar.
|
|
--
|
|
-- Title bar and background colors set the same and title text set to empty
|
|
-- string will effectively "hide" the title bar.
|
|
--
|
|
-- Title text will always be aligned left, and all elements can not currently
|
|
-- be aligned by the window.
|
|
--
|
|
-- @tparam[opt] table o Window
|
|
-- @treturn table Instance of class Window
|
|
-- @usage GraphicsElement:new {
|
|
-- name = "window",
|
|
-- title = "A Window",
|
|
-- x = 1, y = 2,
|
|
-- width = 20, height = 3,
|
|
-- textColor = colors.black, bgColor = colors.lime,
|
|
-- titleColor = colors.blue,
|
|
-- visible = true,
|
|
-- elements = {},
|
|
-- onLoad = function() return true end,
|
|
-- onClose = function() return true end
|
|
-- }
|
|
function Window:new(o)
|
|
o = o or {
|
|
name = "window",
|
|
title = "window",
|
|
x = 1, y = 1,
|
|
-- TODO: Figure out if the currently displayed is less than the
|
|
-- height, if so, allow scrolling
|
|
scrollX = 0, scrollY = 0,
|
|
-- Default is the resolution of a computer, since we're not passing a
|
|
-- terminal object to the Window
|
|
width = 51, height = 19,
|
|
-- This will need to be handled when creating the window if the
|
|
-- terminal does not support color
|
|
textColor = colors.white, bgColor = colors.black,
|
|
titleColor = colors.white,
|
|
elements = {},
|
|
select = 1,
|
|
onLoad = function() return true end,
|
|
onClose = function() return true end
|
|
}
|
|
|
|
setmetatable(o, self)
|
|
self.__index = self
|
|
return o
|
|
end
|
|
|
|
--- Add an element to the Window
|
|
--
|
|
-- @tparam table o Instance of element to add to the Window
|
|
function Window:addElement(o)
|
|
assert(o and type(o) == "table",
|
|
"bad argument #1 for o (expected table, got "..type(o)..")")
|
|
|
|
table.insert(self.elements, o)
|
|
end
|
|
|
|
--- Remove an element from the Window
|
|
--
|
|
-- @tparam table o ID of the element to remove from the Window
|
|
function Window:removeElement(o) table.remove(self.elements, o) end
|
|
|
|
--- Get the table of elements in a Window
|
|
--
|
|
-- @treturn table Elements contained in the Window
|
|
function Window:getElements() return self.elements end
|
|
|
|
--- Get an element contained in the Window by ID
|
|
--
|
|
-- @tparam number id ID of the element to return
|
|
-- @treturn table The element with ID, if it exists in the window
|
|
function Window:getElementById(id)
|
|
if self.elements[id] then
|
|
return self.elements[id]
|
|
else
|
|
return nil
|
|
end
|
|
end
|
|
|
|
--- Get an element contained in the Window by name
|
|
--
|
|
-- @tparam string name Name of the element to return
|
|
-- @treturn table The element with name, if it exists in the window
|
|
function Window:getElementByName(name)
|
|
for _,v in pairs(self.elements) do
|
|
if v.name == name then return v end
|
|
end
|
|
end
|
|
|
|
--- Get the ID of the currently selected element
|
|
--
|
|
-- @treturn number ID of the currently selected element
|
|
function Window:getSelection() return self.select end
|
|
|
|
--- Set the selected element by ID
|
|
--
|
|
-- @tparam number id ID of the element to select
|
|
function Window:setSelection(id)
|
|
-- Check if there is an element with this ID
|
|
assert(self.elements[id], "No element ID "..tostring(id))
|
|
self.select = id
|
|
end
|
|
|
|
--- Get the currently selected element
|
|
--
|
|
-- @treturn table Element which is currently selected
|
|
function Window:getSelectedElement() return self.elements[self.select] end
|
|
|
|
--- Render the Window and it's elements
|
|
--
|
|
-- @tparam table terminal Terminal on which to render the Window
|
|
function Window:render(terminal)
|
|
-- Background
|
|
paintutils.drawFilledBox(
|
|
self.x, self.y,
|
|
self.x + self.width-1, self.y + self.height,
|
|
self.bgColor
|
|
)
|
|
-- Title bar
|
|
paintutils.drawLine(
|
|
self.x, self.y,
|
|
self.x + self.width-1, self.y,
|
|
self.titleColor
|
|
)
|
|
|
|
terminal.setTextColor(self.textColor)
|
|
terminal.setBackgroundColor(self.titleColor)
|
|
|
|
terminal.setCursorPos(self.x, self.y)
|
|
terminal.write(self.title)
|
|
|
|
-- Render each element on the window
|
|
for k,v in pairs(self.elements) do
|
|
v:render(terminal, self.x, self.y, self.width, self.height)
|
|
|
|
-- Add a mark to selected button
|
|
if self.select == k then
|
|
if instanceOf(v, Button) then
|
|
-- Qualify instances of buttons, because we don't want to draw
|
|
-- this selector when it's an Input
|
|
terminal.setCursorPos(self.x + v.x - 1, self.y + v.y + v.height/21)
|
|
terminal.write("*")
|
|
end
|
|
end
|
|
end
|
|
|
|
-- If an input is selected, place the cursor in it and enable cursor
|
|
-- Because inputs are not always the last to render (and therefore
|
|
-- won't be able to set the cursor where they want) we have to do this
|
|
-- here.
|
|
local selectedElement = self:getSelectedElement()
|
|
if instanceOf(selectedElement, Input) then
|
|
if #selectedElement:getValue() <= selectedElement.width - 1 then
|
|
terminal.setCursorPos(
|
|
self.x + selectedElement.x - 1 + #selectedElement:getValue(),
|
|
self.y + selectedElement.y
|
|
)
|
|
else
|
|
terminal.setCursorPos(
|
|
self.x + selectedElement.x - 1 + selectedElement.width - 1,
|
|
self.y + selectedElement.y
|
|
)
|
|
end
|
|
terminal.setCursorBlink(true)
|
|
else
|
|
terminal.setCursorBlink(false)
|
|
end
|
|
|
|
end
|
|
|
|
--- Instantiate a new Compositor
|
|
--
|
|
-- @{Compositor} is the primary class in which @{Window}s reside
|
|
--
|
|
-- @tparam table terminal Terminal on which to render Windows
|
|
-- @tparam[opt] table o Compositor object
|
|
-- @treturn table Instance of class Compositor
|
|
function Compositor:new(terminal, o)
|
|
o = o or {
|
|
-- The last element in this table is always going to be the focused
|
|
-- window. Focused windows will be drawn last over all other windows.
|
|
windows = {},
|
|
terminal = terminal,
|
|
running = true
|
|
}
|
|
|
|
setmetatable(o, self)
|
|
self.__index = self
|
|
return o
|
|
end
|
|
|
|
--- Add a window
|
|
--
|
|
-- @tparam table o Window to add/open
|
|
function Compositor:addWindow(o) return table.insert(self.windows, o) end
|
|
|
|
--- Remove a window
|
|
--
|
|
-- @tparam number id ID of the Window to remove/close
|
|
function Compositor:removeWindow(id) return table.remove(self.windows, id) end
|
|
|
|
--- Get all Windows
|
|
--
|
|
-- @treturn table Table of Windows
|
|
function Compositor:getWindows() return self.windows end
|
|
|
|
--- Get a Window by its ID
|
|
--
|
|
-- @treturn table An instance of class Window
|
|
function Compositor:getWindow(id) return self.windows[id] end
|
|
|
|
--- Get a Window by its name
|
|
--
|
|
-- @treturn table An instance of class Window
|
|
function Compositor:getWindowByName(name)
|
|
for k,v in pairs(self.windows) do
|
|
if v.name == name then
|
|
return v
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Get the focused Windows
|
|
--
|
|
-- @treturn table An instance of class Window
|
|
function Compositor:getFocusedWindow() return self.windows[#self.windows] end
|
|
|
|
--- Set the focused window
|
|
--
|
|
-- @tparam number windowId ID of the window to focus
|
|
function Compositor:setFocusedWindow(windowId)
|
|
-- Move the self.windows[windowId] to the end of the table
|
|
self:addWindow(self:removeWindow(windowId))
|
|
assert(pcall(self:getFocusedWindow().onLoad))
|
|
end
|
|
|
|
--- Remove / close the focused Window
|
|
function Compositor:removeFocusedWindow()
|
|
assert(pcall(self:getFocusedWindow().onClose))
|
|
self:removeWindow(#self.windows)
|
|
end
|
|
|
|
--- Stop the compositor
|
|
--
|
|
-- The compositor handles "terminate" events internally
|
|
function Compositor:stop() self.running = false end
|
|
|
|
--- Run the compositor, rendering @{Window}s and listening and handling events
|
|
-- to navigate and interact with @{GraphicsElement}s
|
|
--
|
|
-- Only the focused window (and all of it's contained elements) will be
|
|
-- rendered each pass to reduce screen flickering.
|
|
--
|
|
-- The compositor handles the following events internally for window and
|
|
-- element interaction and will not pass them to the eventHandler function:
|
|
-- mouse_click, mouse_up, mouse_drag, key, char, and terminate
|
|
--
|
|
-- When the last Window has been closed (is removed), the compositor will stop.
|
|
--
|
|
-- @tparam[opt] Function to be run before rendering the Windows
|
|
-- @tparam[opt] Function to be run after rendering the Windows
|
|
-- @tparam[opt] Function to handle events not handled by the compositor
|
|
function Compositor:run(preRender, postRender, eventHandler)
|
|
preRender = preRender or function(compositor) end
|
|
postRender = postRender or function(compositor) end
|
|
eventHandler = eventHandler or function(compositor, event) end
|
|
|
|
local window = self:getFocusedWindow()
|
|
|
|
-- Run the first window's onLoad function
|
|
if window.onLoad and type(window.onLoad) == "function" then
|
|
assert(pcall(window.onLoad))
|
|
end
|
|
|
|
while self.running do
|
|
|
|
local window = self:getFocusedWindow()
|
|
|
|
-- Pre render
|
|
if preRender and type(preRender) == "function" then
|
|
assert(pcall(preRender, self))
|
|
end
|
|
|
|
-- Render windows
|
|
-- We will only care about rending the focused window for now
|
|
-- Windows behind the focused window should be ignored and overwritten
|
|
-- by the focused window
|
|
-- Eventually, we'll want to be able to drag smaller windows, so we
|
|
-- will need to draw what's behind the focused window, as well, to
|
|
-- prevent artifacting.
|
|
-- window:render(self.terminal)
|
|
|
|
-- Render each window
|
|
-- Lower IDs will be rendered first. Higher IDs are
|
|
window:render(self.terminal)
|
|
-- for k,v in pairs(self:getWindows()) do
|
|
-- v:render(self.terminal)
|
|
-- end
|
|
|
|
-- Post render
|
|
if postRender and type(postRender) == "function" then
|
|
assert(pcall(postRender, self))
|
|
end
|
|
|
|
local selectedElement = window:getSelectedElement()
|
|
|
|
-- Listen for events
|
|
local event = {os.pullEventRaw()}
|
|
if event[1] == "mouse_click" or event[1] == "monitor_touch" then
|
|
-- Check if the mouse is down on the title bar of a Window
|
|
-- If it is, then when a drag event comes we will reposition the window
|
|
-- Do this *before* doing any button actions, in case the button action is to *close this Window*
|
|
if event[2] == 1 and event[3] >= window.x and event[3] <= window.x + window.width
|
|
and event[4] == window.y then
|
|
window.dragging = true
|
|
end
|
|
-- Check to see if click falls within any element's boundaries
|
|
for id, element in pairs(window:getElements()) do
|
|
if event[3] >= window.x + element.x - 1
|
|
and event[3] <= window.x + element.x + element.width - 2
|
|
and event[4] >= window.y + element.y
|
|
and event[4] <= window.y + element.y + element.height - 1 then
|
|
-- Element clicked, run the element's action function
|
|
window:setSelection(id)
|
|
-- Only want to run the action if it's a button
|
|
if instanceOf(element, Button) then
|
|
assert(pcall(element.action))
|
|
elseif instanceOf(element, Dataview) then
|
|
if event[4] <= #element:getData() then
|
|
element:setSelection(event[4] - element.y + element:getScrollY() - 2)
|
|
end
|
|
end
|
|
end
|
|
|
|
end
|
|
elseif event[1] == "mouse_drag" then
|
|
if window.dragging then
|
|
window:move(event[3], event[4])
|
|
end
|
|
elseif event[1] == "mouse_up" then
|
|
window.dragging = false
|
|
elseif event[1] == "mouse_scroll" then
|
|
if instanceOf(selectedElement, Dataview) then
|
|
-- event[2] scroll direction -1: up, 1: down
|
|
if event[2] == -1 and selectedElement:getScrollY() > 1 then
|
|
selectedElement:setScrollY(selectedElement:getScrollY() - 1)
|
|
elseif event[2] == 1 and selectedElement:getScrollY() < #selectedElement:getData() - selectedElement.height then
|
|
selectedElement:setScrollY(selectedElement:getScrollY() + 1)
|
|
end
|
|
end
|
|
elseif event[1] == "key" then
|
|
if event[2] == keys.enter then
|
|
-- Enter key pressed, run the element's action function
|
|
if selectedElement.action
|
|
and type(selectedElement.action) == "function" then
|
|
assert(pcall(selectedElement.action))
|
|
end
|
|
elseif event[2] == keys.up and window:getSelection() > 1 then
|
|
-- Select previous option
|
|
local selection = window:getSelection()-1
|
|
-- Loop through the options that come before the one that is
|
|
-- currently selected
|
|
-- elements = { [...], selection, ... }
|
|
for i = 1,selection do
|
|
-- If it's a button or input, set the selection to that
|
|
-- element's ID
|
|
if instanceOf(window:getElementById(i), Button)
|
|
or instanceOf(window:getElementById(i), Input) then
|
|
window:setSelection(i)
|
|
end
|
|
end
|
|
elseif event[2] == keys.down then
|
|
-- Select next option
|
|
local selection = window:getSelection()+1
|
|
-- elements = { ..., selection, [...] }
|
|
for i = selection,#window:getElements() do
|
|
if instanceOf(window:getElementById(i), Button)
|
|
or instanceOf(window:getElementById(i), Input) then
|
|
window:setSelection(i)
|
|
break
|
|
end
|
|
end
|
|
elseif event[2] == keys.backspace
|
|
and instanceOf(selectedElement, Input) then
|
|
-- If backspace is pressed and we've selected an Input element,
|
|
-- remove the last character from the element title
|
|
window:getSelectedElement():setValue(
|
|
selectedElement:getValue():sub(1, -2)
|
|
)
|
|
end
|
|
elseif event[1] == "char" and instanceOf(selectedElement, Input) then
|
|
-- Qualify with having an input selected, because otherwise we want
|
|
-- to pass the char event to the event handler function.
|
|
selectedElement:setValue(selectedElement:getValue()..event[2])
|
|
elseif event[1] == "terminate" then
|
|
-- Stop the menu
|
|
self.running = false
|
|
else
|
|
-- Pass any other events to the custom event handler function
|
|
if eventHandler and type(eventHandler) == "function" then
|
|
assert(pcall(eventHandler, self, event))
|
|
end
|
|
end
|
|
|
|
-- Stop if the last window has been closed/removed
|
|
if #self:getWindows() < 1 then self.running = false end
|
|
end
|
|
|
|
-- Reset terminal
|
|
term.setBackgroundColor(colors.black)
|
|
term.setTextColor(colors.white)
|
|
term.clear()
|
|
term.setCursorPos(1, 1)
|
|
end
|
|
|
|
-- vim: syntax=lua
|