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