redesign plugin selection process to fix multiple-addition problem
[ardour.git] / gtk2_ardour / plugin_selector.cc
1 /*
2     Copyright (C) 2000-2006 Paul Davis 
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 #include <cstdio>
21 #include <lrdf.h>
22 #include <map>
23
24 #include <algorithm>
25
26 #include <gtkmm/table.h>
27 #include <gtkmm/stock.h>
28 #include <gtkmm/button.h>
29 #include <gtkmm/notebook.h>
30
31 #include <gtkmm2ext/utils.h>
32
33 #include <pbd/convert.h>
34
35 #include <ardour/plugin_manager.h>
36 #include <ardour/plugin.h>
37 #include <ardour/configuration.h>
38
39 #include "ardour_ui.h"
40 #include "plugin_selector.h"
41 #include "gui_thread.h"
42
43 #include "i18n.h"
44
45 using namespace ARDOUR;
46 using namespace PBD;
47 using namespace Gtk;
48 using namespace std;
49
50 static const char* _filter_mode_strings[] = {
51         N_("Name contains"),
52         N_("Type contains"),
53         N_("Author contains"),
54         N_("Library contains"),
55         N_("Favorites only"),
56         0
57 };
58
59 PluginSelector::PluginSelector (PluginManager *mgr)
60         : ArdourDialog (_("ardour: plugins"), true, false),
61           filter_button (Stock::CLEAR)
62 {
63         set_position (Gtk::WIN_POS_MOUSE);
64         set_name ("PluginSelectorWindow");
65         add_events (Gdk::KEY_PRESS_MASK|Gdk::KEY_RELEASE_MASK);
66
67         manager = mgr;
68         session = 0;
69         _menu = 0;
70         in_row_change = false;
71
72         plugin_model = Gtk::ListStore::create (plugin_columns);
73         plugin_display.set_model (plugin_model);
74         /* XXX translators: try to convert "Fav" into a short term
75            related to "favorite"
76         */
77         plugin_display.append_column (_("Fav"), plugin_columns.favorite);
78         plugin_display.append_column (_("Available Plugins"), plugin_columns.name);
79         plugin_display.append_column (_("Type"), plugin_columns.type_name);
80         plugin_display.append_column (_("Category"), plugin_columns.category);
81         plugin_display.append_column (_("Creator"), plugin_columns.creator);
82         plugin_display.append_column (_("# Inputs"),plugin_columns.ins);
83         plugin_display.append_column (_("# Outputs"), plugin_columns.outs);
84         plugin_display.set_headers_visible (true);
85         plugin_display.set_headers_clickable (true);
86         plugin_display.set_reorderable (false);
87         plugin_display.set_rules_hint (true);
88
89         CellRendererToggle* fav_cell = dynamic_cast<CellRendererToggle*>(plugin_display.get_column_cell_renderer (0));
90         fav_cell->property_activatable() = true;
91         fav_cell->property_radio() = false;
92         fav_cell->signal_toggled().connect (mem_fun (*this, &PluginSelector::favorite_changed));
93
94         scroller.set_border_width(10);
95         scroller.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
96         scroller.add(plugin_display);
97
98         amodel = Gtk::ListStore::create(acols);
99         added_list.set_model (amodel);
100         added_list.append_column (_("Plugins to be connected"), acols.text);
101         added_list.set_headers_visible (true);
102         added_list.set_reorderable (false);
103
104         for (int i = 0; i <=3; i++) {
105                 Gtk::TreeView::Column* column = plugin_display.get_column(i);
106                 column->set_sort_column(i);
107         }
108
109         ascroller.set_border_width(10);
110         ascroller.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC);
111         ascroller.add(added_list);
112         btn_add = manage(new Gtk::Button(Stock::ADD));
113         ARDOUR_UI::instance()->tooltips().set_tip(*btn_add, _("Add a plugin to the effect list"));
114         btn_add->set_sensitive (false);
115         btn_remove = manage(new Gtk::Button(Stock::REMOVE));
116         btn_remove->set_sensitive (false);
117         ARDOUR_UI::instance()->tooltips().set_tip(*btn_remove, _("Remove a plugin from the effect list"));
118         Gtk::Button *btn_update = manage(new Gtk::Button(Stock::REFRESH));
119         ARDOUR_UI::instance()->tooltips().set_tip(*btn_update, _("Update available plugins"));
120
121         btn_add->set_name("PluginSelectorButton");
122         btn_remove->set_name("PluginSelectorButton");
123
124         Gtk::Table* table = manage(new Gtk::Table(7, 11));
125         table->set_size_request(750, 500);
126         table->attach(scroller, 0, 7, 0, 5);
127
128         HBox* filter_box = manage (new HBox);
129
130         vector<string> filter_strings = I18N (_filter_mode_strings);
131         Gtkmm2ext::set_popdown_strings (filter_mode, filter_strings);
132         filter_mode.set_active_text (filter_strings.front());
133
134         filter_box->pack_start (filter_mode, false, false);
135         filter_box->pack_start (filter_entry, true, true);
136         filter_box->pack_start (filter_button, false, false);
137
138         filter_entry.signal_changed().connect (mem_fun (*this, &PluginSelector::filter_entry_changed));
139         filter_button.signal_clicked().connect (mem_fun (*this, &PluginSelector::filter_button_clicked));
140         filter_mode.signal_changed().connect (mem_fun (*this, &PluginSelector::filter_mode_changed));
141
142         filter_box->show ();
143         filter_mode.show ();
144         filter_entry.show ();
145         filter_button.show ();
146
147         table->attach (*filter_box, 0, 7, 5, 6, FILL|EXPAND, FILL, 5, 5);
148
149         table->attach(*btn_add, 1, 2, 6, 7, FILL, FILL, 5, 5);
150         table->attach(*btn_remove, 3, 4, 6, 7, FILL, FILL, 5, 5);
151         table->attach(*btn_update, 5, 6, 6, 7, FILL, FILL, 5, 5);
152
153         table->attach(ascroller, 0, 7, 8, 10);
154
155         add_button (Stock::CANCEL, RESPONSE_CANCEL);
156         add_button (_("Insert Plugin(s)"), RESPONSE_APPLY);
157         set_default_response (RESPONSE_APPLY);
158         set_response_sensitive (RESPONSE_APPLY, false);
159         get_vbox()->pack_start (*table);
160
161         table->set_name("PluginSelectorTable");
162         plugin_display.set_name("PluginSelectorDisplay");
163         //plugin_display.set_name("PluginSelectorList");
164         added_list.set_name("PluginSelectorList");
165
166         plugin_display.signal_button_press_event().connect_notify (mem_fun(*this, &PluginSelector::row_clicked));
167         plugin_display.get_selection()->signal_changed().connect (mem_fun(*this, &PluginSelector::display_selection_changed));
168         plugin_display.grab_focus();
169         
170         btn_update->signal_clicked().connect (mem_fun(*this, &PluginSelector::btn_update_clicked));
171         btn_add->signal_clicked().connect(mem_fun(*this, &PluginSelector::btn_add_clicked));
172         btn_remove->signal_clicked().connect(mem_fun(*this, &PluginSelector::btn_remove_clicked));
173         added_list.get_selection()->signal_changed().connect (mem_fun(*this, &PluginSelector::added_list_selection_changed));
174
175         refill ();
176 }
177
178 void
179 PluginSelector::row_clicked(GdkEventButton* event)
180 {
181         if (event->type == GDK_2BUTTON_PRESS)
182                 btn_add_clicked();
183 }
184
185 void
186 PluginSelector::set_session (Session* s)
187 {
188         ENSURE_GUI_THREAD(bind (mem_fun(*this, &PluginSelector::set_session), s));
189         
190         session = s;
191
192         if (session) {
193                 session->GoingAway.connect (bind (mem_fun(*this, &PluginSelector::set_session), static_cast<Session*> (0)));
194         }
195 }
196
197 bool
198 PluginSelector::show_this_plugin (const PluginInfoPtr& info, const std::string& filterstr)
199 {
200         std::string compstr;
201         std::string mode = filter_mode.get_active_text ();
202
203         if (mode == _("Favorites only")) {
204                 return manager->is_a_favorite_plugin (info);
205         }
206
207         if (!filterstr.empty()) {
208                 
209                 if (mode == _("Name contains")) {
210                         compstr = info->name;
211                 } else if (mode == _("Type contains")) {
212                         compstr = info->category;
213                 } else if (mode == _("Author contains")) {
214                         compstr = info->creator;
215                 } else if (mode == _("Library contains")) {
216                         compstr = info->path;
217                 } 
218
219                 transform (compstr.begin(), compstr.end(), compstr.begin(), ::toupper);
220
221                 if (compstr.find (filterstr) != string::npos) {
222                         return true;
223                 } else {
224                         return false;
225                 }
226         }
227
228         return true;
229 }
230
231 void
232 PluginSelector::setup_filter_string (string& filterstr)
233 {
234         filterstr = filter_entry.get_text ();
235         transform (filterstr.begin(), filterstr.end(), filterstr.begin(), ::toupper);
236 }       
237
238 void
239 PluginSelector::refill ()
240 {
241         std::string filterstr;
242
243         in_row_change = true;
244
245         plugin_model->clear ();
246
247         setup_filter_string (filterstr);
248
249         ladspa_refiller (filterstr);
250         lv2_refiller (filterstr);
251         vst_refiller (filterstr);
252         au_refiller (filterstr);
253
254         in_row_change = false;
255 }
256
257 void
258 PluginSelector::refiller (const PluginInfoList& plugs, const::std::string& filterstr, const char* type)
259 {
260         char buf[16];
261
262         for (PluginInfoList::const_iterator i = plugs.begin(); i != plugs.end(); ++i) {
263
264                 if (show_this_plugin (*i, filterstr)) {
265
266                         TreeModel::Row newrow = *(plugin_model->append());
267                         newrow[plugin_columns.favorite] = manager->is_a_favorite_plugin (*i);
268                         newrow[plugin_columns.name] = (*i)->name;
269                         newrow[plugin_columns.type_name] = type;
270                         newrow[plugin_columns.category] = (*i)->category;
271
272                         string creator = (*i)->creator;
273                         string::size_type pos = 0;
274
275                         /* stupid LADSPA creator strings */
276
277                         while (pos < creator.length() && (isalnum (creator[pos]) || isspace (creator[pos]))) ++pos;
278                         creator = creator.substr (0, pos);
279
280                         newrow[plugin_columns.creator] = creator;
281
282                         if ((*i)->n_inputs < 0) {
283                                 newrow[plugin_columns.ins] = "various";
284                         } else {
285                                 snprintf (buf, sizeof(buf), "%d", (*i)->n_inputs);
286                                 newrow[plugin_columns.ins] = buf;
287                         }
288                         if ((*i)->n_outputs < 0) {
289                                 newrow[plugin_columns.outs] = "various";
290                         } else {
291                                 snprintf (buf, sizeof(buf), "%d", (*i)->n_outputs);             
292                                 newrow[plugin_columns.outs] = buf;
293                         }
294
295                         newrow[plugin_columns.plugin] = *i;
296                 }
297         }       
298 }
299
300 void
301 PluginSelector::ladspa_refiller (const std::string& filterstr)
302 {
303         refiller (manager->ladspa_plugin_info(), filterstr, "LADSPA");
304 }
305
306 void
307 PluginSelector::lv2_refiller (const std::string& filterstr)
308 {
309 #ifdef HAVE_SLV2
310         refiller (manager->lv2_plugin_info(), filterstr, "LV2");
311 #endif
312 }
313
314 void
315 PluginSelector::vst_refiller (const std::string& filterstr)
316 {
317 #ifdef VST_SUPPORT
318         refiller (manager->vst_plugin_info(), filterstr, "VST");
319 #endif
320 }
321
322 void
323 PluginSelector::au_refiller (const std::string& filterstr)
324 {
325 #ifdef HAVE_AUDIOUNITS
326         refiller (manager->au_plugin_info(), filterstr, "AU");
327 #endif
328 }
329
330 PluginPtr
331 PluginSelector::load_plugin (PluginInfoPtr pi)
332 {
333         if (session == 0) {
334                 return PluginPtr();
335         }
336
337         return pi->load (*session);
338 }
339
340 void
341 PluginSelector::btn_add_clicked()
342 {
343         std::string name;
344         PluginInfoPtr pi;
345         TreeModel::Row newrow = *(amodel->append());
346         TreeModel::Row row;
347
348         row = *(plugin_display.get_selection()->get_selected());
349         name = row[plugin_columns.name];
350         pi = row[plugin_columns.plugin];
351
352         newrow[acols.text] = name;
353         newrow[acols.plugin] = pi;
354
355         if (!amodel->children().empty()) {
356                 set_response_sensitive (RESPONSE_APPLY, true);
357         }
358 }
359
360 void
361 PluginSelector::btn_remove_clicked()
362 {
363         TreeModel::iterator iter = added_list.get_selection()->get_selected();
364         
365         amodel->erase(iter);
366         if (amodel->children().empty()) {
367                 set_response_sensitive (RESPONSE_APPLY, false);
368         }
369 }
370
371 void
372 PluginSelector::btn_update_clicked()
373 {
374         manager->refresh ();
375         refill();
376 }
377
378 void
379 PluginSelector::display_selection_changed()
380 {
381         if (plugin_display.get_selection()->count_selected_rows() != 0) {
382                 btn_add->set_sensitive (true);
383         } else {
384                 btn_add->set_sensitive (false);
385         }
386 }
387
388 void
389 PluginSelector::added_list_selection_changed()
390 {
391         if (added_list.get_selection()->count_selected_rows() != 0) {
392                 btn_remove->set_sensitive (true);
393         } else {
394                 btn_remove->set_sensitive (false);
395         }
396 }
397
398 int
399 PluginSelector::run ()
400 {
401         ResponseType r;
402         TreeModel::Children::iterator i;
403         SelectedPlugins plugins;
404
405         r = (ResponseType) Dialog::run ();
406
407         switch (r) {
408         case RESPONSE_APPLY:
409                 for (i = amodel->children().begin(); i != amodel->children().end(); ++i) {
410                         PluginInfoPtr pp = (*i)[acols.plugin];
411                         PluginPtr p = load_plugin (pp);
412                         if (p) {
413                                 plugins.push_back (p);
414                         }
415                 }
416                 if (interested_object && !plugins.empty()) {
417                         interested_object->use_plugins (plugins);
418                 }
419                 
420                 break;
421
422         default:
423                 break;
424         }
425
426         hide();
427         amodel->clear();
428         interested_object = 0;
429
430         return (int) r;
431 }
432
433 void
434 PluginSelector::filter_button_clicked ()
435 {
436         filter_entry.set_text ("");
437 }
438
439 void
440 PluginSelector::filter_entry_changed ()
441 {
442         refill ();
443 }
444
445 void 
446 PluginSelector::filter_mode_changed ()
447 {
448         std::string mode = filter_mode.get_active_text ();
449
450         if (mode == _("Favorites only")) {
451                 filter_entry.set_sensitive (false);
452         } else {
453                 filter_entry.set_sensitive (true);
454         }
455
456         refill ();
457 }
458
459 void
460 PluginSelector::on_show ()
461 {
462         ArdourDialog::on_show ();
463         filter_entry.grab_focus ();
464 }
465
466 struct PluginMenuCompare {
467     bool operator() (PluginInfoPtr a, PluginInfoPtr b) const {
468             int cmp;
469
470             cmp = strcasecmp (a->creator.c_str(), b->creator.c_str());
471
472             if (cmp < 0) {
473                     return true;
474             } else if (cmp == 0) {
475                     /* same creator ... compare names */
476                     if (strcasecmp (a->name.c_str(), b->name.c_str()) < 0) {
477                             return true;
478                     } 
479             }
480             return false;
481     }
482 };
483
484 Gtk::Menu&
485 PluginSelector::plugin_menu()
486 {
487         using namespace Menu_Helpers;
488
489         typedef std::map<Glib::ustring,Gtk::Menu*> SubmenuMap;
490         SubmenuMap submenu_map;
491
492         if (!_menu) {
493                 _menu = new Menu();
494         } 
495
496         MenuList& items = _menu->items();
497         Menu* favs = new Menu();
498
499         items.clear ();
500         items.push_back (MenuElem (_("Favorites"), *favs));
501         items.push_back (MenuElem (_("Plugin Manager"), mem_fun (*this, &PluginSelector::show_manager)));
502         items.push_back (SeparatorElem ());
503
504         PluginInfoList all_plugs;
505
506         all_plugs.insert (all_plugs.end(), manager->ladspa_plugin_info().begin(), manager->ladspa_plugin_info().end());
507 #ifdef VST_SUPPORT
508         all_plugs.insert (all_plugs.end(), manager->vst_plugin_info().begin(), manager->vst_plugin_info().end());
509 #endif
510 #ifdef HAVE_AUDIOUNITS
511         all_plugs.insert (all_plugs.end(), manager->au_plugin_info().begin(), manager->au_plugin_info().end());
512 #endif
513 #ifdef HAVE_SLV2
514         all_plugs.insert (all_plugs.end(), manager->lv2_plugin_info().begin(), manager->lv2_plugin_info().end());
515 #endif
516
517         PluginMenuCompare cmp;
518         all_plugs.sort (cmp);
519
520         for (PluginInfoList::const_iterator i = all_plugs.begin(); i != all_plugs.end(); ++i) {
521                 SubmenuMap::iterator x;
522                 Gtk::Menu* submenu;
523
524                 string creator = (*i)->creator;
525                 string::size_type pos = 0;
526
527                 if (manager->is_a_favorite_plugin (*i)) {
528                         favs->items().push_back (MenuElem ((*i)->name, (bind (mem_fun (*this, &PluginSelector::plugin_chosen_from_menu), *i))));
529                 }
530                 
531                 /* stupid LADSPA creator strings */
532                 
533                 while (pos < creator.length() && (isalnum (creator[pos]) || isspace (creator[pos]))) ++pos;
534                 creator = creator.substr (0, pos);
535
536                 if ((x = submenu_map.find (creator)) != submenu_map.end()) {
537                         submenu = x->second;
538                 } else {
539                         submenu = new Gtk::Menu;
540                         items.push_back (MenuElem (creator, *submenu));
541                         submenu_map.insert (pair<Glib::ustring,Menu*> (creator, submenu));
542                 }
543                 
544                 submenu->items().push_back (MenuElem ((*i)->name, (bind (mem_fun (*this, &PluginSelector::plugin_chosen_from_menu), *i))));
545         }
546         
547         return *_menu;
548 }
549
550 void
551 PluginSelector::plugin_chosen_from_menu (const PluginInfoPtr& pi)
552 {
553         PluginPtr p = load_plugin (pi);
554
555         if (p && interested_object) {
556                 SelectedPlugins plugins;
557                 plugins.push_back (p);
558                 interested_object->use_plugins (plugins);
559         }
560
561         interested_object = 0;
562 }
563
564 void 
565 PluginSelector::favorite_changed (const Glib::ustring& path)
566 {
567         PluginInfoPtr pi;
568
569         if (in_row_change) {
570                 return;
571         }
572
573         in_row_change = true;
574         
575         TreeModel::iterator iter = plugin_model->get_iter (path);
576         
577         if (iter) {
578
579                 bool favorite = !(*iter)[plugin_columns.favorite];
580
581                 /* change state */
582
583                 (*iter)[plugin_columns.favorite] = favorite;
584
585                 /* save new favorites list */
586
587                 pi = (*iter)[plugin_columns.plugin];
588                 
589                 if (favorite) {
590                         manager->add_favorite (pi->type, pi->unique_id);
591                 } else {
592                         manager->remove_favorite (pi->type, pi->unique_id);
593                 }
594                 
595                 manager->save_favorites ();
596         }
597         in_row_change = false;
598 }
599
600 void
601 PluginSelector::show_manager ()
602 {
603         show_all();
604         run ();
605 }
606
607 void
608 PluginSelector::set_interested_object (PluginInterestedObject& obj)
609 {
610         interested_object = &obj;
611 }