Files

345 lines
12 KiB
Lua

-- This is a little fake actor class meant for displaying lines of a log.
-- It's placed inside Def, but it's actually just an ActorFrame with some
-- children and special commands.
-- LogDisplay listens for several messages that control its behavior.
-- The name given to the LogDisplay is added to the name of the message to
-- make the messages unique to this LogDisplay. So to issue a command to
-- the LogDisplay, broadcast the message "<command><name>".
-- For a LogDisplay named "Foo", you would broadcase "Foo" to add to the
-- log, "ToggleFoo" to toggle whether it's shown or hidden, and so on.
-- Some of the control messages take tables of args.
-- Arg elements of the form "name= type" are named, and must be a value of
-- the given type.
-- Arg elements of the form "name" are unnamed.
-- The messages are:
-- <name>: params= {message= string, dont_show= bool}
-- Adds message to the log to be shown when revealing.
-- If dont_show is false, then Show will be automatically executed with
-- auto_hide= true.
-- Messages with line breaks will take up multiple lines.
-- The newest message in the log has index 1.
-- Toggle: No params
-- Executes either Show or Hide, depending on the current state.
-- Show: params= {range= {last_message, lines}, auto_hide= bool}
-- Shows the messages currently in the log.
-- range[1] is the index of the last message to show.
-- range[2] is the max number of lines to show.
-- range[1] defaults to a cleavage shot.
-- range[2] defaults to MaxDisplayLines.
-- If auto_hide is non-nil, Hide will be execuated after a short time.
-- If auto_hide is a number, it will be used as the time to wait before
-- hiding. Otherwise, the Times.show will be used.
-- Query: No params
-- Broadcasts "<name>Response" message with a table containing:
-- {log_len= number, hidden= bool}
-- log_len is the number of messages currently in the log.
-- hidden is whether the LogDisplay is currently hidden.
-- Hide: No params
-- Hides the LogDisplay.
-- Clear: params= {messages}
-- Clears the messages in the log. If messages is a number, only clears
-- that many messages, oldest first.
-- params to Def.LogDisplay():
-- {
-- Name= string
-- The name will be used as the name of the main ActorFrame and to
-- control what messages the LogDisplay listens for.
-- MaxLogLen= number
-- The maximum number of messages to store in the log.
-- MaxDisplayLines= number
-- The maximum number of lines that can be displayed at a time.
-- ReplaceLinesWhenHidden= bool
-- If this is true, any messages recieved while the LogDisplay is hidden
-- will replace the message currently at the front of the log.
-- If this is false, messages recieved while the LogDisplay is hidden
-- will push other messages back. Messages pushed past MaxLines will be
-- removed.
-- IgnoreIdentical= bool
-- If true, then a message that is identical to one already in the log
-- will be ignored.
-- Font= font name
-- The name of the font to use. This will be passed to THEME:GetPathF,
-- so it should not be a path.
-- LineHeight= number
-- The height in pixels to use between lines.
-- LineWidth= number
-- The width that lines should be limited to.
-- TextZoom= number
-- The zoom factor to apply to the text.
-- TextColor= color
-- The color to use for the text.
-- Times= {show= number, hide= number}
-- show is the amount of time to wait before automatically hiding when
-- Show is executed with AutoHide == true. It's passed as a table, so
-- if you keep a copy of the table, you should be able to modify it to
-- change the time.
-- hide is used as the time for the default hide command to hide.
-- Indent= number
-- The amount in pixels to indent lines that were part of a multi-line
-- message.
-- Hide= function(self)
-- This command will be executed when the LogDisplay needs to be hidden.
-- Hide will also be executed during the InitCommand, so the
-- LogDisplay will start in the hidden state.
-- Show= function(self, Lines)
-- This command will be executed when the LogDisplay needs to be shown.
-- Lines is the number of lines to show.
-- }
-- Any actors placed in params will be used and drawn behind the text.
-- If there are no actors in params, a quad filling the area will be used.
-- Reasonable defaults are provided for everything except Name. If Name is
-- blank, you get nothing. Defaults assume the LogDisplay should fill the
-- screen when in use.
-- Below is the implementation of the above features.
local log_display_mt= {
__index= {
create_actors= function(self, params)
self.name= params.Name
self.font= params.Font or "Common Normal"
self.line_height= params.LineHeight or 12
self.line_width= params.LineWidth or SCREEN_WIDTH
self.text_zoom= params.TextZoom or .5
self.text_color= params.TextColor or color("#93a1a1")
local lines_on_screen= params.MaxLines or SCREEN_HEIGHT / self.line_height
self.max_lines= math.floor(lines_on_screen)
self.add_to_height= (lines_on_screen - self.max_lines) * self.line_height
self.max_log= params.MaxLogLen or self.max_lines
self.indent= params.Indent or 8
self.times= params.Times
self.param_hide= params.Hide or
function(subself)
subself:linear(params.Times.hide)
subself:y(-SCREEN_HEIGHT)
end
self.param_show= params.Show or
function(subself, lines)
subself:visible(true)
subself:linear(params.Times.hide)
local cover= math.min(self.line_height * (lines+.5), SCREEN_HEIGHT)
subself:y(-SCREEN_HEIGHT + cover + self.add_to_height)
end
self.text_actors= {}
self.message_log= {}
self.messes_since_update= 0
local name_mess= self.name .. "MessageCommand"
local args= {
Name= self.name,
InitCommand= function(subself)
self.container= subself; -- This semicolon ends this statement.
-- Without it, the next would be ambiguous syntax.
-- Let the InitCommand passed in params do something.
(params.InitCommand or function() end)(subself)
self:hide()
end,
OffCommand= function(subself)
subself:visible(false)
for i, actor in ipairs(self.text_actors) do
actor:visible(false)
end
end,
[name_mess]= function(subself, mess)
if not PREFSMAN:GetPreference("ShowThemeErrors")
and self.name == "ScriptError" then
subself:visible(false)
return
end
if not mess.message then return end
if self.messes_since_update > self.max_log then return end
-- Long ago, someone decided that "::" should be an alias for "\n"
-- and hardcoded it into BitmapText.
local message= tostring(mess.message):gsub("::", ":")
if params.IgnoreIdentical and not self.hidden then
for i, prevmess in ipairs(self.message_log) do
if message == prevmess then return end
end
end
if params.ReplaceLinesWhenHidden and self.hidden then
self:clear()
self.message_log[1]= message
else
table.insert(self.message_log, 1, message)
if #self.message_log > self.max_log then
table.remove(self.message_log)
end
end
if not mess.dont_show or not self.hidden then
self.messes_since_update= self.messes_since_update + 1
self.hidden= false
subself:stoptweening()
subself:queuecommand("Update")
end
end,
["Toggle" .. name_mess]= function(subself)
if self.hidden then
self:show()
else
self:hide()
end
end,
["Hide" .. name_mess]= function(subself)
self:hide()
end,
["Show" .. name_mess]= function(subself, mess)
self:show(mess.range, mess.auto_hide)
end,
["Clear" .. name_mess]= function(subself, mess)
self:clear(mess[1])
end,
UpdateCommand= function(subself)
self:show(nil, true)
self.messes_since_update= 0
end,
LoadFont(self.font) ..
{
Name="WidthTester",
InitCommand= function(subself)
self.width_tester= subself
subself:zoom(self.text_zoom)
subself:visible(false)
end
}
}
if #params == 0 then
args[#args+1]= Def.Quad {
Name= "Logbg",
InitCommand= function(subself)
subself:setsize(self.line_width, self.line_height*self.max_lines)
subself:horizalign(left)
subself:vertalign(top)
subself:diffuse(color("#002b36"))
subself:diffusealpha(.85)
end,
}
else
-- Add bg actors passed through params.
for i, actor in ipairs(params) do
args[#args+1]= actor
end
end
-- Add commands passed through params.
for name, command in pairs(params) do
if type(name) == "string" and name:find("Command") and not args[name] then
args[name]= command
end
end
for i= 1, self.max_lines do
args[#args+1]= LoadFont(self.font) ..
{
Name="Text" .. i,
InitCommand= function(subself)
-- Put them in the list in reverse order so the ones at the
-- bottom of the screen are used first.
self.text_actors[self.max_lines-i+1]= subself
subself:horizalign(left)
subself:vertalign(top)
subself:y(self.line_height * (i-1))
subself:zoom(self.text_zoom)
subself:diffuse(self.text_color)
subself:visible(false)
end,
OffCommand= cmd(visible,false),
}
end
return Def.ActorFrame(args)
end,
hide= function(self)
self.container:stoptweening()
self.param_hide(self.container)
self.container:queuecommand("Off")
self.hidden= true
end,
show= function(self, range, auto_hide)
if not range then range= {} end
local start= range[1] or 1
local lmax= range[2] or self.max_lines
local indented_lines= {}
local next_message= start
local num_lines= 0
while num_lines < lmax and self.message_log[next_message] do
self.width_tester:settext(self.message_log[next_message])
local lines_to_add= convert_text_to_indented_lines(
self.width_tester, self.indent, self.line_width, self.text_zoom)
indented_lines[#indented_lines+1]= lines_to_add
num_lines= num_lines + #lines_to_add
next_message= next_message + 1
end
local use_next= 1
local used= 0
for i, mess in ipairs(indented_lines) do
for l, line in ipairs(mess) do
-- Start at the end of the mess because the text actors are in
-- reverse order.
local ind= use_next + (#mess - l)
local actor= self.text_actors[ind]
if actor and used <= lmax then
actor:settext(line[2])
local indent= 8 + self.indent * line[1]
local lw= self.line_width - (indent + 8)
width_limit_text(actor, lw, self.text_zoom)
actor:x(indent)
actor:visible(true)
used= used + 1
end
end
use_next= use_next + #mess
end
self.container:stoptweening()
self.param_show(self.container, used)
self.hidden= false
if auto_hide then
self.container:sleep(self.times.show)
self.container:queuecommand("Hide" .. self.name)
end
end,
clear= function(self, messes)
if #self.message_log < 1 then return end
if not messes then
self.message_log= {}
else
for i= 1, messes do
table.remove(self.message_log)
if #self.message_log < 1 then break end
end
end
end
}}
function Def.LogDisplay(params)
if type(params.Name) ~= "string" or params.Name == "" then
ReportScriptError("Cannot create a LogDisplay without a name.")
return nil
end
if not params.Times then params.Times= {show= 4, hide= .125} end
if not params.Times.show then params.Times.show= 4 end
if not params.Times.hide then params.Times.hide= .125 end
local new_log_display= setmetatable({}, log_display_mt)
_G[params.Name .. "LogDisplay"]= new_log_display
return new_log_display:create_actors(params)
end
--[[
Copyright © 2014 Eric Reese / Kyzentun
All rights reserved.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
]]