From dafdf87b79293c212f4f52ae5faa44d11cb09e5e Mon Sep 17 00:00:00 2001 From: Robin Gareus Date: Wed, 23 Mar 2016 23:44:35 +0100 Subject: towards a proper lua script console UI --- gtk2_ardour/luawindow.cc | 462 ++++++++++++++++++++++++++++++++++++++--- gtk2_ardour/luawindow.h | 82 +++++++- gtk2_ardour/script_selector.cc | 11 +- 3 files changed, 509 insertions(+), 46 deletions(-) diff --git a/gtk2_ardour/luawindow.cc b/gtk2_ardour/luawindow.cc index 5fda4f2e24..6b0ca8e08b 100644 --- a/gtk2_ardour/luawindow.cc +++ b/gtk2_ardour/luawindow.cc @@ -17,24 +17,36 @@ */ +#ifdef PLATFORM_WINDOWS +#define random() rand() +#endif + #ifdef WAF_BUILD #include "gtk2ardour-config.h" #endif -#include -#include -#include +#include +#include + +#include "pbd/basename.h" +#include "pbd/file_utils.h" +#include "pbd/md5.h" + +#include "gtkmm2ext/gtk_ui.h" +#include "gtkmm2ext/utils.h" +#include "gtkmm2ext/window_title.h" + +#include "ardour/luabindings.h" +#include "LuaBridge/LuaBridge.h" #include "ardour_ui.h" #include "gui_thread.h" #include "luainstance.h" #include "luawindow.h" #include "public_editor.h" +#include "tooltips.h" #include "utils.h" -#include "ardour/luabindings.h" -#include "LuaBridge/LuaBridge.h" - #include "i18n.h" using namespace ARDOUR; @@ -46,6 +58,18 @@ using namespace Gtkmm2ext; using namespace std; +inline LuaWindow::BufferFlags operator| (const LuaWindow::BufferFlags& a, const LuaWindow::BufferFlags& b) { + return static_cast (static_cast (a) | static_cast (b)); +} + +inline LuaWindow::BufferFlags operator|= (LuaWindow::BufferFlags& a, const LuaWindow::BufferFlags& b) { + return a = static_cast (static_cast (a) | static_cast (b)); +} + +inline LuaWindow::BufferFlags operator&= (LuaWindow::BufferFlags& a, const LuaWindow::BufferFlags& b) { + return a = static_cast (static_cast (a) & static_cast (b)); +} + LuaWindow* LuaWindow::_instance = 0; LuaWindow* @@ -62,54 +86,86 @@ LuaWindow::LuaWindow () : Window (Gtk::WINDOW_TOPLEVEL) , VisibilityTracker (*((Gtk::Window*) this)) , _visible (false) + , _menu_scratch (0) + , _menu_snippet (0) + , _menu_actions (0) + , _btn_run (_("Run")) + , _btn_clear (_("Clear Outtput")) + , _btn_open (_("Import")) + , _btn_save (_("Save")) + , _btn_delete (_("Delete")) + , _current_buffer (0) { set_name ("Lua"); update_title (); set_wmclass (X_("ardour_mixer"), PROGRAM_NAME); + script_select.disable_scrolling (); + set_border_width (0); outtext.set_editable (false); outtext.set_wrap_mode (Gtk::WRAP_WORD); + outtext.set_cursor_visible (false); signal_delete_event().connect (sigc::mem_fun (*this, &LuaWindow::hide_window)); signal_configure_event().connect (sigc::mem_fun (*ARDOUR_UI::instance(), &ARDOUR_UI::configure_handler)); - scrollwin.set_policy (Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS); - scrollwin.add (outtext); + _btn_run.signal_clicked.connect (sigc::mem_fun(*this, &LuaWindow::run_script)); + _btn_clear.signal_clicked.connect (sigc::mem_fun(*this, &LuaWindow::clear_output)); + _btn_open.signal_clicked.connect (sigc::mem_fun(*this, &LuaWindow::import_script)); + _btn_save.signal_clicked.connect (sigc::mem_fun(*this, &LuaWindow::save_script)); + _btn_delete.signal_clicked.connect (sigc::mem_fun(*this, &LuaWindow::delete_script)); + + _btn_open.set_sensitive (false); // TODO + _btn_save.set_sensitive (false); + _btn_delete.set_sensitive (false); // TODO + + // layout - Gtk::Button *btn_clr = manage (new Button ("Clear")); - btn_clr->signal_clicked().connect (sigc::mem_fun(*this, &LuaWindow::clear_output)); + Gtk::ScrolledWindow *scrollin = manage (new Gtk::ScrolledWindow); + scrollin->set_policy (Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC); + scrollin->add (entry); + scrollout.set_policy (Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS); + scrollout.add (outtext); Gtk::HBox *hbox = manage (new HBox()); - hbox->pack_start (entry, true, true, 2); - hbox->pack_start (*btn_clr, false, false, 0); + hbox->pack_start (_btn_run, false, false, 2); + hbox->pack_start (_btn_clear, false, false, 2); + hbox->pack_start (_btn_open, false, false, 2); + hbox->pack_start (_btn_save, false, false, 2); + hbox->pack_start (_btn_delete, false, false, 2); + hbox->pack_start (script_select, false, false, 2); Gtk::VBox *vbox = manage (new VBox()); - vbox->pack_start (scrollwin, true, true, 0); + vbox->pack_start (*scrollin, true, true, 0); vbox->pack_start (*hbox, false, false, 2); - entry.signal_activate().connect (sigc::mem_fun (*this, &LuaWindow::entry_activated)); + Gtk::VPaned *vpane = manage (new Gtk::VPaned ()); + vpane->pack1 (*vbox, true, false); + vpane->pack2 (scrollout, false, true); - lua.Print.connect (sigc::mem_fun (*this, &LuaWindow::append_text)); - - vbox->show_all (); - add (*vbox); + vpane->show_all (); + add (*vpane); set_size_request (640, 480); // XXX - LuaInstance::register_classes (lua.getState()); - // TODO register some callback functions. + lua.Print.connect (sigc::mem_fun (*this, &LuaWindow::append_text)); lua_State* L = lua.getState(); + LuaInstance::register_classes (L); luabridge::push (L, &PublicEditor::instance()); lua_setglobal (L, "Editor"); - // TODO - // - allow to load files - // - allow to run files directly - // - history buffer - // - multi-line input ?? + + ARDOUR_UI_UTILS::set_tooltip (script_select, _("Select Editor Buffer")); + + setup_buffers (); + LuaScripting::instance().scripts_changed.connect (*this, invalidator (*this), boost::bind (&LuaWindow::refresh_scriptlist, this), gui_context()); + + Glib::RefPtr tb (entry.get_buffer()); + + _script_changed_connection = tb->signal_changed().connect (sigc::mem_fun(*this, &LuaWindow::script_changed)); } LuaWindow::~LuaWindow () @@ -193,18 +249,44 @@ void LuaWindow::scroll_to_bottom () { Gtk::Adjustment *adj; - adj = scrollwin.get_vadjustment(); + adj = scrollout.get_vadjustment(); adj->set_value (MAX(0,(adj->get_upper() - adj->get_page_size()))); } void -LuaWindow::entry_activated () +LuaWindow::run_script () { - std::string cmd = entry.get_text(); - append_text ("> " + cmd); - - if (0 == lua.do_command (cmd)) { - entry.set_text(""); + Glib::RefPtr tb (entry.get_buffer()); + std::string script = tb->get_text(); + const std::string& bytecode = LuaScripting::get_factory_bytecode (script); + if (bytecode.empty()) { + // plain script or faulty script -- run directly + try { + lua.do_command ("function ardour () end"); + if (0 == lua.do_command (script)) { + append_text ("> OK"); + } + } catch (luabridge::LuaException const& e) { + append_text (string_compose (_("LuaException: %1"), e.what())); + } + } else { + // script with factory method + try { + lua_State* L = lua.getState(); + lua.do_command ("function ardour () end"); + + LuaScriptParamList args = LuaScriptParams::script_params (script, "action_param", false); + luabridge::LuaRef tbl_arg (luabridge::newTable(L)); + LuaScriptParams::params_to_ref (&tbl_arg, args); + lua.do_command (script); // register "factory" + luabridge::LuaRef lua_factory = luabridge::getGlobal (L, "factory"); + if (lua_factory.isFunction()) { + lua_factory(tbl_arg)(); + } + lua.do_command ("factory = nil;"); + } catch (luabridge::LuaException const& e) { + append_text (string_compose (_("LuaException: %1"), e.what())); + } } } @@ -222,3 +304,319 @@ LuaWindow::clear_output () Glib::RefPtr tb (outtext.get_buffer()); tb->set_text (""); } + +void +LuaWindow::new_script () +{ +#if 0 + Glib::RefPtr tb (entry.get_buffer()); + tb->set_text (""); +#endif +} + +void +LuaWindow::delete_script () +{ +#if 0 + Glib::RefPtr tb (entry.get_buffer()); + tb->set_text (""); +#endif +} + +void +LuaWindow::import_script () +{ +} + +void +LuaWindow::save_script () +{ + Glib::RefPtr tb (entry.get_buffer()); + std::string script = tb->get_text(); + std::string msg = "Unknown error"; + + std::string path; + LuaScriptInfoPtr lsi = LuaScripting::script_info (script); + ScriptBuffer & sb (*_current_buffer); + + assert (sb.flags & Buffer_Dirty); + + // 1) check if it has a valid header and factory + const std::string& bytecode = LuaScripting::get_factory_bytecode (script); + if (bytecode.empty()) { + msg = _("Missing script header.\nThe script requires an '{ardour}' info table and a 'factory' function."); + goto errorout; + } + + if (!LuaScripting::try_compile (script, LuaScriptParams::script_params (script, "action_param", false))) { + msg = _("Script fails to compile."); + goto errorout; + } + + // 2) check script name & type + lsi = LuaScripting::script_info (script); + if (!lsi) { + msg = _("Invalid or missing script-name or script-type."); + goto errorout; + } + + if (lsi->type != LuaScriptInfo::Snippet && lsi->type != LuaScriptInfo::EditorAction) { + msg = _("Invalid script-type.\nValid types are 'EditorAction' and 'Snippet'."); + goto errorout; + } + + // 3) if there's already a writable file,... + if ((sb.flags & Buffer_HasFile) && !(sb.flags & Buffer_ReadOnly)) { + try { + Glib::file_set_contents (sb.path, script); + sb.flags &= BufferFlags(~Buffer_Dirty); + update_gui_state (); // XXX here? + append_text (X_("> ") + string_compose (_("Saved as %1"), sb.path)); + return; // OK + } catch (Glib::FileError e) { + msg = string_compose (_("Error saving file: %1"), e.what()); + goto errorout; + } + } + + // 4) check if the name is unique for the given type; locally at least + if (true /*sb.flags & Buffer_HasFile*/) { + LuaScriptList& lsl (LuaScripting::instance ().scripts (lsi->type)); + for (LuaScriptList::const_iterator s = lsl.begin(); s != lsl.end(); ++s) { + if ((*s)->name == lsi->name) { + msg = string_compose (_("Script with given name '%1' already exists.\nUse a different name in the descriptor."), lsi->name); + goto errorout; + } + } + } + + // 5) construct filename -- TODO ask user for name, ask to replace file. + do { + char buf[80]; + time_t t = time(0); + struct tm * timeinfo = localtime (&t); + strftime (buf, sizeof(buf), "%s%d", timeinfo); + sprintf (buf, "%s%d", buf, random ()); // is this valid? + MD5 md5; + std::string fn = md5.digestString (buf); + + switch (lsi->type) { + case LuaScriptInfo::EditorAction: + fn = "a_" + fn; + break; + case LuaScriptInfo::Snippet: + fn = "s_" + fn; + break; + default: + break; + } + path = Glib::build_filename (LuaScripting::user_script_dir (), fn.substr(0, 11) + ".lua"); + } while (Glib::file_test (path, Glib::FILE_TEST_EXISTS)); + + try { + Glib::file_set_contents (path, script); + sb.path = path; + sb.flags |= Buffer_HasFile; + sb.flags &= BufferFlags(~Buffer_Dirty); + update_gui_state (); // XXX here? + LuaScripting::instance().refresh (true); + append_text (X_("> ") + string_compose (_("Saved as %1"), path)); + return; // OK + } catch (Glib::FileError e) { + msg = string_compose (_("Error saving file: %1"), e.what()); + goto errorout; + } + +errorout: + MessageDialog am (msg); + am.run (); +} + +void +LuaWindow::setup_buffers () +{ + if (script_buffers.size() > 0) { + return; + } + script_buffers.push_back (ScriptBufferPtr (new LuaWindow::ScriptBuffer("#1"))); + script_buffers.push_back (ScriptBufferPtr (new LuaWindow::ScriptBuffer("#2"))); // XXX + _current_buffer = script_buffers.front(); + + refresh_scriptlist (); + update_gui_state (); +} + +uint32_t +LuaWindow::count_scratch_buffers () const +{ + return 0; +} + +void +LuaWindow::refresh_scriptlist () +{ + for (ScriptBufferList::iterator i = script_buffers.begin (); i != script_buffers.end ();) { + if ((*i)->flags & Buffer_Scratch) { + ++i; + continue; + } + i = script_buffers.erase (i); + } + LuaScriptList& lsa (LuaScripting::instance ().scripts (LuaScriptInfo::EditorAction)); + for (LuaScriptList::const_iterator s = lsa.begin(); s != lsa.end(); ++s) { + script_buffers.push_back (ScriptBufferPtr ( new LuaWindow::ScriptBuffer(*s))); + } + + LuaScriptList& lss (LuaScripting::instance ().scripts (LuaScriptInfo::Snippet)); + for (LuaScriptList::const_iterator s = lss.begin(); s != lss.end(); ++s) { + script_buffers.push_back (ScriptBufferPtr ( new LuaWindow::ScriptBuffer(*s))); + } + rebuild_menu (); +} + +void +LuaWindow::rebuild_menu () +{ + using namespace Menu_Helpers; + + _menu_scratch = manage (new Menu); + _menu_snippet = manage (new Menu); + _menu_actions = manage (new Menu); + + MenuList& items_scratch (_menu_scratch->items()); + MenuList& items_snippet (_menu_snippet->items()); + MenuList& items_actions (_menu_actions->items()); + + { + Menu_Helpers::MenuElem elem = Gtk::Menu_Helpers::MenuElem(_("New"), + sigc::mem_fun(*this, &LuaWindow::new_script)); + items_scratch.push_back(elem); + } + + for (ScriptBufferList::const_iterator i = script_buffers.begin (); i != script_buffers.end (); ++i) { + Menu_Helpers::MenuElem elem = Gtk::Menu_Helpers::MenuElem((*i)->name, + sigc::bind(sigc::mem_fun(*this, &LuaWindow::script_selection_changed), (*i))); + + if ((*i)->flags & Buffer_Scratch) { + items_scratch.push_back(elem); + } + else if ((*i)->type == LuaScriptInfo::EditorAction) { + items_actions.push_back(elem); + } + else if ((*i)->type == LuaScriptInfo::Snippet) { + items_snippet.push_back(elem); + } + } + + script_select.clear_items (); + script_select.AddMenuElem (Menu_Helpers::MenuElem ("Scratch", *_menu_scratch)); + script_select.AddMenuElem (Menu_Helpers::MenuElem ("Snippets", *_menu_snippet)); + script_select.AddMenuElem (Menu_Helpers::MenuElem ("Actions", *_menu_actions)); +} + +void +LuaWindow::script_selection_changed (ScriptBufferPtr n) +{ + if (n == _current_buffer) { + return; + } + + Glib::RefPtr tb (entry.get_buffer()); + _current_buffer->script = tb->get_text(); + + if (!(n->flags & Buffer_Valid)) { + if (!n->load()) { + append_text ("! Failed to load buffer."); + } + } + + if (n->flags & Buffer_Valid) { + _current_buffer = n; + _script_changed_connection.block (); + tb->set_text (n->script); + _script_changed_connection.unblock (); + } else { + append_text ("! Failed to switch buffer."); + } + update_gui_state (); +} + +void +LuaWindow::update_gui_state () +{ + const ScriptBuffer & sb (*_current_buffer); + std::string name; + if (sb.flags & Buffer_Scratch) { + name = string_compose (_("Scratch Buffer %1"), sb.name); + } else if (sb.type == LuaScriptInfo::EditorAction) { + name = string_compose (_("Action: '%1'"), sb.name); + } else if (sb.type == LuaScriptInfo::Snippet) { + name = string_compose (_("Snippet: %1"), sb.name); + } else { + cerr << "Invalid Script type\n"; + assert (0); + return; + } + if (sb.flags & Buffer_Dirty) { + name += " *"; + } + script_select.set_text(name); + + _btn_save.set_sensitive (sb.flags & Buffer_Dirty); + _btn_delete.set_sensitive (sb.flags & Buffer_Scratch); // TODO allow to remove user-scripts +} + +void +LuaWindow::script_changed () { + if (_current_buffer->flags & Buffer_Dirty) { + return; + } + _current_buffer->flags |= Buffer_Dirty; + update_gui_state (); +} + +LuaWindow::ScriptBuffer::ScriptBuffer (const std::string& n) + : name (n) + , flags (Buffer_Scratch | Buffer_Valid) +{ +} + +LuaWindow::ScriptBuffer::ScriptBuffer (LuaScriptInfoPtr p) + : name (p->name) + , path (p->path) + , flags (Buffer_HasFile) + , type (p->type) +{ + if (!PBD::exists_and_writable (path)) { + flags |= Buffer_ReadOnly; + } +} + +#if 0 +LuaWindow::ScriptBuffer::ScriptBuffer (const ScriptBuffer& other) + : script (other.script) + , name (other.name) + , path (other.path) + , flags (other.flags) + , type (other.type) +{ +} +#endif + +LuaWindow::ScriptBuffer::~ScriptBuffer () +{ +} + +bool +LuaWindow::ScriptBuffer::load () +{ + if (!(flags & Buffer_HasFile)) return false; + if (flags & Buffer_Valid) return true; + try { + script = Glib::file_get_contents (path); + flags |= Buffer_Valid; + } catch (Glib::FileError e) { + return false; + } + return true; +} diff --git a/gtk2_ardour/luawindow.h b/gtk2_ardour/luawindow.h index 9a7f281f3e..8faec694a5 100644 --- a/gtk2_ardour/luawindow.h +++ b/gtk2_ardour/luawindow.h @@ -26,17 +26,21 @@ #include #include +#include "pbd/signals.h" +#include "pbd/stateful.h" + #include "ardour/ardour.h" -#include "ardour/types.h" +#include "ardour/luascripting.h" #include "ardour/session_handle.h" - -#include "pbd/stateful.h" -#include "pbd/signals.h" +#include "ardour/types.h" #include "gtkmm2ext/visibility_tracker.h" #include "lua/luastate.h" +#include "ardour_button.h" +#include "ardour_dropdown.h" + class LuaWindow : public Gtk::Window, public PBD::ScopedConnectionList, @@ -52,27 +56,85 @@ class LuaWindow : void set_session (ARDOUR::Session* s); + typedef enum { + Buffer_NOFLAG = 0x00, + Buffer_Valid = 0x01, ///< script is loaded + Buffer_HasFile = 0x02, + Buffer_ReadOnly = 0x04, + Buffer_Dirty = 0x08, + Buffer_Scratch = 0x10, + } BufferFlags; + + class ScriptBuffer { + public: + ScriptBuffer (const std::string&); + ScriptBuffer (ARDOUR::LuaScriptInfoPtr); + //ScriptBuffer (const ScriptBuffer& other); + ~ScriptBuffer (); + + bool load (); + + std::string script; + std::string name; + std::string path; + BufferFlags flags; + ARDOUR::LuaScriptInfo::ScriptType type; + }; + private: LuaWindow (); static LuaWindow* _instance; + LuaState lua; bool _visible; - Gtk::VBox global_vpacker; + + Gtk::Menu* _menu_scratch; + Gtk::Menu* _menu_snippet; + Gtk::Menu* _menu_actions; + + sigc::connection _script_changed_connection; + + Gtk::TextView entry; + Gtk::TextView outtext; + Gtk::ScrolledWindow scrollout; + + ArdourButton _btn_run; + ArdourButton _btn_clear; + ArdourButton _btn_open; + ArdourButton _btn_save; + ArdourButton _btn_delete; + + ArdourDropdown script_select; + + typedef boost::shared_ptr ScriptBufferPtr; + typedef std::vector ScriptBufferList; + + ScriptBufferList script_buffers; + ScriptBufferPtr _current_buffer; void session_going_away (); void update_title (); - Gtk::Entry entry; - Gtk::TextView outtext; - Gtk::ScrolledWindow scrollwin; + void setup_buffers (); + void refresh_scriptlist (); + void rebuild_menu (); + uint32_t count_scratch_buffers () const; + + void script_changed (); + void script_selection_changed (ScriptBufferPtr n); + void update_gui_state (); void append_text (std::string s); void scroll_to_bottom (); void clear_output (); - void entry_activated (); + void run_script (); - LuaState lua; + void new_script (); + void delete_script (); + void import_script (); + void save_script (); }; + #endif diff --git a/gtk2_ardour/script_selector.cc b/gtk2_ardour/script_selector.cc index a8a1b6e957..3fab40a5e7 100644 --- a/gtk2_ardour/script_selector.cc +++ b/gtk2_ardour/script_selector.cc @@ -112,6 +112,7 @@ ScriptSelector::script_combo_changed () void ScriptSelector::refresh () { + LuaScripting::instance ().refresh (); _scripts = LuaScripting::instance ().scripts (_script_type); setup_list (); } @@ -166,10 +167,12 @@ ScriptParameterDialog::ScriptParameterDialog (std::string title, t->attach (_name_entry, 1, 2, ty, ty+1); ++ty; - l = manage (new Label (_("Parameters:"), Gtk::ALIGN_CENTER, Gtk::ALIGN_CENTER, false)); - l->set_use_markup (); - t->attach (_name_entry, 0, 2, ty, ty+1); - ++ty; + if (_lsp.size () > 0) { + l = manage (new Label (_("Instance Parameters"), Gtk::ALIGN_LEFT, Gtk::ALIGN_CENTER, false)); + l->set_use_markup (); + t->attach (*l, 0, 2, ty, ty+1); + ++ty; + } for (size_t i = 0; i < _lsp.size (); ++i) { CheckButton* c = manage (new CheckButton (_lsp[i]->title)); -- cgit v1.2.3