// 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 #include #include #include #include #include #include /////////////////// // 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(&RubyFeatureMacro::RubyRegister), 4); rb_define_module_function(RubyAegisub, "register_filter",reinterpret_cast(&RubyFeatureFilter::RubyRegister), 5); rb_define_module_function(RubyAegisub, "text_extents",reinterpret_cast(&RubyTextExtents), 2); rb_define_module_function(RubyAegisub, "frame_to_time",reinterpret_cast(&RubyFrameToTime), 1); rb_define_module_function(RubyAegisub, "time_to_frame",reinterpret_cast(&RubyTimeToFrame), 1); rb_define_module_function(RubyScript::RubyAegisub, "progress_set",reinterpret_cast(&RubyProgressSink::RubySetProgress), 1); rb_define_module_function(RubyScript::RubyAegisub, "progress_task",reinterpret_cast(&RubyProgressSink::RubySetTask), 1); rb_define_module_function(RubyScript::RubyAegisub, "progress_title",reinterpret_cast(&RubyProgressSink::RubySetTitle), 1); rb_define_module_function(RubyScript::RubyAegisub, "debug_out",reinterpret_cast(&RubyProgressSink::RubyDebugOut), -1); rb_define_module_function(RubyScript::RubyAegisub, "get_cancelled",reinterpret_cast(&RubyProgressSink::RubyGetCancelled), 0); rb_define_module_function(RubyScript::RubyAegisub, "display_dialog",reinterpret_cast(&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(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 &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 &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 &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 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(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(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(arg));} VALUE rb2AssWrapper(VALUE arg){return reinterpret_cast(RubyAssFile::RubyToAssEntry(arg));} };