X-Git-Url: https://main.carlh.net/gitweb/?a=blobdiff_plain;f=gtk2_ardour%2Fkeyeditor.cc;h=aea8185f3bc5758456fbbadf5baf37ba816e5fbc;hb=9e981367e2cf27cf0d19914e03f55a8c405c7188;hp=f23e24695888cb937f51fa5c663747c2b382478d;hpb=2a6a16f980ff9181b138f7a30aedfbde4426a591;p=ardour.git diff --git a/gtk2_ardour/keyeditor.cc b/gtk2_ardour/keyeditor.cc index f23e246958..aea8185f3b 100644 --- a/gtk2_ardour/keyeditor.cc +++ b/gtk2_ardour/keyeditor.cc @@ -22,6 +22,13 @@ #endif #include +#include +#include + +#include + +#include +#include #include #include @@ -29,18 +36,21 @@ #include #include +#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" #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; @@ -48,251 +58,515 @@ using namespace Gdk; using namespace PBD; using Gtkmm2ext::Keyboard; +using Gtkmm2ext::Bindings; + +sigc::signal 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")); - view.get_selection()->signal_changed().connect (sigc::mem_fun (*this, &KeyEditor::action_selected)); + notebook.signal_switch_page ().connect (sigc::mem_fun (*this, &KeyEditor::page_change)); - scroller.add (view); - scroller.set_policy (Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC); - - add (vpacker); + vpacker.pack_start (notebook, true, true); - vpacker.set_spacing (6); - vpacker.pack_start (scroller); + Glib::RefPtr 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); - if (!ARDOUR::Profile->get_sae()) { + 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)); - 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)); + vpacker.pack_start (unbind_box, false, false); + unbind_box.show (); + unbind_button.show (); - vpacker.pack_start (unbind_box, false, false); - unbind_box.show (); - unbind_button.show (); + reset_button.add (reset_label); + reset_label.set_markup (string_compose ("%1", _("Reset Bindings to Defaults"))); - } + print_button.signal_clicked().connect (sigc::mem_fun (*this, &KeyEditor::print)); - vpacker.set_border_width (12); + 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 (); - vpacker.show (); + add (vpacker); unbind_button.set_sensitive (false); + _refresh_connection = 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 (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 paths; vector labels; vector tooltips; vector keys; - vector bindings; + vector > actions; typedef std::map 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::iterator k; vector::iterator p; vector::iterator t; vector::iterator l; + vector >::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 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] == _("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; + + /* now set up the child row that we're about to fill + * out with information + */ - row = *(model->append (parent.children())); + 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 (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)); }