GCI2012: Advanced Keyboard Configuration
authordag10
Sat, 29 Dec 2012 22:50:10 +0100
changeset 8346 3443e0de2c9d
parent 8344 3d18f7f71d65
child 8347 716c05f19401
GCI2012: Advanced Keyboard Configuration - Added "Controls" tab to settings, where you can set master game-wide controls. - Can revert master key bindings to game's default key bind. - Per-team binds now default to "Use my default", but you can override those binds if you want to. - New key binding interface. - Removed redundant second confirmation prompt for deleting a team. - Added "reset all binds" button to the binding interface in both the main settings and team settings. - I discovered that the reason keyboard camera controls were "broken" is because they were never implemented! But don't worry - I took care of that for you, too. :) (this also closes bug #120)
QTfrontend/binds.cpp
QTfrontend/game.cpp
QTfrontend/game.h
QTfrontend/gameuiconfig.cpp
QTfrontend/gameuiconfig.h
QTfrontend/hwform.cpp
QTfrontend/team.cpp
QTfrontend/team.h
QTfrontend/ui/page/pageeditteam.cpp
QTfrontend/ui/page/pageeditteam.h
QTfrontend/ui/page/pageoptions.cpp
QTfrontend/ui/page/pageoptions.h
QTfrontend/ui/widget/keybinder.cpp
QTfrontend/ui/widget/keybinder.h
QTfrontend/util/DataManager.cpp
hedgewars/hwengine.pas
hedgewars/uConsts.pas
hedgewars/uCursor.pas
hedgewars/uInputHandler.pas
hedgewars/uTeams.pas
--- a/QTfrontend/binds.cpp	Fri Dec 28 23:54:42 2012 +0100
+++ b/QTfrontend/binds.cpp	Sat Dec 29 22:50:10 2012 +0100
@@ -20,17 +20,15 @@
 
 const BindAction cbinds[BINDS_NUMBER] =
 {
-    {"+up",       "up",         QT_TRANSLATE_NOOP("binds", "up"),              QT_TRANSLATE_NOOP("binds (categories)", "Basic controls"), QT_TRANSLATE_NOOP("binds (descriptions)", "Move your hogs and aim:")},
+    {"+up",       "up",         QT_TRANSLATE_NOOP("binds", "up"),              QT_TRANSLATE_NOOP("binds (categories)", "Movement"), QT_TRANSLATE_NOOP("binds (descriptions)", "Hedgehog movement")},
     {"+left",     "left",       QT_TRANSLATE_NOOP("binds", "left"),            NULL, NULL},
     {"+right",    "right",      QT_TRANSLATE_NOOP("binds", "right"),           NULL, NULL},
     {"+down",     "down",       QT_TRANSLATE_NOOP("binds", "down"),            NULL, NULL},
     {"+precise",  "left_shift", QT_TRANSLATE_NOOP("binds", "precise aim"),     NULL, NULL},
     {"ljump",     "return",     QT_TRANSLATE_NOOP("binds", "long jump"),       NULL, QT_TRANSLATE_NOOP("binds (descriptions)", "Traverse gaps and obstacles by jumping:")},
     {"hjump",     "backspace",  QT_TRANSLATE_NOOP("binds", "high jump"),       NULL, NULL},
-    {"+attack",   "space",      QT_TRANSLATE_NOOP("binds", "attack"),          NULL, QT_TRANSLATE_NOOP("binds (descriptions)", "Fire your selected weapon or trigger an utility item:")},
-    {"put",       "mousel",     QT_TRANSLATE_NOOP("binds", "put"),             NULL, QT_TRANSLATE_NOOP("binds (descriptions)", "Pick a weapon or a target location under the cursor:")},
     {"switch",    "tab",        QT_TRANSLATE_NOOP("binds", "switch"),          NULL, QT_TRANSLATE_NOOP("binds (descriptions)", "Switch your currently active hog (if possible):")},
-    {"ammomenu",  "mouser",     QT_TRANSLATE_NOOP("binds", "ammo menu"),       QT_TRANSLATE_NOOP("binds (categories)", "Weapon controls"), QT_TRANSLATE_NOOP("binds (descriptions)", "Pick a weapon or utility item:")},
+    {"ammomenu",  "mouser",     QT_TRANSLATE_NOOP("binds", "ammo menu"),       QT_TRANSLATE_NOOP("binds (categories)", "Weapons"), QT_TRANSLATE_NOOP("binds (descriptions)", "Pick a weapon or utility item:")},
     {"slot 1",    "f1",         QT_TRANSLATE_NOOP("binds", "slot 1"),          NULL, NULL},
     {"slot 2",    "f2",         QT_TRANSLATE_NOOP("binds", "slot 2"),          NULL, NULL},
     {"slot 3",    "f3",         QT_TRANSLATE_NOOP("binds", "slot 3"),          NULL, NULL},
@@ -46,7 +44,9 @@
     {"timer 3",   "3",          QT_TRANSLATE_NOOP("binds", "timer 3 sec"),     NULL, NULL},
     {"timer 4",   "4",          QT_TRANSLATE_NOOP("binds", "timer 4 sec"),     NULL, NULL},
     {"timer 5",   "5",          QT_TRANSLATE_NOOP("binds", "timer 5 sec"),     NULL, NULL},
-    {"findhh",    "h",          QT_TRANSLATE_NOOP("binds", "find hedgehog"),   QT_TRANSLATE_NOOP("binds (categories)", "Camera and cursor controls"), QT_TRANSLATE_NOOP("binds (descriptions)", "Move the camera to the active hog:")},
+    {"+attack",   "space",      QT_TRANSLATE_NOOP("binds", "attack"),          NULL, QT_TRANSLATE_NOOP("binds (descriptions)", "Fire your selected weapon or trigger an utility item:")},
+    {"put",       "mousel",     QT_TRANSLATE_NOOP("binds", "put"),             NULL, QT_TRANSLATE_NOOP("binds (descriptions)", "Pick a weapon or a target location under the cursor:")},
+    {"findhh",    "h",          QT_TRANSLATE_NOOP("binds", "find hedgehog"),   QT_TRANSLATE_NOOP("binds (categories)", "Camera"), QT_TRANSLATE_NOOP("binds (descriptions)", "Move the camera to the active hog:")},
     {"+cur_u",    "[8]",        QT_TRANSLATE_NOOP("binds", "up"),              NULL, QT_TRANSLATE_NOOP("binds (descriptions)", "Move the cursor or camera without using the mouse:")},
     {"+cur_l",    "[4]",        QT_TRANSLATE_NOOP("binds", "left"),            NULL, NULL},
     {"+cur_r",    "[6]",        QT_TRANSLATE_NOOP("binds", "right"),           NULL, NULL},
@@ -55,7 +55,7 @@
     {"zoomin",    "wheelup",    QT_TRANSLATE_NOOP("binds", "zoom in"),         NULL, QT_TRANSLATE_NOOP("binds (descriptions)", "Modify the camera's zoom level:")},
     {"zoomout",   "wheeldown",  QT_TRANSLATE_NOOP("binds", "zoom out"),        NULL, NULL},
     {"zoomreset", "mousem",     QT_TRANSLATE_NOOP("binds", "reset zoom"),      NULL, NULL},
-    {"chat",      "t",          QT_TRANSLATE_NOOP("binds", "chat"),            QT_TRANSLATE_NOOP("binds (categories)", "Other"), QT_TRANSLATE_NOOP("binds (descriptions)", "Talk to your team or all participants:")},
+    {"chat",      "t",          QT_TRANSLATE_NOOP("binds", "chat"),            QT_TRANSLATE_NOOP("binds (categories)", "Miscellaneous"), QT_TRANSLATE_NOOP("binds (descriptions)", "Talk to your team or all participants:")},
     {"history",   "`",          QT_TRANSLATE_NOOP("binds", "chat history"),    NULL, NULL},
     {"pause",     "p",          QT_TRANSLATE_NOOP("binds", "pause"),           NULL, QT_TRANSLATE_NOOP("binds (descriptions)", "Pause, continue or leave your game:")},
     {"quit",      "escape",     QT_TRANSLATE_NOOP("binds", "quit"),            NULL, NULL},
@@ -65,7 +65,7 @@
     {"mute",      "8",          QT_TRANSLATE_NOOP("binds", "mute audio"),      NULL, NULL},
     {"fullscr",   "f12",        QT_TRANSLATE_NOOP("binds", "change mode"),     NULL, QT_TRANSLATE_NOOP("binds (descriptions)", "Toggle fullscreen mode:")},
     {"capture",   "c",          QT_TRANSLATE_NOOP("binds", "capture"),         NULL, QT_TRANSLATE_NOOP("binds (descriptions)", "Take a screenshot:")},
-    {"rotmask",   "delete",     QT_TRANSLATE_NOOP("binds", "hedgehogs\ninfo"), NULL, QT_TRANSLATE_NOOP("binds (descriptions)", "Toggle labels above hedgehogs:")},
+    {"rotmask",   "delete",     QT_TRANSLATE_NOOP("binds", "hedgehog info"), NULL, QT_TRANSLATE_NOOP("binds (descriptions)", "Toggle labels above hedgehogs:")},
 #ifdef VIDEOREC
     {"record",    "r",          QT_TRANSLATE_NOOP("binds", "record"),          NULL, QT_TRANSLATE_NOOP("binds (descriptions)", "Record video:")}
 #endif
--- a/QTfrontend/game.cpp	Fri Dec 28 23:54:42 2012 +0100
+++ b/QTfrontend/game.cpp	Sat Dec 29 22:50:10 2012 +0100
@@ -29,6 +29,7 @@
 #include "gamecfgwidget.h"
 #include "teamselect.h"
 #include "proto.h"
+#include "binds.h"
 #include "campaign.h"
 
 #include <QTextStream>
@@ -73,6 +74,18 @@
     SetGameState(gsStopped);
 }
 
+void HWGame::addKeyBindings(QByteArray * buf)
+{
+    for(int i = 0; i < BINDS_NUMBER; i++)
+    {
+        QString value = config->value(QString("Binds/%1").arg(cbinds[i].action), cbinds[i].strbind).toString();
+        if (value.isEmpty() || value == "default") continue;
+        
+        QString bind = QString("edbind " + value + " " + cbinds[i].action);
+        HWProto::addStringToBuffer(*buf, bind);
+    }
+}
+
 void HWGame::commonConfig()
 {
     QByteArray buf;
@@ -90,6 +103,8 @@
     }
     HWProto::addStringToBuffer(buf, gt);
 
+    addKeyBindings(&buf);
+
     buf += gamecfg->getFullConfig();
 
     if (m_pTeamSelWidget)
@@ -102,10 +117,11 @@
             HWProto::addStringToBuffer(buf, QString("eammreinf %1").arg(ammostr.mid(3 * cAmmoNumber, cAmmoNumber)));
             if(gamecfg->schemeData(15).toBool() || !gamecfg->schemeData(21).toBool()) HWProto::addStringToBuffer(buf, QString("eammstore"));
             HWProto::addStringListToBuffer(buf,
-                                           team.teamGameConfig(gamecfg->getInitHealth()));
+                                           team.teamGameConfig(gamecfg->getInitHealth(), config));
             ;
         }
     }
