From bf0cc9ba5f96df518f44f79b6484d83de650ec92 Mon Sep 17 00:00:00 2001 From: Ian Bastos Date: Sat, 27 Jul 2019 00:45:52 +0100 Subject: [PATCH] Implement keyboard hotkey bindings (heavily squashed) - add keyboard bindings button to options - hooked keyboard binding panel to button - WIP bindings drawn in activity - keyboard binding keys finally appear - keyboard binding input widget - reserved keys logic working - removed debug logs - pref saved on key release - more reserved keys - fixed memory bad alloc error - modifier bindings - scroll panel for bindings view - added sdl id to bindings - cache sdl scan id into prefs - function ids are getting picked up - more keyboard mappings - more key bindings + reserved keys - keyboard bindings working - write keyboard bindings pref on load - sync new functions to prefs - final touches - Keep those include trees flat (see 0179cefc) - Fix tabs - WIP - many to one binding - WriteDefaultPrefs is now instance method - model add, remove and save methods - clear prefs before saving - added method for checking binding conflict - notify view on key combo change - route binding notification to gameview - view foundations for overhaul - fixed memory issue + has conflicting key issue - fixed prefs not being cleared before save - override text input to do nothing - fixed many complications due to duplicated hotkeys - missing index on new model caused problems - WIP - view adaptation - WIP - add and remove button layout - more patches - fixed empty textboxes problem - WIP - frontend overhaul - fixed ordering issue - binding removal - wip - function store to hold no shortcut data - reset to defaults button added - add from no shortcut works - error message on conflict - better summary for PopBindingByFunctionId - keyboard bindings hooked to gameview keypress - do not return correcty function id if no shortcut - flatten include trees - remove debug comment - spaces to tabs --- SConscript | 2 +- src/client/Client.cpp | 17 + src/client/Client.h | 2 + src/gui/game/GameModel.cpp | 8 + src/gui/game/GameModel.h | 1 + src/gui/game/GameView.cpp | 244 ++++++------ src/gui/game/GameView.h | 4 + src/gui/interface/ScrollPanel.h | 1 + src/gui/options/OptionsController.cpp | 6 + src/gui/options/OptionsController.h | 1 + src/gui/options/OptionsView.cpp | 25 ++ .../KeyboardBindingsController.cpp | 97 +++++ .../KeyboardBindingsController.h | 41 ++ .../keyboardbindings/KeyboardBindingsMap.h | 152 ++++++++ .../KeyboardBindingsModel.cpp | 359 ++++++++++++++++++ .../keyboardbindings/KeyboardBindingsModel.h | 74 ++++ .../KeyboardBindingsTextbox.cpp | 120 ++++++ .../KeyboardBindingsTextbox.h | 31 ++ .../keyboardbindings/KeyboardBindingsView.cpp | 290 ++++++++++++++ .../keyboardbindings/KeyboardBindingsView.h | 37 ++ 20 files changed, 1385 insertions(+), 127 deletions(-) create mode 100644 src/gui/options/keyboardbindings/KeyboardBindingsController.cpp create mode 100644 src/gui/options/keyboardbindings/KeyboardBindingsController.h create mode 100644 src/gui/options/keyboardbindings/KeyboardBindingsMap.h create mode 100644 src/gui/options/keyboardbindings/KeyboardBindingsModel.cpp create mode 100644 src/gui/options/keyboardbindings/KeyboardBindingsModel.h create mode 100644 src/gui/options/keyboardbindings/KeyboardBindingsTextbox.cpp create mode 100644 src/gui/options/keyboardbindings/KeyboardBindingsTextbox.h create mode 100644 src/gui/options/keyboardbindings/KeyboardBindingsView.cpp create mode 100644 src/gui/options/keyboardbindings/KeyboardBindingsView.h diff --git a/SConscript b/SConscript index 1bc069a757..2715b202d5 100644 --- a/SConscript +++ b/SConscript @@ -551,7 +551,7 @@ if GetOption('no-install-prompt'): #Generate list of sources to compile -sources = Glob("src/*.cpp") + Glob("src/*/*.cpp") + Glob("src/*/*/*.cpp") + Glob("generated/*.cpp") + Glob("data/*.cpp") +sources = Glob("src/*.cpp") + Glob("src/*/*.cpp") + Glob("src/*/*/*.cpp") + Glob("src/*/*/*/*.cpp") + Glob("generated/*.cpp") + Glob("data/*.cpp") if not GetOption('nolua') and not GetOption('renderer') and not GetOption('font'): sources += Glob("src/lua/socket/*.c") + Glob("src/lua/LuaCompat.c") diff --git a/src/client/Client.cpp b/src/client/Client.cpp index d0a806108b..8a762d04ff 100644 --- a/src/client/Client.cpp +++ b/src/client/Client.cpp @@ -1946,6 +1946,11 @@ std::vector Client::GetPrefBoolArray(ByteString prop) return std::vector(); } +Json::Value Client::GetPrefJson(ByteString prop, Json::Value defaultValue) +{ + return GetPref(preferences, prop, defaultValue); +} + // Helper preference setting function. // To actually save any changes to preferences, we need to directly do preferences[property] = thing // any other way will set the value of a copy of preferences, not the original @@ -1979,6 +1984,18 @@ void Client::SetPref(ByteString prop, Json::Value value) } } +void Client::ClearPref(ByteString prop) +{ + try + { + preferences[prop].clear(); + } + catch (std::exception & e) + { + + } +} + void Client::SetPref(ByteString prop, std::vector value) { try diff --git a/src/client/Client.h b/src/client/Client.h index bce8f66703..2f51723d47 100644 --- a/src/client/Client.h +++ b/src/client/Client.h @@ -181,10 +181,12 @@ class Client: public Singleton { std::vector GetPrefIntegerArray(ByteString prop); std::vector GetPrefUIntegerArray(ByteString prop); std::vector GetPrefBoolArray(ByteString prop); + Json::Value GetPrefJson(ByteString prop, Json::Value defaultValue = Json::nullValue); void SetPref(ByteString prop, Json::Value value); void SetPref(ByteString property, std::vector value); void SetPrefUnicode(ByteString prop, String value); + void ClearPref(ByteString prop); }; #endif // CLIENT_H diff --git a/src/gui/game/GameModel.cpp b/src/gui/game/GameModel.cpp index 193e7ea277..250ce54d37 100644 --- a/src/gui/game/GameModel.cpp +++ b/src/gui/game/GameModel.cpp @@ -691,6 +691,14 @@ void GameModel::SetSave(SaveInfo * newSave, bool invertIncludePressure) UpdateQuickOptions(); } +void GameModel::NotifyKeyBindingsChanged() +{ + for (auto observer : observers) + { + observer->NotifyKeyBindingsChanged(this); + } +} + SaveFile * GameModel::GetSaveFile() { return currentFile; diff --git a/src/gui/game/GameModel.h b/src/gui/game/GameModel.h index c005a0b867..dc9ba724cb 100644 --- a/src/gui/game/GameModel.h +++ b/src/gui/game/GameModel.h @@ -208,6 +208,7 @@ class GameModel void SetMouseClickRequired(bool mouseClickRequired); bool GetIncludePressure(); void SetIncludePressure(bool includePressure); + void NotifyKeyBindingsChanged(); std::vector GetNotifications(); void AddNotification(Notification * notification); diff --git a/src/gui/game/GameView.cpp b/src/gui/game/GameView.cpp index 64e986f43f..a267386c63 100644 --- a/src/gui/game/GameView.cpp +++ b/src/gui/game/GameView.cpp @@ -36,6 +36,7 @@ #include "simulation/SimulationData.h" #include "simulation/ElementDefs.h" #include "ElementClasses.h" +#include "gui/options/keyboardbindings/KeyboardBindingsMap.h" #ifdef GetUserName # undef GetUserName // dammit windows @@ -462,6 +463,10 @@ GameView::GameView(): }; colourPicker = new ui::Button(ui::Point((XRES/2)-8, YRES+1), ui::Point(16, 16), "", "Pick Colour"); colourPicker->SetActionCallback(new ColourPickerAction(this)); + + // write keyboard bindings prefs if they are absent + keyboardBindingModel.LoadBindingPrefs(); + keyboardBindingModel.WriteDefaultPrefs(); } GameView::~GameView() @@ -589,6 +594,12 @@ class GameView::ToolAction: public ui::ButtonAction } }; +void GameView::NotifyKeyBindingsChanged(GameModel * sender) +{ + // resync the model + keyboardBindingModel.LoadBindingPrefs(); +} + void GameView::NotifyQuickOptionsChanged(GameModel * sender) { for (size_t i = 0; i < quickOptionButtons.size(); i++) @@ -1440,55 +1451,58 @@ void GameView::OnKeyPress(int key, int scan, bool repeat, bool shift, bool ctrl, } } + int32_t functionId = keyboardBindingModel.GetFunctionForBinding(scan, shift, ctrl, alt); + if (repeat) return; bool didKeyShortcut = true; - switch(scan) + + // please see KeyboardBindingsMap.h for mappings + switch(functionId) + { + case KeyboardBindingFunction::TOGGLE_CONSOLE: { - case SDL_SCANCODE_GRAVE: SDL_StopTextInput(); SDL_StartTextInput(); c->ShowConsole(); break; - case SDL_SCANCODE_SPACE: //Space + } + case KeyboardBindingFunction::PAUSE_SIMULATION: //Space c->SetPaused(); break; - case SDL_SCANCODE_Z: + case KeyboardBindingFunction::UNDO: if (selectMode != SelectNone && isMouseDown) break; - if (ctrl && !isMouseDown) + if (!isMouseDown) { - if (shift) - c->HistoryForward(); - else - c->HistoryRestore(); - } - else - { - isMouseDown = false; - zoomCursorFixed = false; - c->SetZoomEnabled(true); + c->HistoryRestore(); } break; - case SDL_SCANCODE_P: - case SDL_SCANCODE_F2: - if (ctrl) + case KeyboardBindingFunction::REDO: + if (selectMode != SelectNone && isMouseDown) + break; + if (!isMouseDown) { - if (shift) - c->SetActiveTool(1, "DEFAULT_UI_PROPERTY"); - else - c->SetActiveTool(0, "DEFAULT_UI_PROPERTY"); + c->HistoryForward(); } - else - screenshot(); break; - case SDL_SCANCODE_F3: + case KeyboardBindingFunction::ENABLE_ZOOM: + { + isMouseDown = false; + zoomCursorFixed = false; + c->SetZoomEnabled(true); + break; + } + case KeyboardBindingFunction::PROPERTY_TOOL: + c->SetActiveTool(1, "DEFAULT_UI_PROPERTY"); + break; + case KeyboardBindingFunction::TOGGLE_DEBUG_HUD: SetDebugHUD(!GetDebugHUD()); break; - case SDL_SCANCODE_F5: + case KeyboardBindingFunction::RELOAD_SIMULATION: c->ReloadSim(); break; - case SDL_SCANCODE_A: + case KeyboardBindingFunction::SAVE_AUTHORSHIP_INFO: if ((Client::Ref().GetAuthUser().UserElevation == User::ElevationModerator || Client::Ref().GetAuthUser().UserElevation == User::ElevationAdmin) && ctrl) { @@ -1496,120 +1510,96 @@ void GameView::OnKeyPress(int key, int scan, bool repeat, bool shift, bool ctrl, new InformationMessage("Save authorship info", authorString.FromUtf8(), true); } break; - case SDL_SCANCODE_R: - if (ctrl) - c->ReloadSim(); - break; - case SDL_SCANCODE_E: + case KeyboardBindingFunction::OPEN_ELEMENT_SEARCH: c->OpenElementSearch(); break; - case SDL_SCANCODE_F: - if (ctrl) - { - Tool *active = c->GetActiveTool(0); - if (!active->GetIdentifier().Contains("_PT_") || (ren->findingElement == active->GetToolID())) - ren->findingElement = 0; - else - ren->findingElement = active->GetToolID(); - } + case KeyboardBindingFunction::FIND_MODE: + { + Tool *active = c->GetActiveTool(0); + if (!active->GetIdentifier().Contains("_PT_") || (ren->findingElement == active->GetToolID())) + ren->findingElement = 0; else - c->FrameStep(); + ren->findingElement = active->GetToolID(); break; - case SDL_SCANCODE_G: - if (ctrl) - c->ShowGravityGrid(); - else if(shift) - c->AdjustGridSize(-1); - else - c->AdjustGridSize(1); + } + case KeyboardBindingFunction::FRAME_STEP: + c->FrameStep(); + break; + case KeyboardBindingFunction::SHOW_GRAVITY_GRID: + c->ShowGravityGrid(); + break; + case KeyboardBindingFunction::DECREASE_GRAVITY_GRID_SIZE: + c->AdjustGridSize(-1); break; - case SDL_SCANCODE_F1: + case KeyboardBindingFunction::INCREASE_GRAVITY_GRID_SIZE: + c->AdjustGridSize(1); + break; + case KeyboardBindingFunction::TOGGLE_INTRO_TEXT: if(!introText) introText = 8047; else introText = 0; break; - case SDL_SCANCODE_H: - if(ctrl) - { - if(!introText) - introText = 8047; - else - introText = 0; - } - else - showHud = !showHud; + case KeyboardBindingFunction::TOGGLE_HUD: + showHud = !showHud; break; - case SDL_SCANCODE_B: - if(ctrl) - c->SetDecoration(); - else - if (colourPicker->GetParentWindow()) - c->SetActiveMenu(lastMenu); - else - { - c->SetDecoration(true); - c->SetPaused(true); - c->SetActiveMenu(SC_DECO); - } + case KeyboardBindingFunction::TOGGLE_DECORATIONS_LAYER: + c->SetDecoration(); break; - case SDL_SCANCODE_Y: - if (ctrl) - { - c->HistoryForward(); - } + case KeyboardBindingFunction::TOGGLE_DECORATION_TOOL: + if (colourPicker->GetParentWindow()) + c->SetActiveMenu(lastMenu); else { - c->SwitchAir(); + c->SetDecoration(true); + c->SetPaused(true); + c->SetActiveMenu(SC_DECO); } break; - case SDL_SCANCODE_ESCAPE: - case SDL_SCANCODE_Q: + case KeyboardBindingFunction::TOGGLE_AIR_MODE: + c->SwitchAir(); + break; + case KeyboardBindingFunction::QUIT: ui::Engine::Ref().ConfirmExit(); break; - case SDL_SCANCODE_U: + case KeyboardBindingFunction::TOGGLE_HEAT: c->ToggleAHeat(); break; - case SDL_SCANCODE_N: + case KeyboardBindingFunction::TOGGLE_NEWTONIAN_GRAVITY: c->ToggleNewtonianGravity(); break; - case SDL_SCANCODE_EQUALS: - if(ctrl) - c->ResetSpark(); - else - c->ResetAir(); + case KeyboardBindingFunction::RESET_SPARK: + c->ResetSpark(); break; - case SDL_SCANCODE_C: - if(ctrl) - { - selectMode = SelectCopy; - selectPoint1 = selectPoint2 = ui::Point(-1, -1); - isMouseDown = false; - buttonTip = "\x0F\xEF\xEF\020Click-and-drag to specify an area to copy (right click = cancel)"; - buttonTipShow = 120; - } + case KeyboardBindingFunction::RESET_AIR: + c->ResetAir(); break; - case SDL_SCANCODE_X: - if(ctrl) - { - selectMode = SelectCut; - selectPoint1 = selectPoint2 = ui::Point(-1, -1); - isMouseDown = false; - buttonTip = "\x0F\xEF\xEF\020Click-and-drag to specify an area to copy then cut (right click = cancel)"; - buttonTipShow = 120; - } + case KeyboardBindingFunction::COPY: + { + selectMode = SelectCopy; + selectPoint1 = selectPoint2 = ui::Point(-1, -1); + isMouseDown = false; + buttonTip = "\x0F\xEF\xEF\020Click-and-drag to specify an area to copy (right click = cancel)"; + buttonTipShow = 120; break; - case SDL_SCANCODE_V: - if (ctrl) + } + case KeyboardBindingFunction::CUT: + { + selectMode = SelectCut; + selectPoint1 = selectPoint2 = ui::Point(-1, -1); + isMouseDown = false; + buttonTip = "\x0F\xEF\xEF\020Click-and-drag to specify an area to copy then cut (right click = cancel)"; + buttonTipShow = 120; + break; + } + case KeyboardBindingFunction::PASTE: + if (c->LoadClipboard()) { - if (c->LoadClipboard()) - { - selectPoint1 = selectPoint2 = mousePosition; - isMouseDown = false; - } + selectPoint1 = selectPoint2 = mousePosition; + isMouseDown = false; } break; - case SDL_SCANCODE_L: + case KeyboardBindingFunction::STAMP_TOOL: { std::vector stampList = Client::Ref().GetStamps(0, 1); if (stampList.size()) @@ -1624,34 +1614,36 @@ void GameView::OnKeyPress(int key, int scan, bool repeat, bool shift, bool ctrl, break; } } - case SDL_SCANCODE_K: + case KeyboardBindingFunction::OPEN_STAMPS: + { selectMode = SelectNone; selectPoint1 = selectPoint2 = ui::Point(-1, -1); c->OpenStamps(); break; - case SDL_SCANCODE_RIGHTBRACKET: + } + case KeyboardBindingFunction::INCREASE_BRUSH_SIZE: if(zoomEnabled && !zoomCursorFixed) c->AdjustZoomSize(1, !alt); else c->AdjustBrushSize(1, !alt, shiftBehaviour, ctrlBehaviour); break; - case SDL_SCANCODE_LEFTBRACKET: + case KeyboardBindingFunction::DECREASE_BRUSH_SIZE: if(zoomEnabled && !zoomCursorFixed) c->AdjustZoomSize(-1, !alt); else c->AdjustBrushSize(-1, !alt, shiftBehaviour, ctrlBehaviour); break; - case SDL_SCANCODE_I: - if(ctrl) - c->Install(); - else - c->InvertAirSim(); + case KeyboardBindingFunction::INSTALL_GAME: + c->Install(); break; - case SDL_SCANCODE_SEMICOLON: - if (ctrl) - c->SetReplaceModeFlags(c->GetReplaceModeFlags()^SPECIFIC_DELETE); - else - c->SetReplaceModeFlags(c->GetReplaceModeFlags()^REPLACE_MODE); + case KeyboardBindingFunction::INVERT_AIR_SIMULATION: + c->InvertAirSim(); + break; + case KeyboardBindingFunction::TOGGLE_REPLACE_MODE: + c->SetReplaceModeFlags(c->GetReplaceModeFlags()^SPECIFIC_DELETE); + break; + case KeyboardBindingFunction::TOGGLE_SPECIFIC_DELETE_MODE: + c->SetReplaceModeFlags(c->GetReplaceModeFlags()^REPLACE_MODE); break; default: didKeyShortcut = false; diff --git a/src/gui/game/GameView.h b/src/gui/game/GameView.h index 080448b775..297aa07a9e 100644 --- a/src/gui/game/GameView.h +++ b/src/gui/game/GameView.h @@ -6,6 +6,7 @@ #include "common/String.h" #include "gui/interface/Window.h" #include "simulation/Sample.h" +#include "gui/options/keyboardbindings/KeyboardBindingsModel.h" enum DrawMode { @@ -114,6 +115,8 @@ class GameView: public ui::Window SimulationSample sample; + KeyboardBindingsModel keyboardBindingModel; + void updateToolButtonScroll(); void SetSaveButtonTooltips(); @@ -182,6 +185,7 @@ class GameView: public ui::Window void NotifyInfoTipChanged(GameModel * sender); void NotifyQuickOptionsChanged(GameModel * sender); void NotifyLastToolChanged(GameModel * sender); + void NotifyKeyBindingsChanged(GameModel * sender); void ToolTip(ui::Point senderPosition, String toolTip) override; diff --git a/src/gui/interface/ScrollPanel.h b/src/gui/interface/ScrollPanel.h index ecbc8860af..b358c4bc19 100644 --- a/src/gui/interface/ScrollPanel.h +++ b/src/gui/interface/ScrollPanel.h @@ -24,6 +24,7 @@ namespace ui int GetScrollLimit(); void SetScrollPosition(int position); + inline float GetScrollPositionY() const { return offsetY; } void Draw(const Point& screenPos) override; void XTick(float dt) override; diff --git a/src/gui/options/OptionsController.cpp b/src/gui/options/OptionsController.cpp index 0848fd8dd6..a8633d7506 100644 --- a/src/gui/options/OptionsController.cpp +++ b/src/gui/options/OptionsController.cpp @@ -2,6 +2,7 @@ #include "OptionsView.h" #include "OptionsModel.h" +#include "gui/game/GameModel.h" #include "Controller.h" @@ -111,6 +112,11 @@ void OptionsController::Exit() HasExited = true; } +void OptionsController::NotifyKeyBindingsChanged() +{ + gModel->NotifyKeyBindingsChanged(); +} + OptionsController::~OptionsController() { diff --git a/src/gui/options/OptionsController.h b/src/gui/options/OptionsController.h index 237b1c40c5..6a688a199e 100644 --- a/src/gui/options/OptionsController.h +++ b/src/gui/options/OptionsController.h @@ -30,6 +30,7 @@ class OptionsController void SetShowAvatars(bool showAvatars); void SetMouseClickrequired(bool mouseClickRequired); void SetIncludePressure(bool includePressure); + void NotifyKeyBindingsChanged(); void Exit(); OptionsView * GetView(); diff --git a/src/gui/options/OptionsView.cpp b/src/gui/options/OptionsView.cpp index 7a08c680c5..4fc1199f28 100644 --- a/src/gui/options/OptionsView.cpp +++ b/src/gui/options/OptionsView.cpp @@ -18,6 +18,8 @@ #include "gui/interface/DropDown.h" #include "gui/interface/Engine.h" #include "gui/interface/Checkbox.h" +#include "keyboardbindings/KeyboardBindingsView.h" +#include "keyboardbindings/KeyboardBindingsController.h" #include "graphics/Graphics.h" @@ -448,6 +450,29 @@ OptionsView::OptionsView(): tempLabel->Appearance.HorizontalAlign = ui::Appearance::AlignLeft; tempLabel->Appearance.VerticalAlign = ui::Appearance::AlignMiddle; scrollPanel->AddChild(tempLabel); + currentY+=20; + + class KeyboardBindingsAction: public ui::ButtonAction + { + public: + KeyboardBindingsAction() { } + void ActionCallback(ui::Button * sender) override + { + OptionsView* v = (OptionsView*) sender->GetParentWindow(); + KeyboardBindingsController* keyboardBindingsController = new KeyboardBindingsController(v->c); + ui::Engine::Ref().ShowWindow(keyboardBindingsController->GetView()); + } + }; + + ui::Button * keyboardBindingsButton = new ui::Button(ui::Point(8, currentY), ui::Point(130, 16), "Open Keyboard Bindings"); + keyboardBindingsButton->SetActionCallback(new KeyboardBindingsAction()); + scrollPanel->AddChild(keyboardBindingsButton); + + tempLabel = new ui::Label(ui::Point(keyboardBindingsButton->Position.X+keyboardBindingsButton->Size.X+3, currentY), ui::Point(1, 16), "\bg- Change the keyboard bindings"); + autowidth(tempLabel); + tempLabel->Appearance.HorizontalAlign = ui::Appearance::AlignLeft; + tempLabel->Appearance.VerticalAlign = ui::Appearance::AlignMiddle; + scrollPanel->AddChild(tempLabel); class CloseAction: public ui::ButtonAction { diff --git a/src/gui/options/keyboardbindings/KeyboardBindingsController.cpp b/src/gui/options/keyboardbindings/KeyboardBindingsController.cpp new file mode 100644 index 0000000000..87e237852b --- /dev/null +++ b/src/gui/options/keyboardbindings/KeyboardBindingsController.cpp @@ -0,0 +1,97 @@ +#include "KeyboardBindingsController.h" + +#include "KeyboardBindingsView.h" +#include "Controller.h" +#include "client/Client.h" +#include "../OptionsController.h" +#include "KeyboardBindingsModel.h" + +KeyboardBindingsController::KeyboardBindingsController(OptionsController* _parent): + HasExited(false) +{ + parent = _parent; + view = new KeyboardBindingsView(); + model = new KeyboardBindingsModel(); + model->AddObserver(view); + view->AttachController(this); + LoadBindingPrefs(); + view->BuildKeyBindingsListView(); +} + +KeyboardBindingsView* KeyboardBindingsController::GetView() +{ + return view; +} + +void KeyboardBindingsController::CreateModel(BindingModel _model) +{ + model->CreateModel(_model); +} + +void KeyboardBindingsController::Save() +{ + model->Save(); +} + +void KeyboardBindingsController::ChangeModel(BindingModel _model) +{ + model->RemoveModelByIndex(_model.index); + model->AddModel(_model); +} + +void KeyboardBindingsController::Exit() +{ + view->CloseActiveWindow(); + parent->NotifyKeyBindingsChanged(); + HasExited = true; +} + +void KeyboardBindingsController::LoadBindingPrefs() +{ + model->LoadBindingPrefs(); +} + +std::vector KeyboardBindingsController::GetBindingPrefs() +{ + return model->GetBindingPrefs(); +} + +void KeyboardBindingsController::NotifyBindingsChanged() +{ + bool hasConflict = model->HasConflictingCombo(); + model->NotifyBindingsChanged(hasConflict); +} + +void KeyboardBindingsController::ForceHasConflict() +{ + view->OnKeyCombinationChanged(true); +} + +void KeyboardBindingsController::NotifyKeyReleased() +{ + view->OnKeyReleased(); +} + +void KeyboardBindingsController::PopBindingByFunctionId(int32_t functionId) +{ + model->PopBindingByFunctionId(functionId); +} + +void KeyboardBindingsController::ResetToDefaults() +{ + model->WriteDefaultPrefs(true); +} + +bool KeyboardBindingsController::FunctionHasShortcut(int32_t functionId) +{ + return model->FunctionHasShortcut(functionId); +} + +KeyboardBindingsController::~KeyboardBindingsController() +{ + view->CloseActiveWindow(); + delete view; + delete callback; + delete model; +} + diff --git a/src/gui/options/keyboardbindings/KeyboardBindingsController.h b/src/gui/options/keyboardbindings/KeyboardBindingsController.h new file mode 100644 index 0000000000..70adf52454 --- /dev/null +++ b/src/gui/options/keyboardbindings/KeyboardBindingsController.h @@ -0,0 +1,41 @@ +#ifndef KEYBOARDBINDINGSCONTROLLER_H +#define KEYBOARDBINDINGSCONTROLLER_H + +#include + +class ControllerCallback; +class KeyboardBindingsView; +class GameModel; +class OptionsController; +class KeyboardBindingsModel; +struct BindingModel; + +class KeyboardBindingsController +{ + ControllerCallback * callback; + KeyboardBindingsView* view; + KeyboardBindingsModel* model; + OptionsController* parent; +public: + bool HasExited; + KeyboardBindingsController(OptionsController* _parent); + void Exit(); + KeyboardBindingsView * GetView(); + virtual ~KeyboardBindingsController(); + void AddModel(BindingModel model); + void CreateModel(BindingModel model); + void ChangeModel(BindingModel model); + void Save(); + void ForceHasConflict(); + void NotifyKeyReleased(); + void OnKeyReleased(); + void NotifyBindingsChanged(); + void PopBindingByFunctionId(int32_t functionId); + bool FunctionHasShortcut(int32_t functionId); + void ResetToDefaults(); + + void LoadBindingPrefs(); + std::vector GetBindingPrefs(); +}; + +#endif /* KEYBOARDBINDINGSCONTROLLER_H */ diff --git a/src/gui/options/keyboardbindings/KeyboardBindingsMap.h b/src/gui/options/keyboardbindings/KeyboardBindingsMap.h new file mode 100644 index 0000000000..0add34f0b0 --- /dev/null +++ b/src/gui/options/keyboardbindings/KeyboardBindingsMap.h @@ -0,0 +1,152 @@ +#ifndef KEYBOARDBINDINGSMAP_H +#define KEYBOARDBINDINGSMAP_H + +#include "common/String.h" + +typedef struct KeyboardBindingMap +{ + uint32_t id; + String description; + int32_t functionId; +} KeyboardBindingMap; + +typedef struct DefaultKeyboardBindingMap +{ + ByteString keyCombo; + uint32_t bindingId; // KeyboardBindingMap id +} DefaultKeyboardBindingMap; + +static KeyboardBindingMap keyboardBindingFunctionMap[] = +{ + { 0x00, "Reload Simulation", 0 }, + { 0x01, "Open Element Search", 1 }, + { 0x02, "Toggle Air Mode", 2 }, + { 0x03, "Toggle Heat", 3 }, + { 0x04, "Toggle Newtonian Gravity", 4 }, + { 0x05, "Open Stamps", 5 }, + { 0x06, "Invert Air Simulation", 6 }, + { 0x07, "Pause Simulation", 7 }, + { 0x08, "Enable Zoom", 8 }, + { 0x09, "Undo", 9 }, + { 0x0A, "Redo", 10 }, + { 0x0B, "Property Tool", 11 }, + { 0x0C, "Property Tool", 11 }, + { 0x0D, "Screenshot", 12 }, + { 0x0E, "Toggle Debug HUD", 13 }, + { 0x0F, "Save Authorship Info", 14 }, + { 0x10, "Reload Simulation", 0 }, + { 0x11, "Frame Step", 15 }, + { 0x12, "Find Mode", 16 }, + { 0x13, "Show Gravity Grid", 17 }, + { 0x14, "Increase Gravity Grid Size", 18 }, + { 0x15, "Decrease Gravity Grid Size", 19 }, + { 0x16, "Toggle Intro Text", 20 }, + { 0x17, "Toggle Intro Text", 20 }, + { 0x18, "Toggle HUD", 21 }, + { 0x19, "Toggle Decorations Layer", 22 }, + { 0x1A, "Toggle Decoration Tool", 23 }, + { 0x1B, "Redo", 10 }, + { 0x1C, "Quit", 24 }, + { 0x1D, "Quit", 24 }, + { 0x1E, "Reset Spark", 25 }, + { 0x1F, "Reset Air", 26 }, + { 0x20, "Copy", 27 }, + { 0x21, "Cut", 28 }, + { 0x22, "Paste", 29 }, + { 0x23, "Stamp Tool", 30 }, + { 0x24, "Increase Brush Size", 31 }, + { 0x25, "Decrease Brush Size", 32 }, + { 0x26, "Install Game", 33 }, + { 0x27, "Toggle Replace Mode", 34 }, + { 0x28, "Toggle Specific Delete Mode", 35 }, + { 0x29, "Toggle Console", 36 } +}; + +enum KeyboardBindingFunction +{ + RELOAD_SIMULATION, + OPEN_ELEMENT_SEARCH, + TOGGLE_AIR_MODE, + TOGGLE_HEAT, + TOGGLE_NEWTONIAN_GRAVITY, + OPEN_STAMPS, + INVERT_AIR_SIMULATION, + PAUSE_SIMULATION, + ENABLE_ZOOM, + UNDO, + REDO, + PROPERTY_TOOL, + SCREENSHOT, + TOGGLE_DEBUG_HUD, + SAVE_AUTHORSHIP_INFO, + FRAME_STEP, + FIND_MODE, + SHOW_GRAVITY_GRID, + INCREASE_GRAVITY_GRID_SIZE, + DECREASE_GRAVITY_GRID_SIZE, + TOGGLE_INTRO_TEXT, + TOGGLE_HUD, + TOGGLE_DECORATIONS_LAYER, + TOGGLE_DECORATION_TOOL, + QUIT, + RESET_SPARK, + RESET_AIR, + COPY, + CUT, + PASTE, + STAMP_TOOL, + INCREASE_BRUSH_SIZE, + DECREASE_BRUSH_SIZE, + INSTALL_GAME, + TOGGLE_REPLACE_MODE, + TOGGLE_SPECIFIC_DELETE_MODE, + TOGGLE_CONSOLE +}; + +static DefaultKeyboardBindingMap defaultKeyboardBindingMapArray[] = +{ + { "0+62", 0x00 }, + { "0+8", 0x01 }, + { "0+28", 0x02 }, + { "0+24", 0x03 }, + { "0+17", 0x04 }, + { "0+14", 0x05 }, + { "0+12", 0x06 }, + { "0+44", 0x07 }, + { "0+29", 0x08 }, + { "1+29", 0x09 }, + { "5+29", 0x0A }, + { "5+69", 0x0B }, + { "1+69", 0x0C }, + { "0+69", 0x0D }, + { "0+60", 0x0E }, + { "0+4", 0x0F }, + { "1+21", 0x10 }, + { "0+9", 0x11 }, + { "1+9", 0x12 }, + { "1+10", 0x13 }, + { "0+10", 0x14 }, + { "4+10", 0x15 }, + { "0+58", 0x16 }, + { "1+11", 0x17 }, + { "0+11", 0x18 }, + { "1+5", 0x19 }, + { "0+5", 0x1A }, + { "1+28", 0x1B }, + { "0+41", 0x1C }, + { "0+20", 0x1D }, + { "1+46", 0x1E }, + { "0+46", 0x1F }, + { "1+6", 0x20 }, + { "1+27", 0x21 }, + { "1+25", 0x22 }, + { "0+15", 0x23 }, + { "0+48", 0x24 }, + { "0+47", 0x25 }, + { "1+12", 0x26 }, + { "1+51", 0x27 }, + { "0+51", 0x28 }, + { "0+53", 0x29 } +}; + +#endif diff --git a/src/gui/options/keyboardbindings/KeyboardBindingsModel.cpp b/src/gui/options/keyboardbindings/KeyboardBindingsModel.cpp new file mode 100644 index 0000000000..9fab63aa32 --- /dev/null +++ b/src/gui/options/keyboardbindings/KeyboardBindingsModel.cpp @@ -0,0 +1,359 @@ +#include "KeyboardBindingsModel.h" +#include "client/Client.h" +#include "SDLCompat.h" +#include "KeyboardBindingsMap.h" +#include +#include "KeyboardBindingsView.h" + +void KeyboardBindingsModel::WriteDefaultFuncArray(bool force) +{ + if (force) + Client::Ref().ClearPref(ByteString(KEYBOARDBINDING_FUNCS_PREF)); + + for (auto defaultBinding : defaultKeyboardBindingMapArray) + { + int32_t functionId; + String description; + for (auto functions : keyboardBindingFunctionMap) + { + if (functions.id == defaultBinding.bindingId) + { + functionId = functions.functionId; + description = functions.description; + } + } + + ByteString pref = ByteString(KEYBOARDBINDING_FUNCS_PREF) + ByteString(".") + ByteString(functionId); + bool functionExists = Client::Ref().GetPrefJson(pref, Json::nullValue) != Json::nullValue; + + if (!force && functionExists) + continue; + + Json::Value prefValue; + + prefValue["hasShortcut"] = true; + prefValue["functionId"] = functionId; + Client::Ref().SetPref(pref, prefValue); + } + +} + +void KeyboardBindingsModel::WriteDefaultPrefs(bool force) +{ + // Load temporary bindings into memory + // this is so we can add in any new axctions + // from the KeyboardBindingsMap into our prefs + LoadBindingPrefs(); + + if (force) + Client::Ref().ClearPref(ByteString(KEYBOARDBINDING_PREF)); + + WriteDefaultFuncArray(force); + + for (auto defaultBinding : defaultKeyboardBindingMapArray) + { + int32_t functionId; + String description; + for (auto functions : keyboardBindingFunctionMap) + { + if (functions.id == defaultBinding.bindingId) + { + functionId = functions.functionId; + description = functions.description; + } + } + + ByteString pref = ByteString(KEYBOARDBINDING_PREF) + ByteString(".") + defaultBinding.keyCombo; + Json::Value prefValue; + + // if we not forcing then check if the function is already set up as a pref + // if it is then bail the current iteration + if (!force) + { + if (bindingPrefs.size() > 0) + { + for (auto prefBinding : bindingPrefs) + { + if (prefBinding.functionId == functionId) + goto end; // evil but necessary + } + } + } + + prefValue["description"] = description.ToUtf8(); + prefValue["functionId"] = functionId; + Client::Ref().SetPref(pref, prefValue); + + end:; + } + + // force is from a user action so don't write into store + // until user hits OK + if (!force) + Client::Ref().WritePrefs(); + + LoadBindingPrefs(); +} + +void KeyboardBindingsModel::LoadBindingPrefs() +{ + Json::Value bindings = Client::Ref().GetPrefJson(KEYBOARDBINDING_PREF); + bindingPrefs.clear(); + + if (bindings != Json::nullValue) + { + Json::Value::Members keyComboJson = bindings.getMemberNames(); + uint32_t index = 0; + + for (auto& member : keyComboJson) + { + ByteString keyCombo(member); + ByteString pref = ByteString(KEYBOARDBINDING_PREF) + "." + keyCombo; + Json::Value result = Client::Ref().GetPrefJson(pref); + + if (result != Json::nullValue) + { + BindingModel model; + std::pair p = GetModifierAndScanFromString(keyCombo); + model.modifier = p.first; + model.scan = p.second; + model.functionId = result["functionId"].asInt(); + model.description = ByteString(result["description"].asString()).FromUtf8(); + model.index = index; + bindingPrefs.push_back(model); + } + + index++; + } + } +} + +std::pair +KeyboardBindingsModel::GetModifierAndScanFromString(ByteString str) +{ + uint32_t modifier = 0; + uint32_t scan = 0; + + if (str == "NULL") + { + scan = -1; + } + else + { + ByteString::Split split = str.SplitBy("+"); + + // not the last int so its a modifier + ByteString modString = split.Before(); + + modifier |= (std::stoi(modString) & BINDING_MASK); + scan = std::stoi(split.After()); + } + + return std::make_pair(modifier, scan); +} + +void KeyboardBindingsModel::TurnOffFunctionShortcut(int32_t functionId) +{ + ByteString pref = ByteString(KEYBOARDBINDING_FUNCS_PREF) + ByteString(".") + ByteString(functionId) + + ByteString(".hasShortcut"); + + Client::Ref().SetPref(pref, false); +} + +void KeyboardBindingsModel::TurnOnFunctionShortcut(int32_t functionId) +{ + ByteString pref = ByteString(KEYBOARDBINDING_FUNCS_PREF) + ByteString(".") + ByteString(functionId) + + ByteString(".hasShortcut"); + + Client::Ref().SetPref(pref, true); +} + +void KeyboardBindingsModel::RemoveModelByIndex(uint32_t index) +{ + std::vector::iterator it = bindingPrefs.begin(); + + while(it != bindingPrefs.end()) + { + auto& pref = *it; + if (pref.index == index) + { + bindingPrefs.erase(it); + return; + } + + it++; + } +} + +void KeyboardBindingsModel::CreateModel(BindingModel model) +{ + // if the function has no shortcut then just turn it on + if (!FunctionHasShortcut(model.functionId)) + { + TurnOnFunctionShortcut(model.functionId); + return; + } + + // index is just an session based id that we use + // to identify removals/changes + // so whenever a new model is created we just set it to the + // size of the container + model.index = bindingPrefs.size(); + bindingPrefs.push_back(model); +} + +void KeyboardBindingsModel::AddModel(BindingModel model) +{ + bindingPrefs.push_back(model); + TurnOnFunctionShortcut(model.functionId); + bool hasConflict = HasConflictingCombo(); + NotifyBindingsChanged(hasConflict); +} + +bool KeyboardBindingsModel::FunctionHasShortcut(int32_t functionId) +{ + ByteString pref = ByteString(KEYBOARDBINDING_FUNCS_PREF) + ByteString(".") + ByteString(functionId) + + ByteString(".hasShortcut"); + + return Client::Ref().GetPrefBool(pref, false); +} + +void KeyboardBindingsModel::Save() +{ + Client::Ref().ClearPref(KEYBOARDBINDING_PREF); + + for (auto& binding : bindingPrefs) + { + ByteString mod(std::to_string(binding.modifier)); + ByteString scan(std::to_string(binding.scan)); + ByteString pref = ByteString(KEYBOARDBINDING_PREF) + ByteString(".") + mod + ByteString("+") + scan; + + Json::Value val; + val["functionId"] = binding.functionId; + val["description"] = binding.description.ToUtf8(); + Client::Ref().SetPref(pref, val); + } + + Client::Ref().WritePrefs(); +} + +int32_t KeyboardBindingsModel::GetFunctionForBinding(int scan, bool shift, bool ctrl, bool alt) +{ + uint32_t modifier = 0; + + if (ctrl) + modifier |= BINDING_CTRL; + + if (alt) + modifier |= BINDING_ALT; + + if (shift) + modifier |= BINDING_SHIFT; + + auto it = std::find_if(bindingPrefs.begin(), bindingPrefs.end(), [modifier, scan](BindingModel m) + { + return m.modifier == modifier && m.scan == scan; + }); + + if (it != bindingPrefs.end()) + { + BindingModel binding = *it; + + if (FunctionHasShortcut(binding.functionId)) + return binding.functionId; + } + + return -1; +} + +/** + * Here we pop off a hotkey if the user clicks delete + * however if we are on the last remaining hotkey + * then we turn off hasShortcut for the associated function + * so it renders as *No Shortcut* on the view + */ +void KeyboardBindingsModel::PopBindingByFunctionId(int32_t functionId) +{ + std::sort(bindingPrefs.begin(), bindingPrefs.end(), [](BindingModel a, BindingModel b) + { + return a.index > b.index; + }); + + std::vector v; + for (auto b : bindingPrefs) + { + if (b.functionId == functionId) + v.push_back(b); + } + + if (v.size() == 1) + { + auto it = std::find(bindingPrefs.begin(), bindingPrefs.end(), v[0]); + TurnOffFunctionShortcut((*it).functionId); + } + else + { + auto it = bindingPrefs.begin(); + while (it != bindingPrefs.end()) + { + if ((*it).functionId == functionId) + { + bindingPrefs.erase(it); + break; + } + it++; + } + } +} + +String KeyboardBindingsModel::GetDisplayForModel(BindingModel model) +{ + return model.description; +} + +bool KeyboardBindingsModel::HasConflictingCombo() +{ + for (auto& binding : bindingPrefs) + { + // if we have any new bindings then we + // need to return a conflict until + // the user types out a binding + if (binding.isNew) + return true; + + // if the current binding has no shortcut then skip + if (!FunctionHasShortcut(binding.functionId)) + continue; + + // if key combo appears twice then there is a conflicting combo + auto iter = std::find(bindingPrefs.begin(), bindingPrefs.end(), binding); + if (iter != bindingPrefs.end()) + { + // if this time round we don't have a shortcut either + // then we can safely continue because this means + // we don't have a conflict for the current binding + if (!FunctionHasShortcut((*iter).functionId)) + continue; + + iter++; + iter = std::find(iter, bindingPrefs.end(), binding); + if (iter != bindingPrefs.end()) + return true; + } + } + + return false; +} + +void KeyboardBindingsModel::AddObserver(KeyboardBindingsView* observer) +{ + observers.push_back(observer); +} + +void KeyboardBindingsModel::NotifyBindingsChanged(bool hasConflict) +{ + for (auto& observer : observers) + { + observer->OnKeyCombinationChanged(hasConflict); + } +} \ No newline at end of file diff --git a/src/gui/options/keyboardbindings/KeyboardBindingsModel.h b/src/gui/options/keyboardbindings/KeyboardBindingsModel.h new file mode 100644 index 0000000000..6427d1103b --- /dev/null +++ b/src/gui/options/keyboardbindings/KeyboardBindingsModel.h @@ -0,0 +1,74 @@ +#ifndef KEYBOARDBINDINGSMODEL_H +#define KEYBOARDBINDINGSMODEL_H + +#include +#include +#include "common/String.h" + +#define KEYBOARDBINDING_PREF "KeyboardBindings" +#define KEYBOARDBINDING_FUNCS_PREF "KeyboardBindingFunctions" + +#define BINDING_MASK 0x07 +#define BINDING_CTRL 0x01 +#define BINDING_ALT 0x02 +#define BINDING_SHIFT 0x04 + +struct BindingModel +{ + uint32_t modifier; + uint32_t scan; + int32_t functionId; + String description; + uint32_t index; + bool isNew; + bool noShortcut; + + BindingModel() : noShortcut(false), isNew(false){}; + + bool operator==(const BindingModel& other) const + { + return modifier == other.modifier && scan == other.scan; + } + + bool operator< (const BindingModel &other) const + { + if (description == other.description) + return index < other.index; + + return description < other.description; + } +}; + +class KeyboardBindingsView; + +class KeyboardBindingsModel +{ +public: + KeyboardBindingsModel(){} + void WriteDefaultPrefs(bool force = false); // true if user clicks reset to defaults + + inline std::vector GetBindingPrefs() const { return bindingPrefs; } + void LoadBindingPrefs(); + void Save(); + void RemoveModelByIndex(uint32_t index); + void AddModel(BindingModel model); + void CreateModel(BindingModel model); + String GetDisplayForModel(BindingModel model); + void AddObserver(KeyboardBindingsView* observer); + void NotifyBindingsChanged(bool hasConflict); + bool HasConflictingCombo(); + void PopBindingByFunctionId(int32_t functionId); + void WriteDefaultFuncArray(bool force = false); + bool FunctionHasShortcut(int32_t functionId); + int32_t GetFunctionForBinding(int scan, bool shift, bool ctrl, bool alt); + +protected: + void TurnOffFunctionShortcut(int32_t functionId); + void TurnOnFunctionShortcut(int32_t functionId); + + std::vector observers; + std::vector bindingPrefs; + std::pair GetModifierAndScanFromString(ByteString str); +}; + +#endif // KEYBOARDBINDINGSMODEL_H \ No newline at end of file diff --git a/src/gui/options/keyboardbindings/KeyboardBindingsTextbox.cpp b/src/gui/options/keyboardbindings/KeyboardBindingsTextbox.cpp new file mode 100644 index 0000000000..777080fc03 --- /dev/null +++ b/src/gui/options/keyboardbindings/KeyboardBindingsTextbox.cpp @@ -0,0 +1,120 @@ +#include "KeyboardBindingsTextbox.h" + +#include "SDLCompat.h" +#include "gui/interface/Window.h" +#include "client/Client.h" +#include "KeyboardBindingsModel.h" +#include "KeyboardBindingsController.h" + +KeyboardBindingsTextbox::KeyboardBindingsTextbox(ui::Point position, ui::Point size) : + ui::Textbox(position, size) +{ + // reasonable defaults + SetTextColour(ui::Colour(255, 255, 255)); + Appearance.HorizontalAlign = ui::Appearance::AlignCentre; + Appearance.VerticalAlign = ui::Appearance::AlignMiddle; +} + +void KeyboardBindingsTextbox::OnMouseClick(int x, int y, unsigned button) +{ + prevKey = GetText(); + SetText(""); + c->ForceHasConflict(); +} + +void KeyboardBindingsTextbox::AttachController(KeyboardBindingsController* _c) +{ + c = _c; +} + +void KeyboardBindingsTextbox::SetModel(BindingModel _model) +{ + model = _model; +} + +void KeyboardBindingsTextbox::SetTextToPrevious() +{ + SetText(prevKey); +} + +void KeyboardBindingsTextbox::SetTextFromModifierAndScan(uint32_t modifier, uint32_t scan) +{ + ByteString modDisplay; + + if (modifier & BINDING_CTRL) + { + modDisplay += "CTRL+"; + } + + if (modifier & BINDING_ALT) + { + modDisplay += "ALT+"; + } + + if (modifier & BINDING_SHIFT) + { + modDisplay += "SHIFT+"; + } + + const char* scanDisplay = SDL_GetScancodeName((SDL_Scancode) scan); + ByteString keyNameDisplay(scanDisplay); + keyNameDisplay = modDisplay + keyNameDisplay.ToUpper(); + + SetText(keyNameDisplay.FromUtf8()); +} + +void KeyboardBindingsTextbox::OnKeyRelease(int key, int scan, bool repeat, bool shift, bool ctrl, bool alt) +{ + ui::Textbox::OnKeyRelease(key, scan, repeat, shift, ctrl, alt); + + uint32_t mod = 0x00; + ByteString modDisplay = ""; + + if (ctrl) + { + mod |= BINDING_CTRL; + modDisplay += "CTRL+"; + } + + if (alt) + { + mod |= BINDING_ALT; + modDisplay += "ALT+"; + } + + if (shift) + { + mod |= BINDING_SHIFT; + modDisplay += "SHIFT+"; + } + + const char* scanDisplay = SDL_GetScancodeName((SDL_Scancode) scan); + ByteString keyNameDisplay(scanDisplay); + keyNameDisplay = modDisplay + keyNameDisplay.ToUpper(); + + if (!scan) + { + SetText(prevKey); + return; + } + + SetText(keyNameDisplay.FromUtf8()); + GetParentWindow()->FocusComponent(NULL); + + BindingModel newModel; + newModel.modifier = mod; + newModel.scan = (uint32_t) scan; + newModel.functionId = model.functionId; + newModel.description = model.description; + newModel.index = model.index; + newModel.isNew = false; + newModel.noShortcut = false; + + c->ChangeModel(newModel); + + model = newModel; + + // we notify the controller so the view can recover all empty textboxes + // should the user carelessly click about + c->NotifyKeyReleased(); +} diff --git a/src/gui/options/keyboardbindings/KeyboardBindingsTextbox.h b/src/gui/options/keyboardbindings/KeyboardBindingsTextbox.h new file mode 100644 index 0000000000..a805a71b08 --- /dev/null +++ b/src/gui/options/keyboardbindings/KeyboardBindingsTextbox.h @@ -0,0 +1,31 @@ +#ifndef KEYBOARDBINDINGSTEXTBOX_H +#define KEYBOARDBINDINGSTEXTBOX_H + +#include "gui/interface/Textbox.h" +#include "KeyboardBindingsModel.h" + +class KeyboardBindingsController; + +class KeyboardBindingsTextbox: public ui::Textbox +{ +public: + KeyboardBindingsTextbox(ui::Point position, ui::Point size); + + void OnKeyRelease(int key, int scan, bool repeat, bool shift, bool ctrl, bool alt); + void OnMouseClick(int x, int y, unsigned button); + + void SetModel(BindingModel _model); + void SetTextFromModifierAndScan(uint32_t modifier, uint32_t scan); + void SetTextToPrevious(); + + void OnTextInput(String text) {} + + void AttachController(KeyboardBindingsController* _c); + +protected: + String prevKey; + KeyboardBindingsController* c; + BindingModel model; +}; + +#endif /* KEYBOARDBINDINGSTEXTBOX_H */ diff --git a/src/gui/options/keyboardbindings/KeyboardBindingsView.cpp b/src/gui/options/keyboardbindings/KeyboardBindingsView.cpp new file mode 100644 index 0000000000..6f655cab64 --- /dev/null +++ b/src/gui/options/keyboardbindings/KeyboardBindingsView.cpp @@ -0,0 +1,290 @@ +#include "KeyboardBindingsView.h" + +#include "gui/interface/Button.h" +#include "gui/interface/Label.h" +#include "gui/interface/DropDown.h" +#include "gui/interface/Engine.h" +#include "gui/interface/Checkbox.h" +#include "gui/interface/ScrollPanel.h" +#include "gui/Style.h" +#include "graphics/Graphics.h" +#include "KeyboardBindingsMap.h" +#include "KeyboardBindingsTextbox.h" +#include "KeyboardBindingsModel.h" +#include "KeyboardBindingsController.h" +#include "client/Client.h" +#include + +KeyboardBindingsView::KeyboardBindingsView() : + ui::Window(ui::Point(-1, -1), ui::Point(320, 340)) { + + ui::Label * tempLabel = new ui::Label(ui::Point(4, 1), ui::Point(Size.X / 2, 22), "Keyboard Bindings"); + tempLabel->SetTextColour(style::Colour::InformationTitle); + tempLabel->Appearance.HorizontalAlign = ui::Appearance::AlignLeft; + tempLabel->Appearance.VerticalAlign = ui::Appearance::AlignMiddle; + AddComponent(tempLabel); + + class ResetDefaultsAction: public ui::ButtonAction + { + public: + KeyboardBindingsView * v; + ResetDefaultsAction(KeyboardBindingsView * v_) { v = v_; } + void ActionCallback(ui::Button * sender) override + { + v->c->ResetToDefaults(); + v->BuildKeyBindingsListView(); + v->c->NotifyBindingsChanged(); + } + }; + + ui::Button* resetDefaults = new ui::Button(ui::Point(Size.X - 150, 5), ui::Point(140, 18), "Reset to Defaults"); + resetDefaults->SetActionCallback(new ResetDefaultsAction(this)); + AddComponent(resetDefaults); + + conflictLabel = new ui::Label(ui::Point(4, resetDefaults->Size.Y + 10), ui::Point(Size.X / 2, 18), "Please resolve conflicts or empty bindings"); + conflictLabel->SetTextColour(style::Colour::ErrorTitle); + conflictLabel->Appearance.HorizontalAlign = ui::Appearance::AlignLeft; + conflictLabel->Appearance.VerticalAlign = ui::Appearance::AlignMiddle; + conflictLabel->Visible = false; + AddComponent(conflictLabel); + + class CloseAction: public ui::ButtonAction + { + public: + KeyboardBindingsView * v; + CloseAction(KeyboardBindingsView * v_) { v = v_; } + void ActionCallback(ui::Button * sender) override + { + v->c->Save(); + v->c->Exit(); + } + }; + + okayButton = new ui::Button(ui::Point(0, Size.Y-16), ui::Point(Size.X, 16), "OK"); + okayButton->SetActionCallback(new CloseAction(this)); + AddComponent(okayButton); + SetCancelButton(okayButton); + SetOkayButton(okayButton); + scrollPanel = new ui::ScrollPanel(ui::Point(1, 50), ui::Point(Size.X-2, Size.Y-70)); + AddComponent(scrollPanel); +} + +void KeyboardBindingsView::ClearScrollPanel() +{ + int count = scrollPanel->GetChildCount(); + + for (int i = 0; i < count; i++) + { + auto com = scrollPanel->GetChild(i); + scrollPanel->RemoveChild(com); + RemoveComponent(com); + } + + RemoveComponent(scrollPanel); + textboxes.clear(); +} + +void KeyboardBindingsView::BuildKeyBindingsListView() +{ + uint32_t currentY = 0; + float scrollPos = scrollPanel->GetScrollPositionY(); + ClearScrollPanel(); + + scrollPanel = new ui::ScrollPanel(ui::Point(1, 50), ui::Point(Size.X-2, Size.Y-70)); + AddComponent(scrollPanel); + + std::vector bindingModel = c->GetBindingPrefs(); + std::sort(bindingModel.begin(), bindingModel.end()); + + for (int i = 0; i < bindingModel.size(); i++) + { + BindingModel& binding = bindingModel[i]; + + ui::Label * functionLabel = new ui::Label(ui::Point(4, currentY), ui::Point(Size.X / 2, 16), binding.description); + functionLabel->Appearance.HorizontalAlign = ui::Appearance::AlignLeft; + scrollPanel->AddChild(functionLabel); + + KeyboardBindingsTextbox* textbox; + ui::Label* noShortCutLabel; + + bool hasBinding = true; + int removeButtonPosX = 0; + + if (!c->FunctionHasShortcut(binding.functionId)) + { + hasBinding = false; + noShortCutLabel = new ui::Label(ui::Point(functionLabel->Position.X + functionLabel->Size.X + 20, currentY), ui::Point(95, 16), "(No shortcut)"); + noShortCutLabel->Appearance.HorizontalAlign = ui::Appearance::AlignCentre; + scrollPanel->AddChild(noShortCutLabel); + } + else + { + textbox = new KeyboardBindingsTextbox(ui::Point(functionLabel->Position.X + functionLabel->Size.X + 20, currentY), ui::Point(95, 16)); + textbox->SetTextFromModifierAndScan(binding.modifier, binding.scan); + textbox->SetModel(binding); + textbox->AttachController(c); + textboxes.push_back(textbox); + scrollPanel->AddChild(textbox); + removeButtonPosX = textbox->Position.X + textbox->Size.X + 5; + } + + int addButtonPosX = functionLabel->Position.X + functionLabel->Size.X - 5; + int addRemoveButtonsPosY = currentY; + currentY += 20; + + // add in all the bindings associated with the current functionId + if (hasBinding) + { + auto it = bindingModel.begin() + i + 1; + while (it != bindingModel.end()) + { + BindingModel nextBinding = *it; + if (nextBinding.functionId == binding.functionId) + { + KeyboardBindingsTextbox* tb = new KeyboardBindingsTextbox(ui::Point(functionLabel->Position.X + functionLabel->Size.X + 20, currentY), ui::Point(95, 16)); + if (!nextBinding.isNew) + tb->SetTextFromModifierAndScan(nextBinding.modifier, nextBinding.scan); + else + tb->SetText(""); + + tb->SetModel(nextBinding); + tb->AttachController(c); + textboxes.push_back(tb); + scrollPanel->AddChild(tb); + currentY += 20; + i++; + } + else + { + // the vector is sorted so once we hit unequality + // in function id then it means we are onto the next function + break; + } + + it++; + } + } + + ui::Button* addButton = new ui::Button(ui::Point(addButtonPosX, addRemoveButtonsPosY), ui::Point(20, 16), "+", "Add a binding to this action"); + + scrollPanel->AddChild(addButton); + + class AddBindingAction: public ui::ButtonAction + { + public: + KeyboardBindingsView * v; + int32_t functionId; + String desc; + + AddBindingAction(KeyboardBindingsView * v_, int32_t _functionId, String _desc) + { + v = v_; + functionId = _functionId; + desc = _desc; + } + + void ActionCallback(ui::Button * sender) override + { + auto modelArr = v->c->GetBindingPrefs(); + auto it = std::find_if(modelArr.begin(), modelArr.end(), [this](BindingModel b) + { + return b.functionId == functionId && b.isNew; + }); + + // do not add more KBT's if we have an empty one on the + // current function + if (it != modelArr.end()) + return; + + BindingModel model; + model.isNew = true; + model.functionId = functionId; + model.description = desc; // for sorting + v->c->CreateModel(model); + v->BuildKeyBindingsListView(); + v->c->NotifyBindingsChanged(); + } + }; + + class RemoveBindingAction: public ui::ButtonAction + { + public: + KeyboardBindingsView * v; + int32_t functionId; + + RemoveBindingAction(KeyboardBindingsView * v_, int32_t _functionId) + { + v = v_; + functionId = _functionId; + } + + void ActionCallback(ui::Button * sender) override + { + v->c->PopBindingByFunctionId(functionId); + v->BuildKeyBindingsListView(); + v->c->NotifyBindingsChanged(); + } + }; + + addButton->SetActionCallback(new AddBindingAction(this, binding.functionId, binding.description)); + + // only add in a remove button if we have a binding attached to the current function + if (hasBinding) + { + ui::Button* removeButton = new ui::Button(ui::Point(removeButtonPosX, addRemoveButtonsPosY), ui::Point(20, 16), "-", "Remove a binding from this action"); + scrollPanel->AddChild(removeButton); + removeButton->SetActionCallback(new RemoveBindingAction(this, binding.functionId)); + } + } + + scrollPanel->InnerSize = ui::Point(Size.X, currentY); + scrollPanel->SetScrollPosition(scrollPos); +} + +void KeyboardBindingsView::OnKeyReleased() +{ + for (auto textbox : textboxes) + { + if (textbox->GetText().length() == 0) + { + textbox->SetTextToPrevious(); + } + } +} + +void KeyboardBindingsView::AttachController(KeyboardBindingsController* c_) +{ + c = c_; +} + +void KeyboardBindingsView::OnDraw() +{ + Graphics * g = GetGraphics(); + g->clearrect(Position.X-2, Position.Y-2, Size.X+3, Size.Y+3); + g->drawrect(Position.X, Position.Y, Size.X, Size.Y, 255, 255, 255, 255); +} + +void KeyboardBindingsView::OnTryExit(ExitMethod method) +{ + c->Exit(); +} + +void KeyboardBindingsView::OnKeyCombinationChanged(bool hasConflict) +{ + // disable OK button if there's a conflict + if (hasConflict) + { + okayButton->Enabled = false; + conflictLabel->Visible = true; + } + else + { + okayButton->Enabled = true; + conflictLabel->Visible = false; + } +} + +KeyboardBindingsView::~KeyboardBindingsView() +{ + +} diff --git a/src/gui/options/keyboardbindings/KeyboardBindingsView.h b/src/gui/options/keyboardbindings/KeyboardBindingsView.h new file mode 100644 index 0000000000..9e06d8d693 --- /dev/null +++ b/src/gui/options/keyboardbindings/KeyboardBindingsView.h @@ -0,0 +1,37 @@ +#ifndef KEYBOARDBINDINGSVIEW_H +#define KEYBOARDBINDINGSVIEW_H + +#include "gui/interface/Window.h" + +namespace ui +{ + class ScrollPanel; + class Button; + class Label; +} + +class KeyboardBindingsController; +class KeyboardBindingsTextbox; + +class KeyboardBindingsView: public ui::Window +{ + ui::ScrollPanel* scrollPanel; + KeyboardBindingsController* c; +public: + KeyboardBindingsView(); + void OnDraw() override; + void OnTryExit(ExitMethod method) override; + void AttachController(KeyboardBindingsController* controller); + virtual ~KeyboardBindingsView(); + void OnKeyCombinationChanged(bool hasConflict); + void BuildKeyBindingsListView(); + void OnKeyReleased(); + void ClearScrollPanel(); + +protected: + ui::Button* okayButton; + ui::Label* conflictLabel; + std::vector textboxes; +}; + +#endif /* KEYBOARDBINDINGSVIEW_H */