Do not try to restore Route solo state after clearing all solo state
[ardour.git] / gtk2_ardour / luawindow.cc
index 5fda4f2e242b7a5c30c3610a7e7cf211709f87b1..04e2e8c2405f27d0f2d06efd74bee0f0cac4c364 100644 (file)
 
 */
 
+#ifdef PLATFORM_WINDOWS
+#define random() rand()
+#endif
+
 #ifdef WAF_BUILD
 #include "gtk2ardour-config.h"
 #endif
 
-#include <gtkmm2ext/gtk_ui.h>
-#include <gtkmm2ext/utils.h>
-#include <gtkmm2ext/window_title.h>
+#include "pbd/gstdio_compat.h"
+#include <glibmm/fileutils.h>
+#include <gtkmm/messagedialog.h>
+
+#include "pbd/basename.h"
+#include "pbd/file_utils.h"
+#include "pbd/md5.h"
+
+#include "gtkmm2ext/gtk_ui.h"
+#include "gtkmm2ext/pane.h"
+#include "gtkmm2ext/utils.h"
+#include "gtkmm2ext/window_title.h"
+
+#include "ardour/filesystem_paths.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 "utils_videotl.h"
 
-#include "ardour/luabindings.h"
-#include "LuaBridge/LuaBridge.h"
-
-#include "i18n.h"
+#include "pbd/i18n.h"
 
 using namespace ARDOUR;
 using namespace ARDOUR_UI_UTILS;
@@ -46,6 +62,18 @@ using namespace Gtkmm2ext;
 using namespace std;
 
 
+inline LuaWindow::BufferFlags operator| (const LuaWindow::BufferFlags& a, const LuaWindow::BufferFlags& b) {
+       return static_cast<LuaWindow::BufferFlags> (static_cast <int>(a) | static_cast<int> (b));
+}
+
+inline LuaWindow::BufferFlags operator|= (LuaWindow::BufferFlags& a, const LuaWindow::BufferFlags& b) {
+       return a = static_cast<LuaWindow::BufferFlags> (static_cast <int>(a) | static_cast<int> (b));
+}
+
+inline LuaWindow::BufferFlags operator&= (LuaWindow::BufferFlags& a, const LuaWindow::BufferFlags& b) {
+       return a = static_cast<LuaWindow::BufferFlags> (static_cast <int>(a) & static_cast<int> (b));
+}
+
 LuaWindow* LuaWindow::_instance = 0;
 
 LuaWindow*
@@ -61,59 +89,92 @@ LuaWindow::instance ()
 LuaWindow::LuaWindow ()
        : Window (Gtk::WINDOW_TOPLEVEL)
        , VisibilityTracker (*((Gtk::Window*) this))
+       , lua (0)
        , _visible (false)
+       , _menu_scratch (0)
+       , _menu_snippet (0)
+       , _menu_actions (0)
+       , _btn_run (_("Run"))
+       , _btn_clear (_("Clear Output"))
+       , _btn_open (_("Import"))
+       , _btn_save (_("Save"))
+       , _btn_delete (_("Delete"))
+       , _btn_revert (_("Revert"))
+       , _current_buffer ()
 {
        set_name ("Lua");
 
+       reinit_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_revert.signal_clicked.connect (sigc::mem_fun(*this, &LuaWindow::revert_script));
+
+       _btn_open.set_sensitive (false); // TODO
+       _btn_save.set_sensitive (false);
+       _btn_delete.set_sensitive (false);
+       _btn_revert.set_sensitive (false);
 
-       Gtk::Button *btn_clr = manage (new Button ("Clear"));
-       btn_clr->signal_clicked().connect (sigc::mem_fun(*this, &LuaWindow::clear_output));
+       // layout
+
+       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);
+
+       entry.set_name ("ArdourLuaEntry");
+       outtext.set_name ("ArdourLuaEntry");
 
        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 (_btn_revert, 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));
-
-       lua.Print.connect (sigc::mem_fun (*this, &LuaWindow::append_text));
+       Gtkmm2ext::VPane *vpane = manage (new Gtkmm2ext::VPane ());
+       vpane->add (*vbox);
+       vpane->add (scrollout);
 
-       vbox->show_all ();
-       add (*vbox);
+       vpane->show_all ();
+       add (*vpane);
        set_size_request (640, 480); // XXX
+       ARDOUR_UI_UTILS::set_tooltip (script_select, _("Select Editor Buffer"));
 
