diff --git a/Docs/Luadoc/Lua.xml b/Docs/Luadoc/Lua.xml index c2297a38d2..bc4e99d244 100644 --- a/Docs/Luadoc/Lua.xml +++ b/Docs/Luadoc/Lua.xml @@ -1822,6 +1822,7 @@ + diff --git a/Docs/Luadoc/LuaDocumentation.xml b/Docs/Luadoc/LuaDocumentation.xml index bee1948b6f..61886525d3 100644 --- a/Docs/Luadoc/LuaDocumentation.xml +++ b/Docs/Luadoc/LuaDocumentation.xml @@ -749,6 +749,10 @@ save yourself some time, copy this for undocumented things: Tries to read the file at sPath. If successful, it returns the file's contents. If unsuccessful, it returns two values: nil and "error". + + Reports the error through the error reporting system. error_type is the type used for the dialog that is presented, a dialog will not appear for a type the user has chosen to ignore.
+ error is optional an defaults to "Script error occurred.". error_type is optional and defaults to "LUA_ERROR". +
Calls func(...) with two LuaThreadVariables set, and returns the return values of func(). diff --git a/Docs/Themerdocs/actordef.txt b/Docs/Themerdocs/actordef.txt index 3257511314..551600a5cd 100644 --- a/Docs/Themerdocs/actordef.txt +++ b/Docs/Themerdocs/actordef.txt @@ -26,6 +26,7 @@ GrooveRadar HelpDisplay HoldJudgment InputList +LogDisplay MemoryCardDisplay MeterDisplay Model @@ -43,4 +44,4 @@ Sprite StepsDisplay StepsDisplayList TextBanner -WorkoutGraph \ No newline at end of file +WorkoutGraph diff --git a/Themes/_fallback/BGAnimations/ScreenSystemLayer error.lua b/Themes/_fallback/BGAnimations/ScreenSystemLayer error.lua index fed9f3808b..830360a71f 100644 --- a/Themes/_fallback/BGAnimations/ScreenSystemLayer error.lua +++ b/Themes/_fallback/BGAnimations/ScreenSystemLayer error.lua @@ -1,56 +1,9 @@ -- If you are a common themer, DO NOT INCLUDE THIS FILE IN YOUR THEME. -- This layer is purely for error reporting, so that you can see errors on screen easily while running stepmania. -- If you include this file in your theme, you will not benefit from any improvements in error reporting. --- If you want a different background behind the errors, then include "ScreenSystemLayer errorbg.lua" in your theme. -- If you want to adjust how long errors stay on screen for, call the "SetErrorMessageTime" function. (see usage notes in comments above that function) --- Minor text formatting functions from Kyzentun. --- TODO: Figure out why BitmapText:maxwidth doesn't do what I want. -local function width_limit_text(text, limit, natural_zoom) - natural_zoom= natural_zoom or 1 - if text:GetWidth() * natural_zoom > limit then - text:zoomx(limit / text:GetWidth()) - else - text:zoomx(natural_zoom) - end -end - -local function width_clip_text(text, limit) - local full_text= text:GetText() - local fits= text:GetZoomedWidth() <= limit - local prev_max= #full_text - 1 - local prev_min= 0 - if not fits then - while prev_max - prev_min > 1 do - local new_max= math.round((prev_max + prev_min) / 2) - text:settext(full_text:sub(1, 1+new_max)) - if text:GetZoomedWidth() <= limit then - prev_min= new_max - else - prev_max= new_max - end - end - text:settext(full_text:sub(1, 1+prev_min)) - end -end - -local function width_clip_limit_text(text, limit, natural_zoom) - natural_zoom= natural_zoom or text:GetZoomY() - local text_width= text:GetWidth() * natural_zoom - if text_width > limit * 2 then - text:zoomx(natural_zoom * .5) - width_clip_text(text, limit) - else - width_limit_text(text, limit, natural_zoom) - end -end - -local text_actors= {} local line_height= 12 -- A good line height for Common Normal at .5 zoom. -local line_width= SCREEN_WIDTH - 20 - -local next_message_actor= 1 -local show_requested= false local min_message_time= {show= 1, hide= .03125} local default_message_time= {show= 4, hide= .125} @@ -92,130 +45,12 @@ function GetErrorMessageTimeDefault(which) return default_message_time[which] end -local frame_args= { - Name="Error frame", - InitCommand= function(self) - self:y(-SCREEN_HEIGHT) - end, - ScriptErrorMessageCommand = function(self, params) - show_requested= false - self:stoptweening() - self:visible(true) - local covered_height= math.min(line_height * next_message_actor, SCREEN_HEIGHT) - self:y(-SCREEN_HEIGHT + covered_height) - self:sleep(message_time.show) - self:queuecommand("DecNextActor") - -- Shift the text on all the actors being shown up by one. - for i= next_message_actor, 1, -1 do - if text_actors[i] then - text_actors[i]:visible(true) - if i > 1 and text_actors[i-1] then - text_actors[i]:settext(text_actors[i-1]:GetText()) - else - -- Someone long ago decided that it was a good idea for "::" to be - -- a synonym for "\n", so that strings in metrics could have line - -- breaks. - -- So replace "::" with ":" so we don't have unwanted line breaks. - text_actors[i]:settext(params.Message:gsub("::", ":")) - end - width_clip_limit_text(text_actors[i], line_width) - end - end - if next_message_actor <= #text_actors then - next_message_actor= next_message_actor + 1 - end - end, - ToggleErrorsMessageCommand= function(self, params) - if show_requested then - self:playcommand("HideErrors") - else - self:playcommand("ShowErrors") - end - end, - ShowErrorsMessageCommand= function(self, params) - show_requested= true - for i, mactor in ipairs(text_actors) do - if mactor:GetText() ~= "" then - mactor:visible(true) - next_message_actor= next_message_actor + 1 - end - end - next_message_actor= next_message_actor - 1 - local covered_height= math.min(line_height * next_message_actor, SCREEN_HEIGHT) - self:stoptweening() - self:visible(true) - self:linear(message_time.hide) - self:y(-SCREEN_HEIGHT + covered_height) - end, - HideErrorsMessageCommand= function(self, params) - if not show_requested then return end - show_requested= false - self:stoptweening() - next_message_actor= 1 - self:linear(message_time.hide) - self:y(-SCREEN_HEIGHT) - self:queuecommand("HideText") - end, - ClearErrorsMessageCommand= function(self, params) - -- This is so that someone using ShowErrorsMessageCommand can clear ones - -- they've dealt with. - -- Only allow clearing errors that we have scrolled off screen. - for i, mactor in ipairs(text_actors) do - if not mactor:GetVisible() then - mactor:settext("") - end - end - end, - DecNextActorCommand= function(self) - self:linear(message_time.hide) - self:y(self:GetY()-line_height) - if text_actors[next_message_actor] then - text_actors[next_message_actor]:visible(false) - end - next_message_actor= next_message_actor - 1 - if next_message_actor > 1 then - self:queuecommand("DecNextActor") - else - self:queuecommand("Off") - end - end, - HideTextCommand= function(self) - for i, mactor in ipairs(text_actors) do - mactor:visible(false) - end - end, - OffCommand= cmd(visible,false), - Def.Quad { - Name= "errorbg", - InitCommand= function(self) - self:setsize(SCREEN_WIDTH, SCREEN_HEIGHT) - self:horizalign(left) - self:vertalign(top) - self:diffuse(color("0,0,0,0")) - self:diffusealpha(.85) - end, - } +local log_args= { + Name= "ScriptError", + ReplaceLinesWhenHidden= true, + Times= message_time, } --- Create enough text actors that we can fill the screen. -local num_text= SCREEN_HEIGHT / line_height -for i= 1, num_text do - frame_args[#frame_args+1]= LoadFont("Common","Normal") .. { - Name="Text" .. i, - InitCommand= function(self) - -- Put them in the list in reverse order so the ones at the bottom of the screen are used first. - text_actors[num_text-i+1]= self - self:horizalign(left) - self:vertalign(top) - self:x(SCREEN_LEFT + 10) - self:y(SCREEN_TOP + (line_height * (i-1)) + 2) - self:shadowlength(1) - self:zoom(.5) - self:visible(false) - end, - OffCommand= cmd(visible,false), - } -end Trace("Loaded error layer.") -return Def.ActorFrame(frame_args) +return Def.LogDisplay(log_args) diff --git a/Themes/_fallback/Scripts/02 Utilities.lua b/Themes/_fallback/Scripts/02 Utilities.lua index aa99ec0767..2d456e770b 100644 --- a/Themes/_fallback/Scripts/02 Utilities.lua +++ b/Themes/_fallback/Scripts/02 Utilities.lua @@ -371,6 +371,78 @@ function IsUsingWideScreen() end; end; +-- Minor text formatting functions from Kyzentun. +-- TODO: Figure out why BitmapText:maxwidth doesn't do what I want. +-- Intentionally undocumented because they should be moved to BitmapText ASAP +function width_limit_text(text, limit, natural_zoom) + natural_zoom= natural_zoom or 1 + if text:GetWidth() * natural_zoom > limit then + text:zoomx(limit / text:GetWidth()) + else + text:zoomx(natural_zoom) + end +end + +function width_clip_text(text, limit) + local full_text= text:GetText() + local fits= text:GetZoomedWidth() <= limit + local prev_max= #full_text - 1 + local prev_min= 0 + if not fits then + while prev_max - prev_min > 1 do + local new_max= math.round((prev_max + prev_min) / 2) + text:settext(full_text:sub(1, 1+new_max)) + if text:GetZoomedWidth() <= limit then + prev_min= new_max + else + prev_max= new_max + end + end + text:settext(full_text:sub(1, 1+prev_min)) + end +end + +function width_clip_limit_text(text, limit, natural_zoom) + natural_zoom= natural_zoom or text:GetZoomY() + local text_width= text:GetWidth() * natural_zoom + if text_width > limit * 2 then + text:zoomx(natural_zoom * .5) + width_clip_text(text, limit) + else + width_limit_text(text, limit, natural_zoom) + end +end + +function convert_text_to_indented_lines(text, indent, width, text_zoom) + local text_as_lines= split("\n", text:GetText()) + local indented_lines= {} + for i, line in ipairs(text_as_lines) do + local remain= line + local sub_lines= 0 + repeat + text:settext(remain) + local clipped= false + local indent_mult= 0 + if i > 1 then + indent_mult= indent_mult + 1 + end + if sub_lines > 0 then + indent_mult= 2 + end + local usable_width= width - (indent * indent_mult) + if text:GetWidth() * text_zoom > usable_width * 2 then + clipped= true + width_clip_text(text, usable_width * 2) + end + indented_lines[#indented_lines+1]= { + indent_mult, text:GetText()} + remain= remain:sub(#text:GetText()+1) + sub_lines= sub_lines + 1 + until not clipped + end + return indented_lines +end + -- (c) 2005-2011 Glenn Maynard, Chris Danford, SSC -- All rights reserved. -- diff --git a/Themes/_fallback/Scripts/04 LogDisplay.lua b/Themes/_fallback/Scripts/04 LogDisplay.lua new file mode 100644 index 0000000000..4bfb50564b --- /dev/null +++ b/Themes/_fallback/Scripts/04 LogDisplay.lua @@ -0,0 +1,313 @@ +-- 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 "". +-- 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: +-- : 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 "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. +-- 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") + self.max_lines= params.MaxLines or SCREEN_HEIGHT / 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) + 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 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.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 diff --git a/src/LuaManager.cpp b/src/LuaManager.cpp index e76a7d8321..39eb6b452d 100644 --- a/src/LuaManager.cpp +++ b/src/LuaManager.cpp @@ -794,27 +794,17 @@ bool LuaHelpers::LoadScript( Lua *L, const RString &sScript, const RString &sNam void LuaHelpers::ScriptErrorMessage(RString const& Error) { Message msg("ScriptError"); - msg.SetParam("Message", Error); + msg.SetParam("message", Error); MESSAGEMAN->Broadcast(msg); } Dialog::Result LuaHelpers::ReportScriptError(RString const& Error, RString ErrorType, bool UseAbort) { - size_t line_break_pos= Error.find('\n'); - RString short_error; - if(line_break_pos == RString::npos) - { - short_error= Error; - } - else - { - short_error= Error.substr(0, line_break_pos); - } // Protect from a recursion loop resulting from a mistake in the error reporting lua. if(!InReportScriptError) { InReportScriptError= true; - ScriptErrorMessage(short_error); + ScriptErrorMessage(Error); InReportScriptError= false; } LOG->Warn(Error.c_str()); @@ -1154,6 +1144,22 @@ namespace return 1; } + static int ReportScriptError(lua_State* L) + { + RString error= "Script error occurred."; + RString error_type= "LUA_ERROR"; + if(lua_isstring(L, 1)) + { + error= SArg(1); + } + if(lua_isstring(L, 2)) + { + error_type= SArg(2); + } + LuaHelpers::ReportScriptError(error, error_type); + return 0; + } + const luaL_Reg luaTable[] = { LIST_METHOD( Trace ), @@ -1163,6 +1169,7 @@ namespace LIST_METHOD( ReadFile ), LIST_METHOD( RunWithThreadVariables ), LIST_METHOD( GetThreadVariable ), + LIST_METHOD( ReportScriptError ), { NULL, NULL } }; } diff --git a/src/ScreenDebugOverlay.cpp b/src/ScreenDebugOverlay.cpp index bb323496d7..d36355f6a2 100644 --- a/src/ScreenDebugOverlay.cpp +++ b/src/ScreenDebugOverlay.cpp @@ -1054,7 +1054,7 @@ class DebugLineToggleErrors : public IDebugLine virtual RString GetPageName() const { return "Theme"; } virtual void DoAndLog( RString &sMessageOut ) { - Message msg("ToggleErrors"); + Message msg("ToggleScriptError"); MESSAGEMAN->Broadcast(msg); IDebugLine::DoAndLog(sMessageOut); } @@ -1068,7 +1068,7 @@ class DebugLineClearErrors : public IDebugLine virtual RString GetPageName() const { return "Theme"; } virtual void DoAndLog( RString &sMessageOut ) { - Message msg("ClearErrors"); + Message msg("ClearScriptError"); MESSAGEMAN->Broadcast(msg); IDebugLine::DoAndLog(sMessageOut); }