Commit f1b3cd9a authored by Christian Schulte zu Berge's avatar Christian Schulte zu Berge
Browse files

Further work on Lua scripting console:

* Implemented one single gloabel Lua VM for the entire CampVisApplication instead of one VM for each pipeline.
* The global Lua VM has a "pipelines" table/array, holding the pointers to each loaded pipeline
* ScriptingWidget support cycling through last executed commands via the arrow keys
* LuaVmState supports redirecting Lua's print() function to a custom one that uses tgt::Logmanager for printing (just as proof-of-concept). This shall later be extended to pass all Lua output to the scripting console.
parent a33ee468
......@@ -59,6 +59,7 @@ namespace campvis {
, _localContext(0)
, _mainWindow(0)
, _errorTexture(nullptr)
, _luaVmState(nullptr)
, _initialized(false)
, _argc(argc)
, _argv(argv)
......@@ -145,6 +146,30 @@ namespace campvis {
tgt::TextureReaderTga trt;
_errorTexture = trt.loadTexture(CAMPVIS_SOURCE_DIR "/application/data/no_input.tga", tgt::Texture::LINEAR);
#ifdef CAMPVIS_HAS_SCRIPTING
// create and store Lua VM for this very pipeline
_luaVmState = new LuaVmState();
// Let Lua know where CAMPVis modules are located
if (! _luaVmState->execString("package.cpath = '" CAMPVIS_LUA_MODS_PATH "'"))
LERROR("Error setting up Lua VM.");
// Load CAMPVis' core Lua module to have SWIG glue for AutoEvaluationPipeline available
if (! _luaVmState->execString("require(\"campvis\")"))
LERROR("Error setting up Lua VM.");
if (! _luaVmState->execString("require(\"tgt\")"))
LERROR("Error setting up Lua VM.");
if (! _luaVmState->execString("pipelines = {}"))
LERROR("Error setting up Lua VM.");
if (! _luaVmState->execString("inspect = require 'inspect'"))
LERROR("Error setting up Lua VM.");
_luaVmState->redirectLuaPrint();
#endif
// parse argument list and create pipelines
QStringList pipelinesToAdd = this->arguments();
for (int i = 1; i < pipelinesToAdd.size(); ++i) {
......@@ -221,7 +246,7 @@ namespace campvis {
pipeline->setCanvas(canvas);
painter->setErrorTexture(_errorTexture);
PipelineRecord pr = { pipeline, painter, nullptr };
PipelineRecord pr = { pipeline, painter };
_pipelines.push_back(pr);
_mainWindow->addVisualizationPipelineWidget(name, canvas);
......@@ -232,28 +257,12 @@ namespace campvis {
makeJobOnHeap<CampVisApplication, tgt::GLCanvas*, AbstractPipeline*>(this, &CampVisApplication::initGlContextAndPipeline, canvas, pipeline),
OpenGLJobProcessor::SerialJob);
// create and store Lua VM for this very pipeline
#ifdef CAMPVIS_HAS_SCRIPTING
LuaVmState* lvs = new LuaVmState();
// Let Lua know where CAMPVis modules are located
if (!lvs->execString("package.cpath = '" CAMPVIS_LUA_MODS_PATH "'"))
LERROR("Error setting up Lua VM.");
// Load CAMPVis' core Lua module to have SWIG glue for AutoEvaluationPipeline available
if (!lvs->execString("require(\"campvis\")"))
LERROR("Error setting up Lua VM.");
if (!lvs->execString("require(\"tgt\")"))
LERROR("Error setting up Lua VM.");
// if (!lvs->execFile(CAMPVIS_SOURCE_DIR "/application/scripting/inspect.lua"))
// LERROR("Error setting up Lua VM.");
if (!lvs->injectObjectPointer(pipeline, "campvis::AutoEvaluationPipeline *", "pipeline")) {
if (! _luaVmState->injectObjectPointerToTable(pipeline, "campvis::AutoEvaluationPipeline *", "pipelines", static_cast<int>(_pipelines.size())))
//if (! _luaVmState->injectObjectPointerToTableField(pipeline, "campvis::AutoEvaluationPipeline *", "pipelines", name))
LERROR("Could not inject the pipeline into the Lua VM.");
}
_pipelines.back()._luaVmState = std::shared_ptr<LuaVmState>(lvs);
_luaVmState->execString("inspect(pipelines)");
#endif
s_PipelinesChanged();
......@@ -311,5 +320,10 @@ namespace campvis {
}
}
#ifdef CAMPVIS_HAS_SCRIPTING
LuaVmState* CampVisApplication::getLuaVmState() {
return _luaVmState;
}
#endif
}
......@@ -71,9 +71,6 @@ namespace campvis {
struct PipelineRecord {
AbstractPipeline* _pipeline;
CampVisPainter* _painter;
#ifdef CAMPVIS_HAS_SCRIPTING
std::shared_ptr<LuaVmState> _luaVmState;
#endif
};
/**
......@@ -138,6 +135,15 @@ namespace campvis {
*/
void rebuildAllShadersFromFiles();
#ifdef CAMPVIS_HAS_SCRIPTING
/**
* Returns the global LuaVmState of this application.
*/
LuaVmState* getLuaVmState();
#endif
/// Signal emitted when the collection of pipelines has changed.
sigslot::signal0<> s_PipelinesChanged;
......@@ -165,8 +171,12 @@ namespace campvis {
/// Main window hosting GUI stuff
MainWindow* _mainWindow;
/// Error texture to show if there is no output found
tgt::Texture* _errorTexture;
/// the global LuaVmState of this application
LuaVmState* _luaVmState;
/// Flag, whether CampVisApplication was correctly initialized
bool _initialized;
......
......@@ -325,10 +325,11 @@ namespace campvis {
}
void MainWindow::onLuaCommandExecuted(const QString& cmd) {
// FIXME: so far just a hack
if (! _application->_pipelines.empty()){
_application->_pipelines.front()._luaVmState->execString(cmd.toStdString());
#ifdef CAMPVIS_HAS_SCRIPTING
if (_application->getLuaVmState() != nullptr) {
_application->getLuaVmState()->execString(cmd.toStdString());
}
#endif
}
}
......@@ -24,10 +24,17 @@
#include "scriptingwidget.h"
#include <QKeyEvent>
namespace campvis {
ScriptingWidget::ScriptingWidget(QWidget* parent)
: QWidget(parent)
, _consoleDisplay(nullptr)
, _editCommand(nullptr)
, _btnExecute(nullptr)
, _btnClear(nullptr)
, _currentPosition(-1)
{
setupGUI();
}
......@@ -55,6 +62,7 @@ namespace campvis {
_consoleDisplay->document()->setDefaultFont(monoFont);
_editCommand = new QLineEdit(this);
_editCommand->setPlaceholderText(tr("Enter Lua commands here..."));
_editCommand->installEventFilter(this);
controlsLayout->addWidget(_editCommand);
_btnExecute = new QPushButton(tr("&Execute"), this);
......@@ -81,7 +89,33 @@ namespace campvis {
void ScriptingWidget::execute() {
QString command = _editCommand->text();
emit s_commandExecuted(command);
_history.push_front(command);
_currentPosition = -1;
_editCommand->clear();
}
bool ScriptingWidget::eventFilter(QObject* obj, QEvent* event) {
if (obj == _editCommand && event->type() == QEvent::KeyPress) {
QKeyEvent* e = static_cast<QKeyEvent*>(event);
if (e->key() == Qt::Key_Up && _currentPosition < static_cast<int>(_history.size())-1) {
++_currentPosition;
_editCommand->setText(_history[_currentPosition]);
return true;
}
if (e->key() == Qt::Key_Down && _currentPosition >= 0) {
--_currentPosition;
if (_currentPosition >= 0)
_editCommand->setText(_history[_currentPosition]);
else
_editCommand->setText(tr(""));
return true;
}
}
return QObject::eventFilter(obj, event);
}
}
......@@ -32,6 +32,8 @@
#include "application/tools/bufferinglog.h"
#include "application/gui/loghighlighter.h"
#include <deque>
#include <QComboBox>
#include <QLabel>
#include <QWidget>
......@@ -65,6 +67,7 @@ namespace campvis {
*/
void setupGUI();
bool eventFilter(QObject* obj, QEvent* event);
public slots:
/**
......@@ -77,6 +80,7 @@ namespace campvis {
*/
void appendMessage(const QString& message);
private slots:
/**
* Delete all messages from the log viewer
*/
......@@ -92,6 +96,9 @@ namespace campvis {
QLineEdit* _editCommand; ///< Text field to enter Lua commands
QPushButton* _btnExecute; ///< Button to execute command
QPushButton* _btnClear; ///< Button to clear the console output
std::deque<QString> _history; ///< History of executed commands
int _currentPosition; ///< Current position in command history
};
}
......
inspect ={
_VERSION = 'inspect.lua 2.0.0',
_URL = 'http://github.com/kikito/inspect.lua',
_DESCRIPTION = 'human-readable representations of tables',
_LICENSE = [[
MIT LICENSE
Copyright (c) 2013 Enrique García Cota
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]
}
-- Apostrophizes the string if it has quotes, but not aphostrophes
-- Otherwise, it returns a regular quoted string
local function smartQuote(str)
if str:match('"') and not str:match("'") then
return "'" .. str .. "'"
end
return '"' .. str:gsub('"', '\\"') .. '"'
end
local controlCharsTranslation = {
["\a"] = "\\a", ["\b"] = "\\b", ["\f"] = "\\f", ["\n"] = "\\n",
["\r"] = "\\r", ["\t"] = "\\t", ["\v"] = "\\v"
}
local function escapeChar(c) return controlCharsTranslation[c] end
local function escape(str)
local result = str:gsub("\\", "\\\\"):gsub("(%c)", escapeChar)
return result
end
local function isIdentifier(str)
return type(str) == 'string' and str:match( "^[_%a][_%a%d]*$" )
end
local function isArrayKey(k, length)
return type(k) == 'number' and 1 <= k and k <= length
end
local function isDictionaryKey(k, length)
return not isArrayKey(k, length)
end
local defaultTypeOrders = {
['number'] = 1, ['boolean'] = 2, ['string'] = 3, ['table'] = 4,
['function'] = 5, ['userdata'] = 6, ['thread'] = 7
}
local function sortKeys(a, b)
local ta, tb = type(a), type(b)
-- strings and numbers are sorted numerically/alphabetically
if ta == tb and (ta == 'string' or ta == 'number') then return a < b end
local dta, dtb = defaultTypeOrders[ta], defaultTypeOrders[tb]
-- Two default types are compared according to the defaultTypeOrders table
if dta and dtb then return defaultTypeOrders[ta] < defaultTypeOrders[tb]
elseif dta then return true -- default types before custom ones
elseif dtb then return false -- custom types after default ones
end
-- custom types are sorted out alphabetically
return ta < tb
end
local function getDictionaryKeys(t)
local keys, length = {}, #t
for k,_ in pairs(t) do
if isDictionaryKey(k, length) then table.insert(keys, k) end
end
table.sort(keys, sortKeys)
return keys
end
local function getToStringResultSafely(t, mt)
local __tostring = type(mt) == 'table' and rawget(mt, '__tostring')
local str, ok
if type(__tostring) == 'function' then
ok, str = pcall(__tostring, t)
str = ok and str or 'error: ' .. tostring(str)
end
if type(str) == 'string' and #str > 0 then return str end
end
local maxIdsMetaTable = {
__index = function(self, typeName)
rawset(self, typeName, 0)
return 0
end
}
local idsMetaTable = {
__index = function (self, typeName)
local col = setmetatable({}, {__mode = "kv"})
rawset(self, typeName, col)
return col
end
}
local function countTableAppearances(t, tableAppearances)
tableAppearances = tableAppearances or setmetatable({}, {__mode = "k"})
if type(t) == 'table' then
if not tableAppearances[t] then
tableAppearances[t] = 1
for k,v in pairs(t) do
countTableAppearances(k, tableAppearances)
countTableAppearances(v, tableAppearances)
end
countTableAppearances(getmetatable(t), tableAppearances)
else
tableAppearances[t] = tableAppearances[t] + 1
end
end
return tableAppearances
end
local function parse_filter(filter)
if type(filter) == 'function' then return filter end
-- not a function, so it must be a table or table-like
filter = type(filter) == 'table' and filter or {filter}
local dictionary = {}
for _,v in pairs(filter) do dictionary[v] = true end
return function(x) return dictionary[x] end
end
local function makePath(path, key)
local newPath, len = {}, #path
for i=1, len do newPath[i] = path[i] end
newPath[len+1] = key
return newPath
end
-------------------------------------------------------------------
function inspect.inspect(rootObject, options)
options = options or {}
local depth = options.depth or math.huge
local filter = parse_filter(options.filter or {})
local tableAppearances = countTableAppearances(rootObject)
local buffer = {}
local maxIds = setmetatable({}, maxIdsMetaTable)
local ids = setmetatable({}, idsMetaTable)
local level = 0
local blen = 0 -- buffer length
local function puts(...)
local args = {...}
for i=1, #args do
blen = blen + 1
buffer[blen] = tostring(args[i])
end
end
local function down(f)
level = level + 1
f()
level = level - 1
end
local function tabify()
puts("\n", string.rep(" ", level))
end
local function commaControl(needsComma)
if needsComma then puts(',') end
return true
end
local function alreadyVisited(v)
return ids[type(v)][v] ~= nil
end
local function getId(v)
local tv = type(v)
local id = ids[tv][v]
if not id then
id = maxIds[tv] + 1
maxIds[tv] = id
ids[tv][v] = id
end
return id
end
local putValue -- forward declaration that needs to go before putTable & putKey
local function putKey(k)
if isIdentifier(k) then return puts(k) end
puts( "[" )
putValue(k, {})
puts("]")
end
local function putTable(t, path)
if alreadyVisited(t) then
puts('<table ', getId(t), '>')
elseif level >= depth then
puts('{...}')
else
if (not tableAppearances[t]) or (tableAppearances[t] > 1) then puts('<', getId(t), '>') end
local dictKeys = getDictionaryKeys(t)
local length = #t
local mt = getmetatable(t)
local to_string_result = getToStringResultSafely(t, mt)
puts('{')
down(function()
if to_string_result then
puts(' -- ', escape(to_string_result))
if length >= 1 then tabify() end -- tabify the array values
end
local needsComma = false
for i=1, length do
needsComma = commaControl(needsComma)
puts(' ')
putValue(t[i], makePath(path, i))
end
for _,k in ipairs(dictKeys) do
needsComma = commaControl(needsComma)
tabify()
putKey(k)
puts(' = ')
putValue(t[k], makePath(path, k))
end
if mt then
needsComma = commaControl(needsComma)
tabify()
puts('<metatable> = ')
putValue(mt, makePath(path, '<metatable>'))
end
end)
if #dictKeys > 0 or mt then -- dictionary table. Justify closing }
tabify()
elseif length > 0 then -- array tables have one extra space before closing }
puts(' ')
end
puts('}')
end
end
local function gatherSwigMethods(mt)
local toReturn = {}
local localfuncs = mt[".fn"]
if localfuncs then
for _,k in ipairs(getDictionaryKeys(localfuncs)) do
if k ~= "__disown" then
table.insert(toReturn, k)
end
end
end
local subclass = mt[".bases"]
if subclass and subclass[1] then
for _,k in ipairs(gatherSwigMethods(subclass[1])) do
table.insert(toReturn, k)
end
end
return toReturn
end
local function putSwigMetatable(v, path)
local mt = getmetatable(v)
local info = {}
tableAppearances[info] = 1
local funcs = gatherSwigMethods(mt)
table.sort(funcs, sortKeys)
tableAppearances[funcs] = 1
info.ClassType = mt[".type"]
info.Methods = funcs
puts('<',type(v),' ',getId(v),' ')
down(function()
putValue(info, makePath(path, v))
end)
puts('>')
end
-- putvalue is forward-declared before putTable & putKey
putValue = function(v, path)
if filter(v, path) then
puts('<filtered>')
else
local tv = type(v)
if tv == 'string' then
puts(smartQuote(escape(v)))
elseif tv == 'number' or tv == 'boolean' or tv == 'nil' then
puts(tostring(v))
elseif tv == 'table' then
putTable(v, path)
elseif tv == 'userdata' then
if alreadyVisited(v) then
puts('<',tv,' ',getId(v),'>')
else
putSwigMetatable(v, path)
end
-- putTable(getmetatable(v), path)
else
puts('<',tv,' ',getId(v),'>')
end
end
end
putValue(rootObject, {})
return table.concat(buffer)
end
setmetatable(inspect, { __call = function(_, ...) return print(inspect.inspect(...)) end })
return inspect
......@@ -3,12 +3,51 @@
#include <iostream>
#include "globalluatable.h"
#include "tgt/logmanager.h"
extern "C" {
#include "lua.h"
#include "lualib.h"
#include "lauxlib.h"
}
static int lua_campvis_print(lua_State* L) {
int nargs = lua_gettop(L);
lua_getglobal(L, "tostring");
std::string str;
for (int i=1; i <= nargs; i++) {
const char *s;
size_t l;
lua_pushvalue(L, -1); /* function to be called */
lua_pushvalue(L, i); /* value to print */
lua_call(L, 1, 1);
s = lua_tolstring(L, -1, &l); /* get result */
if (s == NULL)
return luaL_error(L, LUA_QL("tostring") " must return a string to " LUA_QL("print"));
if (i>1)
str += "\t";
str += s;
lua_pop(L, 1); /* pop result */
}
LINFOC("Lua", str);
return 0;