f300b4751c45fe76d3d62f293493e23c51dd3449
[ardour.git] / gtk2_ardour / keyeditor.cc
1 /*
2     Copyright (C) 2002 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 #ifdef WAF_BUILD
21 #include "gtk2ardour-config.h"
22 #endif
23
24 #include <map>
25
26 #include <boost/algorithm/string.hpp>
27
28 #include <gtkmm/stock.h>
29 #include <gtkmm/label.h>
30 #include <gtkmm/accelkey.h>
31 #include <gtkmm/accelmap.h>
32 #include <gtkmm/uimanager.h>
33
34 #include "gtkmm2ext/bindings.h"
35 #include "gtkmm2ext/utils.h"
36
37 #include "pbd/strsplit.h"
38
39 #include "ardour/filesystem_paths.h"
40 #include "ardour/profile.h"
41
42 #include "actions.h"
43 #include "keyboard.h"
44 #include "keyeditor.h"
45
46 #include "i18n.h"
47
48 using namespace std;
49 using namespace Gtk;
50 using namespace Gdk;
51 using namespace PBD;
52
53 using Gtkmm2ext::Keyboard;
54 using Gtkmm2ext::Bindings;
55
56 void bindings_collision_dialog (Gtk::Window& parent)
57 {
58         ArdourDialog dialog (parent, _("Colliding keybindings"), true);
59         Label label (_("The key sequence is already bound. Please remove the other binding first."));
60
61         dialog.get_vbox()->pack_start (label, true, true);
62         dialog.add_button (_("Ok"), Gtk::RESPONSE_ACCEPT);
63         dialog.show_all ();
64         dialog.run();
65 }
66
67 KeyEditor::KeyEditor ()
68         : ArdourWindow (_("Key Bindings"))
69         , unbind_button (_("Remove shortcut"))
70         , unbind_box (BUTTONBOX_END)
71         , filter_entry (_("Search..."), true)
72         , filter_string("")
73         , sort_column(0)
74         , sort_type(Gtk::SORT_ASCENDING)
75 {
76
77         notebook.signal_switch_page ().connect (sigc::mem_fun (*this, &KeyEditor::page_change));
78
79         vpacker.pack_start (notebook, true, true);
80
81         Glib::RefPtr<Gdk::Pixbuf> icon = ARDOUR_UI_UTILS::get_icon ("search");
82         filter_entry.set_icon_from_pixbuf (icon);
83         filter_entry.set_icon_tooltip_text (_("Click to reset search string"));
84         filter_entry.signal_search_string_updated ().connect (sigc::mem_fun (*this, &KeyEditor::search_string_updated));
85         vpacker.pack_start (filter_entry, false, false);
86
87         Label* hint = manage (new Label (_("Select an action, then press the key(s) to (re)set its shortcut")));
88         hint->show ();
89         unbind_box.set_spacing (6);
90         unbind_box.pack_start (*hint, false, true);
91         unbind_box.pack_start (unbind_button, false, false);
92         unbind_button.signal_clicked().connect (sigc::mem_fun (*this, &KeyEditor::unbind));
93
94         vpacker.pack_start (unbind_box, false, false);
95         unbind_box.show ();
96         unbind_button.show ();
97
98         reset_button.add (reset_label);
99         reset_label.set_markup (string_compose ("<span size=\"large\" weight=\"bold\">%1</span>", _("Reset Bindings to Defaults")));
100
101         reset_box.pack_start (reset_button, true, false);
102         reset_box.show ();
103         reset_button.show ();
104         reset_label.show ();
105         reset_button.signal_clicked().connect (sigc::mem_fun (*this, &KeyEditor::reset));
106         vpacker.pack_start (reset_box, false, false);
107
108         add (vpacker);
109
110         unbind_button.set_sensitive (false);
111 }
112
113 void
114 KeyEditor::add_tab (string const & name, Bindings& bindings)
115 {
116         Tab* t = new Tab (*this, name, &bindings);
117         t->populate ();
118         t->show_all ();
119         notebook.append_page (*t, name);
120 }
121
122 void
123 KeyEditor::unbind ()
124 {
125         current_tab()->unbind ();
126 }
127
128 void
129 KeyEditor::page_change (GtkNotebookPage*, guint)
130 {
131         current_tab()->view.get_selection()->unselect_all ();
132         unbind_button.set_sensitive (false);
133 }
134
135 bool
136 KeyEditor::Tab::on_key_press_event (GdkEventKey* ev)
137 {
138         if (!ev->is_modifier) {
139                 last_keyval = ev->keyval;
140         }
141
142         /* Don't let anything else handle the key press, because navigation
143          * keys will be used by GTK to change the selection/treeview cursor
144          * position
145          */
146
147         return true;
148 }
149
150 bool
151 KeyEditor::Tab::on_key_release_event (GdkEventKey* ev)
152 {
153         if (last_keyval == 0) {
154                 return false;
155         }
156
157         owner.current_tab()->bind (ev, last_keyval);
158
159         last_keyval = 0;
160         return true;
161 }
162
163 KeyEditor::Tab::Tab (KeyEditor& ke, string const & str, Bindings* b)
164         : owner (ke)
165         , name (str)
166         , bindings (b)
167         , last_keyval (0)
168 {
169         data_model = TreeStore::create(columns);
170         populate ();
171         
172         filter = TreeModelFilter::create(data_model);
173         filter->set_visible_func (sigc::mem_fun (*this, &Tab::visible_func));
174
175         sorted_filter = TreeModelSort::create(filter);
176
177         view.set_model (sorted_filter);
178         view.append_column (_("Action"), columns.name);
179         view.append_column (_("Shortcut"), columns.binding);
180         view.set_headers_visible (true);
181         view.set_headers_clickable (true);
182         view.get_selection()->set_mode (SELECTION_SINGLE);
183         view.set_reorderable (false);
184         view.set_size_request (500,300);
185         view.set_enable_search (false);
186         view.set_rules_hint (true);
187         view.set_name (X_("KeyEditorTree"));
188
189         view.signal_cursor_changed().connect (sigc::mem_fun (*this, &Tab::action_selected));
190
191         view.get_column(0)->set_sort_column (columns.name);
192         view.get_column(1)->set_sort_column (columns.binding);
193         data_model->set_sort_column (owner.sort_column,  owner.sort_type);
194         data_model->signal_sort_column_changed().connect (sigc::mem_fun (*this, &Tab::sort_column_changed));
195
196         signal_map().connect (sigc::mem_fun (*this, &Tab::tab_mapped));
197
198         scroller.add (view);
199         scroller.set_policy (Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);
200
201         set_spacing (6);
202         set_border_width (12);
203         pack_start (scroller);
204 }
205
206 void
207 KeyEditor::Tab::action_selected ()
208 {
209         if (view.get_selection()->count_selected_rows() == 0) {
210                 return;
211         }
212
213         TreeModel::const_iterator it = view.get_selection()->get_selected();
214
215         if (!it) {
216                 return;
217         }
218
219         if (!(*it)[columns.bindable]) {
220                 owner.unbind_button.set_sensitive (false);
221                 return;
222         }
223
224         const string& binding = (*it)[columns.binding];
225
226         if (!binding.empty()) {
227                 owner.unbind_button.set_sensitive (true);
228         }
229 }
230
231 void
232 KeyEditor::Tab::unbind ()
233 {
234         const std::string& action_path = (*view.get_selection()->get_selected())[columns.path];
235
236         TreeModel::iterator it = find_action_path (data_model->children().begin(), data_model->children().end(),  action_path);
237
238         if (!it || !(*it)[columns.bindable]) {
239                 return;
240         }
241
242         bindings->remove (Gtkmm2ext::Bindings::Press,  action_path , true);
243         (*it)[columns.binding] = string ();
244
245         owner.unbind_button.set_sensitive (false);
246 }
247
248 void
249 KeyEditor::Tab::bind (GdkEventKey* release_event, guint pressed_key)
250 {
251         const std::string& action_path = (*view.get_selection()->get_selected())[columns.path];
252         TreeModel::iterator it = find_action_path (data_model->children().begin(), data_model->children().end(),  action_path);
253
254         /* pressed key could be upper case if Shift was used. We want all
255            single keys stored as their lower-case version, so ensure this
256         */
257
258         pressed_key = gdk_keyval_to_lower (pressed_key);
259
260         if (!it || !(*it)[columns.bindable]) {
261                 return;
262         }
263
264         GdkModifierType mod = (GdkModifierType)(Keyboard::RelevantModifierKeyMask & release_event->state);
265         Gtkmm2ext::KeyboardKey new_binding (mod, pressed_key);
266
267         if (bindings->is_bound (new_binding, Gtkmm2ext::Bindings::Press)) {
268                 bindings_collision_dialog (owner);
269                 return;
270         }
271
272         bool result = bindings->replace (new_binding, Gtkmm2ext::Bindings::Press, action_path);
273
274         if (result) {
275                 (*it)[columns.binding] = gtk_accelerator_get_label (new_binding.key(), (GdkModifierType) new_binding.state());
276                 owner.unbind_button.set_sensitive (true);
277         }
278 }
279
280 void
281 KeyEditor::Tab::populate ()
282 {
283         vector<string> paths;
284         vector<string> labels;
285         vector<string> tooltips;
286         vector<string> keys;
287         vector<Glib::RefPtr<Action> > actions;
288         typedef std::map<string,TreeIter> NodeMap;
289         NodeMap nodes;
290         NodeMap::iterator r;
291
292         bindings->get_all_actions (paths, labels, tooltips, keys, actions);
293
294         vector<string>::iterator k;
295         vector<string>::iterator p;
296         vector<string>::iterator t;
297         vector<string>::iterator l;
298         vector<Glib::RefPtr<Action> >::iterator a;
299
300         data_model->clear ();
301
302         for (a = actions.begin(), l = labels.begin(), k = keys.begin(), p = paths.begin(), t = tooltips.begin(); l != labels.end(); ++k, ++p, ++t, ++l, ++a) {
303
304                 TreeModel::Row row;
305                 vector<string> parts;
306
307                 split (*p, parts, '/');
308
309                 string category = parts[1];
310                 string action_name = parts[2];
311
312                 if (action_name.empty()) {
313                         continue;
314                 }
315
316                 //kinda kludgy way to avoid displaying menu items as mappable
317                 if ((action_name.find ("Menu") == action_name.length() - 4) ||
318                     (action_name.find ("menu") == action_name.length() - 4) ||
319                     (action_name == _("RegionList"))) {
320                         continue;
321                 }
322
323                 if ((r = nodes.find (category)) == nodes.end()) {
324
325                         /* category/group is missing, so add it first */
326
327                         TreeIter rowp;
328                         TreeModel::Row parent;
329                         rowp = data_model->append();
330                         nodes[category] = rowp;
331                         parent = *(rowp);
332                         parent[columns.name] = category;
333                         parent[columns.bindable] = false;
334                         parent[columns.action] = *a;
335
336                         /* now set up the child row that we're about to fill
337                          * out with information
338                          */
339
340                         row = *(data_model->append (parent.children()));
341
342                 } else {
343
344                         /* category/group is present, so just add the child row */
345
346                         row = *(data_model->append ((*r->second)->children()));
347
348                 }
349
350                 /* add this action */
351
352                 /* use the "visible label" as the action name */
353
354                 if (l->empty ()) {
355                         /* no label, try using the tooltip instead */
356                         row[columns.name] = *t;
357                 } else {
358                         row[columns.name] = *l;
359                 }
360                 row[columns.path] = string_compose ("%1/%2", category, action_name);
361                 row[columns.bindable] = true;
362
363                 if (*k == ActionManager::unbound_string) {
364                         row[columns.binding] = string();
365                 } else {
366                         row[columns.binding] = *k;
367                 }
368                 row[columns.action] = *a;
369         }
370 }
371
372 void
373 KeyEditor::Tab::sort_column_changed ()
374 {
375         int column;
376         SortType type;
377         if (data_model->get_sort_column_id (column, type)) {
378                 owner.sort_column = column;
379                 owner.sort_type = type;
380         }
381 }
382
383 void
384 KeyEditor::Tab::tab_mapped ()
385 {
386         data_model->set_sort_column (owner.sort_column,  owner.sort_type);
387         filter->refilter ();
388 }
389
390 bool
391 KeyEditor::Tab::visible_func(const Gtk::TreeModel::const_iterator& iter) const
392 {
393         if (!iter) {
394                 return false;
395         }
396
397         // never filter when search string is empty or item is a category
398         if (owner.filter_string.empty () || !(*iter)[columns.bindable]) {
399                 return true;
400         }
401
402         // search name
403         std::string name = (*iter)[columns.name];
404         boost::to_lower (name);
405         if (name.find (owner.filter_string) != std::string::npos) {
406                 return true;
407         }
408
409         // search binding
410         std::string binding = (*iter)[columns.binding];
411         boost::to_lower (binding);
412         if (binding.find (owner.filter_string) != std::string::npos) {
413                 return true;
414         }
415
416         return false;
417 }
418
419 TreeModel::iterator
420 KeyEditor::Tab::find_action_path (TreeModel::const_iterator begin, TreeModel::const_iterator end, const std::string& action_path) const
421 {
422         if (!begin) {
423                 return end;
424         }
425
426         for (TreeModel::iterator it = begin; it != end; ++it) {
427                 if (it->children()) {
428                         TreeModel::iterator jt = find_action_path (it->children().begin(), it->children().end(), action_path);
429                         if (jt != it->children().end()) {
430                                 return jt;
431                         }
432                 }
433                 const std::string& path = (*it)[columns.path];
434                 if (action_path.compare(path) == 0) {
435                         return it;
436                 }
437         }
438         return end;
439 }
440
441 void
442 KeyEditor::reset ()
443 {
444         Keyboard::the_keyboard().reset_bindings ();
445
446         for (Tabs::iterator t = tabs.begin(); t != tabs.end(); ++t) {
447                 (*t)->view.get_selection()->unselect_all ();
448                 (*t)->populate ();
449         }
450 }
451
452 KeyEditor::Tab*
453 KeyEditor::current_tab ()
454 {
455         return dynamic_cast<Tab*> (notebook.get_nth_page (notebook.get_current_page()));
456 }
457
458 void
459 KeyEditor::search_string_updated (const std::string& filter)
460 {
461         filter_string = boost::to_lower_copy(filter);
462         current_tab ()->filter->refilter ();
463 }
464