-       LuaInstance::register_classes (lua.getState());
-       // TODO register some callback functions.
+       setup_buffers ();
+       LuaScripting::instance().scripts_changed.connect (*this, invalidator (*this), boost::bind (&LuaWindow::refresh_scriptlist, this), gui_context());
 
-       lua_State* L = lua.getState();
-       luabridge::push <PublicEditor *> (L, &PublicEditor::instance());
-       lua_setglobal (L, "Editor");
-       // TODO
-       // - allow to load files
-       // - allow to run files directly
-       // - history buffer
-       // - multi-line input ??
+       Glib::RefPtr<Gtk::TextBuffer> tb (entry.get_buffer());
+       _script_changed_connection = tb->signal_changed().connect (sigc::mem_fun(*this, &LuaWindow::script_changed));
 }
 
 LuaWindow::~LuaWindow ()
 {
+       delete lua;
 }
 
 void
@@ -131,6 +192,19 @@ LuaWindow::hide_window (GdkEventAny *ev)
        return just_hide_it (ev, static_cast<Gtk::Window *>(this));
 }
 
+void LuaWindow::reinit_lua ()
+{
+       ENSURE_GUI_THREAD (*this, &LuaWindow::session_going_away);
+       delete lua;
+       lua = new LuaState();
+       lua->Print.connect (sigc::mem_fun (*this, &LuaWindow::append_text));
+
+       lua_State* L = lua->getState();
+       LuaInstance::register_classes (L);
+       luabridge::push <PublicEditor *> (L, &PublicEditor::instance());
+       lua_setglobal (L, "Editor");
+}
+
 void LuaWindow::set_session (Session* s)
 {
        SessionHandlePtr::set_session (s);
@@ -141,8 +215,7 @@ void LuaWindow::set_session (Session* s)
        update_title ();
        _session->DirtyChanged.connect (_session_connections, invalidator (*this), boost::bind (&LuaWindow::update_title, this), gui_context());
 
-       // expose "Session" point directly
-       lua_State* L = lua.getState();
+       lua_State* L = lua->getState();
        LuaBindings::set_session (L, _session);
 }
 
@@ -150,14 +223,13 @@ void
 LuaWindow::session_going_away ()
 {
        ENSURE_GUI_THREAD (*this, &LuaWindow::session_going_away);
-       lua.do_command ("collectgarbage();");
-       //TODO: re-init lua-engine (drop all references) ??
+       reinit_lua (); // drop state (all variables, session references)
 
        SessionHandlePtr::session_going_away ();
        _session = 0;
        update_title ();
 
-       lua_State* L = lua.getState();
+       lua_State* L = lua->getState();
        LuaBindings::set_session (L, _session);
 }
 
@@ -193,18 +265,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<Gtk::TextBuffer> 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()));
+               }
        }
 }
 
@@ -214,6 +312,7 @@ LuaWindow::append_text (std::string s)
        Glib::RefPtr<Gtk::TextBuffer> tb (outtext.get_buffer());
        tb->insert (tb->end(), s + "\n");
        scroll_to_bottom ();
+       Gtkmm2ext::UI::instance()->flush_pending (0.05);
 }
 
 void
@@ -222,3 +321,404 @@ LuaWindow::clear_output ()
        Glib::RefPtr<Gtk::TextBuffer> tb (outtext.get_buffer());
        tb->set_text ("");
 }
