From 614758ac6ccebb39026b27b5b52966aa241489a5 Mon Sep 17 00:00:00 2001 From: pomyk Date: Mon, 29 Jan 2007 17:52:46 +0000 Subject: [PATCH] Auto4 Ruby - not working yet Originally committed to SVN as r906. --- aegisub/auto4_ruby.cpp | 596 ++++++++++++++++++++++++++++++++ aegisub/auto4_ruby.h | 259 ++++++++++++++ aegisub/auto4_ruby_assfile.cpp | 565 ++++++++++++++++++++++++++++++ aegisub/auto4_ruby_dialog.cpp | 605 +++++++++++++++++++++++++++++++++ 4 files changed, 2025 insertions(+) create mode 100644 aegisub/auto4_ruby.cpp create mode 100644 aegisub/auto4_ruby.h create mode 100644 aegisub/auto4_ruby_assfile.cpp create mode 100644 aegisub/auto4_ruby_dialog.cpp diff --git a/aegisub/auto4_ruby.cpp b/aegisub/auto4_ruby.cpp new file mode 100644 index 000000000..f95ab4526 --- /dev/null +++ b/aegisub/auto4_ruby.cpp @@ -0,0 +1,596 @@ +// Copyright (c) 2006, 2007, Niels Martin Hansen +// 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:jiifurusu@gmail.com +// + +#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 "../ruby/include/ruby.h" +#include +#include +#include +#include +#include +#include + +namespace Automation4 { + + // I'm not sure if this will work ?_? + RubyObjects *RubyObjects::inst = NULL; + RubyScript * RubyScript::inst = NULL; // current Ruby Script + RubyProgressSink* RubyProgressSink::inst = NULL; + VALUE RubyScript::RubyAegisub; + RubyAssFile *RubyAssFile::raf = NULL; + + // RubyScriptReader + RubyScriptReader::RubyScriptReader(const wxString &filename) + { +#ifdef WIN32 + f = _tfopen(filename.c_str(), _T("rb")); +#else + f = fopen(filename.fn_str(), "rb"); +#endif + first = true; + databuf = new char[bufsize]; + } + + RubyScriptReader::~RubyScriptReader() + { + if (databuf) + delete databuf; + fclose(f); + } + +/* const char* RubyScriptReader::reader_func( void *data, size_t *size) + { + return self->databuf; + } +*/ + + // 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 + ruby_init(); + ruby_init_loadpath(); + RubyScript::inst = this; + 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(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); + + 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) + { + throw StringValueCStr(ruby_errinfo); + } + + VALUE global_var = rb_gv_get("$script_name"); + name = wxString(StringValueCStr(global_var), wxConvUTF8); + global_var = rb_gv_get("$script_description"); + description = wxString(StringValueCStr(global_var), wxConvUTF8); + global_var = rb_gv_get("$script_author"); + author = wxString(StringValueCStr(global_var), wxConvUTF8); + global_var = rb_gv_get("$script_version"); + 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(); + ruby_cleanup(0); + } + // TODO: would be nice to implement this + // RubyObjects::Get()->UnregisterAll(); + + // 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; + } + + + // 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) + { + RegisterFeature(); + } + + bool RubyFeatureMacro::Validate(AssFile *subs, const std::vector &selected, int active) + { + if (no_validate) + return true; + + try { + RubyAssFile *subsobj = new RubyAssFile(subs, true, true); + VALUE sel = CreateIntegerArray(selected); // selected items + // RubyObjects::Get()->Register(sel); + VALUE result = rbFunCall(rb_mKernel, rb_to_id(validation_fun), 3, subsobj->rbAssFile, sel, rb_int2inum(active)); + if(result != Qnil && result != Qfalse) + return true; + // RubyObjects::Get()->Unregister(sel); + }catch (const char* e) { + wxString *err = new wxString(e, wxConvUTF8); + throw err->c_str(); + } + // wxMessageBox(error, _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) + { + // prepare function call + try { + + RubyProgressSink::inst = new RubyProgressSink(progress_parent, false); + RubyProgressSink::inst->SetTitle(GetName()); + RubyProgressSink::inst->Show(true); + + // do call + RubyAssFile *subsobj = new RubyAssFile(subs, true, true); + VALUE sel = CreateIntegerArray(selected); // selected items + RubyObjects::Get()->Register(sel); + VALUE result = rbFunCall(rb_mKernel, rb_to_id(macro_fun), 3, subsobj->rbAssFile, sel, rb_int2inum(active)); + if(result != Qnil && result != Qfalse) + { + subsobj->RubyUpdateAssFile(result); + RubyProgressSink::inst->script_finished = true; + } + else RubyProgressSink::inst->Show(false); + // RubyObjects::Get()->Unregister(sel); + } catch (const char* e) { + wxString *err = new wxString(e, wxConvUTF8); + wxMessageBox(*err, _T("Error running macro"),wxICON_ERROR | wxOK); + } + delete RubyProgressSink::inst; + RubyProgressSink::inst = NULL; +} + + + // 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) + { + + // TODO: configuration dialog + + try { + if (has_config && config_dialog) { + assert(config_dialog->RubyReadBack() == 1); + // TODO, write back stored options here + } + + RubyProgressSink::inst = new RubyProgressSink(export_dialog, false); + RubyProgressSink::inst->SetTitle(GetName()); + RubyProgressSink::inst->Show(true); + + RubyAssFile *subsobj = new RubyAssFile(subs, true/*modify*/, false/*undo*/); + VALUE result = rbFunCall(rb_mKernel, rb_to_id(filter_fun), 2, subsobj->rbAssFile, Qnil /* config */); + if(result != Qnil && result != Qfalse) + { + subsobj->RubyUpdateAssFile(result); + } + // else RubyProgressSink::inst->Show(false); + } catch (const char* e) { + wxString *err = new wxString(e, wxConvUTF8); + wxMessageBox(*err, _T("Error running filter"),wxICON_ERROR | wxOK); + } + delete RubyProgressSink::inst; + RubyProgressSink::inst = NULL; + + } + + ScriptConfigDialog* RubyFeatureFilter::GenerateConfigDialog(wxWindow *parent) + { + if (!has_config) + return 0; + + //GetFeatureFunction(2); // 2 = config dialog function + + // 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*/); + // stored options + +/* if(RubyProgressSink::inst) + { + delete RubyProgressSink::inst; + RubyProgressSink::inst = NULL; + } + RubyProgressSink::inst = new RubyProgressSink(parent, false); + RubyProgressSink::inst->SetTitle(GetName()); + + // do call TODO + RubyProgressSink::inst->ShowModal(); + +*/ return config_dialog = new RubyConfigDialog(false); + } + + + // RubyProgressSink + + RubyProgressSink::RubyProgressSink(wxWindow *parent, bool allow_config_dialog) + : ProgressSink(parent) + { + } + + RubyProgressSink::~RubyProgressSink() + { + rb_undef_method(RubyScript::RubyAegisub, "progress_set"); + rb_undef_method(RubyScript::RubyAegisub, "progress_task"); + rb_undef_method(RubyScript::RubyAegisub, "progress_title"); + rb_undef_method(RubyScript::RubyAegisub, "debug_out"); + // remove progress reporting stuff + // TODO + } + + VALUE RubyProgressSink::RubySetProgress(VALUE self, VALUE progress) + { + float _progr = rb_num2dbl(progress); + RubyProgressSink::inst->SetProgress(_progr); + RubyProgressSink::inst->DoUpdateDisplay(); + wxSafeYield(RubyProgressSink::inst); + return Qtrue; + } + + VALUE RubyProgressSink::RubySetTask(VALUE self, VALUE task) + { + wxString _t(StringValueCStr(task), wxConvUTF8); + RubyProgressSink::inst->SetTask(_t); + RubyProgressSink::inst->DoUpdateDisplay(); + return Qtrue; + } + + VALUE RubyProgressSink::RubySetTitle(VALUE self, VALUE title) + { + wxString _t(StringValueCStr(title), wxConvUTF8); + RubyProgressSink::inst->SetTitle(_t); + //wxSafeYield(RubyProgressSink::inst); + RubyProgressSink::inst->DoUpdateDisplay(); + return Qtrue; + } + + VALUE RubyProgressSink::RubyGetCancelled(VALUE self) + { + if(RubyProgressSink::inst->cancelled) + return Qtrue; + return Qfalse; + } + + VALUE RubyProgressSink::RubyDebugOut(VALUE self, VALUE msg) + { + wxString _m(StringValueCStr(msg), wxConvUTF8); + RubyProgressSink::inst->AddDebugOutput(_m); + RubyProgressSink::inst->Refresh(); + RubyProgressSink::inst->Update(); + return Qtrue; + } + + int RubyProgressSink::RubyDisplayDialog() + { + return 0; + } + + + // 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 _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; + }; + typedef struct run_safely_arg { + VALUE (*func)(void); + void *arg; + } run_safely_arg_t; + + static VALUE run_safely_0(void *arg) + { + run_safely_arg_t *rsarg = (run_safely_arg_t *) arg; + VALUE result; + result = (*rsarg->func)(); + return result; + } + + static int run_safely(VALUE (*func)(void), void *arg, VALUE *retval) + { + VALUE thread, ret; + run_safely_arg_t rsarg; + int state; + rsarg.func = func; + rsarg.arg = arg; +#if defined(HAVE_SETITIMER) + rb_thread_start_timer(); +#endif + thread = rb_thread_create(reinterpret_cast(&run_safely_0), &rsarg); + rb_thread_kill(thread); +#if defined(HAVE_SETITIMER) + rb_thread_stop_timer(); +#endif + if (retval) + *retval = ret; + return state; + } + 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(/*reinterpret_cast*/(arg), 0); return Qtrue;} + + VALUE rbFunCall(VALUE recv, ID id, int n, ...) + { + VALUE *argv = 0; + if (n > 0) { + argv = ALLOCA_N(VALUE, n); + va_list ar; + va_start(ar, n); + int i; + for(i=0;i(&arg), &error); + if(error) { + throw StringValueCStr(ruby_errinfo); + } + return result; + } +}; diff --git a/aegisub/auto4_ruby.h b/aegisub/auto4_ruby.h new file mode 100644 index 000000000..22e51175e --- /dev/null +++ b/aegisub/auto4_ruby.h @@ -0,0 +1,259 @@ +// Copyright (c) 2006, Niels Martin Hansen +// 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:jiifurusu@gmail.com +// + +#pragma once + +#ifndef _AUTO4_RUBY_H +#define _AUTO4_RUBY_H + +#include "auto4_base.h" +#include +#include +#include "../ruby/include/ruby.h" + +class wxWindow; + +namespace Automation4 { + + + // Manage reading in a Ruby script file + struct RubyScriptReader { + FILE *f; + bool first; + char *databuf; + static const size_t bufsize = 512; + RubyScriptReader(const wxString &filename); + ~RubyScriptReader(); +// static const char* reader_func( void *data, size_t *size); + }; + + // Provides access to an AssFile object (and all lines contained) for a Ruby script + class RubyAssFile { + private: + AssFile *ass; + + bool can_modify; + bool can_set_undo; + //void CheckAllowModify(); // throws an error if modification is disallowed + + // keep a cursor of last accessed item to avoid walking over the entire file on every access + std::list::iterator last_entry_ptr; + int last_entry_id; + void GetAssEntry(int n); // set last_entry_ptr to point to item n + + static int RubyParseTagData(); + static int RubyUnparseTagData(); + static int RubyParseKaraokeData(); + static int RubySetUndoPoint(); + + ~RubyAssFile(); + public: + void RubyUpdateAssFile(VALUE subtitles); + static VALUE AssEntryToRuby(AssEntry *e); // makes a Ruby representation of AssEntry + static AssEntry *RubyToAssEntry(VALUE ass_entry); // creates an AssEntry object from a Ruby representation + RubyAssFile(AssFile *_ass, bool _can_modify, bool _can_set_undo); + + static RubyAssFile *raf; + VALUE rbAssFile; + + }; + + + // Provides progress UI and control functions for a Ruby script + class RubyProgressSink : public ProgressSink { + private: + static int RubyDisplayDialog(); + + public: + RubyProgressSink(wxWindow *parent, bool allow_config_dialog = true); + virtual ~RubyProgressSink(); + static RubyProgressSink* inst; + static VALUE RubySetProgress(VALUE self, VALUE progress); + static VALUE RubySetTask(VALUE self, VALUE task); + static VALUE RubySetTitle(VALUE self, VALUE title); + static VALUE RubyGetCancelled(VALUE self); + static VALUE RubyDebugOut(VALUE self, VALUE msg); + }; + + + // Provides Config UI functions for a Ruby script + class RubyConfigDialogControl { + public: + wxControl *cw; // control window + wxString name, hint; + int x, y, width, height; + + virtual wxControl *Create(wxWindow *parent) = 0; + virtual void ControlReadBack() = 0; + virtual void RubyReadBack() = 0; + + RubyConfigDialogControl(); + virtual ~RubyConfigDialogControl() { } + }; + + class RubyConfigDialog : public ScriptConfigDialog { + private: + std::vector controls; + std::vector buttons; + bool use_buttons; + + class ButtonEventHandler : public wxEvtHandler { + public: + int *button_pushed; + void OnButtonPush(wxCommandEvent &evt); + }; + + ButtonEventHandler *button_event; + int button_pushed; + + protected: + wxWindow* CreateWindow(wxWindow *parent); + + public: + RubyConfigDialog(bool include_buttons); + virtual ~RubyConfigDialog(); + int RubyReadBack(); // read back internal structure to lua structures + + void ReadBack(); // from auto4 base + }; + + + // Second base-class for Ruby implemented Features + class RubyFeature : public virtual Feature { + protected: + + int myid; + + RubyFeature(ScriptFeatureClass _featureclass, const wxString &_name); + + void RegisterFeature(); + VALUE CreateIntegerArray(const std::vector &ints); + void ThrowError(); + }; + + + // Class of Ruby scripts + class RubyScript : public Script { + friend class RubyFeature; + + private: + + void Create(); // load script and create internal structures etc. + void Destroy(); // destroy internal structures, unreg features and delete environment + + static RubyScript* GetScriptObject(); + static RubyScript* inst; + static VALUE RubyTextExtents(VALUE self, VALUE style, VALUE text); + static int RubyInclude(); + + public: + RubyScript(const wxString &filename); + virtual ~RubyScript(); + virtual void Reload(); + static VALUE RubyAegisub; + }; + + + // Implementation of the Macro Feature for Ruby scripts + class RubyFeatureMacro : public FeatureMacro, RubyFeature { + private: + bool no_validate; + VALUE macro_fun; + VALUE validation_fun; + + protected: + RubyFeatureMacro(const wxString &_name, const wxString &_description, VALUE macro_function, VALUE validate_function); + public: + static VALUE RubyRegister(VALUE self, VALUE name, VALUE description, VALUE macro_function, VALUE validate_function); + virtual ~RubyFeatureMacro() { } + + virtual bool Validate(AssFile *subs, const std::vector &selected, int active); + virtual void Process(AssFile *subs, const std::vector &selected, int active, wxWindow * const progress_parent); + }; + + + // Implementation of the Export Filter Feature for Ruby scripts + class RubyFeatureFilter : public FeatureFilter, RubyFeature { + private: + bool has_config; + RubyConfigDialog *config_dialog; + VALUE filter_fun; + VALUE dialog_fun; + + protected: + RubyFeatureFilter(const wxString &_name, const wxString &_description, int merit, VALUE function, VALUE dialog); + + ScriptConfigDialog* GenerateConfigDialog(wxWindow *parent); + + void Init(); + public: + static VALUE RubyRegister(VALUE self, VALUE name, VALUE description, VALUE merit, VALUE macro_function, VALUE validate_function); + + void ProcessSubs(AssFile *subs, wxWindow *export_dialog); + }; + + // class for registering ruby objects created in c++ + // so garbage collector doesn't destroy them + class RubyObjects { + private: + VALUE objects; + static RubyObjects *inst; + RubyObjects(); + public: + ~RubyObjects(); + static RubyObjects *Get(); + void Register(VALUE object); + void Unregister(VALUE object); + }; + + // stuff for safe calling of ruby functions + struct RubyCallArguments { + VALUE recv; + ID id; + int n; + VALUE *argv; + RubyCallArguments(VALUE _recv, ID _id, int _n, VALUE *_argv); + }; + + VALUE rbCallWrapper(VALUE arg); + VALUE rbExecWrapper(VALUE arg); + VALUE rbLoadWrapper(VALUE arg); + VALUE rbFunCall(VALUE recv, ID id, int n, ...); + typedef VALUE (*RB_HOOK)(...); + typedef VALUE (*RB_HOOK2)(void*); + +}; + +#endif diff --git a/aegisub/auto4_ruby_assfile.cpp b/aegisub/auto4_ruby_assfile.cpp new file mode 100644 index 000000000..b78f7dfde --- /dev/null +++ b/aegisub/auto4_ruby_assfile.cpp @@ -0,0 +1,565 @@ +// Copyright (c) 2006, Niels Martin Hansen +// 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:jiifurusu@gmail.com +// + +#include "auto4_ruby.h" +#include "ass_dialogue.h" +#include "ass_style.h" +#include "ass_file.h" +#include "ass_override.h" +#include +#include +#include "../ruby/include/ruby.h" + + +namespace Automation4 { + + // LuaAssFile + +/* void RubyAssFile::CheckAllowModify() + { + if (can_modify) + return; + rb_raise(rb_eRuntimeError, "Attempt to modify subtitles in read-only feature context."); + } +*/ + VALUE RubyAssFile::AssEntryToRuby(AssEntry *e) + { + VALUE ass_entry; + ass_entry = rb_hash_new(); + + wxString section(e->group); + rb_hash_aset(ass_entry, rb_str_new2("section"), rb_str_new2(e->group.mb_str(wxConvUTF8))); + wxString raw(e->GetEntryData()); + rb_hash_aset(ass_entry, rb_str_new2("raw"), rb_str_new2(e->GetEntryData().mb_str(wxConvUTF8))); + VALUE entry_class; + + if (raw.Trim().IsEmpty()) { + entry_class = rb_str_new2("clear"); + + } else if (raw[0] == _T(';')) { + // "text" field, same as "raw" but with semicolon stripped + wxString text(raw, 1, raw.size()-1); + rb_hash_aset(ass_entry, rb_str_new2("text"), rb_str_new2(text.mb_str(wxConvUTF8))); + entry_class = rb_str_new2("comment"); + } else if (raw[0] == _T('[')) { + entry_class = rb_str_new2("head"); + + } else if (section.Lower() == _T("[script info]")) { + // assumed "info" class + // first "key" + wxString key = raw.BeforeFirst(_T(':')); + rb_hash_aset(ass_entry, rb_str_new2("key"), rb_str_new2(key.mb_str(wxConvUTF8))); + + // then "value" + wxString value = raw.AfterFirst(_T(':')); + rb_hash_aset(ass_entry, rb_str_new2("value"), rb_str_new2(value.mb_str(wxConvUTF8))); + entry_class = rb_str_new2("info"); + + } else if (raw.Left(7).Lower() == _T("format:")) { + // TODO: parse the format line; just use a tokenizer + entry_class = rb_str_new2("format"); + + } else if (e->GetType() == ENTRY_DIALOGUE) { + AssDialogue *dia = e->GetAsDialogue(e); + + rb_hash_aset(ass_entry, rb_str_new2("comment"), rb_int2inum((int)dia->Comment)); + rb_hash_aset(ass_entry, rb_str_new2("layer"), rb_int2inum(dia->Layer)); + rb_hash_aset(ass_entry, rb_str_new2("start_time"), rb_int2inum(dia->Start.GetMS())); + rb_hash_aset(ass_entry, rb_str_new2("end_time"), rb_int2inum(dia->End.GetMS())); + + rb_hash_aset(ass_entry, rb_str_new2("style"), rb_str_new2(dia->Style.mb_str(wxConvUTF8))); + rb_hash_aset(ass_entry, rb_str_new2("actor"), rb_str_new2(dia->Actor.mb_str(wxConvUTF8))); + + rb_hash_aset(ass_entry, rb_str_new2("margin_l"), rb_int2inum(dia->Margin[0])); + rb_hash_aset(ass_entry, rb_str_new2("margin_r"), rb_int2inum(dia->Margin[1])); + rb_hash_aset(ass_entry, rb_str_new2("margin_t"), rb_int2inum(dia->Margin[2])); + rb_hash_aset(ass_entry, rb_str_new2("margin_b"), rb_int2inum(dia->Margin[3])); + + rb_hash_aset(ass_entry, rb_str_new2("effect"), rb_str_new2(dia->Effect.mb_str(wxConvUTF8))); + rb_hash_aset(ass_entry, rb_str_new2("userdata"), rb_str_new2("")); + rb_hash_aset(ass_entry, rb_str_new2("text"), rb_str_new2(dia->Text.mb_str(wxConvUTF8))); + + entry_class = rb_str_new2("dialogue"); + + } else if (e->GetType() == ENTRY_STYLE) { + AssStyle *sty = e->GetAsStyle(e); + + rb_hash_aset(ass_entry, rb_str_new2("name"), rb_str_new2(sty->name.mb_str(wxConvUTF8))); + + rb_hash_aset(ass_entry, rb_str_new2("fontname"), rb_str_new2(sty->font.mb_str(wxConvUTF8))); + rb_hash_aset(ass_entry, rb_str_new2("fontsize"), rb_int2inum(sty->fontsize)); + + rb_hash_aset(ass_entry, rb_str_new2("color1"), rb_str_new2(sty->primary.GetASSFormatted(true).mb_str(wxConvUTF8))); + rb_hash_aset(ass_entry, rb_str_new2("color2"), rb_str_new2(sty->secondary.GetASSFormatted(true).mb_str(wxConvUTF8))); + rb_hash_aset(ass_entry, rb_str_new2("color3"), rb_str_new2(sty->outline.GetASSFormatted(true).mb_str(wxConvUTF8))); + rb_hash_aset(ass_entry, rb_str_new2("color4"), rb_str_new2(sty->shadow.GetASSFormatted(true).mb_str(wxConvUTF8))); + + rb_hash_aset(ass_entry, rb_str_new2("bold"), rb_int2inum(sty->bold)); + rb_hash_aset(ass_entry, rb_str_new2("italic"), rb_int2inum(sty->italic)); + rb_hash_aset(ass_entry, rb_str_new2("underline"), rb_int2inum(sty->underline)); + rb_hash_aset(ass_entry, rb_str_new2("strikeout"), rb_int2inum(sty->strikeout)); + + rb_hash_aset(ass_entry, rb_str_new2("scale_x"), rb_int2inum(sty->scalex)); + rb_hash_aset(ass_entry, rb_str_new2("scale_y"), rb_int2inum(sty->scaley)); + + rb_hash_aset(ass_entry, rb_str_new2("spacing"), rb_int2inum(sty->spacing)); + rb_hash_aset(ass_entry, rb_str_new2("angle"), rb_int2inum(sty->angle)); + + rb_hash_aset(ass_entry, rb_str_new2("borderstyle"), rb_int2inum(sty->borderstyle)); + rb_hash_aset(ass_entry, rb_str_new2("outline"), rb_int2inum(sty->outline_w)); + rb_hash_aset(ass_entry, rb_str_new2("shadow"), rb_int2inum(sty->shadow_w)); + rb_hash_aset(ass_entry, rb_str_new2("align"), rb_int2inum(sty->alignment)); + + rb_hash_aset(ass_entry, rb_str_new2("margin_l"), rb_int2inum(sty->Margin[0])); + rb_hash_aset(ass_entry, rb_str_new2("margin_r"), rb_int2inum(sty->Margin[1])); + rb_hash_aset(ass_entry, rb_str_new2("margin_t"), rb_int2inum(sty->Margin[2])); + rb_hash_aset(ass_entry, rb_str_new2("margin_b"), rb_int2inum(sty->Margin[3])); + + rb_hash_aset(ass_entry, rb_str_new2("encoding"), rb_int2inum(sty->encoding)); + + // From STS.h: "0: window, 1: video, 2: undefined (~window)" + rb_hash_aset(ass_entry, rb_str_new2("relative_to"), rb_int2inum(2)); + rb_hash_aset(ass_entry, rb_str_new2("vertical"), Qfalse); + + entry_class = rb_str_new2("style"); + + } else { + entry_class = rb_str_new2("unknown"); + } + // store class of item; last thing done for each class specific code must be pushing the class name + rb_hash_aset(ass_entry, rb_str_new2("class"), entry_class); + + return ass_entry; + } + + AssEntry *RubyAssFile::RubyToAssEntry(VALUE ass_entry) + { + VALUE entry_class = rb_hash_aref(ass_entry, rb_str_new2("class")); + + wxString lclass(StringValueCStr(entry_class), wxConvUTF8); + lclass.MakeLower(); + + VALUE _section = rb_hash_aref(ass_entry, rb_str_new2("section")); + wxString section(StringValueCStr(_section), wxConvUTF8); + + AssEntry *result; + if (lclass == _T("clear")) { + result = new AssEntry(_T("")); + result->group = section; + + } else if (lclass == _T("comment")) { +// GETSTRING(raw, "text", "comment") + VALUE _text = rb_hash_aref(ass_entry, rb_str_new2("text")); + wxString raw(StringValueCStr(_text), wxConvUTF8); + raw.Prepend(_T(";")); + result = new AssEntry(raw); + result->group = section; + + } else if (lclass == _T("head")) { + result = new AssEntry(section); + result->group = section; + + } else if (lclass == _T("info")) { + VALUE _key = rb_hash_aref(ass_entry, rb_str_new2("key")); + wxString key(StringValueCStr(_key), wxConvUTF8); + VALUE _value = rb_hash_aref(ass_entry, rb_str_new2("value")); + wxString value(StringValueCStr(_value), wxConvUTF8); + result = new AssEntry(wxString::Format(_T("%s: %s"), key.c_str(), value.c_str())); + result->group = _T("[Script Info]"); // just so it can be read correctly back + + } else if (lclass == _T("format")) { + // ohshi- ... + // *FIXME* maybe ignore the actual data and just put some default stuff based on section? + result = new AssEntry(_T("Format: Auto4,Is,Broken")); + result->group = section; + + } else if (lclass == _T("style")) { + VALUE _name = rb_hash_aref(ass_entry, rb_str_new2("name")); + wxString name(StringValueCStr(_name), wxConvUTF8); + VALUE _fontname = rb_hash_aref(ass_entry, rb_str_new2("fontname")); + wxString fontname(StringValueCStr(_fontname), wxConvUTF8); + VALUE _fontsize = rb_hash_aref(ass_entry, rb_str_new2("fontsize")); + float fontsize = rb_num2dbl(_fontsize); + VALUE _color1 = rb_hash_aref(ass_entry, rb_str_new2("color1")); + wxString color1(StringValueCStr(_color1), wxConvUTF8); + VALUE _color2 = rb_hash_aref(ass_entry, rb_str_new2("color2")); + wxString color2(StringValueCStr(_color2), wxConvUTF8); + VALUE _color3 = rb_hash_aref(ass_entry, rb_str_new2("color3")); + wxString color3(StringValueCStr(_color3), wxConvUTF8); + VALUE _color4 = rb_hash_aref(ass_entry, rb_str_new2("color4")); + wxString color4(StringValueCStr(_color4), wxConvUTF8); + VALUE _bold = rb_hash_aref(ass_entry, rb_str_new2("bold")); + bool bold = (bool)rb_num2long(_bold); + VALUE _italic = rb_hash_aref(ass_entry, rb_str_new2("italic")); + bool italic = (bool)rb_num2long(_italic); + VALUE _underline = rb_hash_aref(ass_entry, rb_str_new2("underline")); + bool underline = (bool)rb_num2long(_underline); + VALUE _strikeout = rb_hash_aref(ass_entry, rb_str_new2("strikeout")); + bool strikeout = (bool)rb_num2long(_strikeout); + VALUE _scale_x = rb_hash_aref(ass_entry, rb_str_new2("scale_x")); + float scale_x = rb_num2dbl(_scale_x); + VALUE _scale_y = rb_hash_aref(ass_entry, rb_str_new2("scale_y")); + float scale_y = rb_num2dbl(_scale_y); + VALUE _spacing = rb_hash_aref(ass_entry, rb_str_new2("spacing")); + int spacing = rb_num2long(_spacing); + VALUE _angle = rb_hash_aref(ass_entry, rb_str_new2("angle")); + float angle = rb_num2dbl(_angle); + VALUE _borderstyle = rb_hash_aref(ass_entry, rb_str_new2("borderstyle")); + int borderstyle = rb_num2long(_borderstyle); + VALUE _outline = rb_hash_aref(ass_entry, rb_str_new2("outline")); + float outline = rb_num2dbl(_outline); + VALUE _shadow = rb_hash_aref(ass_entry, rb_str_new2("shadow")); + float shadow = rb_num2dbl(_shadow); + VALUE _align = rb_hash_aref(ass_entry, rb_str_new2("align")); + int align = rb_num2long(_align); + VALUE _margin_l = rb_hash_aref(ass_entry, rb_str_new2("margin_l")); + int margin_l = rb_num2long(_margin_l); + VALUE _margin_r = rb_hash_aref(ass_entry, rb_str_new2("margin_r")); + int margin_r = rb_num2long(_margin_r); + VALUE _margin_t = rb_hash_aref(ass_entry, rb_str_new2("margin_t")); + int margin_t = rb_num2long(_margin_t); + VALUE _margin_b = rb_hash_aref(ass_entry, rb_str_new2("margin_b")); + int margin_b = rb_num2long(_margin_b); + VALUE _encoding = rb_hash_aref(ass_entry, rb_str_new2("encoding")); + int encoding = rb_num2long(_encoding); + // leaving out relative_to and vertical + + AssStyle *sty = new AssStyle(); + sty->name = name; + sty->font = fontname; + sty->fontsize = fontsize; + sty->primary.Parse(color1); + sty->secondary.Parse(color2); + sty->outline.Parse(color3); + sty->shadow.Parse(color4); + sty->bold = bold; + sty->italic = italic; + sty->underline = underline; + sty->strikeout = strikeout; + sty->scalex = scale_x; + sty->scaley = scale_y; + sty->spacing = spacing; + sty->angle = angle; + sty->borderstyle = borderstyle; + sty->outline_w = outline; + sty->shadow_w = shadow; + sty->alignment = align; + sty->Margin[0] = margin_l; + sty->Margin[1] = margin_r; + sty->Margin[2] = margin_t; + sty->Margin[3] = margin_b; + sty->encoding = encoding; + sty->UpdateData(); + + result = sty; + } else if (lclass == _T("styleex")) { + rb_raise(rb_eRuntimeError, "Found line with class 'styleex' which is not supported. Wait until AS5 is a reality."); + return 0; + + } else if (lclass == _T("dialogue")) { + VALUE _comment = rb_hash_aref(ass_entry, rb_str_new2("comment")); + bool comment = (bool)rb_num2long(_comment); + VALUE _layer = rb_hash_aref(ass_entry, rb_str_new2("layer")); + int layer = rb_num2long(_layer); + VALUE _start_time = rb_hash_aref(ass_entry, rb_str_new2("start_time")); + int start_time = rb_num2long(_start_time); + VALUE _end_time = rb_hash_aref(ass_entry, rb_str_new2("end_time")); + int end_time = rb_num2long(_end_time); + VALUE _style = rb_hash_aref(ass_entry, rb_str_new2("style")); + wxString style(StringValueCStr(_style), wxConvUTF8); + VALUE _actor = rb_hash_aref(ass_entry, rb_str_new2("actor")); + wxString actor(StringValueCStr(_actor), wxConvUTF8); + VALUE _margin_l = rb_hash_aref(ass_entry, rb_str_new2("margin_l")); + int margin_l = rb_num2long(_margin_l); + VALUE _margin_r = rb_hash_aref(ass_entry, rb_str_new2("margin_r")); + int margin_r = rb_num2long(_margin_r); + VALUE _margin_t = rb_hash_aref(ass_entry, rb_str_new2("margin_t")); + int margin_t = rb_num2long(_margin_t); + VALUE _margin_b = rb_hash_aref(ass_entry, rb_str_new2("margin_b")); + int margin_b = rb_num2long(_margin_b); + VALUE _effect = rb_hash_aref(ass_entry, rb_str_new2("effect")); + wxString effect(StringValueCStr(_effect), wxConvUTF8); + VALUE _text = rb_hash_aref(ass_entry, rb_str_new2("text")); + wxString text(StringValueCStr(_text), wxConvUTF8); + //GETSTRING(userdata, "userdata", "dialogue") + + AssDialogue *dia = new AssDialogue(); + dia->Comment = comment; + dia->Layer = layer; + dia->Start.SetMS(start_time); + dia->End.SetMS(end_time); + dia->Style = style; + dia->Actor = actor; + dia->Margin[0] = margin_l; + dia->Margin[1] = margin_r; + dia->Margin[2] = margin_t; + dia->Margin[3] = margin_b; + dia->Effect = effect; + dia->Text = text; + dia->UpdateData(); + + result = dia; + + } else { + rb_raise(rb_eRuntimeError, "Found line with unknown class: %s", lclass.mb_str(wxConvUTF8).data()); + return 0; + } + + return result; + } + + // Updates the AssFile with data returned by macro/filter + // If the first line is dialogue we leave header from the original (styles, info, etc) + void RubyAssFile::RubyUpdateAssFile(VALUE subtitles) + { + RubyObjects::Get()->Register(subtitles); + int size = rb_num2long(rb_funcall(subtitles, rb_to_id(rb_str_new2("size")), 0)); + + if(size <= 0) return; // empty - leave the original + + VALUE rbEntry = rb_ary_shift(subtitles); + AssEntry *new_entry = RubyToAssEntry(rbEntry); + + entryIter e = ass->Line.begin(); + if(new_entry->GetType() == ENTRY_DIALOGUE) // check if the first line is a dialogue + { + while(e != ass->Line.end() && (*e)->GetType() != ENTRY_DIALOGUE) ++e; + } + while(e != ass->Line.end()) // delete the old lines backwards + { + delete (*e); + e = ass->Line.erase(e); + } + ass->Line.push_back(new_entry); + for(int i = 1; i < size; i++) // insert new lines + { + rbEntry = rb_ary_shift(subtitles); + new_entry = RubyToAssEntry(rbEntry); + ass->Line.push_back(new_entry); + } + RubyObjects::Get()->Unregister(subtitles); + } + + int RubyAssFile::RubyParseTagData() + { + // TODO + return 1; + } + + int RubyAssFile::RubyUnparseTagData() + { + // TODO + return 1; + } + + int RubyAssFile::RubyParseKaraokeData() + { +/* AssEntry *e = RubyToAssEntry(L); + if (e->GetType() != ENTRY_DIALOGUE) { + delete e; + lua_pushstring(L, "Attempt to create karaoke table from non-dialogue subtitle line"); + lua_error(L); + return 0; + } + + AssDialogue *dia = e->GetAsDialogue(e); + dia->ParseASSTags(); + + int kcount = 0; + int kdur = 0; + int ktime = 0; + wxString ktag = _T(""); + wxString ktext = _T(""); + wxString ktext_stripped = _T(""); + + lua_newtable(L); + + for (int i = 0; i < (int)dia->Blocks.size(); i++) { + AssDialogueBlock *block = dia->Blocks[i]; + + switch (block->type) { + + case BLOCK_BASE: + assert(block->type != BLOCK_BASE); + break; + + case BLOCK_PLAIN: + ktext += block->text; + ktext_stripped += block->text; + break; + + case BLOCK_DRAWING: + // a drawing is regarded as a kind of control code here, so it's just stripped away + ktext += block->text; + break; + + case BLOCK_OVERRIDE: { + bool brackets_open = false; + AssDialogueBlockOverride *ovr = block->GetAsOverride(block); + + for (int j = 0; j < (int)ovr->Tags.size(); j++) { + AssOverrideTag *tag = ovr->Tags[j]; + + if (tag->IsValid() && tag->Name.Mid(0,2).CmpNoCase(_T("\\k")) == 0) { + // karaoke tag + if (brackets_open) { + ktext += _T("}"); + brackets_open = false; + } + + // store to lua + lua_newtable(L); + lua_pushnumber(L, kdur); + lua_setfield(L, -2, "duration"); + lua_pushnumber(L, ktime); + lua_setfield(L, -2, "start_time"); + lua_pushnumber(L, ktime+kdur); + lua_setfield(L, -2, "end_time"); + lua_pushstring(L, ktag.mb_str(wxConvUTF8)); + lua_setfield(L, -2, "tag"); + lua_pushstring(L, ktext.mb_str(wxConvUTF8)); + lua_setfield(L, -2, "text"); + lua_pushstring(L, ktext_stripped.mb_str(wxConvUTF8)); + lua_setfield(L, -2, "text_stripped"); + lua_rawseti(L, -2, kcount); + + // prepare new syllable + kcount++; + ktag = tag->Name.Mid(1); + // check if it's a "set time" tag, special handling for that (depends on previous syllable duration) + if (ktag == _T("kt")) { + ktime = tag->Params[0]->AsInt() * 10; + kdur = 0; + } else { + ktime += kdur; // duration of previous syllable + kdur = tag->Params[0]->AsInt() * 10; + } + ktext.clear(); + ktext_stripped.clear(); + + } else { + // not karaoke tag + if (!brackets_open) { + ktext += _T("{"); + brackets_open = true; + } + ktext += tag->ToString(); + } + + } + + if (brackets_open) { + ktext += _T("}"); + brackets_open = false; + } + + break;} + } + } + + dia->ClearBlocks(); + + // store final syllable/block to lua + lua_newtable(L); + lua_pushnumber(L, kdur); + lua_setfield(L, -2, "duration"); + lua_pushnumber(L, ktime); + lua_setfield(L, -2, "start_time"); + lua_pushnumber(L, ktime+kdur); + lua_setfield(L, -2, "end_time"); + lua_pushstring(L, ktag.mb_str(wxConvUTF8)); + lua_setfield(L, -2, "tag"); + lua_pushstring(L, ktext.mb_str(wxConvUTF8)); + lua_setfield(L, -2, "text"); + lua_pushstring(L, ktext_stripped.mb_str(wxConvUTF8)); + lua_setfield(L, -2, "text_stripped"); + lua_rawseti(L, -2, kcount); + + delete dia;*/ + return 1; + } + + int RubyAssFile::RubySetUndoPoint() + { + /* RubyAssFile *laf = GetObjPointer(L, lua_upvalueindex(1)); + if (!laf->can_set_undo) { + lua_pushstring(L, "Attempt to set an undo point in a context without undo-support."); + lua_error(L); + return 0; + } + + wxString description; + if (lua_isstring(L, 1)) { + description = wxString(lua_tostring(L, 1), wxConvUTF8); + lua_pop(L, 1); + } + AssFile::top->FlagAsModified(); // TODO: make undo system support description of action undone + + laf->ass = AssFile::top; // make sure we're still working on the most recent undo point + */ return 0; + } + + RubyAssFile::~RubyAssFile() + { + RubyObjects::Get()->Unregister(rbAssFile); + } + + RubyAssFile::RubyAssFile(AssFile *_ass, bool _can_modify, bool _can_set_undo) + : ass(_ass) + , can_modify(_can_modify) + , can_set_undo(_can_set_undo) + { + if(RubyAssFile::raf) + delete RubyAssFile::raf; // delete previous if there is one + RubyAssFile::raf = this; // set pointer to this obj + + rbAssFile = rb_ary_new2(ass->Line.size()); + RubyObjects::Get()->Register(rbAssFile); + + std::list::iterator entry; + for(entry = ass->Line.begin(); entry != ass->Line.end(); ++entry) + { + rb_ary_push(rbAssFile, AssEntryToRuby(*entry)); + } + + // TODO + rb_define_module_function(RubyScript::RubyAegisub, "parse_tag_data",reinterpret_cast(&RubyParseTagData), 1); + rb_define_module_function(RubyScript::RubyAegisub, "unparse_tag_data",reinterpret_cast(&RubyUnparseTagData), 1); + rb_define_module_function(RubyScript::RubyAegisub, "parse_karaoke_data",reinterpret_cast(&RubyParseKaraokeData), 1); + //rb_define_module_function(RubyScript::RubyAegisub, "set_undo_point",reinterpret_cast(&RubySetUndoPoint), 1); + + } + +}; diff --git a/aegisub/auto4_ruby_dialog.cpp b/aegisub/auto4_ruby_dialog.cpp new file mode 100644 index 000000000..513b2fc47 --- /dev/null +++ b/aegisub/auto4_ruby_dialog.cpp @@ -0,0 +1,605 @@ +// Copyright (c) 2006, 2007, Niels Martin Hansen +// 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:jiifurusu@gmail.com +// + +#include "auto4_ruby.h" +#include "../ruby/include/ruby.h" +#include +#include +#include +#include +#include +#include + +namespace Automation4 { + + + // RubyConfigDialogControl + + RubyConfigDialogControl::RubyConfigDialogControl() + { + // Assume top of stack is a control table (don't do checking) + +/* lua_getfield(L, -1, "name"); + if (lua_isstring(L, -1)) { + name = wxString(lua_tostring(L, -1), wxConvUTF8); + } else { + name = _T(""); + } + lua_pop(L, 1); + + lua_getfield(L, -1, "x"); + if (lua_isnumber(L, -1)) { + x = lua_tointeger(L, -1); + if (x < 0) x = 0; + } else { + x = 0; + } + lua_pop(L, 1); + + lua_getfield(L, -1, "y"); + if (lua_isnumber(L, -1)) { + y = lua_tointeger(L, -1); + if (y < 0) y = 0; + } else { + y = 0; + } + lua_pop(L, 1); + + lua_getfield(L, -1, "width"); + if (lua_isnumber(L, -1)) { + width = lua_tointeger(L, -1); + if (width < 1) width = 1; + } else { + width = 1; + } + lua_pop(L, 1); + + lua_getfield(L, -1, "height"); + if (lua_isnumber(L, -1)) { + height = lua_tointeger(L, -1); + if (height < 1) height = 1; + } else { + height = 1; + } + lua_pop(L, 1); + + lua_getfield(L, -1, "hint"); + if (lua_isstring(L, -1)) { + hint = wxString(lua_tostring(L, -1), wxConvUTF8); + } else { + hint = _T(""); + } + lua_pop(L, 1); +*/ + wxLogDebug(_T("created control: '%s', (%d,%d)(%d,%d), '%s'"), name.c_str(), x, y, width, height, hint.c_str()); + } + + namespace RubyControl { + + // Label + + class Label : public RubyConfigDialogControl { + public: + wxString label; + + Label() + : RubyConfigDialogControl() + { +/* lua_getfield(L, -1, "label"); + label = wxString(lua_tostring(L, -1), wxConvUTF8); + lua_pop(L, 1); +*/ } + + virtual ~Label() { } + + wxControl *Create(wxWindow *parent) + { + return cw = new wxStaticText(parent, -1, label); + } + + void ControlReadBack() + { + // Nothing here + } + + void RubyReadBack() + { + // Label doesn't produce output, so let it be nil +// lua_pushnil(L); + } + }; + + + // Basic edit + + class Edit : public RubyConfigDialogControl { + public: + wxString text; + + Edit() + : RubyConfigDialogControl() + { +/* lua_getfield(L, -1, "text"); + text = wxString(lua_tostring(L, -1), wxConvUTF8); + lua_pop(L, 1); +*/ } + + virtual ~Edit() { } + + wxControl *Create(wxWindow *parent) + { + return cw = new wxTextCtrl(parent, -1, text, wxDefaultPosition, wxDefaultSize, 0); + } + + void ControlReadBack() + { + text = ((wxTextCtrl*)cw)->GetValue(); + } + + void RubyReadBack() + { + // lua_pushstring(L, text.mb_str(wxConvUTF8)); + } + + }; + + + // Multiline edit + + class Textbox : public Edit { + public: + + Textbox() + : Edit() + { + // Nothing more + } + + virtual ~Textbox() { } + + wxControl *Create(wxWindow *parent) + { + cw = new wxTextCtrl(parent, -1, text, wxDefaultPosition, wxDefaultSize, wxTE_MULTILINE); + cw->SetMinSize(wxSize(0, 30)); + return cw; + } + + }; + + + // Integer only edit + + class IntEdit : public Edit { + public: + int value; + bool hasspin; + int min, max; + + IntEdit() + : Edit() + { +/* lua_getfield(L, -1, "value"); + value = lua_tointeger(L, -1); + lua_pop(L, 1); + + hasspin = false; + + lua_getfield(L, -1, "min"); + if (!lua_isnumber(L, -1)) + goto nospin; + min = lua_tointeger(L, -1); + lua_pop(L, 1); + + lua_getfield(L, -1, "max"); + if (!lua_isnumber(L, -1)) + goto nospin; + max = lua_tointeger(L, -1); + lua_pop(L, 1); + + hasspin = true; +nospin: + if (!hasspin) { + lua_pop(L, 1); + } +*/ } + + virtual ~IntEdit() { } + + typedef wxValidator IntTextValidator; // TODO + wxControl *Create(wxWindow *parent) + { + if (hasspin) { + return cw = new wxSpinCtrl(parent, -1, wxString::Format(_T("%d"), value), wxDefaultPosition, wxDefaultSize, min, max, value); + } else { + return cw = new wxTextCtrl(parent, -1, text, wxDefaultPosition, wxDefaultSize, 0); //, IntTextValidator()); + } + } + + void ControlReadBack() + { + if (hasspin) { + value = ((wxSpinCtrl*)cw)->GetValue(); + } else { + long newval; + text = ((wxTextCtrl*)cw)->GetValue(); + if (text.ToLong(&newval)) { + value = newval; + } + } + } + + void RubyReadBack() + { +// lua_pushinteger(L, value); + } + + }; + + + // Float only edit + + class FloatEdit : public Edit { + public: + float value; + // FIXME: Can't support spin button atm + + FloatEdit() + : Edit() + { +/* lua_getfield(L, -1, "value"); + value = lua_tointeger(L, -1); + lua_pop(L, 1); +*/ + // TODO: spin button support + } + + virtual ~FloatEdit() { } + + typedef wxValidator FloatTextValidator; + wxControl *Create(wxWindow *parent) + { + return cw = new wxTextCtrl(parent, -1, text, wxDefaultPosition, wxDefaultSize, 0); //, FloatTextValidator()); + } + + void ControlReadBack() + { + double newval; + text = ((wxTextCtrl*)cw)->GetValue(); + if (text.ToDouble(&newval)) { + value = newval; + } + } + + void RubyReadBack() + { +// lua_pushnumber(L, value); + } + + }; + + + // Dropdown + + class Dropdown : public RubyConfigDialogControl { + public: + wxArrayString items; + wxString value; + + Dropdown() + : RubyConfigDialogControl() + { +/* lua_getfield(L, -1, "value"); + value = wxString(lua_tostring(L, -1), wxConvUTF8); + lua_pop(L, 1); + + lua_getfield(L, -1, "items"); + lua_pushnil(L); + while (lua_next(L, -2)) { + if (lua_isstring(L, -1)) { + items.Add(wxString(lua_tostring(L, -1), wxConvUTF8)); + } + lua_pop(L, 1); + } + lua_pop(L, 1); +*/ } + + virtual ~Dropdown() { } + + wxControl *Create(wxWindow *parent) + { + return cw = new wxComboBox(parent, -1, value, wxDefaultPosition, wxDefaultSize, items, wxCB_READONLY); + } + + void ControlReadBack() + { + value = ((wxComboBox*)cw)->GetValue(); + } + + void RubyReadBack() + { +// lua_pushstring(L, value.mb_str(wxConvUTF8)); + } + + }; + + + // Checkbox + + class Checkbox : public RubyConfigDialogControl { + public: + wxString label; + bool value; + + Checkbox() + : RubyConfigDialogControl() + { +/* lua_getfield(L, -1, "label"); + label = wxString(lua_tostring(L, -1), wxConvUTF8); + lua_pop(L, 1); + + lua_getfield(L, -1, "value"); + value = lua_toboolean(L, -1) != 0; + lua_pop(L, 1); +*/ } + + virtual ~Checkbox() { } + + wxControl *Create(wxWindow *parent) + { + return cw = new wxCheckBox(parent, -1, label); + } + + void ControlReadBack() + { + value = ((wxCheckBox*)cw)->GetValue(); + } + + void RubyReadBack() + { +// lua_pushboolean(L, value); + } + + }; + + }; + + + // RubyConfigDialog + + RubyConfigDialog::RubyConfigDialog(bool include_buttons) + : use_buttons(include_buttons) + { + wxLogDebug(_T("creating RubyConfigDialog, this addr is %p"), this); + button_pushed = 0; +/* if (include_buttons) { + + if (!lua_istable(L, -1)) + // Just to avoid deeper indentation... + goto skipbuttons; + // Iterate over items in table + lua_pushnil(L); // initial key + while (lua_next(L, -2)) { + // Simply skip invalid items... FIXME, warn here? + if (lua_isstring(L, -1)) { + wxString s(lua_tostring(L, -1), wxConvUTF8); + buttons.push_back(s); + } + lua_pop(L, 1); + } +skipbuttons: + lua_pop(L, 1); + } + + // assume top of stack now contains a dialog table + if (!lua_istable(L, -1)) { + lua_pushstring(L, "Cannot create config dialog from something non-table"); + lua_error(L); + assert(false); + } + + // Ok, so there is a table with controls + lua_pushnil(L); // initial key + while (lua_next(L, -2)) { + if (lua_istable(L, -1)) { + // Get control class + lua_getfield(L, -1, "class"); + if (!lua_isstring(L, -1)) + goto badcontrol; + wxString controlclass(lua_tostring(L, -1), wxConvUTF8); + controlclass.LowerCase(); + lua_pop(L, 1); + + RubyConfigDialogControl *ctl; + + // Check control class and create relevant control + if (controlclass == _T("label")) { + ctl = new RubyControl::Label(L); + } else if (controlclass == _T("edit")) { + ctl = new RubyControl::Edit(L); + } else if (controlclass == _T("intedit")) { + ctl = new RubyControl::IntEdit(L); + } else if (controlclass == _T("floatedit")) { + ctl = new RubyControl::FloatEdit(L); + } else if (controlclass == _T("textbox")) { + ctl = new RubyControl::Textbox(L); + } else if (controlclass == _T("dropdown")) { + ctl = new RubyControl::Dropdown(L); + } else if (controlclass == _T("checkbox")) { + ctl = new RubyControl::Checkbox(L); + } else if (controlclass == _T("color")) { + // FIXME + ctl = new RubyControl::Edit(L); + } else if (controlclass == _T("coloralpha")) { + // FIXME + ctl = new RubyControl::Edit(L); + } else if (controlclass == _T("alpha")) { + // FIXME + ctl = new RubyControl::Edit(L); + } else { + goto badcontrol; + } + + controls.push_back(ctl); + + } else { +badcontrol: + // not a control... + // FIXME, better error reporting? + lua_pushstring(L, "bad control table entry"); + lua_error(L); + } + lua_pop(L, 1); + } +*/ } + + RubyConfigDialog::~RubyConfigDialog() + { + for (size_t i = 0; i < controls.size(); ++i) + delete controls[i]; + } + + wxWindow* RubyConfigDialog::CreateWindow(wxWindow *parent) + { + wxWindow *w = new wxPanel(parent); + wxGridBagSizer *s = new wxGridBagSizer(4, 4); + + for (size_t i = 0; i < controls.size(); ++i) { + RubyConfigDialogControl *c = controls[i]; + c->Create(w); + if (dynamic_cast(c)) { + s->Add(c->cw, wxGBPosition(c->y, c->x), wxGBSpan(c->height, c->width), wxALIGN_CENTRE_VERTICAL|wxALIGN_LEFT); + } else { + s->Add(c->cw, wxGBPosition(c->y, c->x), wxGBSpan(c->height, c->width), wxEXPAND); + } + } + + if (use_buttons) { + wxStdDialogButtonSizer *bs = new wxStdDialogButtonSizer(); + if (buttons.size() > 0) { + wxLogDebug(_T("creating user buttons")); + for (size_t i = 0; i < buttons.size(); ++i) { + wxLogDebug(_T("button '%s' gets id %d"), buttons[i].c_str(), 1001+(wxWindowID)i); + bs->Add(new wxButton(w, 1001+(wxWindowID)i, buttons[i])); + } + } else { + wxLogDebug(_T("creating default buttons")); + bs->Add(new wxButton(w, wxID_OK)); + bs->Add(new wxButton(w, wxID_CANCEL)); + } + bs->Realize(); + + button_event = new ButtonEventHandler(); + button_event->button_pushed = &button_pushed; + // passing button_event as userdata because wx will then delete it + w->Connect(wxID_ANY, wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(RubyConfigDialog::ButtonEventHandler::OnButtonPush), button_event, button_event); + wxLogDebug(_T("set event handler, this addr is %p"), this); + + wxBoxSizer *ms = new wxBoxSizer(wxVERTICAL); + ms->Add(s, 0, wxBOTTOM, 5); + ms->Add(bs); + w->SetSizerAndFit(ms); + } else { + w->SetSizerAndFit(s); + } + + return w; + } + + int RubyConfigDialog::RubyReadBack() + { + // First read back which button was pressed, if any + if (use_buttons) { + wxLogDebug(_T("reading back button_pushed")); + int btn = button_pushed; + if (btn == 0) { + wxLogDebug(_T("was zero, cancelled")); + // Always cancel/closed + // lua_pushboolean(L, 0); + } else { + wxLogDebug(_T("nonzero, something else: %d"), btn); + if (buttons.size() > 0) { + wxLogDebug(_T("user button: %s"), buttons[btn-1].c_str()); + // button_pushed is index+1 to reserve 0 for Cancel + // lua_pushstring(L, buttons[btn-1].mb_str(wxConvUTF8)); + } else { + wxLogDebug(_T("default button, must be Ok")); + // Cancel case already covered, must be Ok then + // lua_pushboolean(L, 1); + } + } + } + + // Then read controls back +// lua_newtable(L); + for (size_t i = 0; i < controls.size(); ++i) { + controls[i]->RubyReadBack(); // TODO +// lua_setfield(L, -2, controls[i]->name.mb_str(wxConvUTF8)); + } + + if (use_buttons) { + return 2; + } else { + return 1; + } + } + + void RubyConfigDialog::ReadBack() + { + for (size_t i = 0; i < controls.size(); ++i) { + controls[i]->ControlReadBack(); + } + } + + void RubyConfigDialog::ButtonEventHandler::OnButtonPush(wxCommandEvent &evt) + { + // Let button_pushed == 0 mean "cancelled", such that pushing Cancel or closing the dialog + // will both result in button_pushed == 0 + if (evt.GetId() == wxID_OK) { + wxLogDebug(_T("was wxID_OK")); + *button_pushed = 1; + } else if (evt.GetId() == wxID_CANCEL) { + wxLogDebug(_T("was wxID_CANCEL")); + *button_pushed = 0; + } else { + wxLogDebug(_T("was user button")); + // Therefore, when buttons are numbered from 1001 to 1000+n, make sure to set it to i+1 + *button_pushed = evt.GetId() - 1000; + evt.SetId(wxID_OK); // hack to make sure the dialog will be closed + } + wxLogDebug(_T("button_pushed set to %d"), *button_pushed); + evt.Skip(); + } + +};