+    
     RawSendIPC(buf);
 }
 
@@ -119,6 +135,8 @@
     QByteArray teamscfg;
     ThemeModel * themeModel = DataManager::instance().themeModel();
 
+    addKeyBindings(&teamscfg);
+
     HWProto::addStringToBuffer(teamscfg, "TL");
     HWProto::addStringToBuffer(teamscfg, QString("etheme %1")
                                .arg((themeModel->rowCount() > 0) ? themeModel->index(rand() % themeModel->rowCount()).data().toString() : "steel"));
@@ -132,7 +150,7 @@
     team1.setNumHedgehogs(4);
     HWNamegen::teamRandomNames(team1,true);
     HWProto::addStringListToBuffer(teamscfg,
-                                   team1.teamGameConfig(100));
+                                   team1.teamGameConfig(100, config));
 
     HWTeam team2;
     team2.setDifficulty(4);
@@ -142,7 +160,7 @@
         HWNamegen::teamRandomNames(team2,true);
     while(!team2.name().compare(team1.name()) || !team2.hedgehog(0).Hat.compare(team1.hedgehog(0).Hat));
     HWProto::addStringListToBuffer(teamscfg,
-                                   team2.teamGameConfig(100));
+                                   team2.teamGameConfig(100, config));
 
     HWProto::addStringToBuffer(teamscfg, QString("eammloadt %1").arg(cDefaultAmmoStore->mid(0, cAmmoNumber)));
     HWProto::addStringToBuffer(teamscfg, QString("eammprob %1").arg(cDefaultAmmoStore->mid(cAmmoNumber, cAmmoNumber)));
@@ -150,6 +168,7 @@
     HWProto::addStringToBuffer(teamscfg, QString("eammreinf %1").arg(cDefaultAmmoStore->mid(3 * cAmmoNumber, cAmmoNumber)));
     HWProto::addStringToBuffer(teamscfg, QString("eammstore"));
     HWProto::addStringToBuffer(teamscfg, QString("eammstore"));
+    
     RawSendIPC(teamscfg);
 }
 
@@ -160,6 +179,8 @@
     HWProto::addStringToBuffer(traincfg, "eseed " + QUuid::createUuid().toString());
     HWProto::addStringToBuffer(traincfg, "escript " + training);
 
+    addKeyBindings(&traincfg);
+
     RawSendIPC(traincfg);
 }
 
@@ -171,6 +192,8 @@
 
     HWProto::addStringToBuffer(campaigncfg, "escript " + campaignScript);
 
+    addKeyBindings(&campaigncfg);
+
     RawSendIPC(campaigncfg);
 }
 
