extend API of key binding editor to allow for tab removal
[ardour.git] / gtk2_ardour / keyeditor.cc
index d8d1f2d59263d75f2b01bbfea492da5d55b1a701..f567bbf7512859c09c7860687e93c8fd87328109 100644 (file)
@@ -23,6 +23,8 @@
 
 #include <map>
 
+#include <boost/algorithm/string.hpp>
+
 #include <gtkmm/stock.h>
 #include <gtkmm/label.h>
 #include <gtkmm/accelkey.h>
@@ -51,18 +53,37 @@ using namespace PBD;
 using Gtkmm2ext::Keyboard;
 using Gtkmm2ext::Bindings;
 
+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("")
+       , sort_column(0)
+       , sort_type(Gtk::SORT_ASCENDING)
 {
-       last_keyval = 0;
 
        notebook.signal_switch_page ().connect (sigc::mem_fun (*this, &KeyEditor::page_change));
 
        vpacker.pack_start (notebook, true, true);
 
+       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);
+
        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);
@@ -98,6 +119,24 @@ KeyEditor::add_tab (string const & name, Bindings& bindings)
        notebook.append_page (*t, name);
 }
 
+
+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;
+                       }
+               }
+       }
+}
+
 void
 KeyEditor::unbind ()
 {
@@ -112,7 +151,7 @@ KeyEditor::page_change (GtkNotebookPage*, guint)
 }
 
 bool
-KeyEditor::on_key_press_event (GdkEventKey* ev)
+KeyEditor::Tab::on_key_press_event (GdkEventKey* ev)
 {
        if (!ev->is_modifier) {
                last_keyval = ev->keyval;
@@ -127,13 +166,13 @@ KeyEditor::on_key_press_event (GdkEventKey* ev)
 }
 
 bool
-KeyEditor::on_key_release_event (GdkEventKey* ev)
+KeyEditor::Tab::on_key_release_event (GdkEventKey* ev)
 {
        if (last_keyval == 0) {
                return false;
        }
 
-       current_tab()->bind (ev, last_keyval);
+       owner.current_tab()->bind (ev, last_keyval);
 
        last_keyval = 0;
        return true;
@@ -143,13 +182,21 @@ KeyEditor::Tab::Tab (KeyEditor& ke, string const & str, Bindings* b)
        : owner (ke)
        , name (str)
        , bindings (b)
+       , last_keyval (0)
 {
-       model = TreeStore::create(columns);
+       data_model = TreeStore::create(columns);
+       populate ();
+       
+       filter = TreeModelFilter::create(data_model);
+       filter->set_visible_func (sigc::mem_fun (*this, &Tab::visible_func));
 
-       view.set_model (model);
+       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);
@@ -157,7 +204,14 @@ KeyEditor::Tab::Tab (KeyEditor& ke, string const & str, Bindings* b)
        view.set_rules_hint (true);
        view.set_name (X_("KeyEditorTree"));
 
-       view.get_selection()->signal_changed().connect (sigc::mem_fun (*this, &Tab::action_selected));
+       view.signal_cursor_changed().connect (sigc::mem_fun (*this, &Tab::action_selected));
+
+       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);
@@ -174,67 +228,70 @@ KeyEditor::Tab::action_selected ()
                return;
        }
 
