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