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