Fix another crash at exit:
[ardour.git] / gtk2_ardour / keyeditor.cc
index 37d923a18fda43d9ff3fb04af869e7db2dbba49c..aea8185f3bc5758456fbbadf5baf37ba816e5fbc 100644 (file)
 #endif
 
 #include <map>
+#include <fstream>
+#include <sstream>
+
+#include <boost/algorithm/string.hpp>
+
+#include <glib.h>
+#include <glib/gstdio.h>
 
 #include <gtkmm/stock.h>
 #include <gtkmm/label.h>
@@ -32,6 +39,8 @@
 #include "gtkmm2ext/bindings.h"
 #include "gtkmm2ext/utils.h"
 
+#include "pbd/error.h"
+#include "pbd/openuri.h"
 #include "pbd/strsplit.h"
 
 #include "ardour/filesystem_paths.h"
@@ -41,7 +50,7 @@
 #include "keyboard.h"
 #include "keyeditor.h"
 
-#include "i18n.h"
+#include "pbd/i18n.h"
 
 using namespace std;
 using namespace Gtk;
@@ -51,6 +60,8 @@ 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);
@@ -66,16 +77,24 @@ 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)
 {
-       last_keyval = 0;
 
        notebook.signal_switch_page ().connect (sigc::mem_fun (*this, &KeyEditor::page_change));
 
        vpacker.pack_start (notebook, true, true);
 
-       Label* hint = manage (new Label (_("Select an action, then press the key(s) to (re)set its shortcut")));
+       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 (_("To remove a shortcut select an action then press this: ")));
        hint->show ();
        unbind_box.set_spacing (6);
        unbind_box.pack_start (*hint, false, true);
@@ -89,27 +108,58 @@ KeyEditor::KeyEditor ()
        reset_button.add (reset_label);
        reset_label.set_markup (string_compose ("<span size=\"large\" weight=\"bold\">%1</span>", _("Reset Bindings to Defaults")));
 
+       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);
 
        add (vpacker);
 
        unbind_button.set_sensitive (false);
+       _refresh_connection = UpdateBindings.connect (sigc::mem_fun (*this, &KeyEditor::refresh));
 }
 
 void
 KeyEditor::add_tab (string const & name, Bindings& bindings)
 {
        Tab* t = new Tab (*this, name, &bindings);
-       t->populate ();
+
+       if (t->populate () == 0) {
+               /* no bindings */
+               delete t;
+               return;
+       }
+
+       tabs.push_back (t);
        t->show_all ();
        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;
+                       }
+               }
+       }
+       cerr << "Removed " << name << endl;
+}
+
 void
 KeyEditor::unbind ()
 {
@@ -124,8 +174,12 @@ KeyEditor::page_change (GtkNotebookPage*, guint)
 }
 
 bool
-KeyEditor::on_key_press_event (GdkEventKey* ev)
+KeyEditor::Tab::key_press_event (GdkEventKey* ev)
 {
+       if (view.get_selection()->count_selected_rows() != 1) {
+               return false;
+       }
+
        if (!ev->is_modifier) {
                last_keyval = ev->keyval;
        }
@@ -139,13 +193,17 @@ KeyEditor::on_key_press_event (GdkEventKey* ev)
 }
 
 bool
-KeyEditor::on_key_release_event (GdkEventKey* ev)
+KeyEditor::Tab::key_release_event (GdkEventKey* ev)
 {
+       if (view.get_selection()->count_selected_rows() != 1) {
+               return false;
+       }
+
        if (last_keyval == 0) {
                return false;
        }
 
-       current_tab()->bind (ev, last_keyval);
+       owner.current_tab()->bind (ev, last_keyval);
 
        last_keyval = 0;
        return true;
@@ -155,10 +213,17 @@ 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));
+
+       sorted_filter = TreeModelSort::create(filter);
 
-       view.set_model (model);
+       view.set_model (sorted_filter);
        view.append_column (_("Action"), columns.name);
        view.append_column (_("Shortcut"), columns.binding);
        view.set_headers_visible (true);
@@ -170,12 +235,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.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);
-       model->set_sort_column (owner.sort_column,  owner.sort_type);
-       model->signal_sort_column_changed().connect (sigc::mem_fun (*this, &Tab::sort_column_changed));
+       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));
 
@@ -194,57 +261,54 @@ KeyEditor::Tab::action_selected ()
                return;
        }
 
-       TreeModel::iterator i = view.get_selection()->get_selected();
+       TreeModel::const_iterator it = view.get_selection()->get_selected();
 
-       owner.unbind_button.set_sensitive (false);
-
-       if (i != model->children().end()) {
-
-               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()) {
-               if (!(*i)[columns.bindable]) {
-                       return;
-               }
+       if (!it || !(*it)[columns.bindable]) {
+               return;
+       }
 
-               const std::string& action_path = (*i)[columns.path];
+       bindings->remove (Gtkmm2ext::Bindings::Press,  action_path , true);
+       (*it)[columns.binding] = string ();
 
-               bindings->remove (Gtkmm2ext::Bindings::Press,  action_path , 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()) {
-               return;
-       }
+       /* 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_path = (*i)[columns.path];
+       pressed_key = gdk_keyval_to_lower (pressed_key);
 
-       if (!(*i)[columns.bindable]) {
+       if (!it || !(*it)[columns.bindable]) {
                return;
        }
 
@@ -259,12 +323,12 @@ KeyEditor::Tab::bind (GdkEventKey* release_event, guint pressed_key)
        bool result = bindings->replace (new_binding, Gtkmm2ext::Bindings::Press, action_path);
 
        if (result) {
-               (*i)[columns.binding] = gtk_accelerator_get_label (new_binding.key(), (GdkModifierType) new_binding.state());
+               (*it)[columns.binding] = gtk_accelerator_get_label (new_binding.key(), (GdkModifierType) new_binding.state());
                owner.unbind_button.set_sensitive (true);
        }
 }
 
-void
+uint32_t
 KeyEditor::Tab::populate ()
 {
        vector<string> paths;
@@ -284,7 +348,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) {
 
@@ -313,7 +377,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;
@@ -324,13 +388,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()));
 
                }
 
@@ -354,6 +418,8 @@ KeyEditor::Tab::populate ()
                }
                row[columns.action] = *a;
        }
+
+       return data_model->children().size();
 }
 
 void
@@ -361,7 +427,7 @@ KeyEditor::Tab::sort_column_changed ()
 {
        int column;
        SortType type;
-       if (model->get_sort_column_id (column, type)) {
+       if (data_model->get_sort_column_id (column, type)) {
                owner.sort_column = column;
                owner.sort_type = type;
        }
@@ -370,14 +436,71 @@ KeyEditor::Tab::sort_column_changed ()
 void
 KeyEditor::Tab::tab_mapped ()
 {
-       model->set_sort_column (owner.sort_column,  owner.sort_type);
+       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 ();
@@ -389,3 +512,61 @@ 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));
+}