Robin Gareus
4050ca5633
Copyright-holder and year information is extracted from git log. git history begins in 2005. So (C) from 1998..2005 is lost. Also some (C) assignment of commits where the committer didn't use --author.
584 lines
14 KiB
C++
584 lines
14 KiB
C++
/*
|
|
* Copyright (C) 2007-2015 David Robillard <d@drobilla.net>
|
|
* Copyright (C) 2007-2019 Paul Davis <paul@linuxaudiosystems.com>
|
|
* Copyright (C) 2009-2011 Carl Hetherington <carl@carlh.net>
|
|
* Copyright (C) 2014-2017 Robin Gareus <robin@gareus.org>
|
|
* Copyright (C) 2017 Ben Loftis <ben@harrisonconsoles.com>
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License along
|
|
* with this program; if not, write to the Free Software Foundation, Inc.,
|
|
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
*/
|
|
|
|
#ifdef WAF_BUILD
|
|
#include "gtk2ardour-config.h"
|
|
#endif
|
|
|
|
#include <map>
|
|
#include <fstream>
|
|
#include <sstream>
|
|
|
|
#include <boost/algorithm/string.hpp>
|
|
|
|
#include <glib.h>
|
|
#include <glib/gstdio.h>
|
|
|
|
#include <gtkmm/accelkey.h>
|
|
#include <gtkmm/accelmap.h>
|
|
#include <gtkmm/label.h>
|
|
#include <gtkmm/separator.h>
|
|
#include <gtkmm/stock.h>
|
|
#include <gtkmm/treemodelsort.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 "ardour/filesystem_paths.h"
|
|
#include "ardour/profile.h"
|
|
|
|
#include "actions.h"
|
|
#include "keyboard.h"
|
|
#include "keyeditor.h"
|
|
|
|
#include "pbd/i18n.h"
|
|
|
|
using namespace std;
|
|
using namespace Gtk;
|
|
using namespace Gdk;
|
|
using namespace PBD;
|
|
|
|
using Gtkmm2ext::Keyboard;
|
|
using Gtkmm2ext::Bindings;
|
|
|
|
sigc::signal<void> KeyEditor::UpdateBindings;
|
|
|
|
static void bindings_collision_dialog (Gtk::Window& parent, const std::string& bound_name)
|
|
{
|
|
ArdourDialog dialog (parent, _("Colliding keybindings"), true);
|
|
Label label (string_compose(
|
|
_("The key sequence is already bound to '%1'. Please remove the other binding first."), bound_name));
|
|
|
|
dialog.get_vbox()->pack_start (label, true, true);
|
|
dialog.add_button (_("Ok"), Gtk::RESPONSE_ACCEPT);
|
|
dialog.show_all ();
|
|
dialog.run();
|
|
}
|
|
|
|
KeyEditor::KeyEditor ()
|
|
: ArdourWindow (_("Keyboard Shortcuts"))
|
|
, unbind_button (_("Remove shortcut"))
|
|
, unbind_box (BUTTONBOX_END)
|
|
, filter_entry (_("Search..."), true)
|
|
, filter_string("")
|
|
, sort_column(0)
|
|
, sort_type(Gtk::SORT_ASCENDING)
|
|
{
|
|
|
|
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 (_("To remove a shortcut, select an action then press this: ")));
|
|
hint->show ();
|
|
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.set_spacing (4);
|
|
vpacker.pack_start (unbind_box, false, false);
|
|
unbind_box.show ();
|
|
unbind_button.show ();
|
|
|
|
reset_button.add (reset_label);
|
|
reset_label.set_markup (string_compose (" <span size=\"large\" weight=\"bold\">%1</span> ", _("Reset Bindings to Defaults")));
|
|
|
|
print_button.add (print_label);
|
|
print_label.set_markup (string_compose (" <span size=\"large\" weight=\"bold\">%1</span> ", _("Print Bindings (to your web browser)")));
|
|
|
|
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 (*(manage (new HSeparator())), false, false, 5);
|
|
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);
|
|
|
|
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 ()
|
|
{
|
|
current_tab()->unbind ();
|
|
}
|
|
|
|
void
|
|
KeyEditor::page_change (GtkNotebookPage*, guint)
|
|
{
|
|
current_tab()->view.get_selection()->unselect_all ();
|
|
unbind_button.set_sensitive (false);
|
|
}
|
|
|
|
bool
|
|
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;
|
|
}
|
|
|
|
/* Don't let anything else handle the key press, because navigation
|
|
* keys will be used by GTK to change the selection/treeview cursor
|
|
* position
|
|
*/
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
KeyEditor::Tab::key_release_event (GdkEventKey* ev)
|
|
{
|
|
if (view.get_selection()->count_selected_rows() != 1) {
|
|
return false;
|
|
}
|
|
|
|
if (last_keyval == 0) {
|
|
return false;
|
|
}
|
|
|
|
owner.current_tab()->bind (ev, last_keyval);
|
|
|
|
last_keyval = 0;
|
|
return true;
|
|
}
|
|
|
|
KeyEditor::Tab::Tab (KeyEditor& ke, string const & str, Bindings* b)
|
|
: owner (ke)
|
|
, name (str)
|
|
, bindings (b)
|
|
, last_keyval (0)
|
|
{
|
|
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);
|
|
}
|
|
|
|
void
|
|
KeyEditor::Tab::action_selected ()
|
|
{
|
|
if (view.get_selection()->count_selected_rows() == 0) {
|
|
return;
|
|
}
|
|
|
|
TreeModel::const_iterator it = view.get_selection()->get_selected();
|
|
|
|
if (!it) {
|
|
return;
|
|
}
|
|
|
|
if (!(*it)[columns.bindable]) {
|
|
owner.unbind_button.set_sensitive (false);
|
|
return;
|
|
}
|
|
|
|
const string& binding = (*it)[columns.binding];
|
|
|
|
if (!binding.empty()) {
|
|
owner.unbind_button.set_sensitive (true);
|
|
}
|
|
}
|
|
|
|
void
|
|
KeyEditor::Tab::unbind ()
|
|
{
|
|
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 (!it || !(*it)[columns.bindable]) {
|
|
return;
|
|
}
|
|
|
|
bindings->remove (Gtkmm2ext::Bindings::Press, action_path , true);
|
|
(*it)[columns.binding] = string ();
|
|
|
|
owner.unbind_button.set_sensitive (false);
|
|
}
|
|
|
|
void
|
|
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, bindings->bound_name (new_binding, Gtkmm2ext::Bindings::Press));
|
|
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<Glib::RefPtr<Action> > actions;
|
|
typedef std::map<string,TreeIter> NodeMap;
|
|
NodeMap nodes;
|
|
NodeMap::iterator r;
|
|
|
|
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;
|
|
|
|
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) {
|
|
|
|
TreeModel::Row row;
|
|
vector<string> parts;
|
|
|
|
split (*p, parts, '/');
|
|
|
|
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 ((action_name.find (X_("Menu")) == action_name.length() - 4) ||
|
|
(action_name.find (X_("menu")) == action_name.length() - 4) ||
|
|
(category.find (X_("Menu")) == category.length() - 4) ||
|
|
(category.find (X_("menu")) == category.length() - 4) ||
|
|
(action_name == _("RegionList"))) {
|
|
continue;
|
|
}
|
|
|
|
if ((r = nodes.find (category)) == nodes.end()) {
|
|
|
|
/* category/group is missing, so add it first */
|
|
|
|
TreeIter rowp;
|
|
TreeModel::Row parent;
|
|
rowp = data_model->append();
|
|
nodes[category] = rowp;
|
|
parent = *(rowp);
|
|
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 = *(data_model->append (parent.children()));
|
|
|
|
} else {
|
|
|
|
/* 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 ()) {
|
|
/* no label, try using the tooltip instead */
|
|
row[columns.name] = *t;
|
|
} else {
|
|
row[columns.name] = *l;
|
|
}
|
|
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.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));
|
|
}
|