add a convenient script header
[ardour.git] / gtk2_ardour / luawindow.cc
1 /*
2     Copyright (C) 2016 Robin Gareus <robin@gareus.org>
3
4     This program is free software; you can redistribute it and/or modify
5     it under the terms of the GNU General Public License as published by
6     the Free Software Foundation; either version 2 of the License, or
7     (at your option) any later version.
8
9     This program is distributed in the hope that it will be useful,
10     but WITHOUT ANY WARRANTY; without even the implied warranty of
11     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12     GNU General Public License for more details.
13
14     You should have received a copy of the GNU General Public License
15     along with this program; if not, write to the Free Software
16     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17
18 */
19
20 #ifdef PLATFORM_WINDOWS
21 #define random() rand()
22 #endif
23
24 #ifdef WAF_BUILD
25 #include "gtk2ardour-config.h"
26 #endif
27
28 #include <glibmm/fileutils.h>
29 #include <gtkmm/messagedialog.h>
30
31 #include "pbd/basename.h"
32 #include "pbd/file_utils.h"
33 #include "pbd/md5.h"
34
35 #include "gtkmm2ext/gtk_ui.h"
36 #include "gtkmm2ext/utils.h"
37 #include "gtkmm2ext/window_title.h"
38
39 #include "ardour/luabindings.h"
40 #include "LuaBridge/LuaBridge.h"
41
42 #include "ardour_ui.h"
43 #include "gui_thread.h"
44 #include "luainstance.h"
45 #include "luawindow.h"
46 #include "public_editor.h"
47 #include "tooltips.h"
48 #include "utils.h"
49 #include "utils_videotl.h"
50
51 #include "i18n.h"
52
53 using namespace ARDOUR;
54 using namespace ARDOUR_UI_UTILS;
55 using namespace PBD;
56 using namespace Gtk;
57 using namespace Glib;
58 using namespace Gtkmm2ext;
59 using namespace std;
60
61
62 inline LuaWindow::BufferFlags operator| (const LuaWindow::BufferFlags& a, const LuaWindow::BufferFlags& b) {
63         return static_cast<LuaWindow::BufferFlags> (static_cast <int>(a) | static_cast<int> (b));
64 }
65
66 inline LuaWindow::BufferFlags operator|= (LuaWindow::BufferFlags& a, const LuaWindow::BufferFlags& b) {
67         return a = static_cast<LuaWindow::BufferFlags> (static_cast <int>(a) | static_cast<int> (b));
68 }
69
70 inline LuaWindow::BufferFlags operator&= (LuaWindow::BufferFlags& a, const LuaWindow::BufferFlags& b) {
71         return a = static_cast<LuaWindow::BufferFlags> (static_cast <int>(a) & static_cast<int> (b));
72 }
73
74 LuaWindow* LuaWindow::_instance = 0;
75
76 LuaWindow*
77 LuaWindow::instance ()
78 {
79         if (!_instance) {
80                 _instance  = new LuaWindow;
81         }
82
83         return _instance;
84 }
85
86 LuaWindow::LuaWindow ()
87         : Window (Gtk::WINDOW_TOPLEVEL)
88         , VisibilityTracker (*((Gtk::Window*) this))
89         , lua (0)
90         , _visible (false)
91         , _menu_scratch (0)
92         , _menu_snippet (0)
93         , _menu_actions (0)
94         , _btn_run (_("Run"))
95         , _btn_clear (_("Clear Outtput"))
96         , _btn_open (_("Import"))
97         , _btn_save (_("Save"))
98         , _btn_delete (_("Delete"))
99         , _current_buffer ()
100 {
101         set_name ("Lua");
102
103         reinit_lua ();
104         update_title ();
105         set_wmclass (X_("ardour_mixer"), PROGRAM_NAME);
106
107         script_select.disable_scrolling ();
108
109         set_border_width (0);
110
111         outtext.set_editable (false);
112         outtext.set_wrap_mode (Gtk::WRAP_WORD);
113         outtext.set_cursor_visible (false);
114
115         signal_delete_event().connect (sigc::mem_fun (*this, &LuaWindow::hide_window));
116         signal_configure_event().connect (sigc::mem_fun (*ARDOUR_UI::instance(), &ARDOUR_UI::configure_handler));
117
118         _btn_run.signal_clicked.connect (sigc::mem_fun(*this, &LuaWindow::run_script));
119         _btn_clear.signal_clicked.connect (sigc::mem_fun(*this, &LuaWindow::clear_output));
120         _btn_open.signal_clicked.connect (sigc::mem_fun(*this, &LuaWindow::import_script));
121         _btn_save.signal_clicked.connect (sigc::mem_fun(*this, &LuaWindow::save_script));
122         _btn_delete.signal_clicked.connect (sigc::mem_fun(*this, &LuaWindow::delete_script));
123
124         _btn_open.set_sensitive (false); // TODO
125         _btn_save.set_sensitive (false);
126         _btn_delete.set_sensitive (false);
127
128         // layout
129
130         Gtk::ScrolledWindow *scrollin = manage (new Gtk::ScrolledWindow);
131         scrollin->set_policy (Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
132         scrollin->add (entry);
133         scrollout.set_policy (Gtk::POLICY_NEVER, Gtk::POLICY_ALWAYS);
134         scrollout.add (outtext);
135
136         entry.set_name ("ArdourLuaEntry");
137         outtext.set_name ("ArdourLuaEntry");
138
139         Gtk::HBox *hbox = manage (new HBox());
140
141         hbox->pack_start (_btn_run, false, false, 2);
142         hbox->pack_start (_btn_clear, false, false, 2);
143         hbox->pack_start (_btn_open, false, false, 2);
144         hbox->pack_start (_btn_save, false, false, 2);
145         hbox->pack_start (_btn_delete, false, false, 2);
146         hbox->pack_start (script_select, false, false, 2);
147
148         Gtk::VBox *vbox = manage (new VBox());
149         vbox->pack_start (*scrollin, true, true, 0);
150         vbox->pack_start (*hbox, false, false, 2);
151
152         Gtk::VPaned *vpane = manage (new Gtk::VPaned ());
153         vpane->pack1 (*vbox, true, false);
154         vpane->pack2 (scrollout, false, true);
155
156         vpane->show_all ();
157         add (*vpane);
158         set_size_request (640, 480); // XXX
159         ARDOUR_UI_UTILS::set_tooltip (script_select, _("Select Editor Buffer"));
160
161         setup_buffers ();
162         LuaScripting::instance().scripts_changed.connect (*this, invalidator (*this), boost::bind (&LuaWindow::refresh_scriptlist, this), gui_context());
163
164         Glib::RefPtr<Gtk::TextBuffer> tb (entry.get_buffer());
165         _script_changed_connection = tb->signal_changed().connect (sigc::mem_fun(*this, &LuaWindow::script_changed));
166 }
167
168 LuaWindow::~LuaWindow ()
169 {
170         delete lua;
171 }
172
173 void
174 LuaWindow::show_window ()
175 {
176         present();
177         _visible = true;
178 }
179
180 bool
181 LuaWindow::hide_window (GdkEventAny *ev)
182 {
183         if (!_visible) return 0;
184         _visible = false;
185         return just_hide_it (ev, static_cast<Gtk::Window *>(this));
186 }
187
188 void LuaWindow::reinit_lua ()
189 {
190         delete lua;
191         lua = new LuaState();
192         lua->Print.connect (sigc::mem_fun (*this, &LuaWindow::append_text));
193
194         lua_State* L = lua->getState();
195         LuaInstance::register_classes (L);
196         luabridge::push <PublicEditor *> (L, &PublicEditor::instance());
197         lua_setglobal (L, "Editor");
198 }
199
200 void LuaWindow::set_session (Session* s)
201 {
202         SessionHandlePtr::set_session (s);
203         if (!_session) {
204                 return;
205         }
206
207         update_title ();
208         _session->DirtyChanged.connect (_session_connections, invalidator (*this), boost::bind (&LuaWindow::update_title, this), gui_context());
209
210         lua_State* L = lua->getState();
211         LuaBindings::set_session (L, _session);
212 }
213
214 void
215 LuaWindow::session_going_away ()
216 {
217         ENSURE_GUI_THREAD (*this, &LuaWindow::session_going_away);
218         reinit_lua (); // drop state (all variables, session references)
219
220         SessionHandlePtr::session_going_away ();
221         _session = 0;
222         update_title ();
223
224         lua_State* L = lua->getState();
225         LuaBindings::set_session (L, _session);
226 }
227
228 void
229 LuaWindow::update_title ()
230 {
231         if (_session) {
232                 string n;
233
234                 if (_session->snap_name() != _session->name()) {
235                         n = _session->snap_name ();
236                 } else {
237                         n = _session->name ();
238                 }
239
240                 if (_session->dirty ()) {
241                         n = "*" + n;
242                 }
243
244                 WindowTitle title (n);
245                 title += S_("Window|Lua");
246                 title += Glib::get_application_name ();
247                 set_title (title.get_string());
248
249         } else {
250                 WindowTitle title (S_("Window|Lua"));
251                 title += Glib::get_application_name ();
252                 set_title (title.get_string());
253         }
254 }
255
256 void
257 LuaWindow::scroll_to_bottom ()
258 {
259         Gtk::Adjustment *adj;
260         adj = scrollout.get_vadjustment();
261         adj->set_value (MAX(0,(adj->get_upper() - adj->get_page_size())));
262 }
263
264 void
265 LuaWindow::run_script ()
266 {
267         Glib::RefPtr<Gtk::TextBuffer> tb (entry.get_buffer());
268         std::string script = tb->get_text();
269         const std::string& bytecode = LuaScripting::get_factory_bytecode (script);
270         if (bytecode.empty()) {
271                 // plain script or faulty script -- run directly
272                 try {
273                         lua->do_command ("function ardour () end");
274                         if (0 == lua->do_command (script)) {
275                                 append_text ("> OK");
276                         }
277                 } catch (luabridge::LuaException const& e) {
278                         append_text (string_compose (_("LuaException: %1"), e.what()));
279                 }
280         } else {
281                 // script with factory method
282                 try {
283                         lua_State* L = lua->getState();
284                         lua->do_command ("function ardour () end");
285
286                         LuaScriptParamList args = LuaScriptParams::script_params (script, "action_param", false);
287                         luabridge::LuaRef tbl_arg (luabridge::newTable(L));
288                         LuaScriptParams::params_to_ref (&tbl_arg, args);
289                         lua->do_command (script); // register "factory"
290                         luabridge::LuaRef lua_factory = luabridge::getGlobal (L, "factory");
291                         if (lua_factory.isFunction()) {
292                                 lua_factory(tbl_arg)();
293                         }
294                         lua->do_command ("factory = nil;");
295                 } catch (luabridge::LuaException const& e) {
296                         append_text (string_compose (_("LuaException: %1"), e.what()));
297                 }
298         }
299 }
300
301 void
302 LuaWindow::append_text (std::string s)
303 {
304         Glib::RefPtr<Gtk::TextBuffer> tb (outtext.get_buffer());
305         tb->insert (tb->end(), s + "\n");
306         scroll_to_bottom ();
307 }
308
309 void
310 LuaWindow::clear_output ()
311 {
312         Glib::RefPtr<Gtk::TextBuffer> tb (outtext.get_buffer());
313         tb->set_text ("");
314 }
315
316 void
317 LuaWindow::new_script ()
318 {
319         char buf[32];
320         snprintf (buf, sizeof (buf), "#%d", count_scratch_buffers () + 1);
321         script_buffers.push_back (ScriptBufferPtr (new LuaWindow::ScriptBuffer (buf)));
322         script_selection_changed (script_buffers.back ());
323         refresh_scriptlist ();
324 }
325
326 void
327 LuaWindow::delete_script ()
328 {
329         assert (_current_buffer->flags & Buffer_Scratch);
330         for (ScriptBufferList::iterator i = script_buffers.begin (); i != script_buffers.end (); ++i) {
331                 if ((*i) == _current_buffer) {
332                         script_buffers.erase (i);
333                         break;
334                 }
335         }
336
337         for (ScriptBufferList::const_iterator i = script_buffers.begin (); i != script_buffers.end (); ++i) {
338                 if ((*i)->flags & Buffer_Scratch) {
339                         script_selection_changed (*i);
340                         return;
341                 }
342         }
343         new_script ();
344 }
345
346 void
347 LuaWindow::import_script ()
348 {
349         // TODO: dialog to select file or enter URL
350         // TODO convert a few URL (eg. pastebin) to raw.
351 #if 0
352         char *url = "http://pastebin.com/raw/3UMkZ6nV";
353         char *rv = a3_curl_http_get (url, 0);
354         if (rv) {
355                 new_script ();
356                 Glib::RefPtr<Gtk::TextBuffer> tb (entry.get_buffer());
357                 tb->set_text (rv);
358                 _current_buffer->flags &= BufferFlags(~Buffer_Dirty);
359                 update_gui_state ();
360         }
361         free (rv);
362 #endif
363 }
364
365 void
366 LuaWindow::save_script ()
367 {
368         Glib::RefPtr<Gtk::TextBuffer> tb (entry.get_buffer());
369         std::string script = tb->get_text();
370         std::string msg = "Unknown error";
371
372         std::string path;
373         LuaScriptInfoPtr lsi = LuaScripting::script_info (script);
374         ScriptBuffer & sb (*_current_buffer);
375
376         assert (sb.flags & Buffer_Dirty);
377
378         // 1) check if it has a valid header and factory
379         const std::string& bytecode = LuaScripting::get_factory_bytecode (script);
380         if (bytecode.empty()) {
381                 msg = _("Missing script header.\nThe script requires an '{ardour}' info table and a 'factory' function.");
382                 goto errorout;
383         }
384
385         if (!LuaScripting::try_compile (script, LuaScriptParams::script_params (script, "action_param", false))) {
386                 msg = _("Script fails to compile.");
387                 goto errorout;
388         }
389
390         // 2) check script name & type
391         lsi = LuaScripting::script_info (script);
392         if (!lsi) {
393                 msg = _("Invalid or missing script-name or script-type.");
394                 goto errorout;
395         }
396
397         if (lsi->type != LuaScriptInfo::Snippet && lsi->type != LuaScriptInfo::EditorAction) {
398                 msg = _("Invalid script-type.\nValid types are 'EditorAction' and 'Snippet'.");
399                 goto errorout;
400         }
401
402         // 3) if there's already a writable file,...
403         if ((sb.flags & Buffer_HasFile) && !(sb.flags & Buffer_ReadOnly)) {
404                 try {
405                         Glib::file_set_contents (sb.path, script);
406                         sb.flags &= BufferFlags(~Buffer_Dirty);
407                         update_gui_state (); // XXX here?
408                         append_text (X_("> ") + string_compose (_("Saved as %1"), sb.path));
409                         return; // OK
410                 } catch (Glib::FileError e) {
411                         msg = string_compose (_("Error saving file: %1"), e.what());
412                         goto errorout;
413                 }
414         }
415
416         // 4) check if the name is unique for the given type; locally at least
417         if (true /*sb.flags & Buffer_HasFile*/) {
418                 LuaScriptList& lsl (LuaScripting::instance ().scripts (lsi->type));
419                 for (LuaScriptList::const_iterator s = lsl.begin(); s != lsl.end(); ++s) {
420                         if ((*s)->name == lsi->name) {
421                                 msg = string_compose (_("Script with given name '%1' already exists.\nUse a different name in the descriptor."), lsi->name);
422                                 goto errorout;
423                         }
424                 }
425         }
426
427         // 5) construct filename -- TODO ask user for name, ask to replace file.
428         do {
429                 char buf[80];
430                 time_t t = time(0);
431                 struct tm * timeinfo = localtime (&t);
432                 strftime (buf, sizeof(buf), "%s%d", timeinfo);
433                 sprintf (buf, "%s%ld", buf, random ()); // is this valid?
434                 MD5 md5;
435                 std::string fn = md5.digestString (buf);
436
437                 switch (lsi->type) {
438                         case LuaScriptInfo::EditorAction:
439                                 fn = "a_" + fn;
440                                 break;
441                         case LuaScriptInfo::Snippet:
442                                 fn = "s_" + fn;
443                                 break;
444                         default:
445                                 break;
446                 }
447                 path = Glib::build_filename (LuaScripting::user_script_dir (), fn.substr(0, 11) + ".lua");
448         } while (Glib::file_test (path, Glib::FILE_TEST_EXISTS));
449
450         try {
451                 Glib::file_set_contents (path, script);
452                 sb.path = path;
453                 sb.flags |= Buffer_HasFile;
454                 sb.flags &= BufferFlags(~Buffer_Dirty);
455                 update_gui_state (); // XXX here?
456                 LuaScripting::instance().refresh (true);
457                 append_text (X_("> ") + string_compose (_("Saved as %1"), path));
458                 return; // OK
459         } catch (Glib::FileError e) {
460                 msg = string_compose (_("Error saving file: %1"), e.what());
461                 goto errorout;
462         }
463
464 errorout:
465                 MessageDialog am (msg);
466                 am.run ();
467 }
468
469 void
470 LuaWindow::setup_buffers ()
471 {
472         if (script_buffers.size() > 0) {
473                 return;
474         }
475         script_buffers.push_back (ScriptBufferPtr (new LuaWindow::ScriptBuffer("#1")));
476         _current_buffer = script_buffers.front();
477
478         Glib::RefPtr<Gtk::TextBuffer> tb (entry.get_buffer());
479         tb->set_text (_current_buffer->script);
480
481         refresh_scriptlist ();
482         update_gui_state ();
483 }
484
485 uint32_t
486 LuaWindow::count_scratch_buffers () const
487 {
488         uint32_t n = 0;
489         for (ScriptBufferList::const_iterator i = script_buffers.begin (); i != script_buffers.end (); ++i) {
490                 if ((*i)->flags & Buffer_Scratch) {
491                         ++n;
492                 }
493         }
494         return n;
495 }
496
497 void
498 LuaWindow::refresh_scriptlist ()
499 {
500         for (ScriptBufferList::iterator i = script_buffers.begin (); i != script_buffers.end ();) {
501                 if ((*i)->flags & Buffer_Scratch) {
502                         ++i;
503                         continue;
504                 }
505                 i = script_buffers.erase (i);
506         }
507         LuaScriptList& lsa (LuaScripting::instance ().scripts (LuaScriptInfo::EditorAction));
508         for (LuaScriptList::const_iterator s = lsa.begin(); s != lsa.end(); ++s) {
509                 script_buffers.push_back (ScriptBufferPtr ( new LuaWindow::ScriptBuffer(*s)));
510         }
511
512         LuaScriptList& lss (LuaScripting::instance ().scripts (LuaScriptInfo::Snippet));
513         for (LuaScriptList::const_iterator s = lss.begin(); s != lss.end(); ++s) {
514                 script_buffers.push_back (ScriptBufferPtr ( new LuaWindow::ScriptBuffer(*s)));
515         }
516         rebuild_menu ();
517 }
518
519 void
520 LuaWindow::rebuild_menu ()
521 {
522         using namespace Menu_Helpers;
523
524         _menu_scratch = manage (new Menu);
525         _menu_snippet = manage (new Menu);
526         _menu_actions = manage (new Menu);
527
528         MenuList& items_scratch (_menu_scratch->items());
529         MenuList& items_snippet (_menu_snippet->items());
530         MenuList& items_actions (_menu_actions->items());
531
532         {
533                 Menu_Helpers::MenuElem elem = Gtk::Menu_Helpers::MenuElem(_("New"),
534                                 sigc::mem_fun(*this, &LuaWindow::new_script));
535                 items_scratch.push_back(elem);
536         }
537
538         for (ScriptBufferList::const_iterator i = script_buffers.begin (); i != script_buffers.end (); ++i) {
539                 Menu_Helpers::MenuElem elem = Gtk::Menu_Helpers::MenuElem((*i)->name,
540                                 sigc::bind(sigc::mem_fun(*this, &LuaWindow::script_selection_changed), (*i)));
541
542                 if ((*i)->flags & Buffer_Scratch) {
543                         items_scratch.push_back(elem);
544                 }
545                 else if ((*i)->type == LuaScriptInfo::EditorAction) {
546                                 items_actions.push_back(elem);
547                 }
548                 else if ((*i)->type == LuaScriptInfo::Snippet) {
549                                 items_snippet.push_back(elem);
550                 }
551         }
552
553         script_select.clear_items ();
554         script_select.AddMenuElem (Menu_Helpers::MenuElem ("Scratch", *_menu_scratch));
555         script_select.AddMenuElem (Menu_Helpers::MenuElem ("Snippets", *_menu_snippet));
556         script_select.AddMenuElem (Menu_Helpers::MenuElem ("Actions", *_menu_actions));
557 }
558
559 void
560 LuaWindow::script_selection_changed (ScriptBufferPtr n)
561 {
562         if (n == _current_buffer) {
563                 return;
564         }
565
566         Glib::RefPtr<Gtk::TextBuffer> tb (entry.get_buffer());
567         _current_buffer->script = tb->get_text();
568
569         if (!(n->flags & Buffer_Valid)) {
570                 if (!n->load()) {
571                         append_text ("! Failed to load buffer.");
572                 }
573         }
574
575         if (n->flags & Buffer_Valid) {
576                 _current_buffer = n;
577                 _script_changed_connection.block ();
578                 tb->set_text (n->script);
579                 _script_changed_connection.unblock ();
580         } else {
581                 append_text ("! Failed to switch buffer.");
582         }
583         update_gui_state ();
584 }
585
586 void
587 LuaWindow::update_gui_state ()
588 {
589         const ScriptBuffer & sb (*_current_buffer);
590         std::string name;
591         if (sb.flags & Buffer_Scratch) {
592                 name = string_compose (_("Scratch Buffer %1"), sb.name);
593         } else if (sb.type == LuaScriptInfo::EditorAction) {
594                 name = string_compose (_("Action: '%1'"), sb.name);
595         } else if (sb.type == LuaScriptInfo::Snippet) {
596                 name = string_compose (_("Snippet: %1"), sb.name);
597         } else {
598                 cerr << "Invalid Script type\n";
599                 assert (0);
600                 return;
601         }
602         if (sb.flags & Buffer_Dirty) {
603                 name += " *";
604         }
605         script_select.set_text(name);
606
607         _btn_save.set_sensitive (sb.flags & Buffer_Dirty);
608         _btn_delete.set_sensitive (sb.flags & Buffer_Scratch); // TODO allow to remove user-scripts
609 }
610
611 void
612 LuaWindow::script_changed () {
613         if (_current_buffer->flags & Buffer_Dirty) {
614                 return;
615         }
616         _current_buffer->flags |= Buffer_Dirty;
617         update_gui_state ();
618 }
619
620 LuaWindow::ScriptBuffer::ScriptBuffer (const std::string& n)
621         : name (n)
622         , flags (Buffer_Scratch | Buffer_Valid)
623 {
624         script =
625                 "-- ardour { [\"type\"] = \"Snippet\", name = \"\" }\n"
626                 "-- function factory () return function () -- -- end end\n";
627 }
628
629 LuaWindow::ScriptBuffer::ScriptBuffer (LuaScriptInfoPtr p)
630         : name (p->name)
631         , path (p->path)
632         , flags (Buffer_HasFile)
633         , type (p->type)
634 {
635         if (!PBD::exists_and_writable (path)) {
636                 flags |= Buffer_ReadOnly;
637         }
638 }
639
640 #if 0
641 LuaWindow::ScriptBuffer::ScriptBuffer (const ScriptBuffer& other)
642         : script (other.script)
643         , name (other.name)
644         , path (other.path)
645         , flags (other.flags)
646         , type (other.type)
647 {
648 }
649 #endif
650
651 LuaWindow::ScriptBuffer::~ScriptBuffer ()
652 {
653 }
654
655 bool
656 LuaWindow::ScriptBuffer::load ()
657 {
658         if (!(flags & Buffer_HasFile)) return false;
659         if (flags & Buffer_Valid) return true;
660         try {
661                 script = Glib::file_get_contents (path);
662                 flags |= Buffer_Valid;
663         } catch (Glib::FileError e) {
664                 return false;
665         }
666         return true;
667 }