Aegisub/aegisub/auto4_ruby.cpp

658 lines
19 KiB
C++
Raw Normal View History

// Copyright (c) 2007, Patryk Pomykalski
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
// * Neither the name of the Aegisub Group nor the names of its contributors
// may be used to endorse or promote products derived from this software
// without specific prior written permission.
//
// 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.
//
// -----------------------------------------------------------------------------
//
// AEGISUB
//
// Website: http://aegisub.cellosoft.com
// Contact: mailto:pomyk@go2.pl
//
#include "auto4_ruby.h"
#include "auto4_auto3.h"
#include "ass_dialogue.h"
#include "ass_style.h"
#include "ass_file.h"
#include "ass_override.h"
#include "text_file_reader.h"
#include "options.h"
#include "vfr.h"
#include "main.h"
#include "frame_main.h"
#include "subs_grid.h"
#include <ruby.h>
#include <wx/msgdlg.h>
#include <wx/filename.h>
#include <wx/filefn.h>
#include <wx/window.h>
#include <assert.h>
#include <algorithm>
///////////////////
// Include library
#if __VISUALC__ >= 1200
#pragma comment(lib,"ws2_32.lib")
#pragma comment(lib,"msvcr80-ruby19-static.lib")
#endif
namespace Automation4 {
RubyObjects *RubyObjects::inst = NULL;
RubyScript * RubyScript::inst = NULL; // current Ruby Script
RubyProgressSink* RubyProgressSink::inst = NULL;
VALUE RubyScript::RubyAegisub = Qfalse;
// RubyScript
RubyScript::RubyScript(const wxString &filename)
: Script(filename)
{
try {
Create();
}
catch (wxChar *e) {
description = e;
loaded = false;
throw;
}
}
RubyScript::~RubyScript()
{
}
void RubyScript::Create()
{
Destroy();
try {
#if defined(NT)
int argc = 0;
char **argv = 0;
NtInitialize(&argc, &argv);
#endif
char **opt = new char*[4];
opt[0] = "-d";
ruby_init();
//ruby_options(1, opt);
ruby_init_loadpath();
RubyScript::inst = this;
if(!RubyAegisub) {
RubyAegisub = rb_define_module("Aegisub");
rb_define_module_function(RubyAegisub, "register_macro",reinterpret_cast<RB_HOOK>(&RubyFeatureMacro::RubyRegister), 4);
rb_define_module_function(RubyAegisub, "register_filter",reinterpret_cast<RB_HOOK>(&RubyFeatureFilter::RubyRegister), 5);
rb_define_module_function(RubyAegisub, "text_extents",reinterpret_cast<RB_HOOK>(&RubyTextExtents), 2);
rb_define_module_function(RubyAegisub, "frame_to_time",reinterpret_cast<RB_HOOK>(&RubyFrameToTime), 1);
rb_define_module_function(RubyAegisub, "time_to_frame",reinterpret_cast<RB_HOOK>(&RubyTimeToFrame), 1);
rb_define_module_function(RubyScript::RubyAegisub, "progress_set",reinterpret_cast<RB_HOOK>(&RubyProgressSink::RubySetProgress), 1);
rb_define_module_function(RubyScript::RubyAegisub, "progress_task",reinterpret_cast<RB_HOOK>(&RubyProgressSink::RubySetTask), 1);
rb_define_module_function(RubyScript::RubyAegisub, "progress_title",reinterpret_cast<RB_HOOK>(&RubyProgressSink::RubySetTitle), 1);
rb_define_module_function(RubyScript::RubyAegisub, "debug_out",reinterpret_cast<RB_HOOK>(&RubyProgressSink::RubyDebugOut), -1);
rb_define_module_function(RubyScript::RubyAegisub, "get_cancelled",reinterpret_cast<RB_HOOK>(&RubyProgressSink::RubyGetCancelled), 0);
rb_define_module_function(RubyScript::RubyAegisub, "display_dialog",reinterpret_cast<RB_HOOK>(&RubyProgressSink::RubyDisplayDialog), 2);
}
VALUE paths = rb_gv_get("$:");
for(int i = 0; i < include_path.GetCount(); i++)
{
rb_ary_push(paths, rb_str_new2(include_path[i].mb_str(wxConvISO8859_1)));
}
int status = 0;
wxCharBuffer buf = GetFilename().mb_str(wxConvISO8859_1);
const char *t = buf.data();
rb_protect(rbLoadWrapper, rb_str_new2(t), &status);
if(status > 0) // something bad happened (probably parsing error)
{
VALUE err = rb_errinfo();
if(TYPE(err) == T_STRING)
throw StringValueCStr(err);
else throw "Error loading script";
}
VALUE global_var = rb_gv_get("$script_name");
if(TYPE(global_var) == T_STRING)
name = wxString(StringValueCStr(global_var), wxConvUTF8);
global_var = rb_gv_get("$script_description");
if(TYPE(global_var) == T_STRING)
description = wxString(StringValueCStr(global_var), wxConvUTF8);
global_var = rb_gv_get("$script_author");
if(TYPE(global_var) == T_STRING)
author = wxString(StringValueCStr(global_var), wxConvUTF8);
global_var = rb_gv_get("$script_version");
if(TYPE(global_var) == T_STRING)
version = wxString(StringValueCStr(global_var), wxConvUTF8);
loaded = true;
}
catch (const char* e) {
Destroy();
loaded = false;
wxString *err = new wxString(e, wxConvUTF8);
throw err->c_str();
}
}
void RubyScript::Destroy()
{
if(loaded) {
// ruby_finalize(); // broken in 1.9 ?_?
}
// remove features
for (int i = 0; i < (int)features.size(); i++) {
Feature *f = features[i];
delete f;
}
features.clear();
loaded = false;
RubyScript::inst = NULL;
}
void RubyScript::Reload()
{
Destroy();
Create();
}
RubyScript* RubyScript::GetScriptObject()
{
return RubyScript::inst;
}
VALUE RubyScript::RubyTextExtents(VALUE self, VALUE _style, VALUE _text)
{
if(TYPE(_style) != T_HASH)
rb_raise(rb_eRuntimeError, "text_extents: Style parameter must be a hash");
AssEntry *et = RubyAssFile::RubyToAssEntry(_style);
AssStyle *st = dynamic_cast<AssStyle*>(et);
if (!st) {
delete et; // Make sure to delete the "live" pointer
rb_raise(rb_eRuntimeError, "Not a style entry");
}
wxString text(StringValueCStr(_text), wxConvUTF8);
double width, height, descent, extlead;
if (!CalculateTextExtents(st, text, width, height, descent, extlead)) {
delete st;
rb_raise(rb_eRuntimeError, "Some internal error occurred calculating text_extents");
}
delete st;
VALUE result = rb_ary_new3(4, rb_float_new(width), rb_float_new(height), rb_float_new(descent), rb_float_new(extlead));
return result;
}
VALUE RubyScript::RubyFrameToTime(VALUE self, VALUE frame)
{
if(TYPE(frame) == T_FIXNUM && VFR_Output.IsLoaded())
{
return INT2FIX(VFR_Output.GetTimeAtFrame(FIX2INT(frame), true));
}
return Qnil;
}
VALUE RubyScript::RubyTimeToFrame(VALUE self, VALUE time)
{
if(TYPE(time) == T_FIXNUM && VFR_Output.IsLoaded())
{
return INT2FIX(VFR_Output.GetFrameAtTime(FIX2INT(time), true));
}
return Qnil;
}
// RubyFeature
RubyFeature::RubyFeature(ScriptFeatureClass _featureclass, const wxString &_name)
: Feature(_featureclass, _name)
{
}
void RubyFeature::RegisterFeature()
{
RubyScript::GetScriptObject()->features.push_back(this);
// get the index+1 it was pushed into
myid = (int)RubyScript::GetScriptObject()->features.size()-1;
}
VALUE RubyFeature::CreateIntegerArray(const std::vector<int> &ints)
{
VALUE res = rb_ary_new2(ints.size());
// create an array-style table with an integer vector in it
// leave the new table on top of the stack
for (int i = 0; i != ints.size(); ++i) {
int k = ints[i];
rb_ary_push(res, rb_int2inum(k));
}
return res;
}
void RubyFeature::ThrowError()
{
/* wxString err(lua_tostring(L, -1), wxConvUTF8);
lua_pop(L, 1);
wxLogError(err);
*/ }
// RubyFeatureMacro
VALUE RubyFeatureMacro::RubyRegister(VALUE self, VALUE name, VALUE description, VALUE macro_function, VALUE validate_function)
{
wxString _name(StringValueCStr(name), wxConvUTF8);
wxString _description(StringValueCStr(description), wxConvUTF8);
RubyFeatureMacro *macro = new RubyFeatureMacro(_name, _description, macro_function, validate_function);
return Qtrue;
}
RubyFeatureMacro::RubyFeatureMacro(const wxString &_name, const wxString &_description, VALUE macro_function, VALUE validate_function)
: Feature(SCRIPTFEATURE_MACRO, _name)
, FeatureMacro(_name, _description)
, RubyFeature(SCRIPTFEATURE_MACRO, _name)
, macro_fun(macro_function)
, validation_fun(validate_function)
{
no_validate = validate_function == Qnil;
RegisterFeature();
}
bool RubyFeatureMacro::Validate(AssFile *subs, const std::vector<int> &selected, int active)
{
if (no_validate)
return true;
try {
RubyProgressSink::inst = NULL;
RubyAssFile *subsobj = new RubyAssFile(subs, true, true);
VALUE *argv = ALLOCA_N(VALUE, 3);
argv[0] = subsobj->rbAssFile;
argv[1] = CreateIntegerArray(selected); // selected items;
argv[2] = INT2FIX(active);
RubyCallArguments arg(rb_mKernel, rb_to_id(validation_fun), 3, argv);
VALUE result;
RubyThreadedCall call(&arg, &result);
wxThread::ExitCode code = call.Wait();
if(code)
{
return false;
}
if(result != Qnil && result != Qfalse)
return true;
}catch (const char* e) {
wxString *err = new wxString(e, wxConvUTF8);
wxMessageBox(*err, _T("Error running validation function"),wxICON_ERROR | wxOK);
}
return false;
}
void RubyFeatureMacro::Process(AssFile *subs, const std::vector<int> &selected, int active, wxWindow * const progress_parent)
{
try {
rb_gc_disable();
delete RubyProgressSink::inst;
RubyProgressSink::inst = new RubyProgressSink(progress_parent, false);
RubyProgressSink::inst->SetTitle(GetName());
// do call
RubyAssFile *subsobj = new RubyAssFile(subs, true, true);
VALUE *argv = ALLOCA_N(VALUE, 3);
argv[0] = subsobj->rbAssFile;
argv[1] = CreateIntegerArray(selected); // selected items;
argv[2] = INT2FIX(active);
RubyCallArguments arg(rb_mKernel, rb_to_id(macro_fun), 3, argv);
VALUE result;
RubyThreadedCall call(&arg, &result);
RubyProgressSink::inst->ShowModal();
wxThread::ExitCode code = call.Wait();
delete RubyProgressSink::inst;
RubyProgressSink::inst = NULL;
/*if(code) // error reporting doesn't work in ruby 1.9
{
if(TYPE(result) == T_STRING)
throw StringValueCStr(result);
else throw "Error running macro";
}
else*/ if(TYPE(result) == T_ARRAY)
{
bool end = false;
for(int i = 0; i < RARRAY(result)->len && !end; ++i)
{
VALUE p = RARRAY(result)->ptr[i]; // some magic in code below to allow variable output
if(TYPE(p) != T_ARRAY) {
p = result;
end = true;
}
switch(TYPE(RARRAY(p)->ptr[0])) {
case T_HASH: // array of hashes = subs
subsobj->RubyUpdateAssFile(p);
break;
case T_FIXNUM: // array of ints = selection
int num = RARRAY(p)->len;
std::vector<int> sel(num);
for(int i = 0; i < num; ++i) {
sel[i] = FIX2INT(RARRAY(p)->ptr[i]);
}
FrameMain *frame = AegisubApp::Get()->frame;
frame->SubsBox->LoadFromAss(AssFile::top, true, true);
frame->SubsBox->SetSelectionFromAbsolute(sel);
break;
}
}
}
delete subsobj;
} catch (const char* e) {
wxString *err = new wxString(e, wxConvUTF8);
wxMessageBox(*err, _T("Error running macro"),wxICON_ERROR | wxOK);
}
rb_gc_enable();
rb_gc_start();
}
RubyThreadedCall::RubyThreadedCall(RubyCallArguments *a, VALUE *res)
: wxThread(wxTHREAD_JOINABLE)
,args(a), result(res)
{
int prio = Options.AsInt(_T("Automation Thread Priority"));
if (prio == 0) prio = 50; // normal
else if (prio == 1) prio = 30; // below normal
else if (prio == 2) prio = 10; // lowest
else prio = 50; // fallback normal
Create();
SetPriority(prio);
Run();
}
wxThread::ExitCode RubyThreadedCall::Entry()
{
int error = 0;
*result = rb_protect(rbCallWrapper, reinterpret_cast<VALUE>(args), &error);
if(RubyProgressSink::inst)
{
RubyProgressSink::inst->script_finished = true;
wxWakeUpIdle();
}
if(error) {
*result = rb_errinfo();
return (wxThread::ExitCode)1;
}
return (wxThread::ExitCode)0;
}
// RubyFeatureFilter
RubyFeatureFilter::RubyFeatureFilter(const wxString &_name, const wxString &_description,
int merit, VALUE _filter_fun, VALUE _dialog_fun)
: Feature(SCRIPTFEATURE_FILTER, _name)
, FeatureFilter(_name, _description, merit)
, RubyFeature(SCRIPTFEATURE_FILTER, _name)
, filter_fun(_filter_fun)
, dialog_fun(_dialog_fun)
{
has_config = _dialog_fun != Qnil;
// Works the same as in RubyFeatureMacro
RegisterFeature();
}
void RubyFeatureFilter::Init()
{
// Don't think there's anything to do here... (empty in auto3)
}
VALUE RubyFeatureFilter::RubyRegister(VALUE self, VALUE name, VALUE description, VALUE merit, VALUE function, VALUE dialog)
{
wxString _name(StringValueCStr(name), wxConvUTF8);
wxString _description(StringValueCStr(description), wxConvUTF8);
int _merit = rb_num2long(merit);
RubyFeatureFilter *filter = new RubyFeatureFilter(_name, _description, _merit, function, dialog);
return Qtrue;
}
void RubyFeatureFilter::ProcessSubs(AssFile *subs, wxWindow *export_dialog)
{
try {
rb_gc_disable();
VALUE cfg;
if (has_config && config_dialog) {
cfg = config_dialog->RubyReadBack();
// TODO, write back stored options here
}
RubyProgressSink::inst = new RubyProgressSink(export_dialog, false);
RubyProgressSink::inst->SetTitle(GetName());
RubyAssFile *subsobj = new RubyAssFile(subs, true/*modify*/, false/*undo*/);
VALUE *argv = ALLOCA_N(VALUE, 2);
argv[0] = subsobj->rbAssFile;
argv[1] = cfg; // config
RubyCallArguments arg(rb_mKernel, rb_to_id(filter_fun), 2, argv);
VALUE result;
RubyThreadedCall call(&arg, &result);
RubyProgressSink::inst->ShowModal();
RubyProgressSink::inst = NULL;
wxThread::ExitCode code = call.Wait();
delete RubyProgressSink::inst;
/*if(code) // error reporting doesn't work in ruby 1.9
{
if(TYPE(result) == T_STRING)
throw StringValueCStr(result);
else throw "Unknown Error";
}
else*/ if(TYPE(result) == T_ARRAY)
{
subsobj->RubyUpdateAssFile(result);
}
delete subsobj;
} catch (const char* e) {
wxString *err = new wxString(e, wxConvUTF8);
wxMessageBox(*err, _T("Error running filter"),wxICON_ERROR | wxOK);
}
rb_gc_enable();
rb_gc_start();
}
ScriptConfigDialog* RubyFeatureFilter::GenerateConfigDialog(wxWindow *parent)
{
if (!has_config)
return 0;
delete RubyProgressSink::inst;
RubyProgressSink::inst = new RubyProgressSink(parent, false);
RubyProgressSink::inst->SetTitle(GetName());
// prepare function call
// subtitles (don't allow any modifications during dialog creation, ideally the subs aren't even accessed)
RubyAssFile *subsobj = new RubyAssFile(AssFile::top, false/*allow modifications*/, false/*disallow undo*/);
VALUE *argv = ALLOCA_N(VALUE, 2);
argv[0] = subsobj->rbAssFile;
argv[1] = Qnil; // TODO: stored options
RubyCallArguments arg(rb_mKernel, rb_to_id(dialog_fun), 2, argv);
VALUE dialog_data;
RubyThreadedCall call(&arg, &dialog_data);
RubyProgressSink::inst->ShowModal();
wxThread::ExitCode code = call.Wait();
delete RubyProgressSink::inst;
RubyProgressSink::inst = NULL;
return config_dialog = new RubyConfigDialog(dialog_data, Qnil, false);
}
// RubyProgressSink
RubyProgressSink::RubyProgressSink(wxWindow *parent, bool allow_config_dialog)
: ProgressSink(parent)
{
}
RubyProgressSink::~RubyProgressSink()
{
}
VALUE RubyProgressSink::RubySetProgress(VALUE self, VALUE progress)
{
float _progr = rb_num2dbl(progress);
RubyProgressSink::inst->SetProgress(_progr);
return Qtrue;
}
VALUE RubyProgressSink::RubySetTask(VALUE self, VALUE task)
{
wxString _t(StringValueCStr(task), wxConvUTF8);
RubyProgressSink::inst->SetTask(_t);
return Qtrue;
}
VALUE RubyProgressSink::RubySetTitle(VALUE self, VALUE title)
{
wxString _t(StringValueCStr(title), wxConvUTF8);
RubyProgressSink::inst->SetTitle(_t);
return Qtrue;
}
VALUE RubyProgressSink::RubyGetCancelled(VALUE self)
{
if(RubyProgressSink::inst->cancelled)
return Qtrue;
return Qfalse;
}
VALUE RubyProgressSink::RubyDebugOut(int argc, VALUE *args, VALUE self)
{
if(argc > 1 && TYPE(args[0]) == T_FIXNUM)
{
if(FIX2INT(args[0]) > RubyProgressSink::inst->trace_level)
return Qnil;
}
else args[1] = args[0];
wxString _m(StringValueCStr(args[1]), wxConvUTF8);
RubyProgressSink::inst->AddDebugOutput(_m);
return Qtrue;
}
VALUE RubyProgressSink::RubyDisplayDialog(VALUE self, VALUE dialog_data, VALUE buttons)
{
// Send the "show dialog" event
ShowConfigDialogEvent evt;
RubyConfigDialog dlg(dialog_data, buttons, true); // magically creates the config dialog structure etc
evt.config_dialog = &dlg;
wxSemaphore sema(0, 1);
evt.sync_sema = &sema;
RubyProgressSink::inst->AddPendingEvent(evt);
sema.Wait();
return dlg.RubyReadBack();
}
// Factory class for Ruby scripts
// Not declared in header, since it doesn't need to be accessed from outside
// except through polymorphism
class RubyScriptFactory : public ScriptFactory {
public:
RubyScriptFactory()
{
engine_name = _T("Ruby");
filename_pattern = _T("*.rb");
Register(this);
}
~RubyScriptFactory()
{
}
virtual Script* Produce(const wxString &filename) const
{
// Just check if file extension is .rb
// Reject anything else
if (filename.Right(3).Lower() == _T(".rb")) {
return new RubyScript(filename);
} else {
return 0;
}
}
};
RubyScriptFactory _ruby_script_factory;
RubyObjects::RubyObjects()
{
objects = rb_ary_new();
rb_gc_register_address(&objects);
}
RubyObjects::~RubyObjects()
{
rb_gc_unregister_address(&objects);
}
RubyObjects *RubyObjects::Get()
{
if(inst)
return inst;
else
inst = new RubyObjects;
return inst;
}
void RubyObjects::Register(VALUE obj) {
rb_ary_push(objects, obj);
}
void RubyObjects::Unregister(VALUE obj) {
rb_ary_delete(objects, obj);
}
RubyCallArguments::RubyCallArguments(VALUE _recv, ID _id, int _n, VALUE *_argv)
:id(_id), n(_n), argv(_argv)
{
recv = _recv;
};
VALUE rbCallWrapper(VALUE arg)
{
RubyCallArguments &a = *reinterpret_cast<RubyCallArguments*>(arg);
return rb_funcall2(a.recv, a.id, a.n, a.argv);
}
VALUE rbExecWrapper(VALUE arg){return ruby_exec();}
VALUE rbLoadWrapper(VALUE arg){rb_load(arg, 0); return Qtrue;}
VALUE rbGcWrapper(VALUE arg){rb_gc_start(); return Qtrue;}
VALUE rbAss2RbWrapper(VALUE arg){return RubyAssFile::AssEntryToRuby(reinterpret_cast<AssEntry*>(arg));}
VALUE rb2AssWrapper(VALUE arg){return reinterpret_cast<VALUE>(RubyAssFile::RubyToAssEntry(arg));}
};