enough with umpteen "i18n.h" files. Consolidate on pbd/i18n.h
[ardour.git] / gtk2_ardour / keyeditor.cc
index 055a9bcae06cb806a4f4a039e617f7f04fa40094..b19b6d5fc009611b321026bac56e61bf0d1aecfe 100644 (file)
 #endif
 
 #include <map>
+#include <fstream>
+#include <sstream>
 
-#include "ardour/profile.h"
+#include <boost/algorithm/string.hpp>
+
+#include <glib.h>
+#include <glib/gstdio.h>
 
 #include <gtkmm/stock.h>
 #include <gtkmm/label.h>
 #include <gtkmm/accelmap.h>
 #include <gtkmm/uimanager.h>
 
+#include "gtkmm2ext/bindings.h"
 #include "gtkmm2ext/utils.h"
 
+#include "pbd/error.h"
+#include "pbd/openuri.h"
 #include "pbd/strsplit.h"
-#include "pbd/replace_all.h"
 
+#include "ardour/filesystem_paths.h"
 #include "ardour/profile.h"
 
 #include "actions.h"
 #include "keyboard.h"
 #include "keyeditor.h"
-#include "utils.h"
 
-#include "i18n.h"
+#include "pbd/i18n.h"
 
 using namespace std;
 using namespace Gtk;
@@ -51,247 +58,515 @@ using namespace Gdk;
 using namespace PBD;
 
 using Gtkmm2ext::Keyboard;
+using Gtkmm2ext::Bindings;
+
+sigc::signal<void> KeyEditor::UpdateBindings;
+
+void bindings_collision_dialog (Gtk::Window& parent)
+{
+       ArdourDialog dialog (parent, _("Colliding keybindings"), true);
+       Label label (_("The key sequence is already bound. Please remove the other binding first."));
+
+       dialog.get_vbox()->pack_start (label, true, true);
+       dialog.add_button (_("Ok"), Gtk::RESPONSE_ACCEPT);
+       dialog.show_all ();
+       dialog.run();
+}
 
 KeyEditor::KeyEditor ()
        : ArdourWindow (_("Key Bindings"))
        , unbind_button (_("Remove shortcut"))
        , unbind_box (BUTTONBOX_END)