--- a/QTfrontend/game.h	Fri Dec 28 23:54:42 2012 +0100
+++ b/QTfrontend/game.h	Sat Dec 29 22:50:10 2012 +0100
@@ -103,6 +103,7 @@
         TeamSelWidget* m_pTeamSelWidget;
         GameType gameType;
 
+        void addKeyBindings(QByteArray * buf);
         void commonConfig();
         void SendConfig();
         void SendQuickConfig();
--- a/QTfrontend/gameuiconfig.cpp	Fri Dec 28 23:54:42 2012 +0100
+++ b/QTfrontend/gameuiconfig.cpp	Sat Dec 29 22:50:10 2012 +0100
@@ -52,6 +52,13 @@
 
     connect(Form->ui.pageOptions->CBFrontendMusic, SIGNAL(toggled(bool)), Form, SLOT(Music(bool)));
 
+    for(int i = 0; i < BINDS_NUMBER; i++)
+    {
+        m_binds.append(BindAction());
+        m_binds[i].action = cbinds[i].action;
+        m_binds[i].strbind = cbinds[i].strbind;
+    }
+
     //Form->resize(value("frontend/width", 640).toUInt(), value("frontend/height", 450).toUInt());
     resizeToConfigValues();
 
@@ -135,6 +142,15 @@
         for(int i = model->rowCount() - 1; i >= 0; --i)
             model->item(i)->setData(QColor(value(QString("colors/color%1").arg(i), model->item(i)->data().value<QColor>()).value<QColor>()));
     }
+
+    { // load binds
+        QStandardItemModel * binds = DataManager::instance().bindsModel();
+        for(int i = 0; i < BINDS_NUMBER; i++)
+        {
+            m_binds[i].strbind = value(QString("Binds/%1").arg(m_binds[i].action), cbinds[i].strbind).toString();
+            if (m_binds[i].strbind.isEmpty() || m_binds[i].strbind == "default") m_binds[i].strbind = cbinds[i].strbind;
+        }
+    }
 }
 
 void GameUIConfig::reloadVideosValues(void)
@@ -581,3 +597,16 @@
 {
     return Form->ui.pageOptions->checkRecordAudio->isChecked();
 }
+
+// Gets a bind for a bindID
+QString GameUIConfig::bind(int bindID)
+{
+    return m_binds[bindID].strbind;
+}
+
+// Sets a bind for a bindID and saves it
+void GameUIConfig::setBind(int bindID, QString & strbind)
+{
+    m_binds[bindID].strbind = strbind;
+    setValue(QString("Binds/%1").arg(m_binds[bindID].action), strbind);
+}
--- a/QTfrontend/gameuiconfig.h	Fri Dec 28 23:54:42 2012 +0100
+++ b/QTfrontend/gameuiconfig.h	Sat Dec 29 22:50:10 2012 +0100
@@ -23,6 +23,8 @@
 #include <QStringList>
 #include <QRect>
 #include <QEvent>
+#include <QList>
+#include "binds.h"
 
 class HWForm;
 class QSettings;
@@ -64,6 +66,8 @@
         void resizeToConfigValues();
         quint32 stereoMode() const;
         void setValue(const QString & key, const QVariant & value);
+        QString bind(int bindID);
+        void setBind(int bindID, QString & strbind);
 
         QString AVFormat();
         QString videoCodec();
@@ -91,7 +95,8 @@
     private:
         bool netPasswordIsValid();
         bool eventFilter(QObject *object, QEvent *event);
-	QString temphash;
+	    QString temphash;
+        QList<BindAction> m_binds;
 };
 
 #endif
--- a/QTfrontend/hwform.cpp	Fri Dec 28 23:54:42 2012 +0100
+++ b/QTfrontend/hwform.cpp	Sat Dec 29 22:50:10 2012 +0100
@@ -155,9 +155,9 @@
     ui.pageRoomsList->setSettings(config);
     ui.pageNetGame->chatWidget->setSettings(config);
     ui.pageRoomsList->chatWidget->setSettings(config);
+    ui.pageOptions->setConfig(config);
 #ifdef VIDEOREC
     ui.pageVideos->init(config);
-    ui.pageOptions->setConfig(config);
 #endif
 
 #ifdef __APPLE__
@@ -975,18 +975,8 @@
 
 void HWForm::DeleteTeam(const QString & teamName)
 {
-    QMessageBox reallyDeleteMsg(this);
-    reallyDeleteMsg.setIcon(QMessageBox::Question);
-    reallyDeleteMsg.setWindowTitle(QMessageBox::tr("Teams - Are you sure?"));
-    reallyDeleteMsg.setText(QMessageBox::tr("Do you really want to delete the team '%1'?").arg(teamName));
-    reallyDeleteMsg.setWindowModality(Qt::WindowModal);
-    reallyDeleteMsg.setStandardButtons(QMessageBox::Ok | QMessageBox::Cancel);
-
-    if (reallyDeleteMsg.exec() == QMessageBox::Ok)
-    {
-        ui.pageEditTeam->deleteTeam(teamName);
-        UpdateTeamsLists();
-    }
+    ui.pageEditTeam->deleteTeam(teamName);
+    UpdateTeamsLists();
 }
 
 void HWForm::DeleteScheme()
--- a/QTfrontend/team.cpp	Fri Dec 28 23:54:42 2012 +0100
+++ b/QTfrontend/team.cpp	Sat Dec 29 22:50:10 2012 +0100
@@ -23,10 +23,12 @@
 #include <QCryptographicHash>
 #include <QSettings>
 #include <QStandardItemModel>
+#include <QDebug>
 
 #include "team.h"
 #include "hwform.h"
 #include "DataManager.h"
+#include "gameuiconfig.h"
 
 HWTeam::HWTeam(const QString & teamname) :
     QObject(0)
@@ -50,7 +52,7 @@
     {
         m_binds.append(BindAction());
         m_binds[i].action = cbinds[i].action;
-        m_binds[i].strbind = cbinds[i].strbind;
+        m_binds[i].strbind = QString();
     }
     m_rounds = 0;
     m_wins = 0;
@@ -110,7 +112,7 @@
     {
         m_binds.append(BindAction());
         m_binds[i].action = cbinds[i].action;
-        m_binds[i].strbind = cbinds[i].strbind;
+        m_binds[i].strbind = QString();
     }
     m_rounds = 0;
     m_wins = 0;
@@ -191,7 +193,7 @@
         m_hedgehogs[i].Suicides = teamfile.value(hh + "Suicides", 0).toInt();
     }
     for(int i = 0; i < BINDS_NUMBER; i++)
-        m_binds[i].strbind = teamfile.value(QString("Binds/%1").arg(m_binds[i].action), cbinds[i].strbind).toString();
+        m_binds[i].strbind = teamfile.value(QString("Binds/%1").arg(m_binds[i].action), QString()).toString();
     for(int i = 0; i < MAX_ACHIEVEMENTS; i++)
         if(achievements[i][0][0])
             AchievementProgress[i] = teamfile.value(QString("Achievements/%1").arg(achievements[i][0]), 0).toUInt();