+
+void
+LuaWindow::new_script ()
+{
+       char buf[32];
+       snprintf (buf, sizeof (buf), "#%d", count_scratch_buffers () + 1);
+       script_buffers.push_back (ScriptBufferPtr (new LuaWindow::ScriptBuffer (buf)));
+       script_selection_changed (script_buffers.back ());
+       refresh_scriptlist ();
+}
+
+void
+LuaWindow::delete_script ()
+{
+       assert ((_current_buffer->flags & Buffer_Scratch) || !(_current_buffer->flags & Buffer_ReadOnly));
+       bool refresh = false;
+       bool neednew = true;
+       if (_current_buffer->flags & Buffer_HasFile) {
+               if (0 == ::g_unlink (_current_buffer->path.c_str())) {
+                       append_text (X_("> ") + string_compose (_("Deleted %1"), _current_buffer->path));
+                       refresh = true;
+               } else {
+                       append_text (X_("> ") + string_compose (_("Failed to delete %1"), _current_buffer->path));
+               }
+       }
+       for (ScriptBufferList::iterator i = script_buffers.begin (); i != script_buffers.end (); ++i) {
+               if ((*i) == _current_buffer) {
+                       script_buffers.erase (i);
+                       break;
+               }
+       }
+
+       for (ScriptBufferList::const_iterator i = script_buffers.begin (); i != script_buffers.end (); ++i) {
+               if ((*i)->flags & Buffer_Scratch) {
+                       script_selection_changed (*i);
+                       neednew = false;
+               }
+       }
+       if (neednew) {
+               new_script ();
+       }
+       if (refresh) {
+               LuaScripting::instance ().refresh (true);
+       }
+}
+
+void
+LuaWindow::revert_script ()
+{
+       _current_buffer->flags &= BufferFlags(~Buffer_Valid);
+       script_selection_changed (_current_buffer, true);
+}
+
+void
+LuaWindow::import_script ()
+{
+       // TODO: dialog to select file or enter URL
+       // TODO convert a few URL (eg. pastebin) to raw.
+#if 0
+       char *url = "http://pastebin.com/raw/3UMkZ6nV";
+       char *rv = ArdourCurl::http_get (url, 0);
+       if (rv) {
+               new_script ();
+               Glib::RefPtr<Gtk::TextBuffer> tb (entry.get_buffer());
+               tb->set_text (rv);
+               _current_buffer->flags &= BufferFlags(~Buffer_Dirty);
+               update_gui_state ();
+       }
+       free (rv);
+#endif
+}
+
+void
+LuaWindow::save_script ()
+{
+       Glib::RefPtr<Gtk::TextBuffer> 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.name = lsi->name;
+                       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%ld", 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.name = lsi->name;
+               sb.flags |= Buffer_HasFile;
+               sb.flags &= BufferFlags(~Buffer_Dirty);
+               sb.flags &= BufferFlags(~Buffer_ReadOnly);
+               update_gui_state (); // XXX here? .refresh (true) may trigger this, too
+               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")));
+       _current_buffer = script_buffers.front();
+
+       Glib::RefPtr<Gtk::TextBuffer> tb (entry.get_buffer());
+       tb->set_text (_current_buffer->script);
+
+       refresh_scriptlist ();
+       update_gui_state ();
+}
+
+uint32_t
+LuaWindow::count_scratch_buffers () const
+{
+       uint32_t n = 0;
+       for (ScriptBufferList::const_iterator i = script_buffers.begin (); i != script_buffers.end (); ++i) {
+               if ((*i)->flags & Buffer_Scratch) {
+                       ++n;
+               }
+       }
+       return n;
+}
+
+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);
+       }
+
+       items_scratch.push_back(SeparatorElem());
+
+       for (ScriptBufferList::const_iterator i = script_buffers.begin (); i != script_buffers.end (); ++i) {
+               std::string name;
+               if ((*i)->flags & Buffer_ReadOnly) {
+                       name = "[R] " + (*i)->name;
+               } else {
+                       name = (*i)->name;
+               }
+               Menu_Helpers::MenuElem elem = Gtk::Menu_Helpers::MenuElem(name,
+                               sigc::bind(sigc::mem_fun(*this, &LuaWindow::script_selection_changed), (*i), false));
+
+               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, bool force)
+{
+       if (n == _current_buffer && !force) {
+               return;
+       }
+
+       Glib::RefPtr<Gtk::TextBuffer> tb (entry.get_buffer());
+
+       if (_current_buffer->flags & Buffer_Valid) {
+               _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);
+
+       if (sb.flags & Buffer_ReadOnly) {
+               _btn_save.set_text (_("Save as"));
+       } else {
+               _btn_save.set_text (_("Save"));
+       }
+       _btn_save.set_sensitive (sb.flags & Buffer_Dirty);
+       _btn_delete.set_sensitive (sb.flags & Buffer_Scratch || ((sb.flags & (Buffer_ReadOnly | Buffer_HasFile)) == Buffer_HasFile));
+       _btn_revert.set_sensitive ((sb.flags & Buffer_Dirty) && (sb.flags & Buffer_HasFile));
+}
+
+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)
+{
+       script =
+               "---- this header is (only) required to save the script\n"
+               "-- ardour { [\"type\"] = \"Snippet\", name = \"\" }\n"
+               "-- function factory () return function () -- -- end end\n";
+}
+
+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 (path.find (user_config_directory ()) != 0) {
+               // mark non-user scripts as read-only
+               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 ()
+{
+       assert (!(flags & Buffer_Valid));
+       if (!(flags & Buffer_HasFile)) return false;
+       try {
+               script = Glib::file_get_contents (path);
+               flags |= Buffer_Valid;
+               flags &= BufferFlags(~Buffer_Dirty);
+       } catch (Glib::FileError e) {
+               return false;
+       }
+       return true;
+}