-
+       , filter_entry (_("Search..."), true)
+       , filter_string("")
+       , print_button (_("Print"))
+       , sort_column(0)
+       , sort_type(Gtk::SORT_ASCENDING)
 {
-       can_bind = false;
-       last_state = 0;
-
-       model = TreeStore::create(columns);
 
-       view.set_model (model);
-       view.append_column (_("Action"), columns.action);
-       view.append_column (_("Shortcut"), columns.binding);
-       view.set_headers_visible (true);
-       view.get_selection()->set_mode (SELECTION_SINGLE);
-       view.set_reorderable (false);
-       view.set_size_request (500,300);
-       view.set_enable_search (false);
-       view.set_rules_hint (true);
-       view.set_name (X_("KeyEditorTree"));
+       notebook.signal_switch_page ().connect (sigc::mem_fun (*this, &KeyEditor::page_change));
 
-       view.get_selection()->signal_changed().connect (sigc::mem_fun (*this, &KeyEditor::action_selected));
+       vpacker.pack_start (notebook, true, true);
 
-       scroller.add (view);
-       scroller.set_policy (Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
+       Glib::RefPtr<Gdk::Pixbuf> icon = ARDOUR_UI_UTILS::get_icon ("search");
+       filter_entry.set_icon_from_pixbuf (icon);
+       filter_entry.set_icon_tooltip_text (_("Click to reset search string"));
+       filter_entry.signal_search_string_updated ().connect (sigc::mem_fun (*this, &KeyEditor::search_string_updated));
+       vpacker.pack_start (filter_entry, false, false);
 
-       add (scroller);
+       Label* hint = manage (new Label (_("To remove a shortcut select an action then press this: ")));
+       hint->show ();
+       unbind_box.set_spacing (6);
+       unbind_box.pack_start (*hint, false, true);
+       unbind_box.pack_start (unbind_button, false, false);
+       unbind_button.signal_clicked().connect (sigc::mem_fun (*this, &KeyEditor::unbind));
 
-       if (!ARDOUR::Profile->get_sae()) {
+       vpacker.pack_start (unbind_box, false, false);
+       unbind_box.show ();
+       unbind_button.show ();
 
-               Label* hint = manage (new Label (_("Select an action, then press the key(s) to (re)set its shortcut")));
-               hint->show ();
-               unbind_box.set_spacing (6);
-               unbind_box.pack_start (*hint, false, true);
-               unbind_box.pack_start (unbind_button, false, false);
-               unbind_button.signal_clicked().connect (sigc::mem_fun (*this, &KeyEditor::unbind));
+       reset_button.add (reset_label);
+       reset_label.set_markup (string_compose ("<span size=\"large\" weight=\"bold\">%1</span>", _("Reset Bindings to Defaults")));
 
-               add (unbind_box);
-               unbind_box.show ();
-               unbind_button.show ();
+       print_button.signal_clicked().connect (sigc::mem_fun (*this, &KeyEditor::print));
 
-       }
+       reset_box.pack_start (reset_button, true, false);
+       reset_box.pack_start (print_button, true, false);
+       reset_box.show ();
+       reset_button.show ();
+       reset_label.show ();
+       print_button.show ();
+       reset_button.signal_clicked().connect (sigc::mem_fun (*this, &KeyEditor::reset));
+       vpacker.pack_start (reset_box, false, false);
 
-       view.show ();
-       scroller.show ();
+       add (vpacker);
 
        unbind_button.set_sensitive (false);
+       UpdateBindings.connect (sigc::mem_fun (*this, &KeyEditor::refresh));
 }
 
 void
-KeyEditor::unbind ()
+KeyEditor::add_tab (string const & name, Bindings& bindings)
 {
-       TreeModel::iterator i = view.get_selection()->get_selected();
+       Tab* t = new Tab (*this, name, &bindings);
 
-       unbind_button.set_sensitive (false);
+       if (t->populate () == 0) {
+               /* no bindings */
+               delete t;
+               return;
+       }
 
-       if (i != model->children().end()) {
-               string path = (*i)[columns.path];
+       tabs.push_back (t);
+       t->show_all ();
+       notebook.append_page (*t, name);
+}
 
-               if (!(*i)[columns.bindable]) {
-                       return;
-               }
 
-               bool result = AccelMap::change_entry (path,
-                                                     0,
-                                                     (ModifierType) 0,
-                                                     true);
-               if (result) {
-                       (*i)[columns.binding] = string ();
+void
+KeyEditor::remove_tab (string const &name)
+{
+       guint npages = notebook.get_n_pages ();
+
+       for (guint n = 0; n < npages; ++n) {
+               Widget* w = notebook.get_nth_page (n);
+               Tab* tab = dynamic_cast<Tab*> (w);
+               if (tab) {
+                       if (tab->name == name) {
+                               notebook.remove_page (*w);
+                               return;
+                       }
                }
        }
+       cerr << "Removed " << name << endl;
 }
 
 void
-KeyEditor::on_show ()
+KeyEditor::unbind ()
 {
-       populate ();
-       view.get_selection()->unselect_all ();
-       ArdourWindow::on_show ();
+       current_tab()->unbind ();
 }
 
 void
-KeyEditor::on_unmap ()
+KeyEditor::page_change (GtkNotebookPage*, guint)
 {
-       ArdourWindow::on_unmap ();
+       current_tab()->view.get_selection()->unselect_all ();
+       unbind_button.set_sensitive (false);
 }
 
-void
-KeyEditor::action_selected ()
+bool
+KeyEditor::Tab::key_press_event (GdkEventKey* ev)
 {
-       if (view.get_selection()->count_selected_rows() == 0) {
-               return;
+       if (view.get_selection()->count_selected_rows() != 1) {
+               return false;
        }
 
-       TreeModel::iterator i = view.get_selection()->get_selected();
+       if (!ev->is_modifier) {
+               last_keyval = ev->keyval;
+       }
 
-       unbind_button.set_sensitive (false);
+       /* Don't let anything else handle the key press, because navigation
+        * keys will be used by GTK to change the selection/treeview cursor
+        * position
+        */
 
-       if (i != model->children().end()) {
+       return true;
+}
 
-               string path = (*i)[columns.path];
+bool
+KeyEditor::Tab::key_release_event (GdkEventKey* ev)
+{
+       if (view.get_selection()->count_selected_rows() != 1) {
+               return false;
+       }
 
-               if (!(*i)[columns.bindable]) {
-                       return;
-               }
+       if (last_keyval == 0) {
+               return false;
+       }
 
-               string binding = (*i)[columns.binding];
+       owner.current_tab()->bind (ev, last_keyval);
 
-               if (!binding.empty()) {
-                       unbind_button.set_sensitive (true);
-               }
-       }
+       last_keyval = 0;
+       return true;
 }
 
-bool
-KeyEditor::on_key_press_event (GdkEventKey* ev)
+KeyEditor::Tab::Tab (KeyEditor& ke, string const & str, Bindings* b)
+       : owner (ke)
+       , name (str)
+       , bindings (b)
+       , last_keyval (0)
 {
-       can_bind = true;
-       last_state = ev->state;
-       return false;
+       data_model = TreeStore::create(columns);
+       populate ();
+
+       filter = TreeModelFilter::create(data_model);
+       filter->set_visible_func (sigc::mem_fun (*this, &Tab::visible_func));
+
+       sorted_filter = TreeModelSort::create(filter);
+
+       view.set_model (sorted_filter);
+       view.append_column (_("Action"), columns.name);
+       view.append_column (_("Shortcut"), columns.binding);
+       view.set_headers_visible (true);
+       view.set_headers_clickable (true);
+       view.get_selection()->set_mode (SELECTION_SINGLE);
+       view.set_reorderable (false);
+       view.set_size_request (500,300);
+       view.set_enable_search (false);
+       view.set_rules_hint (true);
+       view.set_name (X_("KeyEditorTree"));
+
+       view.signal_cursor_changed().connect (sigc::mem_fun (*this, &Tab::action_selected));
+       view.signal_key_press_event().connect (sigc::mem_fun (*this, &Tab::key_press_event), false);
+       view.signal_key_release_event().connect (sigc::mem_fun (*this, &Tab::key_release_event), false);
+
+       view.get_column(0)->set_sort_column (columns.name);
+       view.get_column(1)->set_sort_column (columns.binding);
+       data_model->set_sort_column (owner.sort_column,  owner.sort_type);
+       data_model->signal_sort_column_changed().connect (sigc::mem_fun (*this, &Tab::sort_column_changed));
+
+       signal_map().connect (sigc::mem_fun (*this, &Tab::tab_mapped));
+
+       scroller.add (view);
+       scroller.set_policy (Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
+
+       set_spacing (6);
+       set_border_width (12);
+       pack_start (scroller);
 }
 
-bool
-KeyEditor::on_key_release_event (GdkEventKey* ev)
+void
+KeyEditor::Tab::action_selected ()
 {
-       if (ARDOUR::Profile->get_sae() || !can_bind || ev->state != last_state) {
-               return false;
+       if (view.get_selection()->count_selected_rows() == 0) {
+               return;
        }
 
-       TreeModel::iterator i = view.get_selection()->get_selected();
+       TreeModel::const_iterator it = view.get_selection()->get_selected();
 
-       if (i != model->children().end()) {
-               string path = (*i)[columns.path];
+       if (!it) {
+               return;
+       }
 
-               if (!(*i)[columns.bindable]) {
-                       goto out;
-               }
+       if (!(*it)[columns.bindable]) {
+               owner.unbind_button.set_sensitive (false);
+               return;
+       }
 
-                Gtkmm2ext::possibly_translate_keyval_to_make_legal_accelerator (ev->keyval);
+       const string& binding = (*it)[columns.binding];
 
+       if (!binding.empty()) {
+               owner.unbind_button.set_sensitive (true);
+       }
+}
 
-               bool result = AccelMap::change_entry (path,
-                                                     ev->keyval,
-                                                     ModifierType (Keyboard::RelevantModifierKeyMask & ev->state),
-                                                     true);
+void
+KeyEditor::Tab::unbind ()
+{
+       const std::string& action_path = (*view.get_selection()->get_selected())[columns.path];
 
-               if (result) {
-                       AccelKey key;
-                       (*i)[columns.binding] = ActionManager::get_key_representation (path, key);
-               }
+       TreeModel::iterator it = find_action_path (data_model->children().begin(), data_model->children().end(),  action_path);
+
+       if (!it || !(*it)[columns.bindable]) {
+               return;
        }
 
-  out:
-       can_bind = false;
-       return true;
+       bindings->remove (Gtkmm2ext::Bindings::Press,  action_path , true);
+       (*it)[columns.binding] = string ();
+
+       owner.unbind_button.set_sensitive (false);
 }
 
 void
-KeyEditor::populate ()
+KeyEditor::Tab::bind (GdkEventKey* release_event, guint pressed_key)
+{
+       const std::string& action_path = (*view.get_selection()->get_selected())[columns.path];
+       TreeModel::iterator it = find_action_path (data_model->children().begin(), data_model->children().end(),  action_path);
+
+       /* pressed key could be upper case if Shift was used. We want all
+          single keys stored as their lower-case version, so ensure this
+       */
+
+       pressed_key = gdk_keyval_to_lower (pressed_key);
+
+       if (!it || !(*it)[columns.bindable]) {
+               return;
+       }
+
+       GdkModifierType mod = (GdkModifierType)(Keyboard::RelevantModifierKeyMask & release_event->state);
+       Gtkmm2ext::KeyboardKey new_binding (mod, pressed_key);
+
+       if (bindings->is_bound (new_binding, Gtkmm2ext::Bindings::Press)) {
+               bindings_collision_dialog (owner);
+               return;
+       }
+
+       bool result = bindings->replace (new_binding, Gtkmm2ext::Bindings::Press, action_path);
+
+       if (result) {
+               (*it)[columns.binding] = gtk_accelerator_get_label (new_binding.key(), (GdkModifierType) new_binding.state());
+               owner.unbind_button.set_sensitive (true);
+       }
+}
+
+uint32_t
+KeyEditor::Tab::populate ()
 {
        vector<string> paths;
        vector<string> labels;
        vector<string> tooltips;
        vector<string> keys;
-       vector<AccelKey> bindings;
+       vector<Glib::RefPtr<Action> > actions;
        typedef std::map<string,TreeIter> NodeMap;
        NodeMap nodes;
        NodeMap::iterator r;
 
-       ActionManager::get_all_actions (labels, paths, tooltips, keys, bindings);
+       bindings->get_all_actions (paths, labels, tooltips, keys, actions);
 
        vector<string>::iterator k;
        vector<string>::iterator p;
        vector<string>::iterator t;
        vector<string>::iterator l;
+       vector<Glib::RefPtr<Action> >::iterator a;
 
-       model->clear ();
+       data_model->clear ();
 
-       for (l = labels.begin(), k = keys.begin(), p = paths.begin(), t = tooltips.begin(); l != labels.end(); ++k, ++p, ++t, ++l) {
+       for (a = actions.begin(), l = labels.begin(), k = keys.begin(), p = paths.begin(), t = tooltips.begin(); l != labels.end(); ++k, ++p, ++t, ++l, ++a) {
 
                TreeModel::Row row;
                vector<string> parts;
 
-               parts.clear ();
-
                split (*p, parts, '/');
 
-               if (parts.empty()) {
+               string category = parts[1];
+               string action_name = parts[2];
+
+               if (action_name.empty()) {
                        continue;
                }
 
                //kinda kludgy way to avoid displaying menu items as mappable
-               if ( parts[1] == _("Main_menu") )
-                       continue;
-               if ( parts[1] == _("JACK") )
-                       continue;
-               if ( parts[1] == _("redirectmenu") )
-                       continue;
-               if ( parts[1] == _("Editor_menus") )
-                       continue;
-               if ( parts[1] == _("RegionList") )
-                       continue;
-               if ( parts[1] == _("ProcessorMenu") )
+               if ((action_name.find ("Menu") == action_name.length() - 4) ||
+                   (action_name.find ("menu") == action_name.length() - 4) ||
+                   (action_name == _("RegionList"))) {
                        continue;
+               }
 
-               if ((r = nodes.find (parts[1])) == nodes.end()) {
+               if ((r = nodes.find (category)) == nodes.end()) {
 
-                       /* top level is missing */
+                       /* category/group is missing, so add it first */
 
                        TreeIter rowp;
                        TreeModel::Row parent;
-                       rowp = model->append();
-                       nodes[parts[1]] = rowp;
+                       rowp = data_model->append();
+                       nodes[category] = rowp;
                        parent = *(rowp);
-                       parent[columns.action] = parts[1];
+                       parent[columns.name] = category;
                        parent[columns.bindable] = false;
+                       parent[columns.action] = *a;
 
-                       row = *(model->append (parent.children()));
+                       /* now set up the child row that we're about to fill
+                        * out with information
+                        */
+
+                       row = *(data_model->append (parent.children()));
 
                } else {
 
-                       row = *(model->append ((*r->second)->children()));
+                       /* category/group is present, so just add the child row */
+
+                       row = *(data_model->append ((*r->second)->children()));
 
                }
 
                /* add this action */
 
+               /* use the "visible label" as the action name */
+
                if (l->empty ()) {
-                       row[columns.action] = *t;
+                       /* no label, try using the tooltip instead */
+                       row[columns.name] = *t;
                } else {
-                       row[columns.action] = *l;
+                       row[columns.name] = *l;
                }
-               row[columns.path] = (*p);
+               row[columns.path] = string_compose ("%1/%2", category, action_name);
                row[columns.bindable] = true;
 
                if (*k == ActionManager::unbound_string) {
                        row[columns.binding] = string();
                } else {
-                       row[columns.binding] = (*k);
+                       row[columns.binding] = *k;
+               }
+               row[columns.action] = *a;
+       }
+
+       return data_model->children().size();
+}
+
+void
+KeyEditor::Tab::sort_column_changed ()
+{
+       int column;
+       SortType type;
+       if (data_model->get_sort_column_id (column, type)) {
+               owner.sort_column = column;
+               owner.sort_type = type;
+       }
+}
+
+void
+KeyEditor::Tab::tab_mapped ()
+{
+       data_model->set_sort_column (owner.sort_column,  owner.sort_type);
+       filter->refilter ();
+}
+
+bool
+KeyEditor::Tab::visible_func(const Gtk::TreeModel::const_iterator& iter) const
+{
+       if (!iter) {
+               return false;
+       }
+
+       // never filter when search string is empty or item is a category
+       if (owner.filter_string.empty () || !(*iter)[columns.bindable]) {
+               return true;
+       }
+
+       // search name
+       std::string name = (*iter)[columns.name];
+       boost::to_lower (name);
+       if (name.find (owner.filter_string) != std::string::npos) {
+               return true;
+       }
+
+       // search binding
+       std::string binding = (*iter)[columns.binding];
+       boost::to_lower (binding);
+       if (binding.find (owner.filter_string) != std::string::npos) {
+               return true;
+       }
+
+       return false;
+}
+
+TreeModel::iterator
+KeyEditor::Tab::find_action_path (TreeModel::const_iterator begin, TreeModel::const_iterator end, const std::string& action_path) const
+{
+       if (!begin) {
+               return end;
+       }
+
+       for (TreeModel::iterator it = begin; it != end; ++it) {
+               if (it->children()) {
+                       TreeModel::iterator jt = find_action_path (it->children().begin(), it->children().end(), action_path);
+                       if (jt != it->children().end()) {
+                               return jt;
+                       }
+               }
+               const std::string& path = (*it)[columns.path];
+               if (action_path.compare(path) == 0) {
+                       return it;
                }
        }
+       return end;
+}
+
+void
+KeyEditor::reset ()
+{
+       Keyboard::the_keyboard().reset_bindings ();
+       refresh ();
+}
+
+void
+KeyEditor::refresh ()
+{
+       for (Tabs::iterator t = tabs.begin(); t != tabs.end(); ++t) {
+               (*t)->view.get_selection()->unselect_all ();
+               (*t)->populate ();
+       }
+}
+
+KeyEditor::Tab*
+KeyEditor::current_tab ()
+{
+       return dynamic_cast<Tab*> (notebook.get_nth_page (notebook.get_current_page()));
+}
+
+void
+KeyEditor::search_string_updated (const std::string& filter)
+{
+       filter_string = boost::to_lower_copy(filter);
+       KeyEditor::Tab* tab = current_tab ();
+       if (tab) {
+               tab->filter->refilter ();
+       }
+}
+
+void
+KeyEditor::print () const
+{
+       stringstream sstr;
+       Bindings::save_all_bindings_as_html (sstr);
+
+       if (sstr.str().empty()) {
+               return;
+       }
+
+
+       gchar* file_name;
+       GError *err = NULL;
+       gint fd;
+
+       if ((fd = g_file_open_tmp ("akprintXXXXXX.html", &file_name, &err)) < 0) {
+               if (err) {
+                       error << string_compose (_("Could not open temporary file to print bindings (%1)"), err->message) << endmsg;
+                       g_error_free (err);
+               }
+               return;
+       }
+
+#ifdef PLATFORM_WINDOWS
+       ::close (fd);
+#endif
+
+       err = NULL;
+
+       if (!g_file_set_contents (file_name, sstr.str().c_str(), sstr.str().size(), &err)) {
+#ifndef PLATFORM_WINDOWS
+               ::close (fd);
+#endif
+               g_unlink (file_name);
+               if (err) {
+                       error << string_compose (_("Could not save bindings to file (%1)"), err->message) << endmsg;
+                       g_error_free (err);
+               }
+               return;
+       }
+
+#ifndef PLATFORM_WINDOWS
+       ::close (fd);
+#endif
+
+       PBD::open_uri (string_compose ("file:///%1", file_name));
 }