Dear all, My previous email was sent by accident, please ignore it.
Please test the attached patch that almost complete the shortcut configuration panel. From a user's point of view, it works like this: 1. lyx loads rc.bind_file, and ~/.lyx/bind/user.bind. user.bind is automatically generated from the preference dialog. 2. user.bind has regular \bind items, as well as \unbind items that nullify corresponding \bind definition in the system bind file (rc.bind_file). 3. shortcutsTW lists all bindings from system bind file (in black). It lists \unbind in user.bind in red, \bind in user.bind in green. 4. If a system binding is removed, it is recorded as \unbind in user.bind, and is marked red. 5. If a user binding is removed, it is removed. If a user unbinding is removed, the system binding is restored (red -> black). 6. Pressing the new button, or double clicking an item will trigger the shortcut edit dialog. Right now, you have to type in lfun and shortcut. Abdel (or Edwin, Peter?), could you please implement KKeySequenceButton? Please refer to my another thread 'Capture keystroke sequence to a QLineEdit.' for links to the KDE implementation. 7. you can search lfun and shortcut using the search button. Only the matched items are displayed. All items are displayed when the search string is cleared. 8. When you apply or save, the shortcuts are immediately used. No need to restart lyx. Please test the above actions, and keep an eye on ~/.lyx/bind/user.bind when you test. Technique details: Several functions are added to KeyMap.h and .cpp, 1. KeyMap::unbind and KeyMap::delkey: remove an binding from a KeyMap 2. KeyMap::hasBinding: test if a binding is already defined 3. KeyMap::clear: clear current keybinding 4. KeyMap::write: write a bind file (that can be read back). 5. KeyMap::listBindings: add a parameter tag, that can be used to tag the list. This is used to separate system bindings, user-defined bindings, and unbind lists. 6. KeyMap::read: an additional parameter unbind_map is added. \unbind will nullify \bind if this parameter is NULL. Otherwise, \unbind will be registered in unbind_map as a regular keybinding. This is needed because bind files are loaded to a single theTopLevelKeyMap regularly, but separately in the preference dialog. KeySequnce: Add print_bind() that print C-S-I instead of Ctrl+Shift+I. This is needed to write to a bind file. PrefShortcut: 1. PrefShortcuts::apply: write user.bind, and reset theTopLevelKeyMap so that new keybindings can be used immediately. 2. PrefShortcuts::update: read rc.bind_file and user.bind into three separate KeyMap: system bind, user bind, user unbind. 3. PrefShortcuts::setItemType: set item type and color. (not tested for qt 4.1) 4. PrefShortcuts::insertShortcutItem: insert an item. For user unbind items, try to locate their system counterpart first. 5. PrefShortcuts::on_removePB_pressed: For system bind, add to user unbind; for user bind and unbind, remove. 6. PrefShortcuts::shortcut_okPB_pressed: add a new item. Validation should better be done before the OK button is clicked, but I do not know how to do it. Enjoy! Bo
Index: src/LyX.cpp =================================================================== --- src/LyX.cpp (revision 21075) +++ src/LyX.cpp (working copy) @@ -967,6 +967,8 @@ pimpl_->toplevel_keymap_.reset(new KeyMap); defaultKeyBindings(pimpl_->toplevel_keymap_.get()); pimpl_->toplevel_keymap_->read(lyxrc.bind_file); + // load user bind file user.bind + pimpl_->toplevel_keymap_->read("user"); pimpl_->lyxfunc_.initKeySequences(pimpl_->toplevel_keymap_.get()); Index: src/KeyMap.cpp =================================================================== --- src/KeyMap.cpp (revision 21075) +++ src/KeyMap.cpp (working copy) @@ -22,12 +22,15 @@ #include "support/filetools.h" +#include <fstream> #include <sstream> #include <utility> using std::endl; +using std::ios; +using std::ofstream; using std::string; -using std::make_pair; +using boost::make_tuple; namespace lyx { @@ -71,26 +74,75 @@ << endl; } - return res; + return res == string::npos ? 0 : res; } +size_t KeyMap::unbind(string const & seq, FuncRequest const & func) +{ + KeySequence k(0, 0); + + string::size_type const res = k.parse(seq); + if (res == string::npos) + delkey(&k, func); + else + LYXERR(Debug::KBMAP) << "Parse error at position " << res + << " in key sequence '" << seq << "'." + << endl; + return res == string::npos ? 0 : res; +} + + +bool KeyMap::hasBinding(KeySequence const & seq, FuncRequest const & func, + unsigned int r) +{ + KeySymbol code = seq.sequence[r]; + if (!code.isOK()) + return false; + + KeyModifier const mod1 = seq.modifiers[r].first; + KeyModifier const mod2 = seq.modifiers[r].second; + + // check if key is already there + Table::iterator end = table.end(); + for (Table::iterator it = table.begin(); it != end; ++it) { + if (code == it->code + && mod1 == it->mod.first + && mod2 == it->mod.second) { + if (r + 1 == seq.length()) + return it->func == func; + else if (it->table.get()) + return it->table->hasBinding(seq, func, r + 1); + } + } + return false; +} + + +void KeyMap::clear() +{ + table.clear(); +} + + namespace { enum BindTags { BN_BIND, - BN_BINDFILE + BN_BINDFILE, + BN_UNBIND, }; keyword_item bindTags[] = { { "\\bind", BN_BIND }, - { "\\bind_file", BN_BINDFILE } + { "\\bind_file", BN_BINDFILE }, + { "\\unbind", BN_UNBIND }, }; } -bool KeyMap::read(string const & bind_file) +bool KeyMap::read(string const & bind_file, KeyMap * unbind_map) { const int bindCount = sizeof(bindTags) / sizeof(keyword_item); @@ -148,10 +200,44 @@ bind(seq, func); break; } + case BN_UNBIND: + { + string seq, cmd; + + if (lexrc.next()) { + seq = lexrc.getString(); + } else { + lexrc.printError("BN_UNBIND: Missing key sequence"); + error = true; + break; + } + + if (lexrc.next(true)) { + cmd = lexrc.getString(); + } else { + lexrc.printError("BN_UNBIND: missing command"); + error = true; + break; + } + + FuncRequest func = lyxaction.lookupFunc(cmd); + if (func. action == LFUN_UNKNOWN_ACTION) { + lexrc.printError("BN_UNBIND: Unknown LyX" + " function `$$Token'"); + error = true; + break; + } + + if (unbind_map) + unbind_map->bind(seq, func); + else + unbind(seq, func); + break; + } case BN_BINDFILE: if (lexrc.next()) { string const tmp(lexrc.getString()); - error |= !read(tmp); + error != !read(tmp, unbind_map); } else { lexrc.printError("BN_BINDFILE: Missing file name"); error = true; @@ -169,6 +255,34 @@ } +void KeyMap::write(string const & bind_file, bool append, bool unbind) const +{ + ofstream os(bind_file.c_str(), + append ? (ios::app | ios:: out): ios::out); + + if (!append) + os << "## This file is automatically generated by lyx\n" + << "## All modifications will be lost\n\n"; + + string tag = unbind ? "\\unbind" : "\\bind"; + BindingList const list = listBindings(false); + BindingList::const_iterator it = list.begin(); + BindingList::const_iterator it_end = list.end(); + for (; it != it_end; ++it) { + kb_action action = it->get<0>().action; + string arg = to_utf8(it->get<0>().argument()); + + os << tag << " \"" + << it->get<1>().print_bind() << "\" \"" + << lyxaction.getActionName(action) + << (arg.empty() ? "" : " ") << arg + << "\"\n"; + } + os << "\n"; + os.close(); +} + + FuncRequest const & KeyMap::lookup(KeySymbol const &key, KeyModifier mod, KeySequence * seq) const { @@ -276,6 +390,44 @@ } +void KeyMap::delkey(KeySequence * seq, FuncRequest const & func, unsigned int r) +{ + KeySymbol code = seq->sequence[r]; + if (!code.isOK()) + return; + + KeyModifier const mod1 = seq->modifiers[r].first; + KeyModifier const mod2 = seq->modifiers[r].second; + + // check if key is already there + Table::iterator end = table.end(); + Table::iterator remove = end; + for (Table::iterator it = table.begin(); it != end; ++it) { + if (code == it->code + && mod1 == it->mod.first + && mod2 == it->mod.second) { + // remove + if (r + 1 == seq->length()) { + if (it->func == func) { + remove = it; + if (it->table.get()) + it->table.reset(); + } + } else if (it->table.get()) { + it->table->delkey(seq, func, r + 1); + if (it->table->empty()) + remove = it; + return; + } + } + } + if (remove != end) { + table.erase(remove); + return; + } +} + + docstring const KeyMap::printbindings(FuncRequest const & func) const { Bindings bindings = findbindings(func); @@ -326,10 +478,10 @@ } -KeyMap::BindingList const KeyMap::listBindings(bool unbound) const +KeyMap::BindingList KeyMap::listBindings(bool unbound, int tag) const { BindingList list; - listBindings(list, KeySequence(0, 0)); + listBindings(list, KeySequence(0, 0), tag); if (unbound) { LyXAction::const_func_iterator fit = lyxaction.func_begin(); LyXAction::const_func_iterator fit_end = lyxaction.func_end(); @@ -339,12 +491,12 @@ BindingList::const_iterator it = list.begin(); BindingList::const_iterator it_end = list.end(); for (; it != it_end; ++it) - if (it->first.action == action) { + if (it->get<0>().action == action) { has_action = true; break; } if (!has_action) - list.push_back(make_pair(action, KeySequence(0, 0))); + list.push_back(make_tuple(action, KeySequence(0, 0), tag)); } } return list; @@ -352,7 +504,7 @@ void KeyMap::listBindings(BindingList & list, - KeySequence const & prefix) const + KeySequence const & prefix, int tag) const { Table::const_iterator it = table.begin(); Table::const_iterator it_end = table.end(); @@ -361,11 +513,11 @@ if (it->table.get()) { KeySequence seq = prefix; seq.addkey(it->code, it->mod.first); - it->table->listBindings(list, seq); + it->table->listBindings(list, seq, tag); } else { KeySequence seq = prefix; seq.addkey(it->code, it->mod.first); - list.push_back(make_pair(it->func, seq)); + list.push_back(make_tuple(it->func, seq, tag)); } } } Index: src/frontends/qt4/GuiPrefs.cpp =================================================================== --- src/frontends/qt4/GuiPrefs.cpp (revision 21075) +++ src/frontends/qt4/GuiPrefs.cpp (working copy) @@ -31,8 +31,12 @@ #include "paper.h" #include "Session.h" +#include "support/FileName.h" +#include "support/filetools.h" #include "support/lstrings.h" +#include "support/lyxlib.h" #include "support/os.h" +#include "support/Package.h" #include "support/FileFilterList.h" #include "frontend_helpers.h" @@ -75,7 +79,12 @@ using support::os::external_path_list; using support::os::internal_path; using support::os::internal_path_list; +using support::FileName; using support::FileFilterList; +using support::addPath; +using support::addName; +using support::mkdir; +using support::package; ///////////////////////////////////////////////////////////////////// @@ -1700,123 +1709,222 @@ this, SLOT(select_bind())); connect(bindFileED, SIGNAL(textChanged(const QString &)), this, SIGNAL(changed())); + connect(removePB, SIGNAL(clicked()), + this, SIGNAL(changed())); shortcut_ = new GuiShortcutDialog(this); shortcut_bc_.setPolicy(ButtonPolicy::OkCancelPolicy); shortcut_bc_.setOK(shortcut_->okPB); shortcut_bc_.setCancel(shortcut_->cancelPB); + connect(shortcut_->okPB, SIGNAL(clicked()), + shortcut_, SLOT(accept())); connect(shortcut_->okPB, SIGNAL(clicked()), - shortcut_, SLOT(accept())); + this, SIGNAL(changed())); connect(shortcut_->cancelPB, SIGNAL(clicked()), shortcut_, SLOT(reject())); connect(shortcut_->okPB, SIGNAL(clicked()), - shortcut_, SLOT(setShortcut())); + this, SLOT(shortcut_okPB_pressed())); } void PrefShortcuts::apply(LyXRC & rc) const { rc.bind_file = internal_path(fromqstr(bindFileED->text())); + // write user_bind and user_unbind to .lyx/bind/user.bind + string bind_dir = addPath(package().user_support().absFilename(), "bind"); + if (!FileName(bind_dir).exists() && mkdir(FileName(bind_dir), 0777)) { + lyxerr << "LyX could not create the user bind directory '" + << bind_dir << "'. All user-defined key bindings will be lost." << endl; + return; + } + if (!FileName(bind_dir).isDirWritable()) { + lyxerr << "LyX could not write to the user bind directory '" + << bind_dir << "'. All user-defined key bindings will be lost." << endl; + return; + } + FileName user_bind_file = FileName(addName(bind_dir, "user.bind")); + user_bind_.write(user_bind_file.toFilesystemEncoding(), false, false); + user_unbind_.write(user_bind_file.toFilesystemEncoding(), true, true); + // immediately apply the keybindings. Why this is not done before? + // The good thing is that the menus are updated automatically. + theTopLevelKeymap().clear(); + theTopLevelKeymap().read(rc.bind_file); + theTopLevelKeymap().read("user"); } void PrefShortcuts::update(LyXRC const & rc) { bindFileED->setText(toqstr(external_path(rc.bind_file))); + // + system_bind_.clear(); + user_bind_.clear(); + user_unbind_.clear(); + system_bind_.read(rc.bind_file); + // \unbind in user.bind is added to user_unbind_ + user_bind_.read("user", &user_unbind_); + updateShortcutsTW(); +} + + +void PrefShortcuts::updateShortcutsTW() +{ shortcutsTW->clear(); - QTreeWidgetItem * editItem = new QTreeWidgetItem(shortcutsTW); - editItem->setText(0, toqstr("Cursor, Mouse and Editing functions")); - editItem->setFlags(editItem->flags() & ~Qt::ItemIsSelectable); + editItem_ = new QTreeWidgetItem(shortcutsTW); + editItem_->setText(0, toqstr("Cursor, Mouse and Editing functions")); + editItem_->setFlags(editItem_->flags() & ~Qt::ItemIsSelectable); - // first insert a few categories - QTreeWidgetItem * mathItem = new QTreeWidgetItem(shortcutsTW); - mathItem->setText(0, toqstr("Mathematical Symbols")); - mathItem->setFlags(mathItem->flags() & ~Qt::ItemIsSelectable); + mathItem_ = new QTreeWidgetItem(shortcutsTW); + mathItem_->setText(0, toqstr("Mathematical Symbols")); + mathItem_->setFlags(mathItem_->flags() & ~Qt::ItemIsSelectable); - QTreeWidgetItem * bufferItem = new QTreeWidgetItem(shortcutsTW); - bufferItem->setText(0, toqstr("Buffer and Window")); - bufferItem->setFlags(bufferItem->flags() & ~Qt::ItemIsSelectable); + bufferItem_ = new QTreeWidgetItem(shortcutsTW); + bufferItem_->setText(0, toqstr("Buffer and Window")); + bufferItem_->setFlags(bufferItem_->flags() & ~Qt::ItemIsSelectable); - QTreeWidgetItem * layoutItem = new QTreeWidgetItem(shortcutsTW); - layoutItem->setText(0, toqstr("Font, Layouts and Textclasses")); - layoutItem->setFlags(layoutItem->flags() & ~Qt::ItemIsSelectable); + layoutItem_ = new QTreeWidgetItem(shortcutsTW); + layoutItem_->setText(0, toqstr("Font, Layouts and Textclasses")); + layoutItem_->setFlags(layoutItem_->flags() & ~Qt::ItemIsSelectable); - QTreeWidgetItem * systemItem = new QTreeWidgetItem(shortcutsTW); - systemItem->setText(0, toqstr("System and Miscellaneous")); - systemItem->setFlags(systemItem->flags() & ~Qt::ItemIsSelectable); + systemItem_ = new QTreeWidgetItem(shortcutsTW); + systemItem_->setText(0, toqstr("System and Miscellaneous")); + systemItem_->setFlags(systemItem_->flags() & ~Qt::ItemIsSelectable); // listBindings(unbound=true) lists all bound and unbound lfuns - KeyMap::BindingList const bindinglist = theTopLevelKeymap().listBindings(true); + // Items in this list is tagged by its source. + KeyMap::BindingList bindinglist = system_bind_.listBindings(true, + static_cast<int>(System)); + KeyMap::BindingList user_bindinglist = user_bind_.listBindings(false, + static_cast<int>(UserBind)); + KeyMap::BindingList user_unbindinglist = user_unbind_.listBindings(false, + static_cast<int>(UserUnbind)); + bindinglist.insert(bindinglist.end(), user_bindinglist.begin(), + user_bindinglist.end()); + bindinglist.insert(bindinglist.end(), user_unbindinglist.begin(), + user_unbindinglist.end()); KeyMap::BindingList::const_iterator it = bindinglist.begin(); KeyMap::BindingList::const_iterator it_end = bindinglist.end(); - for (; it != it_end; ++it) { - kb_action action = it->first.action; - string const action_name = lyxaction.getActionName(action); - QString const lfun = toqstr(from_utf8(action_name) - + " " + it->first.argument()); - QString const shortcut = toqstr(it->second.print(false)); - - QTreeWidgetItem * newItem = NULL; - // if an item with the same lfun already exists, insert as a - // child item to that item. - // WARNING: this can be slow. - QList<QTreeWidgetItem*> const items = shortcutsTW->findItems(lfun, - Qt::MatchFlags(Qt::MatchExactly | Qt::MatchRecursive), 0); - if (!items.empty()) - newItem = new QTreeWidgetItem(items[0]); - else { - switch(lyxaction.getActionType(action)) { - case LyXAction::Hidden: - continue; - case LyXAction::Edit: - newItem = new QTreeWidgetItem(editItem); - break; - case LyXAction::Math: - newItem = new QTreeWidgetItem(mathItem); - break; - case LyXAction::Buffer: - newItem = new QTreeWidgetItem(bufferItem); - break; - case LyXAction::Layout: - newItem = new QTreeWidgetItem(layoutItem); - break; - case LyXAction::System: - newItem = new QTreeWidgetItem(systemItem); - break; - default: - // this should not happen - newItem = new QTreeWidgetItem(shortcutsTW); - } - } + for (; it != it_end; ++it) + insertShortcutItem(it->get<0>(), it->get<1>(), + static_cast<item_type>(it->get<2>())); - newItem->setText(0, lfun); - newItem->setText(1, shortcut); - - //newItem->setFlags(newItem->flags() | Qt::ItemIsEditable - // | Qt::ItemIsSelectable | Qt::ItemIsUserCheckable); - } shortcutsTW->sortItems(0, Qt::AscendingOrder); QList<QTreeWidgetItem*> items = shortcutsTW->selectedItems(); - modifyPB->setEnabled(!items.isEmpty()); removePB->setEnabled(!items.isEmpty() && !items[0]->text(1).isEmpty()); searchPB->setEnabled(!searchLE->text().isEmpty()); } +void PrefShortcuts::setItemType(QTreeWidgetItem * item, item_type tag) +{ + item->setData(0, Qt::UserRole, QVariant(tag)); + + switch (tag) { + case System: +#if QT_VERSION >= 0x040200 + item->setForeground(0, QBrush("black")); + item->setForeground(1, QBrush("black")); +#else + item->setTextColor(QColor("black")); +#endif + break; + case UserBind: +#if QT_VERSION >= 0x040200 + item->setForeground(0, QBrush("green")); + item->setForeground(1, QBrush("green")); +#else + item->setTextColor(QColor("green")); +#endif + break; + case UserUnbind: +#if QT_VERSION >= 0x040200 + item->setForeground(0, QBrush("red")); + item->setForeground(1, QBrush("red")); +#else + item->setTextColor(QColor("red")); +#endif + } +} + + +QTreeWidgetItem * PrefShortcuts::insertShortcutItem(FuncRequest const & lfun, + KeySequence const & seq, item_type tag) +{ + kb_action action = lfun.action; + string const action_name = lyxaction.getActionName(action); + QString const lfun_name = toqstr(from_utf8(action_name) + + " " + lfun.argument()); + // use print_bind instead of a more verbose form print(true) + // if the Shortcut dialog can hide all the bind file stuff, + // print(true) can be used. + QString const shortcut = toqstr(seq.print_bind()); + + QTreeWidgetItem * newItem = NULL; + // for unbind items, try to find an existing item in the system bind list + if (tag == UserUnbind) { + QList<QTreeWidgetItem*> const items = shortcutsTW->findItems(lfun_name, + Qt::MatchFlags(Qt::MatchExactly | Qt::MatchRecursive), 0); + if (!items.empty()) + newItem = items[0]; + } + if (!newItem) { + switch(lyxaction.getActionType(action)) { + case LyXAction::Hidden: + return NULL; + case LyXAction::Edit: + newItem = new QTreeWidgetItem(editItem_); + break; + case LyXAction::Math: + newItem = new QTreeWidgetItem(mathItem_); + break; + case LyXAction::Buffer: + newItem = new QTreeWidgetItem(bufferItem_); + break; + case LyXAction::Layout: + newItem = new QTreeWidgetItem(layoutItem_); + break; + case LyXAction::System: + newItem = new QTreeWidgetItem(systemItem_); + break; + default: + // this should not happen + newItem = new QTreeWidgetItem(shortcutsTW); + } + } + + newItem->setText(0, lfun_name); + newItem->setText(1, shortcut); + setItemType(newItem, tag); + return newItem; +} + + void PrefShortcuts::on_shortcutsTW_itemSelectionChanged() { QList<QTreeWidgetItem*> items = shortcutsTW->selectedItems(); - modifyPB->setEnabled(!items.isEmpty()); removePB->setEnabled(!items.isEmpty() && !items[0]->text(1).isEmpty()); + if (items.isEmpty()) + return; + + item_type tag = static_cast<item_type>(items[0]->data(0, Qt::UserRole).toInt()); + if (tag == UserUnbind) + removePB->setText(toqstr("Restore")); + else + removePB->setText(toqstr("Remove")); } void PrefShortcuts::on_shortcutsTW_itemDoubleClicked() { - on_modifyPB_pressed(); + QTreeWidgetItem * item = shortcutsTW->currentItem(); + if (item->flags() & Qt::ItemIsSelectable) { + shortcut_->lfunLE->setText(item->text(0)); + shortcut_->shortcutLE->setText(item->text(1)); + shortcut_->exec(); + } } @@ -1825,43 +1933,68 @@ docstring const name = from_utf8(internal_path(fromqstr(bindFileED->text()))); docstring file = form_->browsebind(name); - if (!file.empty()) + if (!file.empty()) { bindFileED->setText(toqstr(file)); + system_bind_ = KeyMap(); + system_bind_.read(to_utf8(file)); + updateShortcutsTW(); + } } void PrefShortcuts::on_newPB_pressed() { - shortcut_->lfunED->clear(); - shortcut_->shortcutED->clear(); - shortcut_->setWindowTitle(toqstr("Create new shortcut")); + shortcut_->lfunLE->clear(); + shortcut_->shortcutLE->clear(); shortcut_->exec(); } -void PrefShortcuts::on_modifyPB_pressed() -{ - QTreeWidgetItem * item = shortcutsTW->currentItem(); - shortcut_->lfunED->setText(item->text(0)); - shortcut_->shortcutED->setText(item->text(1)); - shortcut_->setWindowTitle(toqstr("Modify shortcut")); - shortcut_->exec(); -} - - void PrefShortcuts::on_removePB_pressed() { + // it seems that only one item can be selected, but I am + // removing all selected items anyway. QList<QTreeWidgetItem*> items = shortcutsTW->selectedItems(); - for (int i = 0; i < items.size(); ++i) - items[i]->setText(1, QString()); + for (int i = 0; i < items.size(); ++i) { + string shortcut = fromqstr(items[i]->text(1)); + string lfun = fromqstr(items[i]->text(0)); + FuncRequest func = lyxaction.lookupFunc(lfun); + item_type tag = static_cast<item_type>(items[i]->data(0, Qt::UserRole).toInt()); + + switch (tag) { + case System: { + // for system bind, we do not touch the item + // but add an user unbind item + user_unbind_.bind(shortcut, func); + setItemType(items[i], UserUnbind); + break; + } + case UserBind: { + // for user_bind, we remove this bind + QTreeWidgetItem * parent = items[i]->parent(); + parent->takeChild(parent->indexOfChild(items[i])); + user_bind_.unbind(shortcut, func); + break; + } + case UserUnbind: { + // for user_unbind, we remove the unbind, and the item + // become System again. + user_unbind_.unbind(shortcut, func); + setItemType(items[i], System); + break; + } + } + } } void PrefShortcuts::on_searchPB_pressed() { - QList<QTreeWidgetItem *> matched = shortcutsTW->findItems( - searchLE->text(), - Qt::MatchFlags(Qt::MatchContains | Qt::MatchRecursive)); + // search both columns + QList<QTreeWidgetItem *> matched = shortcutsTW->findItems(searchLE->text(), + Qt::MatchFlags(Qt::MatchContains | Qt::MatchRecursive), 0); + matched += shortcutsTW->findItems(searchLE->text(), + Qt::MatchFlags(Qt::MatchContains | Qt::MatchRecursive), 1); // hide everyone (to avoid searching in matched QList repeatedly QTreeWidgetItemIterator it(shortcutsTW, QTreeWidgetItemIterator::Selectable); @@ -1870,7 +2003,7 @@ // show matched items for (int i = 0; i < matched.size(); ++i) { shortcutsTW->setItemHidden(matched[i], false); - shortcutsTW->setItemExpanded(matched[i], true); + shortcutsTW->setItemExpanded(matched[i]->parent(), true); } } @@ -1887,8 +2020,44 @@ } -void PrefShortcuts::setShortcut() +void PrefShortcuts::shortcut_okPB_pressed() { + string shortcut = fromqstr(shortcut_->shortcutLE->text()); + string lfun = fromqstr(shortcut_->lfunLE->text()); + FuncRequest func = lyxaction.lookupFunc(lfun); + + if (func.action == LFUN_UNKNOWN_ACTION) { + Alert::error(_("Failed to create shortcut"), + _("Unknown or invalid lyx function")); + return; + } + + KeySequence k(0, 0); + string::size_type const res = k.parse(shortcut); + if (res != string::npos) { + Alert::error(_("Failed to create shortcut"), + _("Invalid key sequence")); + return; + } + + // if both lfun and shortcut is valid + if (user_bind_.hasBinding(k, func) || system_bind_.hasBinding(k, func)) { + Alert::error(_("Failed to create shortcut"), + _("Shortcut is alreay defined")); + return; + } + + QTreeWidgetItem * item = insertShortcutItem(func, k, UserBind); + if (item) { + user_bind_.bind(shortcut, func); + shortcutsTW->sortItems(0, Qt::AscendingOrder); + shortcutsTW->setItemExpanded(item->parent(), true); + shortcutsTW->scrollToItem(item); + } else { + Alert::error(_("Failed to create shortcut"), + _("Can not insert shortcut to the list")); + return; + } } Index: src/frontends/qt4/ui/ShortcutUi.ui =================================================================== --- src/frontends/qt4/ui/ShortcutUi.ui (revision 21075) +++ src/frontends/qt4/ui/ShortcutUi.ui (working copy) @@ -94,7 +94,7 @@ </widget> </item> <item row="1" column="1" > - <widget class="QLineEdit" name="shortcutED" > + <widget class="QLineEdit" name="shortcutLE" > <property name="sizePolicy" > <sizepolicy> <hsizetype>7</hsizetype> @@ -109,7 +109,7 @@ </widget> </item> <item row="0" column="1" > - <widget class="QLineEdit" name="lfunED" > + <widget class="QLineEdit" name="lfunLE" > <property name="sizePolicy" > <sizepolicy> <hsizetype>7</hsizetype> @@ -126,7 +126,7 @@ </layout> </widget> <tabstops> - <tabstop>lfunED</tabstop> + <tabstop>lfunLE</tabstop> <tabstop>okPB</tabstop> <tabstop>cancelPB</tabstop> </tabstops> Index: src/frontends/qt4/ui/PrefShortcutsUi.ui =================================================================== --- src/frontends/qt4/ui/PrefShortcutsUi.ui (revision 21075) +++ src/frontends/qt4/ui/PrefShortcutsUi.ui (working copy) @@ -122,13 +122,6 @@ </property> </widget> </item> - <item row="4" column="2" > - <widget class="QPushButton" name="modifyPB" > - <property name="text" > - <string>Modify</string> - </property> - </widget> - </item> </layout> </widget> <tabstops> Index: src/frontends/qt4/GuiPrefs.h =================================================================== --- src/frontends/qt4/GuiPrefs.h (revision 21075) +++ src/frontends/qt4/GuiPrefs.h (working copy) @@ -17,6 +17,8 @@ #include "Color.h" #include "Converter.h" #include "Format.h" +#include "KeyMap.h" +#include "lfuns.h" #include "LyXRC.h" #include "Mover.h" @@ -359,28 +361,55 @@ class PrefShortcuts : public PrefModule, public Ui::PrefShortcuts { Q_OBJECT +private: + enum item_type { + System, //< loaded from a bind file + UserBind, //< \bind loaded from user.bind + UserUnbind //< \unbind loaded from user.bind + }; public: PrefShortcuts(GuiPreferences * form, QWidget * parent = 0); void apply(LyXRC & rc) const; void update(LyXRC const & rc); + void updateShortcutsTW(); + /// + void setItemType(QTreeWidgetItem * item, item_type tag); + QTreeWidgetItem * insertShortcutItem(FuncRequest const & lfun, + KeySequence const & shortcut, item_type tag); public Q_SLOTS: void select_bind(); void on_newPB_pressed(); - void on_modifyPB_pressed(); void on_removePB_pressed(); void on_searchPB_pressed(); void on_searchLE_textChanged(); /// void on_shortcutsTW_itemSelectionChanged(); - void setShortcut(); + void shortcut_okPB_pressed(); void on_shortcutsTW_itemDoubleClicked(); + private: /// GuiShortcutDialog * shortcut_; /// ButtonController shortcut_bc_; + /// category items + QTreeWidgetItem * editItem_; + QTreeWidgetItem * mathItem_; + QTreeWidgetItem * bufferItem_; + QTreeWidgetItem * layoutItem_; + QTreeWidgetItem * systemItem_; + // system_bind_ holds bindings from rc.bind_file + // user_bind_ holds \bind bindings from user.bind + // user_unbind_ holds \unbind bindings from user.bind + // When an item is inserted, it is added to user_bind_ + // When an item from system_bind_ is deleted, it is added to user_unbind_ + // When an item in user_bind_ or user_unbind_ is deleted, it is + // deleted (unbind) + KeyMap system_bind_; + KeyMap user_bind_; + KeyMap user_unbind_; }; Index: src/KeySequence.cpp =================================================================== --- src/KeySequence.cpp (revision 21075) +++ src/KeySequence.cpp (working copy) @@ -143,6 +143,31 @@ } +string const KeySequence::print_bind() const +{ + string buf; + + size_t const length = sequence.size(); + + for (size_t i = 0; i != length; ++i) { + KeyModifier mod = modifiers[i].first; + if (mod & ShiftModifier) + buf += "S-"; + if (mod & ControlModifier) + buf += "C-"; + if (mod & AltModifier) + buf += "M-"; + + buf += sequence[i].getSymbolName(); + + // append a blank + if (i + 1 != length) + buf += ' '; + } + return buf; +} + + docstring const KeySequence::printOptions(bool forgui) const { docstring buf = print(forgui); Index: src/KeyMap.h =================================================================== --- src/KeyMap.h (revision 21075) +++ src/KeyMap.h (working copy) @@ -22,6 +22,7 @@ #include "support/docstream.h" #include <boost/shared_ptr.hpp> +#include <boost/tuple/tuple.hpp> #include <vector> #include <deque> @@ -42,9 +43,33 @@ */ size_t bind(std::string const & seq, FuncRequest const & func); - // Parse a bind file - bool read(std::string const & bind_file); + // Unbind a key sequence + size_t unbind(std::string const & seq, FuncRequest const & func); + // if a keybinding has been defined. + bool hasBinding(KeySequence const & seq, FuncRequest const & func, + unsigned int r = 0); + + // clear all bindings + void clear(); + + /** Parse a bind file. If a valid unbind_map is given, put \unbind + * bindings to a separate KeyMap. This is used in the Shortcut preference + * dialog where main and user bind files are loaded separately so \unbind + * in user.bind can not nullify \bind in the master bind file. + * + * @param bind_file bind file + * @param unbind_map pointer to a KeyMap that holds \unbind bindings + */ + bool read(std::string const & bind_file, KeyMap * unbind_map = NULL); + + /** write to a bind file. + * @param append append to the bind_file instead of overwrite it + * @param unbind use \unbind instead of \bind, indicating this KeyMap + * actually record unbind maps. + */ + void write(std::string const & bind_file, bool append, bool unbind=false) const; + /** * print all available keysyms * @param forgui true if the string should use translations and @@ -71,13 +96,14 @@ /// Given an action, print the keybindings. docstring const printbindings(FuncRequest const & func) const; - typedef std::pair<FuncRequest, KeySequence> Binding; + typedef boost::tuple<FuncRequest, KeySequence, int> Binding; typedef std::vector<Binding> BindingList; /** * Return all lfun and their associated bindings. * @param unbound list unbound (func without any keybinding) as well + * @param tag an optional tag to indicate the source of the bindinglist */ - BindingList const listBindings(bool unbound) const; + BindingList listBindings(bool unbound, int tag = 0) const; /** * Given an action, find the first 1-key binding (if it exists). @@ -121,6 +147,8 @@ */ void defkey(KeySequence * seq, FuncRequest const & func, unsigned int r = 0); + void delkey(KeySequence * seq, FuncRequest const & func, + unsigned int r = 0); /** * Given an action, find all keybindings @@ -130,8 +158,8 @@ Bindings findbindings(FuncRequest const & func, KeySequence const & prefix) const; - void listBindings(BindingList & list, - KeySequence const & prefix) const; + void listBindings(BindingList & list, KeySequence const & prefix, + int tag) const; /// is the table empty ? bool empty() const { return table.empty(); } Index: src/KeySequence.h =================================================================== --- src/KeySequence.h (revision 21075) +++ src/KeySequence.h (working copy) @@ -69,6 +69,9 @@ */ docstring const print(bool forgui) const; + // print in a format used in the bind file + std::string const print_bind() const; + /** * Return the current sequence and available options as * a string. No options are added if no curmap kb map exists.