@@ -257,7 +259,7 @@
     return true;
 }
 
-QStringList HWTeam::teamGameConfig(quint32 InitHealth) const
+QStringList HWTeam::teamGameConfig(quint32 InitHealth, GameUIConfig * config) const
 {
     QStringList sl;
     if (m_isNetTeam)
@@ -273,9 +275,15 @@
     sl.push_back(QString("eflag " + m_flag));
 
     if (!m_isNetTeam)
+    {
         for(int i = 0; i < BINDS_NUMBER; i++)
-            if(!m_binds[i].strbind.isEmpty())
+        {
+            if(m_binds[i].strbind.isEmpty() || m_binds[i].strbind == "default")
+                sl.push_back(QString("ebind " + config->bind(i) + " " + m_binds[i].action));
+            else
                 sl.push_back(QString("ebind " + m_binds[i].strbind + " " + m_binds[i].action));
+        }
+    }
 
     for (int t = 0; t < m_numHedgehogs; t++)
     {
--- a/QTfrontend/team.h	Fri Dec 28 23:54:42 2012 +0100
+++ b/QTfrontend/team.h	Sat Dec 29 22:50:10 2012 +0100
@@ -93,7 +93,7 @@
         void incWins();
 
         // convert team info into strings for further computation
-        QStringList teamGameConfig(quint32 InitHealth) const;
+        QStringList teamGameConfig(quint32 InitHealth, GameUIConfig * config) const;
 
         // comparison operators
         bool operator == (const HWTeam& t1) const;
--- a/QTfrontend/ui/page/pageeditteam.cpp	Fri Dec 28 23:54:42 2012 +0100
+++ b/QTfrontend/ui/page/pageeditteam.cpp	Sat Dec 29 22:50:10 2012 +0100
@@ -30,6 +30,7 @@
 #include <QDebug>
 #include "SquareLabel.h"
 #include "HWApplication.h"
+#include "keybinder.h"
 
 #include "DataManager.h"
 #include "HatModel.h"
@@ -39,16 +40,16 @@
 QLayout * PageEditTeam::bodyLayoutDefinition()
 {
     QGridLayout * pageLayout = new QGridLayout();
-    QTabWidget * tbw = new QTabWidget();
+    tbw = new QTabWidget();
     QWidget * page1 = new QWidget(this);
-    QWidget * page2 = new QWidget(this);
+    binder = new KeyBinder(this, tr("Select an action to choose a custom key bind for this team"), tr("Use my default"), tr("Reset all binds"));
+    connect(binder, SIGNAL(resetAllBinds()), this, SLOT(resetAllBinds()));
     tbw->addTab(page1, tr("General"));
-    tbw->addTab(page2, tr("Advanced"));
+    tbw->addTab(binder, tr("Custom Controls"));
     pageLayout->addWidget(tbw, 0, 0, 1, 3);
 
     QHBoxLayout * page1Layout = new QHBoxLayout(page1);
     page1Layout->setAlignment(Qt::AlignTop);
-    QGridLayout * page2Layout = new QGridLayout(page2);
 
 // ====== Page 1 ======
     QVBoxLayout * vbox1 = new QVBoxLayout();
@@ -157,52 +158,6 @@
     vbox1->addStretch();
     vbox2->addStretch();
 
-// ====== Page 2 ======
-    GBoxBinds = new QGroupBox(this);
-    GBoxBinds->setTitle(QGroupBox::tr("Key binds"));
-    QGridLayout * GBBLayout = new QGridLayout(GBoxBinds);
-    BindsBox = new QToolBox(GBoxBinds);
-    BindsBox->setLineWidth(0);
-    GBBLayout->addWidget(BindsBox);
-    page2Layout->addWidget(GBoxBinds, 0, 0);
-
-    quint16 i = 0;
-    quint16 num = 0;
-    QWidget * curW = NULL;
-    QGridLayout * pagelayout = NULL;
-    QLabel* l = NULL;
-    while (i < BINDS_NUMBER)
-    {
-        if(cbinds[i].category != NULL)
-        {
-            if(curW != NULL)
-            {
-                l = new QLabel(curW);
-                l->setText("");
-                pagelayout->addWidget(l, num++, 0, 1, 2);
-            }
-            curW = new QWidget(this);
-            BindsBox->addItem(curW, HWApplication::translate("binds (categories)", cbinds[i].category));
-            pagelayout = new QGridLayout(curW);
-            num = 0;
-        }
-        if(cbinds[i].description != NULL)
-        {
-            l = new QLabel(curW);
-            l->setText((num > 0 ? QString("\n") : QString("")) + HWApplication::translate("binds (descriptions)", cbinds[i].description));
-            pagelayout->addWidget(l, num++, 0, 1, 2);
-        }
-
-        l = new QLabel(curW);
-        l->setText(HWApplication::translate("binds", cbinds[i].name));
-        l->setAlignment(Qt::AlignRight);
-        pagelayout->addWidget(l, num, 0);
-
-        CBBind[i] = new QComboBox(curW);
-        CBBind[i]->setModel(DataManager::instance().bindsModel());
-        pagelayout->addWidget(CBBind[i++], num++, 1);
-    }
-
     return pageLayout;
 }
 
@@ -407,6 +362,9 @@
 
 void PageEditTeam::loadTeam(const HWTeam & team)
 {
+    tbw->setCurrentIndex(0);
+    binder->resetInterface();
+    
     TeamNameEdit->setText(team.name());
     CBTeamLvl->setCurrentIndex(team.difficulty());
 
@@ -431,10 +389,12 @@
     QStandardItemModel * binds = DataManager::instance().bindsModel();
     for(int i = 0; i < BINDS_NUMBER; i++)
     {
+        if (team.keyBind(i).isEmpty()) continue;
+        
         QModelIndexList mdl = binds->match(binds->index(0, 0), Qt::UserRole + 1, team.keyBind(i), 1, Qt::MatchExactly);
 
         if(mdl.size() == 1)
-            CBBind[i]->setCurrentIndex(mdl[0].row());
+            binder->setBindIndex(i, mdl[0].row());
         else
             qDebug() << "Binds: cannot find" << team.keyBind(i);
     }
@@ -465,7 +425,7 @@
     QStandardItemModel * binds = DataManager::instance().bindsModel();
     for(int i = 0; i < BINDS_NUMBER; i++)
     {
-        team.bindKey(i, binds->index(CBBind[i]->currentIndex(), 0).data(Qt::UserRole + 1).toString());
+        team.bindKey(i, binds->index(binder->bindIndex(i), 0).data(Qt::UserRole + 1).toString());
     }
 
     return team;
@@ -475,3 +435,10 @@
 {
     data().saveToFile();
 }
+
+// When the "Use default for all binds" is pressed...
+void PageEditTeam::resetAllBinds()
+{
+    for (int i = 0; i < BINDS_NUMBER; i++)
+        binder->setBindIndex(i, 0);
+}
--- a/QTfrontend/ui/page/pageeditteam.h	Fri Dec 28 23:54:42 2012 +0100
+++ b/QTfrontend/ui/page/pageeditteam.h	Sat Dec 29 22:50:10 2012 +0100
@@ -28,6 +28,7 @@
 #include "team.h"
 
 class SquareLabel;
+class KeyBinder;
 
 class PageEditTeam : public AbstractPage
 {
@@ -44,6 +45,7 @@
         void CBFort_activated(const QString & gravename);
 
     private:
+        QTabWidget * tbw;
         QSignalMapper* signalMapper1;
         QSignalMapper* signalMapper2;
         QGroupBox *GBoxHedgehogs;
@@ -60,9 +62,9 @@
         QLineEdit * TeamNameEdit;
         QLineEdit * HHNameEdit[HEDGEHOGS_PER_TEAM];
         QComboBox * HHHats[HEDGEHOGS_PER_TEAM];
-        QComboBox * CBBind[BINDS_NUMBER];
         HWTeam data();
         QString m_playerHash;
+        KeyBinder * binder;
 
         QLayout * bodyLayoutDefinition();
         QLayout * footerLayoutDefinition();
@@ -85,6 +87,7 @@
         void testSound();
 
         void fixHHname(int idx);
+        void resetAllBinds();
 };
 
 #endif
--- a/QTfrontend/ui/page/pageoptions.cpp	Fri Dec 28 23:54:42 2012 +0100
+++ b/QTfrontend/ui/page/pageoptions.cpp	Sat Dec 29 22:50:10 2012 +0100
@@ -23,14 +23,17 @@
 #include <QComboBox>
 #include <QCheckBox>
 #include <QLabel>
+#include <QTableWidget>
 #include <QLineEdit>
 #include <QSpinBox>
 #include <QTextBrowser>
-#include <QTableWidget>
+#include <QScrollArea>
+#include <QHeaderView>
 #include <QSlider>
 #include <QSignalMapper>
 #include <QColorDialog>
 #include <QStandardItemModel>
+#include <QDebug>
 
 #include "pageoptions.h"
 #include "gameuiconfig.h"
@@ -40,6 +43,8 @@
 #include "DataManager.h"
 #include "LibavInteraction.h"
 #include "AutoUpdater.h"
+#include "HWApplication.h"
+#include "keybinder.h"
 
 #ifdef __APPLE__
 #ifdef SPARKLE_ENABLED
@@ -56,9 +61,15 @@
     pageLayout->addWidget(tabs);
     QWidget * page1 = new QWidget(this);
     QWidget * page2 = new QWidget(this);
+    binder = new KeyBinder(this, tr("Select an action to change what key controls it"), tr("Reset to default"), tr("Reset all binds"));
+    connect(binder, SIGNAL(bindUpdate(int)), this, SLOT(bindUpdated(int)));
+    connect(binder, SIGNAL(resetAllBinds()), this, SLOT(resetAllBinds()));
     tabs->addTab(page1, tr("General"));
+    binderTab = tabs->addTab(binder, tr("Controls"));
     tabs->addTab(page2, tr("Advanced"));
 
+    connect(tabs, SIGNAL(currentChanged(int)), this, SLOT(tabIndexChanged(int)));
+
 #ifdef VIDEOREC
     QWidget * page3 = new QWidget(this);
     tabs->addTab(page3, tr("Video Recording"));
@@ -914,3 +925,57 @@
 
     return true;
 }
+
+// When the current tab is switched
+void PageOptions::tabIndexChanged(int index)
+{
+    if (index == binderTab) // Switched to bind tab
+    {
+        binder->resetInterface();
+
+        if (!config) return;
+
+        QStandardItemModel * binds = DataManager::instance().bindsModel();
+        for(int i = 0; i < BINDS_NUMBER; i++)
+        {
+            QString value = config->bind(i);
+            QModelIndexList mdl = binds->match(binds->index(0, 0), Qt::UserRole + 1, value, 1, Qt::MatchExactly);
+            if(mdl.size() == 1) binder->setBindIndex(i, mdl[0].row());
+        }
+    }
+
+    currentTab = index;
+}
+
+// When a key bind combobox is changed
+void PageOptions::bindUpdated(int bindID)
+{
+    int bindIndex = binder->bindIndex(bindID);
+    
+    if (bindIndex == 0) bindIndex = resetBindToDefault(bindID);
+
+    // Save bind
+    QStandardItemModel * binds = DataManager::instance().bindsModel();
+    QString strbind = binds->index(binder->bindIndex(bindID), 0).data(Qt::UserRole + 1).toString();
+    config->setBind(bindID, strbind);
+}
+
+// Changes a key bind (bindID) to its default value. This updates the bind's combo-box in the UI.
+// Returns: The bind model index of the default.
+int PageOptions::resetBindToDefault(int bindID)
+{
+    QStandardItemModel * binds = DataManager::instance().bindsModel();
+    QModelIndexList mdl = binds->match(binds->index(0, 0), Qt::UserRole + 1, cbinds[bindID].strbind, 1, Qt::MatchExactly);
+    if(mdl.size() == 1) binder->setBindIndex(bindID, mdl[0].row());
+    return mdl[0].row();
+}
+
+// Called when "reset all binds" button is pressed
+void PageOptions::resetAllBinds()
+{
+    for (int i = 0; i < BINDS_NUMBER; i++)
+    {
+        resetBindToDefault(i);
+        bindUpdated(i);
+    }
+}
--- a/QTfrontend/ui/page/pageoptions.h	Fri Dec 28 23:54:42 2012 +0100
+++ b/QTfrontend/ui/page/pageoptions.h	Sat Dec 29 22:50:10 2012 +0100
@@ -25,6 +25,7 @@
 class FPSEdit;
 class IconedGroupBox;
 class QSignalMapper;
+class KeyBinder;
 
 class PageOptions : public AbstractPage
 {
@@ -118,6 +119,7 @@
         QLayout * bodyLayoutDefinition();
         QLayout * footerLayoutDefinition();
         void connectSignals();
+        int resetBindToDefault(int bindID);
 
         bool previousFullscreenValue;
         int previousResolutionIndex;
@@ -134,6 +136,9 @@
         QPushButton *btnDefaults;
         QPushButton *btnUpdateNow;
         GameUIConfig * config;
+        KeyBinder * binder;
+        int currentTab;
+        int binderTab;
 
     private slots:
         void forceFullscreen(int index);
@@ -151,6 +156,9 @@
         void changeUseGameRes(int state);
         void changeRecordAudio(int state);
         void checkForUpdates();
+        void tabIndexChanged(int);
+        void bindUpdated(int bindID);
+        void resetAllBinds();
 
     public slots:
         void setDefaultOptions();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/QTfrontend/ui/widget/keybinder.cpp	Sat Dec 29 22:50:10 2012 +0100
@@ -0,0 +1,301 @@
+/*
+ * Hedgewars, a free turn based strategy game
+ * Copyright (c) 2004-2012 Andrey Korotaev <unC0Rr@gmail.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; version 2 of the License
+ *
+ * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
+ */
+
+#include "keybinder.h"
+#include "HWApplication.h"
+#include "DataManager.h"
+#include <QHBoxLayout>
+#include <QScrollArea>
+#include <QTableWidget>
+#include <QTableWidgetItem>
+#include <QStandardItemModel>
+#include <QAbstractItemModel>
+#include <QListWidget>
+#include <QListWidgetItem>
+#include <QPushButton>
+#include <QHeaderView>
+#include <QComboBox>
+#include <QLabel>
+#include <QFrame>
+#include <QDebug>
+
+KeyBinder::KeyBinder(QWidget * parent, const QString & helpText, const QString & defaultText, const QString & resetButtonText) : QWidget(parent)
+{
+    this->defaultText = defaultText;
+    enableSignal = false;
+    
+    // Two-column tab layout
+    QHBoxLayout * pageKeysLayout = new QHBoxLayout(this);
+    pageKeysLayout->setSpacing(0);
+    pageKeysLayout->setContentsMargins(0, 0, 0, 0);
+    
+    // Table for category list
+    QVBoxLayout * catListContainer = new QVBoxLayout();
+    catListContainer->setContentsMargins(10, 10, 10, 10);
+    catList = new QListWidget();
+    catList->setFixedWidth(180);
+    catList->setStyleSheet("QListWidget::item { font-size: 14px; } QListWidget:hover { border-color: #F6CB1C; } QListWidget::item:selected { background: #150A61; color: yellow; }");
+    catList->setFocusPolicy(Qt::NoFocus);
+    connect(catList, SIGNAL(currentRowChanged(int)), this, SLOT(changeBindingsPage(int)));
+    catListContainer->addWidget(catList);
+    pageKeysLayout->addLayout(catListContainer);
+
+    // Reset all binds button
+    if (!resetButtonText.isEmpty())
+    {
+        QPushButton * btnResetAll = new QPushButton(resetButtonText);
+        catListContainer->addWidget(btnResetAll);
+        btnResetAll->setFixedHeight(40);
+        catListContainer->setStretch(1, 0);
+        catListContainer->setSpacing(10);
+        connect(btnResetAll, SIGNAL(clicked()), this, SIGNAL(resetAllBinds()));
+    }
+
+    // Container for pages of key bindings
+    QWidget * bindingsPagesContainer = new QWidget();
+    QVBoxLayout * rightLayout = new QVBoxLayout(bindingsPagesContainer);
+
+    // Scroll area for key bindings
+    QScrollArea * scrollArea = new QScrollArea();
+    scrollArea->setContentsMargins(0, 0, 0, 0);
+    scrollArea->setWidget(bindingsPagesContainer);
+    scrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
+    scrollArea->setWidgetResizable(true);
+    scrollArea->setFrameShape(QFrame::NoFrame);
+    scrollArea->setStyleSheet("background: #130F2A;");
+
+    // Add key binding pages to bindings tab
+    pageKeysLayout->addWidget(scrollArea);
+    pageKeysLayout->setStretch(1, 1);
+
+    // Custom help text
+    QLabel * helpLabel = new QLabel();
+    helpLabel->setText(helpText);
+    helpLabel->setStyleSheet("color: #130F2A; background: #F6CB1C; border: solid 4px #F6CB1C; border-radius: 10px; padding: auto 20px;");
+    helpLabel->setFixedHeight(24);
+    rightLayout->addWidget(helpLabel, 0, Qt::AlignCenter);
+
+    // Category list and bind table row heights
+    const int rowHeight = 20;
+    QSize catSize, headerSize;
+    catSize.setHeight(36);
+    headerSize.setHeight(24);
+
+    // Category list header
+    QListWidgetItem * catListHeader = new QListWidgetItem(tr("Category"));
+    catListHeader->setSizeHint(headerSize);
+    catListHeader->setFlags(Qt::NoItemFlags);
+    catListHeader->setForeground(QBrush(QColor("#130F2A")));
+    catListHeader->setBackground(QBrush(QColor("#F6CB1C")));
+    catListHeader->setTextAlignment(Qt::AlignCenter);
+    catList->addItem(catListHeader);
+
+    // Populate
+    bindingsPages = new QHBoxLayout();
+    bindingsPages->setContentsMargins(0, 0, 0, 0);
+    rightLayout->addLayout(bindingsPages);
+    QWidget * curPage = NULL;
+    QVBoxLayout * curLayout = NULL;
+    QTableWidget * curTable = NULL;
+    bool bFirstPage = true;
+    selectedBindTable = NULL;
+    bindComboBoxCellMappings = new QHash<QObject *, QTableWidgetItem *>();
+    bindCellComboBoxMappings = new QHash<QTableWidgetItem *, QComboBox *>();
+    for (int i = 0; i < BINDS_NUMBER; i++)
+    {
+        if (cbinds[i].category != NULL)
+        {
+            // Add stretch at end of previous layout
+            if (curLayout != NULL) curLayout->insertStretch(-1, 1);
+            
+            // Category list item
+            QListWidgetItem * catItem = new QListWidgetItem(HWApplication::translate("binds (categories)", cbinds[i].category));
+            catItem->setSizeHint(catSize);
+            catList->addItem(catItem);
+            
+            // Create new page
+            curPage = new QWidget();
+            curLayout = new QVBoxLayout(curPage);
+            curLayout->setSpacing(2);
+            bindingsPages->addWidget(curPage);
+            if (!bFirstPage) curPage->setVisible(false);
+        }
+
+        // Description
+        if (cbinds[i].description != NULL)
+        {
+            QLabel * desc = new QLabel(HWApplication::translate("binds (descriptions)", cbinds[i].description));
+            curLayout->addWidget(desc, 0);
+            QFrame * divider = new QFrame();
+            divider->setFrameShape(QFrame::HLine);
+            divider->setFrameShadow(QFrame::Plain);
+            curLayout->addWidget(divider, 0);
+        }
+
+        // New table
+        if (cbinds[i].category != NULL || cbinds[i].description != NULL)
+        {
+            curTable = new QTableWidget(0, 2);
+            curTable->verticalHeader()->setVisible(false);
+            curTable->horizontalHeader()->setVisible(false);
+            curTable->horizontalHeader()->setResizeMode(QHeaderView::Stretch);
+            curTable->verticalHeader()->setDefaultSectionSize(rowHeight);
+            curTable->setShowGrid(false);
+            curTable->setStyleSheet("QTableWidget { border: none; } ");
+            curTable->setSelectionBehavior(QAbstractItemView::SelectRows);
+            curTable->setSelectionMode(QAbstractItemView::SingleSelection);
+            curTable->setFocusPolicy(Qt::NoFocus);
+            connect(curTable, SIGNAL(itemSelectionChanged()), this, SLOT(bindSelectionChanged()));
+            connect(curTable, SIGNAL(itemClicked(QTableWidgetItem *)), this, SLOT(bindCellClicked(QTableWidgetItem *)));
+            curLayout->addWidget(curTable, 0);
+        }
+
+        // Hidden combo box
+        QComboBox * comboBox = CBBind[i] = new QComboBox(curTable);
+        comboBox->setModel((QAbstractItemModel*)DataManager::instance().bindsModel());
+        comboBox->setVisible(false);
+        comboBox->setFixedWidth(200);
+        
+        // Table row
+        int row = curTable->rowCount();
+        QTableWidgetItem * nameCell = new QTableWidgetItem(HWApplication::translate("binds", cbinds[i].name));
+        nameCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
+        curTable->insertRow(row);
+        curTable->setItem(row, 0, nameCell);
+        QTableWidgetItem * bindCell = new QTableWidgetItem(comboBox->currentText());
+        bindCell->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
+        curTable->setItem(row, 1, bindCell);
+        curTable->resizeColumnsToContents();
+        curTable->setFixedHeight(curTable->verticalHeader()->length() + 10);
+
+        // Updates the text in the table cell
+        connect(comboBox, SIGNAL(currentIndexChanged(const QString &)), this, SLOT(bindChanged(const QString &)));
+
+        // Map combo box and that row's cells to each other
+        bindComboBoxCellMappings->insert(comboBox, bindCell);
+        bindCellComboBoxMappings->insert(nameCell, comboBox);
+        bindCellComboBoxMappings->insert(bindCell, comboBox);
+    }
+
+    // Add stretch at end of last layout
+    if (curLayout != NULL) curLayout->insertStretch(-1, 1);
+
+    // Go to first page
+    catList->setCurrentItem(catList->item(1));
+
+    enableSignal = true;
+}
+
+KeyBinder::~KeyBinder()
+{
+    delete bindComboBoxCellMappings;
+    delete bindCellComboBoxMappings;
+}
+
+// Switches between different pages of key binds
+void KeyBinder::changeBindingsPage(int page)
+{
+    page--; // Disregard first item (the list header)
+    int pages = bindingsPages->count();
+    for (int i = 0; i < pages; i++)
+        bindingsPages->itemAt(i)->widget()->setVisible(false);
+    bindingsPages->itemAt(page)->widget()->setVisible(true);
+}
+
+// When a key bind combobox value is changed, updates the table cell text
+void KeyBinder::bindChanged(const QString & text)
+{
+    bindComboBoxCellMappings->value(sender())->setText(text);
+
+    if (enableSignal)
+    {
+        for (int i = 0; i < BINDS_NUMBER; i++)
+        {
+            if (CBBind[i] == sender())
+            {
+                emit bindUpdate(i);
+                break;
+            }
+        }
+    }
+}
+
+// When a row in a key bind table is clicked, this shows the popup
+void KeyBinder::bindCellClicked(QTableWidgetItem * item)
+{
+    QComboBox * box = bindCellComboBoxMappings->value(item);
+    QTableWidget * table = item->tableWidget();
+    QFrame * frame = box->findChild<QFrame*>();
+    
+    box->showPopup();
+    frame->move(
+        frame->x() + table->horizontalHeader()->sectionSize(0),
+        frame->y() + (table->verticalHeader()->defaultSectionSize() * item->row())
+    );
+}
+
+// When a new row in a bind table is *selected*, this clears selection in any other table
+void KeyBinder::bindSelectionChanged()
+{
+    QTableWidget * theSender = (QTableWidget*)sender();
+    if (theSender != selectedBindTable)
+    {
+        if (selectedBindTable != NULL)
+            selectedBindTable->clearSelection();
+        selectedBindTable = theSender;
+    }
+}
+
+// Set a combobox's index
+void KeyBinder::setBindIndex(int keyIndex, int bindIndex)
+{
+    enableSignal = false;
+    CBBind[keyIndex]->setCurrentIndex(bindIndex);
+    enableSignal = true;
+}
+
+// Return a combobox's selected index
+int KeyBinder::bindIndex(int keyIndex)
+{
+    return CBBind[keyIndex]->currentIndex();
+}
+
+// Clears selection and goes to first category
+void KeyBinder::resetInterface()
+{
+    enableSignal = false;
+    
+    catList->setCurrentItem(catList->item(1));
+    changeBindingsPage(1);
+    if (selectedBindTable != NULL)
+    {
+        selectedBindTable->clearSelection();
+        selectedBindTable = NULL;
+    }
+    
+    // Default bind text
+    DataManager::instance().bindsModel()->item(0)->setData(defaultText, Qt::DisplayRole);
+    for (int i = 0; i < BINDS_NUMBER; i++)
+    {
+        CBBind[i]->setModel(DataManager::instance().bindsModel());
+        CBBind[i]->setCurrentIndex(0);
+        bindComboBoxCellMappings->value(CBBind[i])->setText(defaultText);
+    }
+
+    enableSignal = true;
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/QTfrontend/ui/widget/keybinder.h	Sat Dec 29 22:50:10 2012 +0100
@@ -0,0 +1,68 @@
+/*
+ * Hedgewars, a free turn based strategy game
+ * Copyright (c) 2004-2012 Andrey Korotaev <unC0Rr@gmail.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; version 2 of the License
+ *
+ * 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA
+ */
+
+#ifndef _KEY_BINDER_H
+#define _KEY_BINDER_H
+
+#include <QWidget>
+#include <QHash>
+
+#include "binds.h"
+
+class QListWidget;
+class QTableWidgetItem;
+class QTableWidget;
+class QBoxLayout;
+class QComboBox;
+
+// USAGE NOTE: Every time the widget comes into view, you must call resetInterface()
+
+class KeyBinder : public QWidget
+{
+    Q_OBJECT
+
+    public:
+        KeyBinder(QWidget * parent = NULL, const QString & helpText = QString(), const QString & defaultText = QString(), const QString & resetButtonText = QString());
+        ~KeyBinder();
+
+        void setBindIndex(int keyIndex, int bindIndex);
+        int bindIndex(int keyIndex);
+        void resetInterface();
+
+    private:
+        QHash<QObject *, QTableWidgetItem *> * bindComboBoxCellMappings;
+        QHash<QTableWidgetItem *, QComboBox *> * bindCellComboBoxMappings;
+        QTableWidget * selectedBindTable;
+        QListWidget * catList;
+        QBoxLayout *bindingsPages;
+        QComboBox * CBBind[BINDS_NUMBER];
+        QString defaultText;
+        bool enableSignal;
+
+    signals:
+        void bindUpdate(int bindID);
+        void resetAllBinds();
+
+    private slots:
+        void changeBindingsPage(int page);
+        void bindChanged(const QString &);
+        void bindCellClicked(QTableWidgetItem * item);
+        void bindSelectionChanged();
+};
+
+#endif // _KEY_BINDER_H
--- a/QTfrontend/util/DataManager.cpp	Fri Dec 28 23:54:42 2012 +0100
+++ b/QTfrontend/util/DataManager.cpp	Sat Dec 29 22:50:10 2012 +0100
@@ -135,6 +135,11 @@
     {
         m_bindsModel = new QStandardItemModel();
 
+        QStandardItem * firstItem = new QStandardItem();
+        firstItem->setData(tr("Use Default"), Qt::DisplayRole);
+        firstItem->setData("default", Qt::UserRole + 1);
+        m_bindsModel->appendRow(firstItem);
+
         for(int j = 0; sdlkeys[j][1][0] != '\0'; j++)
         {
             QStandardItem * item = new QStandardItem();
--- a/hedgewars/hwengine.pas	Fri Dec 28 23:54:42 2012 +0100
+++ b/hedgewars/hwengine.pas	Sat Dec 29 22:50:10 2012 +0100
@@ -264,6 +264,9 @@
             end; //end case event.type_ of
         end; //end while SDL_PollEvent(@event) <> 0 do
 
+        if (CursorMovementX <> 0) or (CursorMovementY <> 0) then
+            handlePositionUpdate(CursorMovementX * cameraKeyboardSpeed, CursorMovementY * cameraKeyboardSpeed);
+
         if (cScreenResizeDelay <> 0) and (cScreenResizeDelay < RealTicks) and
            ((cNewScreenWidth <> cScreenWidth) or (cNewScreenHeight <> cScreenHeight)) then
         begin
--- a/hedgewars/uConsts.pas	Fri Dec 28 23:54:42 2012 +0100
+++ b/hedgewars/uConsts.pas	Sat Dec 29 22:50:10 2012 +0100
@@ -43,6 +43,9 @@
     msgFailedSize        = 'failed due to size';
     msgGettingConfig     = 'Getting game config...';
 
+    // camera movement multipliers
+    cameraKeyboardSpeed : ShortInt = 10;
+
     // color constants
     cWhiteColorChannels : TSDL_Color = (r:$FF; g:$FF; b:$FF; unused:$FF);
     cNearBlackColorChannels : TSDL_Color = (r:$00; g:$00; b:$10; unused:$FF);
--- a/hedgewars/uCursor.pas	Fri Dec 28 23:54:42 2012 +0100
+++ b/hedgewars/uCursor.pas	Sat Dec 29 22:50:10 2012 +0100
@@ -5,6 +5,7 @@
 procedure init;
 procedure resetPosition;
 procedure updatePosition;
+procedure handlePositionUpdate(x, y: LongInt);
 
 implementation
 
@@ -24,15 +25,20 @@
 var x, y: LongInt;
 begin
     SDL_GetMouseState(@x, @y);
-
+    
     if(x <> cScreenWidth div 2) or (y <> cScreenHeight div 2) then
-        begin
-        CursorPoint.X:= CursorPoint.X + x - cScreenWidth div 2;
-        CursorPoint.Y:= CursorPoint.Y - y + cScreenHeight div 2;
+    begin
+        handlePositionUpdate(x - cScreenWidth div 2, y - cScreenHeight div 2);
 
         if cHasFocus then
             SDL_WarpMouse(cScreenWidth div 2, cScreenHeight div 2);
-        end
+    end
+end;
+
+procedure handlePositionUpdate(x, y: LongInt);
+begin
+    CursorPoint.X:= CursorPoint.X + x;
+    CursorPoint.Y:= CursorPoint.Y - y;
 end;
 
 end.
--- a/hedgewars/uInputHandler.pas	Fri Dec 28 23:54:42 2012 +0100
+++ b/hedgewars/uInputHandler.pas	Sat Dec 29 22:50:10 2012 +0100
@@ -39,6 +39,7 @@
 
 procedure SetBinds(var binds: TBinds);
 procedure SetDefaultBinds;
+procedure chDefaultBind(var id: shortstring);
 
 procedure ControllerInit;
 procedure ControllerAxisEvent(joy, axis: Byte; value: Integer);
@@ -70,6 +71,7 @@
     //ControllerBalls: array[0..5] of array[0..19] of array[0..1] of Integer;
     ControllerHats: array[0..5] of array[0..19] of Byte;
     ControllerButtons: array[0..5] of array[0..19] of Byte;
+    usingDBinds: boolean;
 
 function  KeyNameToCode(name: shortstring): LongInt; inline;
 begin
@@ -329,9 +331,9 @@
     binds:= binds; // avoid hint
     CurrentBinds:= DefaultBinds;
 {$ELSE}
-for t:= 0 to cKbdMaxIndex do
-    if (CurrentBinds[t] <> binds[t]) and tkbd[t] then
-        ProcessKey(t, False);
+    for t:= 0 to cKbdMaxIndex do
+        if (CurrentBinds[t] <> binds[t]) and tkbd[t] then
+            ProcessKey(t, False);
 
     CurrentBinds:= binds;
 {$ENDIF}
@@ -450,8 +452,45 @@
     ProcessKey(k +  ControllerNumAxes[joy]*2 + ControllerNumHats[joy]*4 + button, pressed);
 end;
 
+// Bind that isn't a team bind, but overrides defaultbinds.
+// When first called, DefaultBinds is cleared, because we assume we are getting a full list of dbinds.
+procedure chDefaultBind(var id: shortstring);
+var KeyName, Modifier, tmp: shortstring;
+    b: LongInt;
+begin
+KeyName:= '';
+Modifier:= '';
+
+if (not usingDBinds) then
+    begin
+    usingDBinds:= true;
+    FillByte(DefaultBinds, SizeOf(DefaultBinds), 0);
+    end;
+
+if (Pos('mod:', id) <> 0) then
+    begin
+    tmp:= '';
+    SplitBySpace(id, tmp);
+    Modifier:= id;
+    id:= tmp;
+    end;
+
+SplitBySpace(id, KeyName);
+if KeyName[1]='"' then
+    Delete(KeyName, 1, 1);
+if KeyName[byte(KeyName[0])]='"' then
+    Delete(KeyName, byte(KeyName[0]), 1);
+b:= KeyNameToCode(id, Modifier);
+if b = 0 then
+    OutError(errmsgUnknownVariable + ' "' + id + '"', false)
+else
+    DefaultBinds[b]:= KeyName;
+end;
+
 procedure initModule;
 begin
+    usingDBinds:= false;
+    RegisterVariable('dbind', @chDefaultBind, true );
 end;
 
 procedure freeModule;
--- a/hedgewars/uTeams.pas	Fri Dec 28 23:54:42 2012 +0100
+++ b/hedgewars/uTeams.pas	Sat Dec 29 22:50:10 2012 +0100
@@ -330,7 +330,7 @@
 
 function AddTeam(TeamColor: Longword): PTeam;
 var team: PTeam;
-    c: LongInt;
+    c, t: LongInt;
 begin
 TryDo(TeamsCount < cMaxTeams, 'Too many teams', true);
 New(team);
@@ -343,6 +343,9 @@
 TeamsArray[TeamsCount]:= team;
 inc(TeamsCount);
 
+for t:= 0 to cKbdMaxIndex do
+    team^.Binds[t]:= '';
+
 c:= Pred(ClansCount);
 while (c >= 0) and (ClansArray[c]^.Color <> TeamColor) do dec(c);
 if c < 0 then