diff --git a/doomsday/client/client.pro b/doomsday/client/client.pro index e02e689d40..8465879cb6 100644 --- a/doomsday/client/client.pro +++ b/doomsday/client/client.pro @@ -398,6 +398,7 @@ DENG_HEADERS += \ include/ui/widgets/gameuiwidget.h \ include/ui/widgets/gamewidget.h \ include/ui/widgets/icvarwidget.h \ + include/ui/widgets/inputbindingwidget.h \ include/ui/widgets/keygrabberwidget.h \ include/ui/widgets/mpselectionwidget.h \ include/ui/widgets/multiplayermenuwidget.h \ @@ -733,6 +734,7 @@ SOURCES += \ src/ui/widgets/gamesessionwidget.cpp \ src/ui/widgets/gamewidget.cpp \ src/ui/widgets/gameuiwidget.cpp \ + src/ui/widgets/inputbindingwidget.cpp \ src/ui/widgets/keygrabberwidget.cpp \ src/ui/widgets/mpselectionwidget.cpp \ src/ui/widgets/multiplayermenuwidget.cpp \ diff --git a/doomsday/client/include/ui/b_command.h b/doomsday/client/include/ui/b_command.h index bb6afc608e..5b6d8bffb9 100644 --- a/doomsday/client/include/ui/b_command.h +++ b/doomsday/client/include/ui/b_command.h @@ -52,6 +52,8 @@ evbinding_t* B_NewCommandBinding(evbinding_t* listRoot, const char* desc, const void B_DestroyCommandBinding(evbinding_t* eb); void B_EventBindingToString(const evbinding_t* eb, ddstring_t* str); +evbinding_t *B_FindCommandBinding(evbinding_t const *listRoot, char const *command, uint device); + /** * Checks if the event matches the binding's conditions, and if so, returns an * action with the bound command. diff --git a/doomsday/client/include/ui/b_context.h b/doomsday/client/include/ui/b_context.h index edd84aeaa5..d28198c9da 100644 --- a/doomsday/client/include/ui/b_context.h +++ b/doomsday/client/include/ui/b_context.h @@ -98,8 +98,13 @@ de::Action *B_ActionForEvent(ddevent_t const *event); */ de::Action *BindContext_ActionForEvent(bcontext_t *bc, ddevent_t const *event, bool respectHigherAssociatedContexts); -dd_bool B_FindMatchingBinding(bcontext_t* bc, evbinding_t* match1, dbinding_t* match2, - evbinding_t** evResult, dbinding_t** dResult); +/** + * Looks through context @a bc and looks for a binding that matches either + * @a match1 or @a match2. + */ +dd_bool B_FindMatchingBinding(bcontext_t* bc, evbinding_t* match1, dbinding_t* match2, + evbinding_t** evResult, dbinding_t** dResult); + void B_PrintContexts(void); void B_PrintAllBindings(void); void B_WriteContextToFile(const bcontext_t* bc, FILE* file); diff --git a/doomsday/client/include/ui/b_main.h b/doomsday/client/include/ui/b_main.h index b97f3be56b..552da1fd23 100644 --- a/doomsday/client/include/ui/b_main.h +++ b/doomsday/client/include/ui/b_main.h @@ -57,6 +57,8 @@ struct evbinding_s* B_BindCommand(const char* eventDesc, const char* command); struct dbinding_s* B_BindControl(const char* controlDesc, const char* device); struct dbinding_s* B_GetControlDeviceBindings(int localNum, int control, struct bcontext_s** bContext); +bool B_UnbindCommand(char const *command); + // Utils /// @todo: move to b_util.h int B_NewIdentifier(void); diff --git a/doomsday/client/include/ui/widgets/inputbindingwidget.h b/doomsday/client/include/ui/widgets/inputbindingwidget.h new file mode 100644 index 0000000000..4da48ee381 --- /dev/null +++ b/doomsday/client/include/ui/widgets/inputbindingwidget.h @@ -0,0 +1,62 @@ +/** @file inputbindingwidget.h Widget for creating input bindings. + * + * @authors Copyright (c) 2014 Jaakko Keränen + * + * @par License + * GPL: http://www.gnu.org/licenses/gpl.html + * + * 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, see: + * http://www.gnu.org/licenses + */ + +#ifndef DENG_CLIENT_INPUTBINDINGWIDGET_H +#define DENG_CLIENT_INPUTBINDINGWIDGET_H + +#include + +/** + * Widget for viewing and changing an input binding. + */ +class InputBindingWidget : public de::AuxButtonWidget +{ +public: + InputBindingWidget(); + + void setDefaultBinding(de::String const &eventDesc); + + void setCommand(de::String const &command); + + /** + * Enables or disables explicitly specified modifier conditions. Bindings can be + * specified with or without modifiers -- if modifiers are not specified, the binding + * can be triggered regardless of modifier state. + * + * @param mods @c true to includes modifiers in the binding. If @c false, modifiers + * can be bound like any other input. + */ + void enableModifiers(bool mods); + + void setContext(QString const &bindingContext) { + setContexts(QStringList() << bindingContext); + } + + void setContexts(QStringList const &contexts); + + // Events. + bool handleEvent(de::Event const &event); + +public: + static InputBindingWidget *newTaskBarShortcut(); + +private: + DENG2_PRIVATE(d) +}; + +#endif // DENG_CLIENT_INPUTBINDINGWIDGET_H diff --git a/doomsday/client/src/ui/b_command.cpp b/doomsday/client/src/ui/b_command.cpp index ada7064967..0acea744ae 100644 --- a/doomsday/client/src/ui/b_command.cpp +++ b/doomsday/client/src/ui/b_command.cpp @@ -514,3 +514,16 @@ void B_EventBindingToString(const evbinding_t* eb, ddstring_t* str) B_AppendConditionToString(&eb->conds[i], str); } } + +evbinding_t *B_FindCommandBinding(evbinding_t const *listRoot, char const *command, uint device) +{ + for(evbinding_t *i = listRoot->next; i != listRoot; i = i->next) + { + if(!qstricmp(i->command, command) && + (device >= NUM_INPUT_DEVICES || i->device == device)) + { + return i; + } + } + return 0; +} diff --git a/doomsday/client/src/ui/b_context.cpp b/doomsday/client/src/ui/b_context.cpp index 9572956f65..63dc8afe80 100644 --- a/doomsday/client/src/ui/b_context.cpp +++ b/doomsday/client/src/ui/b_context.cpp @@ -404,11 +404,9 @@ void B_ReorderContext(bcontext_t* bc, int pos) controlbinding_t* B_NewControlBinding(bcontext_t* bc) { - int i; - controlbinding_t* conBin = (controlbinding_t *) M_Calloc(sizeof(controlbinding_t)); conBin->bid = B_NewIdentifier(); - for(i = 0; i < DDMAXPLAYERS; ++i) + for(int i = 0; i < DDMAXPLAYERS; ++i) { B_InitDeviceBindingList(&conBin->deviceBinds[i]); } @@ -724,10 +722,6 @@ dd_bool B_AreConditionsEqual(int count1, const statecondition_t* conds1, return true; } -/** - * Looks through context @a bc and looks for a binding that matches either - * @a match1 or @a match2. - */ dd_bool B_FindMatchingBinding(bcontext_t* bc, evbinding_t* match1, dbinding_t* match2, evbinding_t** evResult, dbinding_t** dResult) diff --git a/doomsday/client/src/ui/b_main.cpp b/doomsday/client/src/ui/b_main.cpp index 823d64f4b5..b084937ba3 100644 --- a/doomsday/client/src/ui/b_main.cpp +++ b/doomsday/client/src/ui/b_main.cpp @@ -725,3 +725,17 @@ DENG_EXTERN_C int DD_GetKeyCode(const char* key) int code = B_KeyForShortName(key); return (code ? code : key[0]); } + +bool B_UnbindCommand(const char *command) +{ + bool deleted = false; + for(int i = 0; i < B_ContextCount(); ++i) + { + bcontext_t *bc = B_ContextByPos(i); + while(evbinding_t *ev = B_FindCommandBinding(&bc->commandBinds, command, NUM_INPUT_DEVICES)) + { + deleted |= B_DeleteBinding(bc, ev->bid); + } + } + return deleted; +} diff --git a/doomsday/client/src/ui/widgets/inputbindingwidget.cpp b/doomsday/client/src/ui/widgets/inputbindingwidget.cpp new file mode 100644 index 0000000000..036bf8bfb9 --- /dev/null +++ b/doomsday/client/src/ui/widgets/inputbindingwidget.cpp @@ -0,0 +1,278 @@ +/** @file inputbindingwidget.cpp + * + * @authors Copyright (c) 2014 Jaakko Keränen + * + * @par License + * GPL: http://www.gnu.org/licenses/gpl.html + * + * 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, see: + * http://www.gnu.org/licenses + */ + +#include "ui/widgets/inputbindingwidget.h" +#include "ui/b_main.h" +#include "ui/b_context.h" +#include +#include + +using namespace de; + +#ifdef MACOSX +# define CONTROL_MOD KeyEvent::Meta +# define CONTROL_CHAR DENG2_CHAR_MAC_CONTROL_KEY +#else +# define CONTROL_MOD KeyEvent::Control +# define CONTROL_CHAR DENG2_CHAR_CONTROL_KEY +#endif + +DENG_GUI_PIMPL(InputBindingWidget) +, DENG2_OBSERVES(ButtonWidget, Press) +{ + String defaultEvent; + String command; + QStringList contexts; + uint device; + bool useModifiers; + + Instance(Public *i) + : Base(i) + , device(IDEV_KEYBOARD) + , useModifiers(false) + { + //self.setTextLineAlignment(ui::AlignLeft); + self.setSizePolicy(ui::Fixed, ui::Expand); + + self.auxiliary().setText(_E(l) + tr("Reset")); + + self.audienceForPress() += this; + self.auxiliary().audienceForPress() += this; + } + + String prettyKey(String const &eventDesc) + { + if(!eventDesc.startsWith("key-")) + { + // Doesn't look like a key. + return eventDesc; + } + + String name = eventDesc.mid(4, eventDesc.indexOf("-", 4) - 4); + name = name.mid(0, 1).toUpper() + name.mid(1).toLower(); + + // Any modifiers? + int idx = eventDesc.indexOf("+"); + if(idx > 0) + { + String const conds = eventDesc.mid(idx + 1); + if(conds.contains("key-ctrl-down") || conds.contains("key-control-down")) + { + name = String(CONTROL_CHAR) + name; + } + if(conds.contains("key-alt-down")) + { + name = String(DENG2_CHAR_ALT_KEY) + name; + } + if(conds.contains("key-shift-down")) + { + name = String(DENG2_CHAR_SHIFT_KEY) + name; + } + } + return name; + } + + /// Checks the current binding and updates the label to show which event/input + /// is bound. + void updateLabel() + { + String text = _E(l) + tr("(not bound)"); + + // Check all the contexts associated with this widget. + foreach(QString bcName, contexts) + { + bcontext_t const *bc = B_ContextByName(bcName.toLatin1()); + evbinding_t const *com = B_FindCommandBinding(&bc->commandBinds, + command.toLatin1(), device); + if(com) + { + // This'll do. + AutoStr *str = AutoStr_New(); + B_EventBindingToString(com, str); + text = prettyKey(Str_Text(str)); + break; + } + } + + self.setText(_E(b) + text); + } + + void bind(String const &eventDesc) + { + B_UnbindCommand(command.toLatin1()); + + foreach(QString bcName, contexts) + { + String ev = String("%1:%2").arg(bcName, eventDesc); + B_BindCommand(ev.toLatin1(), command.toLatin1()); + } + } + + void buttonPressed(ButtonWidget &button) + { + if(&button == thisPublic) + { + if(!self.hasFocus()) + { + focus(); + } + else + { + unfocus(); + } + } + else + { + // The reset button. + bind(defaultEvent); + updateLabel(); + } + } + + void focus() + { + root().setFocus(thisPublic); + self.auxiliary().disable(); + self.useInvertedStyle(); + } + + void unfocus() + { + root().setFocus(0); + self.auxiliary().enable(); + self.useNormalStyle(); + } +}; + +InputBindingWidget::InputBindingWidget() : d(new Instance(this)) +{ + auxiliary().hide(); +} + +void InputBindingWidget::setDefaultBinding(String const &eventDesc) +{ + d->defaultEvent = eventDesc; + auxiliary().show(); +} + +void InputBindingWidget::setCommand(String const &command) +{ + d->command = command; + d->updateLabel(); +} + +void InputBindingWidget::enableModifiers(bool mods) +{ + d->useModifiers = mods; +} + +void InputBindingWidget::setContexts(QStringList const &contexts) +{ + d->contexts = contexts; + d->updateLabel(); +} + +bool InputBindingWidget::handleEvent(Event const &event) +{ + if(hasFocus()) + { + if(KeyEvent const *key = event.maybeAs()) + { + if(key->state() != KeyEvent::Pressed) return false; + + // Include modifier keys if they will be included in the binding. + if(d->useModifiers && key->isModifier()) + { + return false; + } + + if(key->ddKey() == DDKEY_ESCAPE) + { + d->unfocus(); + return true; + } + + AutoStr *name = AutoStr_New(); + ddevent_t ev; + DD_ConvertEvent(event, &ev); + B_AppendEventToString(&ev, name); + + String desc = Str_Text(name); + + // Apply current modifiers as conditions. + if(d->useModifiers) + { + if(key->modifiers().testFlag(KeyEvent::Shift)) + { + desc += " + key-shift-down"; + } + else + { + desc += " + key-shift-up"; + } + + if(key->modifiers().testFlag(KeyEvent::Alt)) + { + desc += " + key-alt-down"; + } + else + { + desc += " + key-alt-up"; + } + + if(key->modifiers().testFlag(CONTROL_MOD)) + { + desc += " + key-ctrl-down"; + } + else + { + desc += " + key-ctrl-up"; + } + } + + d->bind(desc); + d->updateLabel(); + d->unfocus(); + return true; + } + + if(MouseEvent const *mouse = event.maybeAs()) + { + if(mouse->type() == Event::MouseButton && + mouse->state() == MouseEvent::Released && + !hitTest(event)) + { + // Clicking outside clears focus. + d->unfocus(); + return true; + } + } + } + + return AuxButtonWidget::handleEvent(event); +} + +InputBindingWidget *InputBindingWidget::newTaskBarShortcut() +{ + InputBindingWidget *bind = new InputBindingWidget; + bind->setCommand("taskbar"); + bind->setDefaultBinding("key-tilde-down + key-shift-up"); + bind->enableModifiers(true); + bind->setContexts(QStringList() << "global" << "console"); + return bind; +}