-       TreeModel::iterator i = view.get_selection()->get_selected();
-
-       owner.unbind_button.set_sensitive (false);
-
-       if (i != model->children().end()) {
+       TreeModel::const_iterator it = view.get_selection()->get_selected();
 
-               string path = (*i)[columns.path];
+       if (!it) {
+               return;
+       }
 
-               if (!(*i)[columns.bindable]) {
-                       return;
-               }
+       if (!(*it)[columns.bindable]) {
+               owner.unbind_button.set_sensitive (false);
+               return;
+       }
 
-               string binding = (*i)[columns.binding];
+       const string& binding = (*it)[columns.binding];
 
-               if (!binding.empty()) {
-                       owner.unbind_button.set_sensitive (true);
-               }
+       if (!binding.empty()) {
+               owner.unbind_button.set_sensitive (true);
        }
 }
 
 void
 KeyEditor::Tab::unbind ()
 {
-       TreeModel::iterator i = view.get_selection()->get_selected();
+       const std::string& action_path = (*view.get_selection()->get_selected())[columns.path];
 
-       owner.unbind_button.set_sensitive (false);
+       TreeModel::iterator it = find_action_path (data_model->children().begin(), data_model->children().end(),  action_path);
 
-       if (i != model->children().end()) {
-               Glib::RefPtr<Action> action = (*i)[columns.action];
+       if (!it || !(*it)[columns.bindable]) {
+               return;
+       }
 
-               if (!(*i)[columns.bindable]) {
-                       return;
-               }
+       bindings->remove (Gtkmm2ext::Bindings::Press,  action_path , true);
+       (*it)[columns.binding] = string ();
 
-               bindings->remove (action, Gtkmm2ext::Bindings::Press, true);
-               (*i)[columns.binding] = string ();
-       }
+       owner.unbind_button.set_sensitive (false);
 }
 
 void
 KeyEditor::Tab::bind (GdkEventKey* release_event, guint pressed_key)
 {
-       TreeModel::iterator i = view.get_selection()->get_selected();
+       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);
 
-       if (i != model->children().end()) {
+       /* pressed key could be upper case if Shift was used. We want all
+          single keys stored as their lower-case version, so ensure this
+       */
 
-               string action_name = (*i)[columns.path];
+       pressed_key = gdk_keyval_to_lower (pressed_key);
 
-               if (!(*i)[columns.bindable]) {
-                       return;
-               }
+       if (!it || !(*it)[columns.bindable]) {
+               return;
+       }
 
-               GdkModifierType mod = (GdkModifierType)(Keyboard::RelevantModifierKeyMask & release_event->state);
-               Gtkmm2ext::KeyboardKey new_binding (mod, pressed_key);
+       GdkModifierType mod = (GdkModifierType)(Keyboard::RelevantModifierKeyMask & release_event->state);
+       Gtkmm2ext::KeyboardKey new_binding (mod, pressed_key);
 
-               bool result = bindings->replace (new_binding, Gtkmm2ext::Bindings::Press, action_name);
+       if (bindings->is_bound (new_binding, Gtkmm2ext::Bindings::Press)) {
+               bindings_collision_dialog (owner);
+               return;
+       }
 
-               if (result) {
-                       (*i)[columns.binding] = gtk_accelerator_get_label (new_binding.key(), (GdkModifierType) new_binding.state());
-                       owner.unbind_button.set_sensitive (true);
-               }
+       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);
        }
 }
 
@@ -258,7 +315,7 @@ KeyEditor::Tab::populate ()
        vector<string>::iterator l;
        vector<Glib::RefPtr<Action> >::iterator a;
 
-       model->clear ();
+       data_model->clear ();
 
        for (a = actions.begin(), l = labels.begin(), k = keys.begin(), p = paths.begin(), t = tooltips.begin(); l != labels.end(); ++k, ++p, ++t, ++l, ++a) {
 
@@ -287,7 +344,7 @@ KeyEditor::Tab::populate ()
 
                        TreeIter rowp;
                        TreeModel::Row parent;
-                       rowp = model->append();
+                       rowp = data_model->append();
                        nodes[category] = rowp;
                        parent = *(rowp);
                        parent[columns.name] = category;
@@ -298,13 +355,13 @@ KeyEditor::Tab::populate ()
                         * out with information
                         */
 
-                       row = *(model->append (parent.children()));
+                       row = *(data_model->append (parent.children()));
 
                } else {
 
                        /* category/group is present, so just add the child row */
 
-                       row = *(model->append ((*r->second)->children()));
+                       row = *(data_model->append ((*r->second)->children()));
 
                }
 
@@ -330,6 +387,75 @@ KeyEditor::Tab::populate ()
        }
 }
 
+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 ()
 {
@@ -346,3 +472,11 @@ 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);
+       current_tab ()->filter->refilter ();
+}
+