2
0
Эх сурвалжийг харах

Merge pull request #4013 from IvanSavenko/shortcuts_additions

[1.5.2] Shortcuts additions
Ivan Savenko 1 жил өмнө
parent
commit
d1aa4ecd10
52 өөрчлөгдсөн 1178 нэмэгдсэн , 616 устгасан
  1. 2 0
      client/CMakeLists.txt
  2. 1 0
      client/CPlayerInterface.cpp
  3. 2 2
      client/adventureMap/AdventureMapInterface.cpp
  4. 1 1
      client/adventureMap/AdventureMapInterface.h
  5. 78 9
      client/adventureMap/AdventureMapShortcuts.cpp
  6. 5 0
      client/adventureMap/AdventureMapShortcuts.h
  7. 1 1
      client/adventureMap/AdventureOptions.cpp
  8. 25 0
      client/battle/BattleWindow.cpp
  9. 2 0
      client/battle/BattleWindow.h
  10. 1 1
      client/eventsSDL/InputSourceKeyboard.cpp
  11. 2 1
      client/globalLobby/GlobalLobbyInviteWindow.cpp
  12. 3 4
      client/globalLobby/GlobalLobbyLoginWindow.cpp
  13. 3 4
      client/globalLobby/GlobalLobbyRoomWindow.cpp
  14. 3 2
      client/globalLobby/GlobalLobbyServerSetup.cpp
  15. 4 3
      client/gui/InterfaceObjectConfigurable.cpp
  16. 1 1
      client/gui/InterfaceObjectConfigurable.h
  17. 91 13
      client/gui/Shortcut.h
  18. 121 11
      client/gui/ShortcutHandler.cpp
  19. 17 5
      client/lobby/CBonusSelection.cpp
  20. 3 3
      client/lobby/CLobbyScreen.cpp
  21. 6 5
      client/lobby/CSelectionBase.cpp
  22. 12 13
      client/lobby/SelectionTab.cpp
  23. 7 5
      client/mainmenu/CHighScoreScreen.cpp
  24. 4 4
      client/mainmenu/CMainMenu.cpp
  25. 2 2
      client/mapView/MapView.cpp
  26. 1 1
      client/mapView/MapView.h
  27. 2 2
      client/mapView/MapViewActions.cpp
  28. 8 5
      client/mapView/MapViewController.cpp
  29. 2 2
      client/mapView/MapViewController.h
  30. 1 0
      client/widgets/CGarrisonInt.cpp
  31. 4 3
      client/widgets/markets/CAltarArtifacts.cpp
  32. 3 2
      client/widgets/markets/CAltarCreatures.cpp
  33. 2 1
      client/widgets/markets/CArtifactsBuying.cpp
  34. 2 1
      client/widgets/markets/CArtifactsSelling.cpp
  35. 2 1
      client/widgets/markets/CFreelancerGuild.cpp
  36. 2 1
      client/widgets/markets/CMarketBase.cpp
  37. 2 1
      client/widgets/markets/CMarketResources.cpp
  38. 2 1
      client/widgets/markets/CTransferResources.cpp
  39. 68 24
      client/windows/CCastleInterface.cpp
  40. 3 1
      client/windows/CCastleInterface.h
  41. 376 0
      client/windows/CExchangeWindow.cpp
  42. 78 0
      client/windows/CExchangeWindow.h
  43. 5 5
      client/windows/CHeroWindow.cpp
  44. 9 9
      client/windows/CMarketWindow.cpp
  45. 1 1
      client/windows/CQuestLog.cpp
  46. 1 1
      client/windows/CTutorialWindow.cpp
  47. 5 4
      client/windows/CWindowWithArtifacts.cpp
  48. 1 1
      client/windows/CreaturePurchaseCard.cpp
  49. 1 290
      client/windows/GUIClasses.cpp
  50. 5 57
      client/windows/GUIClasses.h
  51. 190 107
      config/shortcutsConfig.json
  52. 5 5
      config/widgets/settings/settingsMainContainer.json

+ 2 - 0
client/CMakeLists.txt

@@ -138,6 +138,7 @@ set(client_SRCS
 
 	windows/CCastleInterface.cpp
 	windows/CCreatureWindow.cpp
+	windows/CExchangeWindow.cpp
 	windows/CHeroOverview.cpp
 	windows/CHeroWindow.cpp
 	windows/CKingdomInterface.cpp
@@ -332,6 +333,7 @@ set(client_HEADERS
 
 	windows/CCastleInterface.h
 	windows/CCreatureWindow.h
+	windows/CExchangeWindow.h
 	windows/CHeroOverview.h
 	windows/CHeroWindow.h
 	windows/CKingdomInterface.h

+ 1 - 0
client/CPlayerInterface.cpp

@@ -51,6 +51,7 @@
 
 #include "windows/CCastleInterface.h"
 #include "windows/CCreatureWindow.h"
+#include "windows/CExchangeWindow.h"
 #include "windows/CHeroWindow.h"
 #include "windows/CKingdomInterface.h"
 #include "windows/CMarketWindow.h"

+ 2 - 2
client/adventureMap/AdventureMapInterface.cpp

@@ -886,9 +886,9 @@ void AdventureMapInterface::hotkeySwitchMapLevel()
 	widget->getMapView()->onMapLevelSwitched();
 }
 
-void AdventureMapInterface::hotkeyZoom(int delta)
+void AdventureMapInterface::hotkeyZoom(int delta, bool useDeadZone)
 {
-	widget->getMapView()->onMapZoomLevelChanged(delta);
+	widget->getMapView()->onMapZoomLevelChanged(delta, useDeadZone);
 }
 
 void AdventureMapInterface::onScreenResize()

+ 1 - 1
client/adventureMap/AdventureMapInterface.h

@@ -120,7 +120,7 @@ public:
 	void hotkeyEndingTurn();
 	void hotkeyNextTown();
 	void hotkeySwitchMapLevel();
-	void hotkeyZoom(int delta);
+	void hotkeyZoom(int delta, bool useDeadZone);
 
 	/// Called by PlayerInterface when specified player is ready to start his turn
 	void onHotseatWaitStarted(PlayerColor playerID);

+ 78 - 9
client/adventureMap/AdventureMapShortcuts.cpp

@@ -12,6 +12,7 @@
 #include "AdventureMapShortcuts.h"
 
 #include "../CGameInfo.h"
+#include "../CMT.h"
 #include "../CPlayerInterface.h"
 #include "../CServerHandler.h"
 #include "../PlayerLocalState.h"
@@ -77,21 +78,26 @@ std::vector<AdventureMapShortcutState> AdventureMapShortcuts::getShortcuts()
 		{ EShortcut::ADVENTURE_CAST_SPELL,       optionHeroSelected(),   [this]() { this->showSpellbook(); } },
 		{ EShortcut::ADVENTURE_GAME_OPTIONS,     optionInMapView(),      [this]() { this->adventureOptions(); } },
 		{ EShortcut::GLOBAL_OPTIONS,             optionInMapView(),      [this]() { this->systemOptions(); } },
+		{ EShortcut::ADVENTURE_FIRST_HERO,       optionInMapView(),      [this]() { this->firstHero(); } },
 		{ EShortcut::ADVENTURE_NEXT_HERO,        optionHasNextHero(),    [this]() { this->nextHero(); } },
-		{ EShortcut::GAME_END_TURN,              optionCanEndTurn(),     [this]() { this->endTurn(); } },
+		{ EShortcut::ADVENTURE_END_TURN,         optionCanEndTurn(),     [this]() { this->endTurn(); } },
 		{ EShortcut::ADVENTURE_THIEVES_GUILD,    optionInMapView(),      [this]() { this->showThievesGuild(); } },
 		{ EShortcut::ADVENTURE_VIEW_SCENARIO,    optionInMapView(),      [this]() { this->showScenarioInfo(); } },
-		{ EShortcut::GAME_SAVE_GAME,             optionInMapView(),      [this]() { this->saveGame(); } },
-		{ EShortcut::GAME_LOAD_GAME,             optionInMapView(),      [this]() { this->loadGame(); } },
+		{ EShortcut::ADVENTURE_QUIT_GAME,        optionInMapView(),      [this]() { this->quitGame(); } },
+		{ EShortcut::ADVENTURE_TO_MAIN_MENU,     optionInMapView(),      [this]() { this->toMainMenu(); } },
+		{ EShortcut::ADVENTURE_SAVE_GAME,        optionInMapView(),      [this]() { this->saveGame(); } },
+		{ EShortcut::ADVENTURE_NEW_GAME,         optionInMapView(),      [this]() { this->newGame(); } },
+		{ EShortcut::ADVENTURE_LOAD_GAME,        optionInMapView(),      [this]() { this->loadGame(); } },
+		{ EShortcut::ADVENTURE_RESTART_GAME,     optionInMapView(),      [this]() { this->restartGame(); } },
 		{ EShortcut::ADVENTURE_DIG_GRAIL,        optionHeroSelected(),   [this]() { this->digGrail(); } },
 		{ EShortcut::ADVENTURE_VIEW_PUZZLE,      optionSidePanelActive(),[this]() { this->viewPuzzleMap(); } },
-		{ EShortcut::GAME_RESTART_GAME,          optionInMapView(),      [this]() { this->restartGame(); } },
 		{ EShortcut::ADVENTURE_VISIT_OBJECT,     optionCanVisitObject(), [this]() { this->visitObject(); } },
 		{ EShortcut::ADVENTURE_VIEW_SELECTED,    optionInMapView(),      [this]() { this->openObject(); } },
-		{ EShortcut::GAME_OPEN_MARKETPLACE,      optionInMapView(),      [this]() { this->showMarketplace(); } },
-		{ EShortcut::ADVENTURE_ZOOM_IN,          optionSidePanelActive(),[this]() { this->zoom(+1); } },
-		{ EShortcut::ADVENTURE_ZOOM_OUT,         optionSidePanelActive(),[this]() { this->zoom(-1); } },
+		{ EShortcut::ADVENTURE_MARKETPLACE,      optionInMapView(),      [this]() { this->showMarketplace(); } },
+		{ EShortcut::ADVENTURE_ZOOM_IN,          optionSidePanelActive(),[this]() { this->zoom(+10); } },
+		{ EShortcut::ADVENTURE_ZOOM_OUT,         optionSidePanelActive(),[this]() { this->zoom(-10); } },
 		{ EShortcut::ADVENTURE_ZOOM_RESET,       optionSidePanelActive(),[this]() { this->zoom( 0); } },
+		{ EShortcut::ADVENTURE_FIRST_TOWN,       optionInMapView(),      [this]() { this->firstTown(); } },
 		{ EShortcut::ADVENTURE_NEXT_TOWN,        optionInMapView(),      [this]() { this->nextTown(); } },
 		{ EShortcut::ADVENTURE_NEXT_OBJECT,      optionInMapView(),      [this]() { this->nextObject(); } },
 		{ EShortcut::ADVENTURE_MOVE_HERO_SW,     optionHeroSelected(),   [this]() { this->moveHeroDirectional({-1, +1}); } },
@@ -223,6 +229,16 @@ void AdventureMapShortcuts::systemOptions()
 	GH.windows().createAndPushWindow<SettingsMainWindow>();
 }
 
+void AdventureMapShortcuts::firstHero()
+{
+	if (!LOCPLINT->localState->getWanderingHeroes().empty())
+	{
+		const auto * hero = LOCPLINT->localState->getWanderingHero(0);
+		LOCPLINT->localState->setSelection(hero);
+		owner.centerOnObject(hero);
+	}
+}
+
 void AdventureMapShortcuts::nextHero()
 {
 	const auto * currHero = LOCPLINT->localState->getCurrentHero();
@@ -288,6 +304,49 @@ void AdventureMapShortcuts::showScenarioInfo()
 	AdventureOptions::showScenarioInfo();
 }
 
+void AdventureMapShortcuts::toMainMenu()
+{
+	LOCPLINT->showYesNoDialog(
+		CGI->generaltexth->allTexts[578],
+		[]()
+		{
+			CSH->endGameplay();
+			GH.defActionsDef = 63;
+			CMM->menu->switchToTab("main");
+		},
+		0
+		);
+}
+
+void AdventureMapShortcuts::newGame()
+{
+	LOCPLINT->showYesNoDialog(
+		CGI->generaltexth->allTexts[578],
+		[]()
+		{
+			CSH->endGameplay();
+			GH.defActionsDef = 63;
+			CMM->menu->switchToTab("new");
+		},
+		nullptr
+		);
+}
+
+void AdventureMapShortcuts::quitGame()
+{
+	LOCPLINT->showYesNoDialog(
+		CGI->generaltexth->allTexts[578],
+		[]()
+		{
+			GH.dispatchMainThread( []()
+			{
+				handleQuit(false);
+			});
+		},
+		0
+		);
+}
+
 void AdventureMapShortcuts::saveGame()
 {
 	GH.windows().createAndPushWindow<CSavingScreen>();
@@ -366,6 +425,16 @@ void AdventureMapShortcuts::showMarketplace()
 		LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithMarket"));
 }
 
+void AdventureMapShortcuts::firstTown()
+{
+	if (!LOCPLINT->localState->getOwnedTowns().empty())
+	{
+		const auto * town = LOCPLINT->localState->getOwnedTown(0);
+		LOCPLINT->localState->setSelection(town);
+		owner.centerOnObject(town);
+	}
+}
+
 void AdventureMapShortcuts::nextTown()
 {
 	owner.hotkeyNextTown();
@@ -373,7 +442,7 @@ void AdventureMapShortcuts::nextTown()
 
 void AdventureMapShortcuts::zoom( int distance)
 {
-	owner.hotkeyZoom(distance);
+	owner.hotkeyZoom(distance, false);
 }
 
 void AdventureMapShortcuts::nextObject()
@@ -494,7 +563,7 @@ bool AdventureMapShortcuts::optionInWorldView()
 
 bool AdventureMapShortcuts::optionSidePanelActive()
 {
-	return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || state == EAdventureState::OTHER_HUMAN_PLAYER_TURN;
+return state == EAdventureState::MAKING_TURN || state == EAdventureState::WORLD_VIEW || state == EAdventureState::OTHER_HUMAN_PLAYER_TURN;
 }
 
 bool AdventureMapShortcuts::optionMapScrollingActive()

+ 5 - 0
client/adventureMap/AdventureMapShortcuts.h

@@ -49,10 +49,14 @@ class AdventureMapShortcuts
 	void showSpellbook();
 	void adventureOptions();
 	void systemOptions();
+	void firstHero();
 	void nextHero();
 	void endTurn();
 	void showThievesGuild();
 	void showScenarioInfo();
+	void toMainMenu();
+	void newGame();
+	void quitGame();
 	void saveGame();
 	void loadGame();
 	void digGrail();
@@ -61,6 +65,7 @@ class AdventureMapShortcuts
 	void visitObject();
 	void openObject();
 	void showMarketplace();
+	void firstTown();
 	void nextTown();
 	void nextObject();
 	void zoom( int distance);

+ 1 - 1
client/adventureMap/AdventureOptions.cpp

@@ -45,7 +45,7 @@ AdventureOptions::AdventureOptions()
 	scenInfo = std::make_shared<CButton>(Point(24, 198), AnimationPath::builtin("ADVINFO.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_SCENARIO);
 	scenInfo->addCallback(AdventureOptions::showScenarioInfo);
 	
-	replay = std::make_shared<CButton>(Point(24, 257), AnimationPath::builtin("ADVTURN.DEF"), CButton::tooltip(), [&](){ close(); });
+	replay = std::make_shared<CButton>(Point(24, 257), AnimationPath::builtin("ADVTURN.DEF"), CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_REPLAY_TURN);
 	replay->addCallback([]{ LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.replayOpponentTurnNotImplemented")); });
 
 	exit = std::make_shared<CButton>(Point(203, 313), AnimationPath::builtin("IOK6432.DEF"), CButton::tooltip(), std::bind(&AdventureOptions::close, this), EShortcut::GLOBAL_RETURN);

+ 25 - 0
client/battle/BattleWindow.cpp

@@ -27,6 +27,7 @@
 #include "../widgets/Buttons.h"
 #include "../widgets/Images.h"
 #include "../windows/CMessage.h"
+#include "../windows/CCreatureWindow.h"
 #include "../render/CAnimation.h"
 #include "../render/Canvas.h"
 #include "../render/IRenderHandler.h"
@@ -77,6 +78,8 @@ BattleWindow::BattleWindow(BattleInterface & owner):
 	addShortcut(EShortcut::BATTLE_TACTICS_NEXT, std::bind(&BattleWindow::bTacticNextStack, this));
 	addShortcut(EShortcut::BATTLE_TACTICS_END, std::bind(&BattleWindow::bTacticPhaseEnd, this));
 	addShortcut(EShortcut::BATTLE_SELECT_ACTION, std::bind(&BattleWindow::bSwitchActionf, this));
+	addShortcut(EShortcut::BATTLE_OPEN_ACTIVE_UNIT, std::bind(&BattleWindow::bOpenActiveUnit, this));
+	addShortcut(EShortcut::BATTLE_OPEN_HOVERED_UNIT, std::bind(&BattleWindow::bOpenHoveredUnit, this));
 
 	addShortcut(EShortcut::BATTLE_TOGGLE_QUEUE, [this](){ this->toggleQueueVisibility();});
 	addShortcut(EShortcut::BATTLE_TOGGLE_HEROES_STATS, [this](){ this->toggleStickyHeroWindowsVisibility();});
@@ -755,6 +758,8 @@ void BattleWindow::blockUI(bool on)
 	bool canWait = owner.stacksController->getActiveStack() ? !owner.stacksController->getActiveStack()->waitedThisTurn : false;
 
 	setShortcutBlocked(EShortcut::GLOBAL_OPTIONS, on);
+	setShortcutBlocked(EShortcut::BATTLE_OPEN_ACTIVE_UNIT, on);
+	setShortcutBlocked(EShortcut::BATTLE_OPEN_HOVERED_UNIT, on);
 	setShortcutBlocked(EShortcut::BATTLE_RETREAT, on || !owner.getBattle()->battleCanFlee());
 	setShortcutBlocked(EShortcut::BATTLE_SURRENDER, on || owner.getBattle()->battleGetSurrenderCost() < 0);
 	setShortcutBlocked(EShortcut::BATTLE_CAST_SPELL, on || owner.tacticsMode || !canCastSpells);
@@ -769,6 +774,26 @@ void BattleWindow::blockUI(bool on)
 	setShortcutBlocked(EShortcut::BATTLE_CONSOLE_UP, on && !owner.tacticsMode);
 }
 
+void BattleWindow::bOpenActiveUnit()
+{
+	const auto * unit = owner.stacksController->getActiveStack();
+
+	if (unit)
+		GH.windows().createAndPushWindow<CStackWindow>(unit, false);;
+}
+
+void BattleWindow::bOpenHoveredUnit()
+{
+	const auto units = owner.stacksController->getHoveredStacksUnitIds();
+
+	if (!units.empty())
+	{
+		const auto * unit = owner.getBattle()->battleGetStackByID(units[0]);
+		if (unit)
+			GH.windows().createAndPushWindow<CStackWindow>(unit, false);
+	}
+}
+
 std::optional<uint32_t> BattleWindow::getQueueHoveredUnitId()
 {
 	return queue->getHoveredUnitIdIfAny();

+ 2 - 0
client/battle/BattleWindow.h

@@ -56,6 +56,8 @@ class BattleWindow : public InterfaceObjectConfigurable
 	void bConsoleDownf();
 	void bTacticNextStack();
 	void bTacticPhaseEnd();
+	void bOpenActiveUnit();
+	void bOpenHoveredUnit();
 
 	/// functions for handling actions after they were confirmed by popup window
 	void reallyFlee();

+ 1 - 1
client/eventsSDL/InputSourceKeyboard.cpp

@@ -75,7 +75,7 @@ void InputSourceKeyboard::handleEventKeyDown(const SDL_KeyboardEvent & key)
 
 	auto shortcutsVector = GH.shortcuts().translateKeycode(keyName);
 
-	if (vstd::contains(shortcutsVector, EShortcut::LOBBY_ACTIVATE_INTERFACE))
+	if (vstd::contains(shortcutsVector, EShortcut::MAIN_MENU_LOBBY))
 		CSH->getGlobalLobby().activateInterface();
 
 	if (vstd::contains(shortcutsVector, EShortcut::SPECTATE_TRACK_HERO))

+ 2 - 1
client/globalLobby/GlobalLobbyInviteWindow.cpp

@@ -15,6 +15,7 @@
 
 #include "../CServerHandler.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/GraphicalPrimitiveCanvas.h"
 #include "../widgets/Images.h"
@@ -95,7 +96,7 @@ GlobalLobbyInviteWindow::GlobalLobbyInviteWindow()
 	listBackground = std::make_shared<TransparentFilledRectangle>(Rect(8, 48, 220, 324), ColorRGBA(0, 0, 0, 64), ColorRGBA(64, 80, 128, 255), 1);
 	accountList = std::make_shared<CListBox>(createAccountCardCallback, Point(10, 50), Point(0, 40), 8, 0, 0, 1 | 4, Rect(200, 0, 320, 320));
 
-	buttonClose = std::make_shared<CButton>(Point(86, 384), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this]() { close(); } );
+	buttonClose = std::make_shared<CButton>(Point(86, 384), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this]() { close(); }, EShortcut::GLOBAL_RETURN );
 
 	center();
 }

+ 3 - 4
client/globalLobby/GlobalLobbyLoginWindow.cpp

@@ -12,12 +12,11 @@
 #include "GlobalLobbyLoginWindow.h"
 
 #include "GlobalLobbyClient.h"
-#include "GlobalLobbyWindow.h"
 
 #include "../CGameInfo.h"
 #include "../CServerHandler.h"
 #include "../gui/CGuiHandler.h"
-#include "../gui/WindowHandler.h"
+#include "../gui/Shortcut.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/CTextInput.h"
 #include "../widgets/Images.h"
@@ -47,8 +46,8 @@ GlobalLobbyLoginWindow::GlobalLobbyLoginWindow()
 	labelUsername = std::make_shared<CLabel>( 10, 65, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, loginAs.toString());
 	backgroundUsername = std::make_shared<TransparentFilledRectangle>(Rect(10, 90, 264, 20), ColorRGBA(0,0,0,128), ColorRGBA(64,64,64,64));
 	inputUsername = std::make_shared<CTextInput>(Rect(15, 93, 260, 16), FONT_SMALL, ETextAlignment::CENTERLEFT, true);
-	buttonLogin = std::make_shared<CButton>(Point(10, 180), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onLogin(); });
-	buttonClose = std::make_shared<CButton>(Point(210, 180), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){ onClose(); });
+	buttonLogin = std::make_shared<CButton>(Point(10, 180), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onLogin(); }, EShortcut::GLOBAL_ACCEPT);
+	buttonClose = std::make_shared<CButton>(Point(210, 180), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){ onClose(); }, EShortcut::GLOBAL_CANCEL);
 	labelStatus = std::make_shared<CTextBox>( "", Rect(15, 115, 255, 60), 1, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE);
 
 	auto buttonRegister = std::make_shared<CToggleButton>(Point(10, 40),  AnimationPath::builtin("GSPBUT2"), CButton::tooltip(), 0);

+ 3 - 4
client/globalLobby/GlobalLobbyRoomWindow.cpp

@@ -18,6 +18,7 @@
 #include "../CGameInfo.h"
 #include "../CServerHandler.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
 #include "../mainmenu/CMainMenu.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/Images.h"
@@ -25,10 +26,8 @@
 #include "../widgets/GraphicalPrimitiveCanvas.h"
 #include "../widgets/ObjectLists.h"
 
-#include "../../lib/CConfigHandler.h"
 #include "../../lib/CGeneralTextHandler.h"
 #include "../../lib/MetaString.h"
-#include "../../lib/VCMI_Lib.h"
 #include "../../lib/modding/CModHandler.h"
 #include "../../lib/modding/CModInfo.h"
 
@@ -148,8 +147,8 @@ GlobalLobbyRoomWindow::GlobalLobbyRoomWindow(GlobalLobbyWindow * window, const s
 	labelVersionTitle = std::make_shared<CLabel>( 10, 60, FONT_MEDIUM, ETextAlignment::CENTERLEFT, Colors::YELLOW, MetaString::createFromTextID("vcmi.lobby.preview.version").toString());
 	labelVersionValue = std::make_shared<CLabel>( 10, 80, FONT_MEDIUM, ETextAlignment::CENTERLEFT, Colors::WHITE, roomDescription.gameVersion);
 
-	buttonJoin = std::make_shared<CButton>(Point(10, 360), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onJoin(); });
-	buttonClose = std::make_shared<CButton>(Point(100, 360), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){ onClose(); });
+	buttonJoin = std::make_shared<CButton>(Point(10, 360), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onJoin(); }, EShortcut::GLOBAL_ACCEPT);
+	buttonClose = std::make_shared<CButton>(Point(100, 360), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){ onClose(); }, EShortcut::GLOBAL_CANCEL);
 
 	MetaString joinStatusText;
 	std::string errorMessage = getJoinRoomErrorMessage(roomDescription, modVerificationList);

+ 3 - 2
client/globalLobby/GlobalLobbyServerSetup.cpp

@@ -16,6 +16,7 @@
 #include "../CGameInfo.h"
 #include "../CServerHandler.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
 #include "../mainmenu/CMainMenu.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/Images.h"
@@ -74,8 +75,8 @@ GlobalLobbyServerSetup::GlobalLobbyServerSetup()
 
 	labelDescription = std::make_shared<CTextBox>("", Rect(10, 195, pos.w - 20, 80), 1, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE);
 
-	buttonCreate = std::make_shared<CButton>(Point(10, 300), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onCreate(); });
-	buttonClose = std::make_shared<CButton>(Point(210, 300), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){ onClose(); });
+	buttonCreate = std::make_shared<CButton>(Point(10, 300), AnimationPath::builtin("MuBchck"), CButton::tooltip(), [this](){ onCreate(); }, EShortcut::GLOBAL_ACCEPT);
+	buttonClose = std::make_shared<CButton>(Point(210, 300), AnimationPath::builtin("MuBcanc"), CButton::tooltip(), [this](){ onClose(); }, EShortcut::GLOBAL_CANCEL);
 
 	filledBackground->playerColored(PlayerColor(1));
 

+ 4 - 3
client/gui/InterfaceObjectConfigurable.cpp

@@ -492,7 +492,7 @@ void InterfaceObjectConfigurable::loadButtonHotkey(std::shared_ptr<CButton> butt
 		return;
 
 	button->addCallback(target->second.callback);
-	target->second.assignedToButton = true;
+	target->second.assignedButtons.push_back(button);
 }
 
 std::shared_ptr<CLabelGroup> InterfaceObjectConfigurable::buildLabelGroup(const JsonNode & config) const
@@ -821,8 +821,9 @@ void InterfaceObjectConfigurable::keyPressed(EShortcut key)
 	if (target == shortcuts.end())
 		return;
 
-	if (target->second.assignedToButton)
-		return; // will be handled by button instance
+	for (auto const & button :target->second.assignedButtons)
+		if (button->isActive())
+			return; // will be handled by button instance
 
 	if (target->second.blocked)
 		return;

+ 1 - 1
client/gui/InterfaceObjectConfigurable.h

@@ -118,7 +118,7 @@ private:
 	struct ShortcutState
 	{
 		std::function<void()> callback;
-		mutable bool assignedToButton = false;
+		mutable std::vector<std::shared_ptr<CButton>> assignedButtons;
 		bool blocked = false;
 	};
 	

+ 91 - 13
client/gui/Shortcut.h

@@ -66,25 +66,55 @@ enum class EShortcut
 	MAIN_MENU_CAMPAIGN_AB,
 	MAIN_MENU_CAMPAIGN_CUSTOM,
 
+	MAIN_MENU_HOTSEAT,
+	MAIN_MENU_LOBBY,
+	MAIN_MENU_HOST_GAME,
+	MAIN_MENU_JOIN_GAME,
+
+	HIGH_SCORES_CAMPAIGNS,
+	HIGH_SCORES_SCENARIOS,
+	HIGH_SCORES_RESET,
+
 	// Game lobby / scenario selection
 	LOBBY_BEGIN_STANDARD_GAME, // b
 	LOBBY_BEGIN_CAMPAIGN, // Return
 	LOBBY_LOAD_GAME,  // l, Return
 	LOBBY_SAVE_GAME,  // s, Return
 	LOBBY_RANDOM_MAP, // Open random map tab
-	LOBBY_HIDE_CHAT,
+	LOBBY_TOGGLE_CHAT,
 	LOBBY_ADDITIONAL_OPTIONS, // Open additional options tab
 	LOBBY_SELECT_SCENARIO,    // Open map list tab
+	LOBBY_REPLAY_VIDEO,
+	LOBBY_EXTRA_OPTIONS,
+	LOBBY_TURN_OPTIONS,
+	LOBBY_INVITE_PLAYERS,
+	LOBBY_FLIP_COIN,
+	LOBBY_RANDOM_TOWN,
+	LOBBY_RANDOM_TOWN_VS,
+
+	MAPS_SIZE_S,
+	MAPS_SIZE_M,
+	MAPS_SIZE_L,
+	MAPS_SIZE_XL,
+	MAPS_SIZE_ALL,
+
+	MAPS_SORT_PLAYERS,
+	MAPS_SORT_SIZE,
+	MAPS_SORT_FORMAT,
+	MAPS_SORT_NAME,
+	MAPS_SORT_VICTORY,
+	MAPS_SORT_DEFEAT,
+	MAPS_SORT_MAPS,
+	MAPS_SORT_CHANGEDATE,
+
+	SETTINGS_LOAD_GAME,
+	SETTINGS_SAVE_GAME,
+	SETTINGS_NEW_GAME,
+	SETTINGS_RESTART_GAME,
+	SETTINGS_TO_MAIN_MENU,
+	SETTINGS_QUIT_GAME,
 
 	// In-game hotkeys, require game state but may be available in windows other than adventure map
-	GAME_END_TURN,
-	GAME_LOAD_GAME,
-	GAME_SAVE_GAME,
-	GAME_RESTART_GAME,
-	GAME_TO_MAIN_MENU,
-	GAME_QUIT_GAME,
-	GAME_OPEN_MARKETPLACE,
-	GAME_OPEN_THIEVES_GUILD,
 	GAME_ACTIVATE_CONSOLE, // Tab, activates in-game console
 
 	// Adventure map screen
@@ -98,10 +128,11 @@ enum class EShortcut
 	ADVENTURE_VIEW_SELECTED,// Open window with currently selected hero/town
 	ADVENTURE_NEXT_TOWN,
 	ADVENTURE_NEXT_HERO,
-	ADVENTURE_NEXT_OBJECT,  // TODO: context-sensitive next object - select next hero/town, depending on current selection
-	ADVENTURE_FIRST_TOWN,   // TODO: select first available town in the list
-	ADVENTURE_FIRST_HERO,   // TODO: select first available hero in the list
+	ADVENTURE_NEXT_OBJECT,  // context-sensitive next object - select next hero/town, depending on current selection
+	ADVENTURE_FIRST_TOWN,   // select first available town in the list
+	ADVENTURE_FIRST_HERO,   // select first available hero in the list
 	ADVENTURE_VIEW_SCENARIO,// View Scenario Information window
+	ADVENTURE_REPLAY_TURN,
 	ADVENTURE_DIG_GRAIL,
 	ADVENTURE_VIEW_PUZZLE,
 	ADVENTURE_VIEW_WORLD,
@@ -113,11 +144,19 @@ enum class EShortcut
 	ADVENTURE_KINGDOM_OVERVIEW,
 	ADVENTURE_QUEST_LOG,
 	ADVENTURE_CAST_SPELL,
+	ADVENTURE_MARKETPLACE,
 	ADVENTURE_THIEVES_GUILD,
 	ADVENTURE_EXIT_WORLD_VIEW,
 	ADVENTURE_ZOOM_IN,
 	ADVENTURE_ZOOM_OUT,
 	ADVENTURE_ZOOM_RESET,
+	ADVENTURE_END_TURN,
+	ADVENTURE_LOAD_GAME,
+	ADVENTURE_SAVE_GAME,
+	ADVENTURE_NEW_GAME,
+	ADVENTURE_RESTART_GAME,
+	ADVENTURE_TO_MAIN_MENU,
+	ADVENTURE_QUIT_GAME,
 
 	// Move hero one tile in specified direction. Bound to cursors & numpad buttons
 	ADVENTURE_MOVE_HERO_SW,
@@ -145,8 +184,20 @@ enum class EShortcut
 	BATTLE_TACTICS_END,
 	BATTLE_SELECT_ACTION, // Alternative actions toggle
 	BATTLE_TOGGLE_HEROES_STATS,
+	BATTLE_OPEN_ACTIVE_UNIT,
+	BATTLE_OPEN_HOVERED_UNIT,
 
-	LOBBY_ACTIVATE_INTERFACE,
+	MARKET_DEAL,
+	MARKET_MAX_AMOUNT,
+	MARKET_SACRIFICE_ALL,
+	MARKET_SACRIFICE_BACKPACK,
+	MARKET_RESOURCE_PLAYER,
+	MARKET_ARTIFACT_RESOURCE,
+	MARKET_RESOURCE_ARTIFACT,
+	MARKET_CREATURE_RESOURCE,
+	MARKET_RESOURCE_RESOURCE,
+	MARKET_CREATURE_EXPERIENCE,
+	MARKET_ARTIFACT_EXPERIENCE,
 
 	SPECTATE_TRACK_HERO,
 	SPECTATE_SKIP_BATTLE,
@@ -154,11 +205,22 @@ enum class EShortcut
 
 	// Town screen
 	TOWN_OPEN_TAVERN,
+	TOWN_OPEN_HALL,
+	TOWN_OPEN_FORT,
+	TOWN_OPEN_MARKET,
+	TOWN_OPEN_MAGE_GUILD,
+	TOWN_OPEN_THIEVES_GUILD,
+	TOWN_OPEN_RECRUITMENT,
+	TOWN_OPEN_HERO_EXCHANGE,
+	TOWN_OPEN_HERO,
+	TOWN_OPEN_VISITING_HERO,
+	TOWN_OPEN_GARRISONED_HERO,
 	TOWN_SWAP_ARMIES, // Swap garrisoned and visiting armies
 
 	// Creature & creature recruitment screen
 	RECRUITMENT_MAX, // Set number of creatures to recruit to max
 	RECRUITMENT_MIN, // Set number of creatures to recruit to min (1)
+	RECRUITMENT_SWITCH_LEVEL,
 	RECRUITMENT_UPGRADE, // Upgrade current creature
 	RECRUITMENT_UPGRADE_ALL, // Upgrade all creatures (Hill Fort / Skeleton Transformer)
 
@@ -172,6 +234,7 @@ enum class EShortcut
 	HERO_LOOSE_FORMATION,
 	HERO_TIGHT_FORMATION,
 	HERO_TOGGLE_TACTICS, // b
+	HERO_ARMY_SPLIT,
 	HERO_BACKPACK,
 	HERO_COSTUME_SAVE_0,
 	HERO_COSTUME_SAVE_1,
@@ -195,6 +258,21 @@ enum class EShortcut
 	HERO_COSTUME_LOAD_8,
 	HERO_COSTUME_LOAD_9,
 
+	EXCHANGE_ARMY_TO_LEFT,
+	EXCHANGE_ARMY_TO_RIGHT,
+	EXCHANGE_ARMY_SWAP,
+	EXCHANGE_ARTIFACTS_TO_LEFT,
+	EXCHANGE_ARTIFACTS_TO_RIGHT,
+	EXCHANGE_ARTIFACTS_SWAP,
+	EXCHANGE_EQUIPPED_TO_LEFT,
+	EXCHANGE_EQUIPPED_TO_RIGHT,
+	EXCHANGE_EQUIPPED_SWAP,
+	EXCHANGE_BACKPACK_TO_LEFT,
+	EXCHANGE_BACKPACK_TO_RIGHT,
+	EXCHANGE_BACKPACK_SWAP,
+	EXCHANGE_BACKPACK_LEFT,
+	EXCHANGE_BACKPACK_RIGHT,
+
 	// Spellbook screen
 	SPELLBOOK_TAB_ADVENTURE,
 	SPELLBOOK_TAB_COMBAT,

+ 121 - 11
client/gui/ShortcutHandler.cpp

@@ -22,6 +22,25 @@ ShortcutHandler::ShortcutHandler()
 	mappedKeyboardShortcuts = loadShortcuts(config["keyboard"]);
 	mappedJoystickShortcuts = loadShortcuts(config["joystickButtons"]);
 	mappedJoystickAxes = loadShortcuts(config["joystickAxes"]);
+
+#ifndef ENABLE_GOLDMASTER
+	std::vector<EShortcut> assignedShortcuts;
+	std::vector<EShortcut> missingShortcuts;
+
+	for (auto const & entry : config["keyboard"].Struct())
+	{
+		EShortcut shortcutID = findShortcut(entry.first);
+		assert(!vstd::contains(assignedShortcuts, shortcutID));
+		assignedShortcuts.push_back(shortcutID);
+	}
+
+	for (EShortcut id = vstd::next(EShortcut::NONE, 1); id < EShortcut::AFTER_LAST; id = vstd::next(id, 1))
+		if (!vstd::contains(assignedShortcuts, id))
+			missingShortcuts.push_back(id);
+
+	if (!missingShortcuts.empty())
+		logGlobal->error("Found %d shortcuts without config entry!", missingShortcuts.size());
+#endif
 }
 
 std::multimap<std::string, EShortcut> ShortcutHandler::loadShortcuts(const JsonNode & data) const
@@ -131,22 +150,23 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const
 		{"mainMenuCampaignRoe",      EShortcut::MAIN_MENU_CAMPAIGN_ROE    },
 		{"mainMenuCampaignAb",       EShortcut::MAIN_MENU_CAMPAIGN_AB     },
 		{"mainMenuCampaignCustom",   EShortcut::MAIN_MENU_CAMPAIGN_CUSTOM },
+		{"mainMenuLobby",            EShortcut::MAIN_MENU_LOBBY           },
 		{"lobbyBeginStandardGame",   EShortcut::LOBBY_BEGIN_STANDARD_GAME },
 		{"lobbyBeginCampaign",       EShortcut::LOBBY_BEGIN_CAMPAIGN      },
 		{"lobbyLoadGame",            EShortcut::LOBBY_LOAD_GAME           },
 		{"lobbySaveGame",            EShortcut::LOBBY_SAVE_GAME           },
 		{"lobbyRandomMap",           EShortcut::LOBBY_RANDOM_MAP          },
-		{"lobbyHideChat",            EShortcut::LOBBY_HIDE_CHAT           },
+		{"lobbyToggleChat",          EShortcut::LOBBY_TOGGLE_CHAT         },
 		{"lobbyAdditionalOptions",   EShortcut::LOBBY_ADDITIONAL_OPTIONS  },
 		{"lobbySelectScenario",      EShortcut::LOBBY_SELECT_SCENARIO     },
-		{"gameEndTurn",              EShortcut::GAME_END_TURN             },
-		{"gameLoadGame",             EShortcut::GAME_LOAD_GAME            },
-		{"gameSaveGame",             EShortcut::GAME_SAVE_GAME            },
-		{"gameRestartGame",          EShortcut::GAME_RESTART_GAME         },
-		{"gameMainMenu",             EShortcut::GAME_TO_MAIN_MENU         },
-		{"gameQuitGame",             EShortcut::GAME_QUIT_GAME            },
-		{"gameOpenMarketplace",      EShortcut::GAME_OPEN_MARKETPLACE     },
-		{"gameOpenThievesGuild",     EShortcut::GAME_OPEN_THIEVES_GUILD   },
+		{"adventureEndTurn",         EShortcut::ADVENTURE_END_TURN        },
+		{"adventureLoadGame",        EShortcut::ADVENTURE_LOAD_GAME       },
+		{"adventureSaveGame",        EShortcut::ADVENTURE_SAVE_GAME       },
+		{"adventureRestartGame",     EShortcut::ADVENTURE_RESTART_GAME    },
+		{"adventureMainMenu",        EShortcut::ADVENTURE_TO_MAIN_MENU    },
+		{"adventureQuitGame",        EShortcut::ADVENTURE_QUIT_GAME       },
+		{"adventureMarketplace",     EShortcut::ADVENTURE_MARKETPLACE     },
+		{"adventureThievesGuild",    EShortcut::ADVENTURE_THIEVES_GUILD   },
 		{"gameActivateConsole",      EShortcut::GAME_ACTIVATE_CONSOLE     },
 		{"adventureGameOptions",     EShortcut::ADVENTURE_GAME_OPTIONS    },
 		{"adventureToggleGrid",      EShortcut::ADVENTURE_TOGGLE_GRID     },
@@ -201,7 +221,6 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const
 		{"battleTacticsNext",        EShortcut::BATTLE_TACTICS_NEXT       },
 		{"battleTacticsEnd",         EShortcut::BATTLE_TACTICS_END        },
 		{"battleSelectAction",       EShortcut::BATTLE_SELECT_ACTION      },
-		{"lobbyActivateInterface",   EShortcut::LOBBY_ACTIVATE_INTERFACE  },
 		{"spectateTrackHero",        EShortcut::SPECTATE_TRACK_HERO       },
 		{"spectateSkipBattle",       EShortcut::SPECTATE_SKIP_BATTLE      },
 		{"spectateSkipBattleResult", EShortcut::SPECTATE_SKIP_BATTLE_RESULT },
@@ -239,9 +258,100 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const
 		{"heroCostumeLoad8",         EShortcut::HERO_COSTUME_LOAD_8       },
 		{"heroCostumeLoad9",         EShortcut::HERO_COSTUME_LOAD_9       },
 		{"spellbookTabAdventure",    EShortcut::SPELLBOOK_TAB_ADVENTURE   },
-		{"spellbookTabCombat",       EShortcut::SPELLBOOK_TAB_COMBAT      }
+		{"spellbookTabCombat",       EShortcut::SPELLBOOK_TAB_COMBAT      },
+		{"mainMenuHotseat",          EShortcut::MAIN_MENU_HOTSEAT         },
+		{"mainMenuHostGame",         EShortcut::MAIN_MENU_HOST_GAME       },
+		{"mainMenuJoinGame",         EShortcut::MAIN_MENU_JOIN_GAME       },
+		{"highScoresCampaigns",      EShortcut::HIGH_SCORES_CAMPAIGNS     },
+		{"highScoresScenarios",      EShortcut::HIGH_SCORES_SCENARIOS     },
+		{"highScoresReset",          EShortcut::HIGH_SCORES_RESET         },
+		{"lobbyReplayVideo",         EShortcut::LOBBY_REPLAY_VIDEO        },
+		{"lobbyExtraOptions",        EShortcut::LOBBY_EXTRA_OPTIONS       },
+		{"lobbyTurnOptions",         EShortcut::LOBBY_TURN_OPTIONS        },
+		{"lobbyInvitePlayers",       EShortcut::LOBBY_INVITE_PLAYERS      },
+		{"lobbyFlipCoin",            EShortcut::LOBBY_FLIP_COIN           },
+		{"lobbyRandomTown",          EShortcut::LOBBY_RANDOM_TOWN         },
+		{"lobbyRandomTownVs",        EShortcut::LOBBY_RANDOM_TOWN_VS      },
+		{"mapsSizeS",                EShortcut::MAPS_SIZE_S               },
+		{"mapsSizeM",                EShortcut::MAPS_SIZE_M               },
+		{"mapsSizeL",                EShortcut::MAPS_SIZE_L               },
+		{"mapsSizeXl",               EShortcut::MAPS_SIZE_XL              },
+		{"mapsSizeAll",              EShortcut::MAPS_SIZE_ALL             },
+		{"mapsSortPlayers",          EShortcut::MAPS_SORT_PLAYERS         },
+		{"mapsSortSize",             EShortcut::MAPS_SORT_SIZE            },
+		{"mapsSortFormat",           EShortcut::MAPS_SORT_FORMAT          },
+		{"mapsSortName",             EShortcut::MAPS_SORT_NAME            },
+		{"mapsSortVictory",          EShortcut::MAPS_SORT_VICTORY         },
+		{"mapsSortDefeat",           EShortcut::MAPS_SORT_DEFEAT          },
+		{"mapsSortMaps",             EShortcut::MAPS_SORT_MAPS            },
+		{"mapsSortChangedate",       EShortcut::MAPS_SORT_CHANGEDATE      },
+		{"settingsLoadGame",         EShortcut::SETTINGS_LOAD_GAME        },
+		{"settingsSaveGame",         EShortcut::SETTINGS_SAVE_GAME        },
+		{"settingsNewGame",          EShortcut::SETTINGS_NEW_GAME         },
+		{"settingsRestartGame",      EShortcut::SETTINGS_RESTART_GAME     },
+		{"settingsToMainMenu",       EShortcut::SETTINGS_TO_MAIN_MENU     },
+		{"settingsQuitGame",         EShortcut::SETTINGS_QUIT_GAME        },
+		{"adventureReplayTurn",      EShortcut::ADVENTURE_REPLAY_TURN     },
+		{"adventureNewGame",         EShortcut::ADVENTURE_NEW_GAME        },
+		{"battleOpenActiveUnit",     EShortcut::BATTLE_OPEN_ACTIVE_UNIT   },
+		{"battleOpenHoveredUnit",    EShortcut::BATTLE_OPEN_HOVERED_UNIT  },
+		{"marketDeal",               EShortcut::MARKET_DEAL               },
+		{"marketMaxAmount",          EShortcut::MARKET_MAX_AMOUNT         },
+		{"marketSacrificeAll",       EShortcut::MARKET_SACRIFICE_ALL      },
+		{"marketSacrificeBackpack",  EShortcut::MARKET_SACRIFICE_BACKPACK },
+		{"marketResourcePlayer",     EShortcut::MARKET_RESOURCE_PLAYER    },
+		{"marketArtifactResource",   EShortcut::MARKET_ARTIFACT_RESOURCE  },
+		{"marketResourceArtifact",   EShortcut::MARKET_RESOURCE_ARTIFACT  },
+		{"marketCreatureResource",   EShortcut::MARKET_CREATURE_RESOURCE  },
+		{"marketResourceResource",   EShortcut::MARKET_RESOURCE_RESOURCE  },
+		{"marketCreatureExperience", EShortcut::MARKET_CREATURE_EXPERIENCE },
+		{"marketArtifactExperience", EShortcut::MARKET_ARTIFACT_EXPERIENCE },
+		{"townOpenHall",             EShortcut::TOWN_OPEN_HALL            },
+		{"townOpenFort",             EShortcut::TOWN_OPEN_FORT            },
+		{"townOpenMarket",           EShortcut::TOWN_OPEN_MARKET          },
+		{"townOpenMageGuild",        EShortcut::TOWN_OPEN_MAGE_GUILD      },
+		{"townOpenThievesGuild",     EShortcut::TOWN_OPEN_THIEVES_GUILD   },
+		{"townOpenRecruitment",      EShortcut::TOWN_OPEN_RECRUITMENT     },
+		{"townOpenHeroExchange",     EShortcut::TOWN_OPEN_HERO_EXCHANGE   },
+		{"townOpenHero",             EShortcut::TOWN_OPEN_HERO            },
+		{"townOpenVisitingHero",     EShortcut::TOWN_OPEN_VISITING_HERO   },
+		{"townOpenGarrisonedHero",   EShortcut::TOWN_OPEN_GARRISONED_HERO },
+		{"recruitmentSwitchLevel",   EShortcut::RECRUITMENT_SWITCH_LEVEL  },
+		{"heroArmySplit",            EShortcut::HERO_ARMY_SPLIT           },
+		{"heroBackpack",             EShortcut::HERO_BACKPACK             },
+		{"exchangeArmyToLeft",       EShortcut::EXCHANGE_ARMY_TO_LEFT     },
+		{"exchangeArmyToRight",      EShortcut::EXCHANGE_ARMY_TO_RIGHT    },
+		{"exchangeArmySwap",         EShortcut::EXCHANGE_ARMY_SWAP        },
+		{"exchangeArtifactsToLeft",  EShortcut::EXCHANGE_ARTIFACTS_TO_LEFT },
+		{"exchangeArtifactsToRight", EShortcut::EXCHANGE_ARTIFACTS_TO_RIGHT },
+		{"exchangeArtifactsSwap",    EShortcut::EXCHANGE_ARTIFACTS_SWAP   },
+		{"exchangeBackpackLeft",     EShortcut::EXCHANGE_BACKPACK_LEFT    },
+		{"exchangeBackpackRight",    EShortcut::EXCHANGE_BACKPACK_RIGHT   },
+		{"exchangeEquippedToLeft",   EShortcut::EXCHANGE_EQUIPPED_TO_LEFT },
+		{"exchangeEquippedToRight",  EShortcut::EXCHANGE_EQUIPPED_TO_RIGHT},
+		{"exchangeEquippedSwap",     EShortcut::EXCHANGE_EQUIPPED_SWAP    },
+		{"exchangeBackpackToLeft",   EShortcut::EXCHANGE_BACKPACK_TO_LEFT },
+		{"exchangeBackpackToRight",  EShortcut::EXCHANGE_BACKPACK_TO_RIGHT},
+		{"exchangeBackpackSwap",     EShortcut::EXCHANGE_BACKPACK_SWAP    },
 	};
 
+#ifndef ENABLE_GOLDMASTER
+	std::vector<EShortcut> assignedShortcuts;
+	std::vector<EShortcut> missingShortcuts;
+	for (auto const & entry : shortcutNames)
+	{
+		assert(!vstd::contains(assignedShortcuts, entry.second));
+		assignedShortcuts.push_back(entry.second);
+	}
+
+	for (EShortcut id = vstd::next(EShortcut::NONE, 1); id < EShortcut::AFTER_LAST; id = vstd::next(id, 1))
+		if (!vstd::contains(assignedShortcuts, id))
+			missingShortcuts.push_back(id);
+
+	if (!missingShortcuts.empty())
+		logGlobal->error("Found %d shortcuts without assigned string name!", missingShortcuts.size());
+#endif
+
 	if (shortcutNames.count(identifier))
 		return shortcutNames.at(identifier);
 	return EShortcut::NONE;

+ 17 - 5
client/lobby/CBonusSelection.cpp

@@ -71,9 +71,18 @@ CBonusSelection::CBonusSelection()
 
 	panelBackground = std::make_shared<CPicture>(ImagePath::builtin("CAMPBRF.BMP"), 456, 6);
 
-	buttonStart = std::make_shared<CButton>(Point(475, 536), AnimationPath::builtin("CBBEGIB.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::startMap, this), EShortcut::GLOBAL_ACCEPT);
+	const auto & playVideo = [this]()
+	{
+		GH.windows().createAndPushWindow<CPrologEpilogVideo>(
+			getCampaign()->scenario(CSH->campaignMap).prolog,
+			[this]() { redraw(); } );
+	};
+
+	buttonStart = std::make_shared<CButton>(
+		Point(475, 536), AnimationPath::builtin("CBBEGIB.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::startMap, this), EShortcut::GLOBAL_ACCEPT
+		);
 	buttonRestart = std::make_shared<CButton>(Point(475, 536), AnimationPath::builtin("CBRESTB.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::restartMap, this), EShortcut::GLOBAL_ACCEPT);
-	buttonVideo = std::make_shared<CButton>(Point(705, 214), AnimationPath::builtin("CBVIDEB.DEF"), CButton::tooltip(), [this](){ GH.windows().createAndPushWindow<CPrologEpilogVideo>(getCampaign()->scenario(CSH->campaignMap).prolog, [this](){ redraw(); }); });
+	buttonVideo = std::make_shared<CButton>(Point(705, 214), AnimationPath::builtin("CBVIDEB.DEF"), CButton::tooltip(), playVideo, EShortcut::LOBBY_REPLAY_VIDEO);
 	buttonBack = std::make_shared<CButton>(Point(624, 536), AnimationPath::builtin("CBCANCB.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::goBack, this), EShortcut::GLOBAL_CANCEL);
 
 	campaignName = std::make_shared<CLabel>(481, 28, FONT_BIG, ETextAlignment::TOPLEFT, Colors::YELLOW, CSH->si->getCampaignName());
@@ -104,8 +113,11 @@ CBonusSelection::CBonusSelection()
 
 	if(getCampaign()->playerSelectedDifficulty())
 	{
-		buttonDifficultyLeft = std::make_shared<CButton>(settings["general"]["enableUiEnhancements"].Bool() ? Point(693, 495) : Point(694, 508), AnimationPath::builtin("SCNRBLF.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::decreaseDifficulty, this));
-		buttonDifficultyRight = std::make_shared<CButton>(settings["general"]["enableUiEnhancements"].Bool() ? Point(739, 495) : Point(738, 508), AnimationPath::builtin("SCNRBRT.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::increaseDifficulty, this));
+		Point posLeft = settings["general"]["enableUiEnhancements"].Bool() ? Point(693, 495) : Point(694, 508);
+		Point posRight = settings["general"]["enableUiEnhancements"].Bool() ? Point(739, 495) : Point(738, 508);
+
+		buttonDifficultyLeft = std::make_shared<CButton>(posLeft, AnimationPath::builtin("SCNRBLF.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::decreaseDifficulty, this), EShortcut::MOVE_LEFT);
+		buttonDifficultyRight = std::make_shared<CButton>(posRight, AnimationPath::builtin("SCNRBRT.DEF"), CButton::tooltip(), std::bind(&CBonusSelection::increaseDifficulty, this), EShortcut::MOVE_RIGHT);
 	}
 
 	for(auto scenarioID : getCampaign()->allScenarios())
@@ -125,7 +137,7 @@ CBonusSelection::CBonusSelection()
 		tabExtraOptions->recActions = UPDATE | SHOWALL | LCLICK | RCLICK_POPUP;
 		tabExtraOptions->recreate(true);
 		tabExtraOptions->setEnabled(false);
-		buttonExtraOptions = std::make_shared<CButton>(Point(643, 431), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[46], [this]{ tabExtraOptions->setEnabled(!tabExtraOptions->isActive()); GH.windows().totalRedraw(); }, EShortcut::NONE);
+		buttonExtraOptions = std::make_shared<CButton>(Point(643, 431), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[46], [this]{ tabExtraOptions->setEnabled(!tabExtraOptions->isActive()); GH.windows().totalRedraw(); }, EShortcut::LOBBY_EXTRA_OPTIONS);
 		buttonExtraOptions->setTextOverlay(CGI->generaltexth->translate("vcmi.optionsTab.extraOptions.hover"), FONT_SMALL, Colors::WHITE);
 	}
 }

+ 3 - 3
client/lobby/CLobbyScreen.cpp

@@ -57,12 +57,12 @@ CLobbyScreen::CLobbyScreen(ESelectionScreen screenType)
 		buttonOptions = std::make_shared<CButton>(Point(411, 510), AnimationPath::builtin("GSPBUTT.DEF"), CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabOpt), EShortcut::LOBBY_ADDITIONAL_OPTIONS);
 		if(settings["general"]["enableUiEnhancements"].Bool())
 		{
-			buttonTurnOptions = std::make_shared<CButton>(Point(619, 105), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabTurnOptions), EShortcut::NONE);
-			buttonExtraOptions = std::make_shared<CButton>(Point(619, 510), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabExtraOptions), EShortcut::NONE);
+			buttonTurnOptions = std::make_shared<CButton>(Point(619, 105), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabTurnOptions), EShortcut::LOBBY_TURN_OPTIONS);
+			buttonExtraOptions = std::make_shared<CButton>(Point(619, 510), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[46], std::bind(&CLobbyScreen::toggleTab, this, tabExtraOptions), EShortcut::LOBBY_EXTRA_OPTIONS);
 		}
 	};
 
-	buttonChat = std::make_shared<CButton>(Point(619, 80), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[48], std::bind(&CLobbyScreen::toggleChat, this), EShortcut::LOBBY_HIDE_CHAT);
+	buttonChat = std::make_shared<CButton>(Point(619, 80), AnimationPath::builtin("GSPBUT2.DEF"), CGI->generaltexth->zelp[48], std::bind(&CLobbyScreen::toggleChat, this), EShortcut::LOBBY_TOGGLE_CHAT);
 	buttonChat->setTextOverlay(CGI->generaltexth->allTexts[532], FONT_SMALL, Colors::WHITE);
 
 	switch(screenType)

+ 6 - 5
client/lobby/CSelectionBase.cpp

@@ -33,6 +33,7 @@
 #include "../widgets/CTextInput.h"
 #include "../widgets/GraphicalPrimitiveCanvas.h"
 #include "../widgets/Images.h"
+#include "../widgets/MiscWidgets.h"
 #include "../widgets/ObjectLists.h"
 #include "../widgets/Slider.h"
 #include "../widgets/TextControls.h"
@@ -143,8 +144,8 @@ InfoCard::InfoCard()
 	chat = std::make_shared<CChatBox>(Rect(18, 126, 335, 143));
 	pvpBox = std::make_shared<PvPBox>(Rect(17, 396, 338, 105));
 
-	buttonInvitePlayers = std::make_shared<CButton>(Point(20, 365), AnimationPath::builtin("pregameInvitePlayers"), CGI->generaltexth->zelp[105], [](){ CSH->getGlobalLobby().activateRoomInviteInterface(); } );
-	buttonOpenGlobalLobby = std::make_shared<CButton>(Point(188, 365), AnimationPath::builtin("pregameReturnToLobby"), CGI->generaltexth->zelp[105], [](){ CSH->getGlobalLobby().activateInterface(); });
+	buttonInvitePlayers = std::make_shared<CButton>(Point(20, 365), AnimationPath::builtin("pregameInvitePlayers"), CGI->generaltexth->zelp[105], [](){ CSH->getGlobalLobby().activateRoomInviteInterface(); }, EShortcut::LOBBY_INVITE_PLAYERS );
+	buttonOpenGlobalLobby = std::make_shared<CButton>(Point(188, 365), AnimationPath::builtin("pregameReturnToLobby"), CGI->generaltexth->zelp[105], [](){ CSH->getGlobalLobby().activateInterface(); }, EShortcut::MAIN_MENU_LOBBY );
 
 	buttonInvitePlayers->setTextOverlay  (MetaString::createFromTextID("vcmi.lobby.invite.header").toString(), EFonts::FONT_SMALL, Colors::WHITE);
 	buttonOpenGlobalLobby->setTextOverlay(MetaString::createFromTextID("vcmi.lobby.backToLobby").toString(), EFonts::FONT_SMALL, Colors::WHITE);
@@ -418,7 +419,7 @@ PvPBox::PvPBox(const Rect & rect)
 		LobbyPvPAction lpa;
 		lpa.action = LobbyPvPAction::COIN;
 		CSH->sendLobbyPack(lpa);
-	}, EShortcut::NONE);
+	}, EShortcut::LOBBY_FLIP_COIN);
 	buttonFlipCoin->setTextOverlay(CGI->generaltexth->translate("vcmi.lobby.pvp.coin.hover"), EFonts::FONT_SMALL, Colors::WHITE);
 
 	buttonRandomTown = std::make_shared<CButton>(Point(190, 31), AnimationPath::builtin("GSPBUT2.DEF"), CButton::tooltip("", CGI->generaltexth->translate("vcmi.lobby.pvp.randomTown.help")), [getBannedTowns](){
@@ -426,7 +427,7 @@ PvPBox::PvPBox(const Rect & rect)
 		lpa.action = LobbyPvPAction::RANDOM_TOWN;
 		lpa.bannedTowns = getBannedTowns();
 		CSH->sendLobbyPack(lpa);
-	}, EShortcut::NONE);
+	}, EShortcut::LOBBY_RANDOM_TOWN);
 	buttonRandomTown->setTextOverlay(CGI->generaltexth->translate("vcmi.lobby.pvp.randomTown.hover"), EFonts::FONT_SMALL, Colors::WHITE);
 
 	buttonRandomTownVs = std::make_shared<CButton>(Point(190, 56), AnimationPath::builtin("GSPBUT2.DEF"), CButton::tooltip("", CGI->generaltexth->translate("vcmi.lobby.pvp.randomTownVs.help")), [getBannedTowns](){
@@ -434,7 +435,7 @@ PvPBox::PvPBox(const Rect & rect)
 		lpa.action = LobbyPvPAction::RANDOM_TOWN_VS;
 		lpa.bannedTowns = getBannedTowns();
 		CSH->sendLobbyPack(lpa);
-	}, EShortcut::NONE);
+	}, EShortcut::LOBBY_RANDOM_TOWN_VS);
 	buttonRandomTownVs->setTextOverlay(CGI->generaltexth->translate("vcmi.lobby.pvp.randomTownVs.hover"), EFonts::FONT_SMALL, Colors::WHITE);
 }
 

+ 12 - 13
client/lobby/SelectionTab.cpp

@@ -169,24 +169,23 @@ SelectionTab::SelectionTab(ESelectionScreen Type)
 		labelMapSizes = std::make_shared<CLabel>(87, 62, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[510]);
 
 		// TODO: Global constants?
-		int sizes[] = {CMapHeader::MAP_SIZE_SMALL,
-						CMapHeader::MAP_SIZE_MIDDLE,
-						CMapHeader::MAP_SIZE_LARGE,
-						CMapHeader::MAP_SIZE_XLARGE,
-						0};
-		const char * filterIconNmes[] = {"SCSMBUT.DEF", "SCMDBUT.DEF", "SCLGBUT.DEF", "SCXLBUT.DEF", "SCALBUT.DEF"};
+		constexpr std::array sizes = {CMapHeader::MAP_SIZE_SMALL, CMapHeader::MAP_SIZE_MIDDLE, CMapHeader::MAP_SIZE_LARGE, CMapHeader::MAP_SIZE_XLARGE, 0};
+		constexpr std::array filterIconNmes = {"SCSMBUT.DEF", "SCMDBUT.DEF", "SCLGBUT.DEF", "SCXLBUT.DEF", "SCALBUT.DEF"};
+		constexpr std::array filterShortcuts = { EShortcut::MAPS_SIZE_S, EShortcut::MAPS_SIZE_M, EShortcut::MAPS_SIZE_L, EShortcut::MAPS_SIZE_XL, EShortcut::MAPS_SIZE_ALL };
+
 		for(int i = 0; i < 5; i++)
-			buttonsSortBy.push_back(std::make_shared<CButton>(Point(158 + 47 * i, 46), AnimationPath::builtin(filterIconNmes[i]), CGI->generaltexth->zelp[54 + i], std::bind(&SelectionTab::filter, this, sizes[i], true)));
+			buttonsSortBy.push_back(std::make_shared<CButton>(Point(158 + 47 * i, 46), AnimationPath::builtin(filterIconNmes[i]), CGI->generaltexth->zelp[54 + i], std::bind(&SelectionTab::filter, this, sizes[i], true), filterShortcuts[i]));
 
-		int xpos[] = {23, 55, 88, 121, 306, 339};
-		const char * sortIconNames[] = {"SCBUTT1.DEF", "SCBUTT2.DEF", "SCBUTCP.DEF", "SCBUTT3.DEF", "SCBUTT4.DEF", "SCBUTT5.DEF"};
+		constexpr std::array xpos = {23, 55, 88, 121, 306, 339};
+		constexpr std::array sortIconNames = {"SCBUTT1.DEF", "SCBUTT2.DEF", "SCBUTCP.DEF", "SCBUTT3.DEF", "SCBUTT4.DEF", "SCBUTT5.DEF"};
+		constexpr std::array sortShortcuts = { EShortcut::MAPS_SORT_PLAYERS, EShortcut::MAPS_SORT_SIZE, EShortcut::MAPS_SORT_FORMAT, EShortcut::MAPS_SORT_NAME, EShortcut::MAPS_SORT_VICTORY, EShortcut::MAPS_SORT_DEFEAT };
 		for(int i = 0; i < 6; i++)
 		{
 			ESortBy criteria = (ESortBy)i;
 			if(criteria == _name)
 				criteria = generalSortingBy;
 
-			buttonsSortBy.push_back(std::make_shared<CButton>(Point(xpos[i], 86), AnimationPath::builtin(sortIconNames[i]), CGI->generaltexth->zelp[107 + i], std::bind(&SelectionTab::sortBy, this, criteria)));
+			buttonsSortBy.push_back(std::make_shared<CButton>(Point(xpos[i], 86), AnimationPath::builtin(sortIconNames[i]), CGI->generaltexth->zelp[107 + i], std::bind(&SelectionTab::sortBy, this, criteria), sortShortcuts[i]));
 		}
 	}
 
@@ -212,8 +211,8 @@ SelectionTab::SelectionTab(ESelectionScreen Type)
 		pos.x += 3;
 		pos.y += 6;
 
-		buttonsSortBy.push_back(std::make_shared<CButton>(Point(23, 86), AnimationPath::builtin("CamCusM.DEF"), CButton::tooltip(), std::bind(&SelectionTab::sortBy, this, _numOfMaps)));
-		buttonsSortBy.push_back(std::make_shared<CButton>(Point(55, 86), AnimationPath::builtin("CamCusL.DEF"), CButton::tooltip(), std::bind(&SelectionTab::sortBy, this, _name)));
+		buttonsSortBy.push_back(std::make_shared<CButton>(Point(23, 86), AnimationPath::builtin("CamCusM.DEF"), CButton::tooltip(), std::bind(&SelectionTab::sortBy, this, _numOfMaps), EShortcut::MAPS_SORT_MAPS));
+		buttonsSortBy.push_back(std::make_shared<CButton>(Point(55, 86), AnimationPath::builtin("CamCusL.DEF"), CButton::tooltip(), std::bind(&SelectionTab::sortBy, this, _name), EShortcut::MAPS_SORT_NAME));
 		break;
 	default:
 		assert(0);
@@ -222,7 +221,7 @@ SelectionTab::SelectionTab(ESelectionScreen Type)
 
 	if(enableUiEnhancements)
 	{
-		auto sortByDate = std::make_shared<CButton>(Point(371, 85), AnimationPath::builtin("selectionTabSortDate"), CButton::tooltip("", CGI->generaltexth->translate("vcmi.lobby.sortDate")), std::bind(&SelectionTab::sortBy, this, ESortBy::_changeDate));
+		auto sortByDate = std::make_shared<CButton>(Point(371, 85), AnimationPath::builtin("selectionTabSortDate"), CButton::tooltip("", CGI->generaltexth->translate("vcmi.lobby.sortDate")), std::bind(&SelectionTab::sortBy, this, ESortBy::_changeDate), EShortcut::MAPS_SORT_CHANGEDATE);
 		sortByDate->setOverlay(std::make_shared<CPicture>(ImagePath::builtin("lobby/selectionTabSortDate")));
 		buttonsSortBy.push_back(sortByDate);
 	}

+ 7 - 5
client/mainmenu/CHighScoreScreen.cpp

@@ -111,10 +111,10 @@ void CHighScoreScreen::addButtons()
 	
 	buttons.clear();
 
-	buttons.push_back(std::make_shared<CButton>(Point(31, 113), AnimationPath::builtin("HISCCAM.DEF"), CButton::tooltip(), [&](){ buttonCampaignClick(); }));
-	buttons.push_back(std::make_shared<CButton>(Point(31, 345), AnimationPath::builtin("HISCSTA.DEF"), CButton::tooltip(), [&](){ buttonScenarioClick(); }));
-	buttons.push_back(std::make_shared<CButton>(Point(726, 113), AnimationPath::builtin("HISCRES.DEF"), CButton::tooltip(), [&](){ buttonResetClick(); }));
-	buttons.push_back(std::make_shared<CButton>(Point(726, 345), AnimationPath::builtin("HISCEXT.DEF"), CButton::tooltip(), [&](){ buttonExitClick(); }));
+	buttons.push_back(std::make_shared<CButton>(Point(31, 113), AnimationPath::builtin("HISCCAM.DEF"), CButton::tooltip(), [&](){ buttonCampaignClick(); }, EShortcut::HIGH_SCORES_CAMPAIGNS));
+	buttons.push_back(std::make_shared<CButton>(Point(31, 345), AnimationPath::builtin("HISCSTA.DEF"), CButton::tooltip(), [&](){ buttonScenarioClick(); }, EShortcut::HIGH_SCORES_SCENARIOS));
+	buttons.push_back(std::make_shared<CButton>(Point(726, 113), AnimationPath::builtin("HISCRES.DEF"), CButton::tooltip(), [&](){ buttonResetClick(); }, EShortcut::HIGH_SCORES_RESET));
+	buttons.push_back(std::make_shared<CButton>(Point(726, 345), AnimationPath::builtin("HISCEXT.DEF"), CButton::tooltip(), [&](){ buttonExitClick(); }, EShortcut::GLOBAL_RETURN));
 }
 
 void CHighScoreScreen::addHighScores()
@@ -335,6 +335,7 @@ void CHighScoreInputScreen::deactivate()
 {
 	CCS->videoh->close();
 	CCS->soundh->stopSound(videoSoundHandle);
+	CIntObject::deactivate();
 }
 
 void CHighScoreInputScreen::clickPressed(const Point & cursorPosition)
@@ -381,7 +382,8 @@ CHighScoreInput::CHighScoreInput(std::string playerName, std::function<void(std:
 
 	buttonOk = std::make_shared<CButton>(Point(26, 142), AnimationPath::builtin("MUBCHCK.DEF"), CGI->generaltexth->zelp[560], std::bind(&CHighScoreInput::okay, this), EShortcut::GLOBAL_ACCEPT);
 	buttonCancel = std::make_shared<CButton>(Point(142, 142), AnimationPath::builtin("MUBCANC.DEF"), CGI->generaltexth->zelp[561], std::bind(&CHighScoreInput::abort, this), EShortcut::GLOBAL_CANCEL);
-	statusBar = CGStatusBar::create(std::make_shared<CPicture>(background->getSurface(), Rect(7, 186, 218, 18), 7, 186));
+// FIXME: broken. Never activates?
+//	statusBar = CGStatusBar::create(std::make_shared<CPicture>(background->getSurface(), Rect(7, 186, 218, 18), 7, 186));
 	textInput = std::make_shared<CTextInput>(Rect(18, 104, 200, 25), FONT_SMALL, ETextAlignment::CENTER, true);
 	textInput->setText(playerName);
 }

+ 4 - 4
client/mainmenu/CMainMenu.cpp

@@ -457,11 +457,11 @@ CMultiMode::CMultiMode(ESelectionScreen ScreenType)
 	playerName->setText(getPlayerName());
 	playerName->setCallback(std::bind(&CMultiMode::onNameChange, this, _1));
 
-	buttonHotseat = std::make_shared<CButton>(Point(373, 78 + 57 * 0), AnimationPath::builtin("MUBHOT.DEF"), CGI->generaltexth->zelp[266], std::bind(&CMultiMode::hostTCP, this));
-	buttonLobby = std::make_shared<CButton>(Point(373, 78 + 57 * 1), AnimationPath::builtin("MUBONL.DEF"), CGI->generaltexth->zelp[265], std::bind(&CMultiMode::openLobby, this));
+	buttonHotseat = std::make_shared<CButton>(Point(373, 78 + 57 * 0), AnimationPath::builtin("MUBHOT.DEF"), CGI->generaltexth->zelp[266], std::bind(&CMultiMode::hostTCP, this), EShortcut::MAIN_MENU_HOTSEAT);
+	buttonLobby = std::make_shared<CButton>(Point(373, 78 + 57 * 1), AnimationPath::builtin("MUBONL.DEF"), CGI->generaltexth->zelp[265], std::bind(&CMultiMode::openLobby, this), EShortcut::MAIN_MENU_LOBBY);
 
-	buttonHost = std::make_shared<CButton>(Point(373, 78 + 57 * 3), AnimationPath::builtin("MUBHOST.DEF"), CButton::tooltip(CGI->generaltexth->translate("vcmi.mainMenu.hostTCP"), ""), std::bind(&CMultiMode::hostTCP, this));
-	buttonJoin = std::make_shared<CButton>(Point(373, 78 + 57 * 4), AnimationPath::builtin("MUBJOIN.DEF"), CButton::tooltip(CGI->generaltexth->translate("vcmi.mainMenu.joinTCP"), ""), std::bind(&CMultiMode::joinTCP, this));
+	buttonHost = std::make_shared<CButton>(Point(373, 78 + 57 * 3), AnimationPath::builtin("MUBHOST.DEF"), CButton::tooltip(CGI->generaltexth->translate("vcmi.mainMenu.hostTCP"), ""), std::bind(&CMultiMode::hostTCP, this), EShortcut::MAIN_MENU_HOST_GAME);
+	buttonJoin = std::make_shared<CButton>(Point(373, 78 + 57 * 4), AnimationPath::builtin("MUBJOIN.DEF"), CButton::tooltip(CGI->generaltexth->translate("vcmi.mainMenu.joinTCP"), ""), std::bind(&CMultiMode::joinTCP, this), EShortcut::MAIN_MENU_JOIN_GAME);
 
 	buttonCancel = std::make_shared<CButton>(Point(373, 424), AnimationPath::builtin("MUBCANC.DEF"), CGI->generaltexth->zelp[288], [=](){ close();}, EShortcut::GLOBAL_CANCEL);
 }

+ 2 - 2
client/mapView/MapView.cpp

@@ -239,9 +239,9 @@ void MapView::onViewWorldActivated(uint32_t tileSize)
 	controller->setTileSize(Point(tileSize, tileSize));
 }
 
-void MapView::onMapZoomLevelChanged(int stepsChange)
+void MapView::onMapZoomLevelChanged(int stepsChange, bool useDeadZone)
 {
-	controller->modifyTileSize(stepsChange);
+	controller->modifyTileSize(stepsChange, useDeadZone);
 }
 
 void MapView::onViewMapActivated()

+ 1 - 1
client/mapView/MapView.h

@@ -87,7 +87,7 @@ public:
 	void onViewWorldActivated(uint32_t tileSize);
 
 	/// Changes zoom level / tile size of current view by specified factor
-	void onMapZoomLevelChanged(int stepsChange);
+	void onMapZoomLevelChanged(int stepsChange, bool useDeadZone);
 
 	/// Switches view from View World mode back to standard view
 	void onViewMapActivated();

+ 2 - 2
client/mapView/MapViewActions.cpp

@@ -87,7 +87,7 @@ void MapViewActions::mouseMoved(const Point & cursorPosition, const Point & last
 
 void MapViewActions::wheelScrolled(int distance)
 {
-	adventureInt->hotkeyZoom(distance * 4);
+	adventureInt->hotkeyZoom(distance * 4, true);
 }
 
 void MapViewActions::mouseDragged(const Point & cursorPosition, const Point & lastUpdateDistance)
@@ -114,7 +114,7 @@ void MapViewActions::gesturePinch(const Point & centerPosition, double lastUpdat
 	int oldZoomSteps = std::round(std::log(pinchZoomFactor) / std::log(1.01));
 
 	if (newZoomSteps != oldZoomSteps)
-		adventureInt->hotkeyZoom(newZoomSteps - oldZoomSteps);
+		adventureInt->hotkeyZoom(newZoomSteps - oldZoomSteps, true);
 
 	pinchZoomFactor = newZoom;
 }

+ 8 - 5
client/mapView/MapViewController.cpp

@@ -89,7 +89,7 @@ void MapViewController::setTileSize(const Point & tileSize)
 	setViewCenter(newViewCenter, model->getLevel());
 }
 
-void MapViewController::modifyTileSize(int stepsChange)
+void MapViewController::modifyTileSize(int stepsChange, bool useDeadZone)
 {
 	// we want to zoom in/out in fixed 10% steps, to allow player to return back to exactly 100% zoom just by scrolling
 	// so, zooming in for 5 steps will put game at 1.1^5 = 1.61 scale
@@ -118,10 +118,13 @@ void MapViewController::modifyTileSize(int stepsChange)
 	if (actualZoom != currentZoom)
 	{
 		targetTileSize = actualZoom;
-		if(actualZoom.x >= defaultTileSize - zoomTileDeadArea && actualZoom.x <= defaultTileSize + zoomTileDeadArea)
-			actualZoom.x = defaultTileSize;
-		if(actualZoom.y >= defaultTileSize - zoomTileDeadArea && actualZoom.y <= defaultTileSize + zoomTileDeadArea)
-			actualZoom.y = defaultTileSize;
+		if (useDeadZone)
+		{
+			if(actualZoom.x >= defaultTileSize - zoomTileDeadArea && actualZoom.x <= defaultTileSize + zoomTileDeadArea)
+				actualZoom.x = defaultTileSize;
+			if(actualZoom.y >= defaultTileSize - zoomTileDeadArea && actualZoom.y <= defaultTileSize + zoomTileDeadArea)
+				actualZoom.y = defaultTileSize;
+		}
 		
 		bool isInDeadZone = targetTileSize != actualZoom || actualZoom == Point(defaultTileSize, defaultTileSize);
 

+ 2 - 2
client/mapView/MapViewController.h

@@ -55,7 +55,7 @@ class MapViewController : public IMapObjectObserver
 
 private:
 	const int defaultTileSize = 32;
-	const int zoomTileDeadArea = 5;
+	const int zoomTileDeadArea = 4;
 	Point targetTileSize = Point(32, 32);
 	bool wasInDeadZone = true;
 
@@ -97,7 +97,7 @@ public:
 	void setViewCenter(const int3 & position);
 	void setViewCenter(const Point & position, int level);
 	void setTileSize(const Point & tileSize);
-	void modifyTileSize(int stepsChange);
+	void modifyTileSize(int stepsChange, bool useDeadZone);
 	void tick(uint32_t timePassed);
 	void afterRender();
 

+ 1 - 0
client/widgets/CGarrisonInt.cpp

@@ -19,6 +19,7 @@
 #include "../render/IImage.h"
 #include "../render/Graphics.h"
 #include "../windows/CCreatureWindow.h"
+#include "../windows/CWindowWithArtifacts.h"
 #include "../windows/GUIClasses.h"
 #include "../CGameInfo.h"
 #include "../CPlayerInterface.h"

+ 4 - 3
client/widgets/markets/CAltarArtifacts.cpp

@@ -12,6 +12,7 @@
 #include "CAltarArtifacts.h"
 
 #include "../../gui/CGuiHandler.h"
+#include "../../gui/Shortcut.h"
 #include "../../widgets/Buttons.h"
 #include "../../widgets/TextControls.h"
 
@@ -36,16 +37,16 @@ CAltarArtifacts::CAltarArtifacts(const IMarket * market, const CGHeroInstance *
 	altarArtifacts = altarObj;
 
 	deal = std::make_shared<CButton>(Point(269, 520), AnimationPath::builtin("ALTSACR.DEF"),
-		CGI->generaltexth->zelp[585], [this]() {CAltarArtifacts::makeDeal(); });
+		CGI->generaltexth->zelp[585], [this]() {CAltarArtifacts::makeDeal(); }, EShortcut::MARKET_DEAL);
 	labels.emplace_back(std::make_shared<CLabel>(450, 32, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[477]));
 	labels.emplace_back(std::make_shared<CLabel>(302, 424, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[478]));
 
 	sacrificeAllButton = std::make_shared<CButton>(Point(393, 520), AnimationPath::builtin("ALTFILL.DEF"),
-		CGI->generaltexth->zelp[571], std::bind(&CExperienceAltar::sacrificeAll, this));
+		CGI->generaltexth->zelp[571], std::bind(&CExperienceAltar::sacrificeAll, this), EShortcut::MARKET_SACRIFICE_ALL);
 	sacrificeAllButton->block(hero->artifactsInBackpack.empty() && hero->artifactsWorn.empty());
 
 	sacrificeBackpackButton = std::make_shared<CButton>(Point(147, 520), AnimationPath::builtin("ALTEMBK.DEF"),
-		CGI->generaltexth->zelp[570], std::bind(&CAltarArtifacts::sacrificeBackpack, this));
+		CGI->generaltexth->zelp[570], std::bind(&CAltarArtifacts::sacrificeBackpack, this), EShortcut::MARKET_SACRIFICE_BACKPACK);
 	sacrificeBackpackButton->block(hero->artifactsInBackpack.empty());
 
 	// Hero's artifacts

+ 3 - 2
client/widgets/markets/CAltarCreatures.cpp

@@ -12,6 +12,7 @@
 #include "CAltarCreatures.h"
 
 #include "../../gui/CGuiHandler.h"
+#include "../../gui/Shortcut.h"
 #include "../../widgets/Buttons.h"
 #include "../../widgets/TextControls.h"
 
@@ -33,7 +34,7 @@ CAltarCreatures::CAltarCreatures(const IMarket * market, const CGHeroInstance *
 	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE);
 
 	deal = std::make_shared<CButton>(dealButtonPosWithSlider, AnimationPath::builtin("ALTSACR.DEF"),
-		CGI->generaltexth->zelp[584], [this]() {CAltarCreatures::makeDeal();});
+		CGI->generaltexth->zelp[584], [this]() {CAltarCreatures::makeDeal();}, EShortcut::MARKET_DEAL);
 	labels.emplace_back(std::make_shared<CLabel>(155, 30, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW,
 		boost::str(boost::format(CGI->generaltexth->allTexts[272]) % hero->getNameTranslated())));
 	labels.emplace_back(std::make_shared<CLabel>(450, 30, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[479]));
@@ -44,7 +45,7 @@ CAltarCreatures::CAltarCreatures(const IMarket * market, const CGHeroInstance *
 	unitsOnAltar.resize(GameConstants::ARMY_SIZE, 0);
 	expPerUnit.resize(GameConstants::ARMY_SIZE, 0);
 	sacrificeAllButton = std::make_shared<CButton>(
-		Point(393, 520), AnimationPath::builtin("ALTARMY.DEF"), CGI->generaltexth->zelp[579], std::bind(&CExperienceAltar::sacrificeAll, this));
+		Point(393, 520), AnimationPath::builtin("ALTARMY.DEF"), CGI->generaltexth->zelp[579], std::bind(&CExperienceAltar::sacrificeAll, this), EShortcut::MARKET_SACRIFICE_ALL);
 
 	// Hero creatures panel
 	assert(bidTradePanel);

+ 2 - 1
client/widgets/markets/CArtifactsBuying.cpp

@@ -12,6 +12,7 @@
 #include "CArtifactsBuying.h"
 
 #include "../../gui/CGuiHandler.h"
+#include "../../gui/Shortcut.h"
 #include "../../widgets/Buttons.h"
 #include "../../widgets/TextControls.h"
 
@@ -38,7 +39,7 @@ CArtifactsBuying::CArtifactsBuying(const IMarket * market, const CGHeroInstance
 		title = CGI->generaltexth->allTexts[349];
 	labels.emplace_back(std::make_shared<CLabel>(titlePos.x, titlePos.y, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, title));
 	deal = std::make_shared<CButton>(dealButtonPos, AnimationPath::builtin("TPMRKB.DEF"),
-		CGI->generaltexth->zelp[595], [this](){CArtifactsBuying::makeDeal();});
+		CGI->generaltexth->zelp[595], [this](){CArtifactsBuying::makeDeal();}, EShortcut::MARKET_DEAL);
 	labels.emplace_back(std::make_shared<CLabel>(445, 148, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[168]));
 
 	// Player's resources

+ 2 - 1
client/widgets/markets/CArtifactsSelling.cpp

@@ -12,6 +12,7 @@
 #include "CArtifactsSelling.h"
 
 #include "../../gui/CGuiHandler.h"
+#include "../../gui/Shortcut.h"
 #include "../../widgets/Buttons.h"
 #include "../../widgets/TextControls.h"
 
@@ -43,7 +44,7 @@ CArtifactsSelling::CArtifactsSelling(const IMarket * market, const CGHeroInstanc
 	labels.emplace_back(std::make_shared<CLabel>(titlePos.x, titlePos.y, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, title));
 	labels.push_back(std::make_shared<CLabel>(155, 56, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, boost::str(boost::format(CGI->generaltexth->allTexts[271]) % hero->getNameTranslated())));
 	deal = std::make_shared<CButton>(dealButtonPos, AnimationPath::builtin("TPMRKB.DEF"),
-		CGI->generaltexth->zelp[595], [this](){CArtifactsSelling::makeDeal();});
+		CGI->generaltexth->zelp[595], [this](){CArtifactsSelling::makeDeal();}, EShortcut::MARKET_DEAL);
 	bidSelectedSlot = std::make_shared<CTradeableItem>(Rect(Point(123, 470), Point(69, 66)), EType::ARTIFACT_TYPE, 0, 0);
 
 	// Market resources panel

+ 2 - 1
client/widgets/markets/CFreelancerGuild.cpp

@@ -12,6 +12,7 @@
 #include "CFreelancerGuild.h"
 
 #include "../../gui/CGuiHandler.h"
+#include "../../gui/Shortcut.h"
 #include "../../widgets/Buttons.h"
 #include "../../widgets/TextControls.h"
 
@@ -39,7 +40,7 @@ CFreelancerGuild::CFreelancerGuild(const IMarket * market, const CGHeroInstance
 	labels.emplace_back(std::make_shared<CLabel>(155, 103, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE,
 		boost::str(boost::format(CGI->generaltexth->allTexts[272]) % hero->getNameTranslated())));
 	deal = std::make_shared<CButton>(dealButtonPosWithSlider, AnimationPath::builtin("TPMRKB.DEF"),
-		CGI->generaltexth->zelp[595], [this]() {CFreelancerGuild::makeDeal();});
+		CGI->generaltexth->zelp[595], [this]() {CFreelancerGuild::makeDeal();}, EShortcut::MARKET_DEAL);
 	offerSlider->moveTo(pos.topLeft() + Point(232, 489));
 
 	// Hero creatures panel

+ 2 - 1
client/widgets/markets/CMarketBase.cpp

@@ -14,6 +14,7 @@
 
 #include "../Images.h"
 #include "../../gui/CGuiHandler.h"
+#include "../../gui/Shortcut.h"
 #include "../../widgets/Buttons.h"
 #include "../../widgets/TextControls.h"
 
@@ -200,7 +201,7 @@ CMarketSlider::CMarketSlider(const CSlider::SliderMovingFunctor & movingCallback
 		[this]()
 		{
 			offerSlider->scrollToMax();
-		});
+		}, EShortcut::MARKET_MAX_AMOUNT);
 }
 
 void CMarketSlider::deselect()

+ 2 - 1
client/widgets/markets/CMarketResources.cpp

@@ -12,6 +12,7 @@
 #include "CMarketResources.h"
 
 #include "../../gui/CGuiHandler.h"
+#include "../../gui/Shortcut.h"
 #include "../../widgets/Buttons.h"
 #include "../../widgets/TextControls.h"
 
@@ -36,7 +37,7 @@ CMarketResources::CMarketResources(const IMarket * market, const CGHeroInstance
 
 	labels.emplace_back(std::make_shared<CLabel>(titlePos.x, titlePos.y, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[158]));
 	deal = std::make_shared<CButton>(dealButtonPosWithSlider, AnimationPath::builtin("TPMRKB.DEF"),
-		CGI->generaltexth->zelp[595], [this]() {CMarketResources::makeDeal(); });
+		CGI->generaltexth->zelp[595], [this]() {CMarketResources::makeDeal(); }, EShortcut::MARKET_DEAL);
 
 	// Player's resources
 	assert(bidTradePanel);

+ 2 - 1
client/widgets/markets/CTransferResources.cpp

@@ -12,6 +12,7 @@
 #include "CTransferResources.h"
 
 #include "../../gui/CGuiHandler.h"
+#include "../../gui/Shortcut.h"
 #include "../../widgets/Buttons.h"
 #include "../../widgets/TextControls.h"
 
@@ -34,7 +35,7 @@ CTransferResources::CTransferResources(const IMarket * market, const CGHeroInsta
 	labels.emplace_back(std::make_shared<CLabel>(titlePos.x, titlePos.y, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[158]));
 	labels.emplace_back(std::make_shared<CLabel>(445, 56, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[169]));
 	deal = std::make_shared<CButton>(dealButtonPosWithSlider, AnimationPath::builtin("TPMRKB.DEF"),
-		CGI->generaltexth->zelp[595], [this](){CTransferResources::makeDeal();});
+		CGI->generaltexth->zelp[595], [this](){CTransferResources::makeDeal();}, EShortcut::MARKET_DEAL);
 
 	// Player's resources
 	assert(bidTradePanel);

+ 68 - 24
client/windows/CCastleInterface.cpp

@@ -1034,6 +1034,40 @@ void CCastleBuildings::openTownHall()
 	GH.windows().createAndPushWindow<CHallInterface>(town);
 }
 
+void CCastleBuildings::enterAnyThievesGuild()
+{
+	std::vector<const CGTownInstance*> towns = LOCPLINT->cb->getTownsInfo(true);
+	for(auto & town : towns)
+	{
+		if(town->builtBuildings.count(BuildingID::TAVERN))
+		{
+			LOCPLINT->showThievesGuildWindow(town);
+			return;
+		}
+	}
+	LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithTavern"));
+}
+
+void CCastleBuildings::enterAnyMarket()
+{
+	if(town->builtBuildings.count(BuildingID::MARKETPLACE))
+	{
+		GH.windows().createAndPushWindow<CMarketWindow>(town, nullptr, nullptr, EMarketMode::RESOURCE_RESOURCE);
+		return;
+	}
+
+	std::vector<const CGTownInstance*> towns = LOCPLINT->cb->getTownsInfo(true);
+	for(auto & town : towns)
+	{
+		if(town->builtBuildings.count(BuildingID::MARKETPLACE))
+		{
+			GH.windows().createAndPushWindow<CMarketWindow>(town, nullptr, nullptr, EMarketMode::RESOURCE_RESOURCE);
+			return;
+		}
+	}
+	LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithMarket"));
+}
+
 CCreaInfo::CCreaInfo(Point position, const CGTownInstance * Town, int Level, bool compact, bool _showAvailable):
 	town(Town),
 	level(Level),
@@ -1215,7 +1249,7 @@ CCastleInterface::CCastleInterface(const CGTownInstance * Town, const CGTownInst
 	exit = std::make_shared<CButton>(Point(744, 544), AnimationPath::builtin("TSBTNS"), CButton::tooltip(CGI->generaltexth->tcommands[8]), [&](){close();}, EShortcut::GLOBAL_RETURN);
 	exit->setImageOrder(4, 5, 6, 7);
 
-	auto split = std::make_shared<CButton>(Point(744, 382), AnimationPath::builtin("TSBTNS"), CButton::tooltip(CGI->generaltexth->tcommands[3]), [this]() { garr->splitClick(); });
+	auto split = std::make_shared<CButton>(Point(744, 382), AnimationPath::builtin("TSBTNS"), CButton::tooltip(CGI->generaltexth->tcommands[3]), [this]() { garr->splitClick(); }, EShortcut::HERO_ARMY_SPLIT);
 	garr->addSplitBtn(split);
 
 	Rect barRect(9, 182, 732, 18);
@@ -1224,8 +1258,8 @@ CCastleInterface::CCastleInterface(const CGTownInstance * Town, const CGTownInst
 	resdatabar = std::make_shared<CResDataBar>(ImagePath::builtin("ARESBAR"), 3, 575, 37, 3, 84, 78);
 
 	townlist = std::make_shared<CTownList>(3, Rect(Point(743, 414), Point(48, 128)), Point(1,16), Point(0, 32), LOCPLINT->localState->getOwnedTowns().size() );
-	townlist->setScrollUpButton( std::make_shared<CButton>( Point(744, 414), AnimationPath::builtin("IAM014"), CButton::tooltipLocalized("core.help.306")));
-	townlist->setScrollDownButton( std::make_shared<CButton>( Point(744, 526), AnimationPath::builtin("IAM015"), CButton::tooltipLocalized("core.help.307")));
+	townlist->setScrollUpButton( std::make_shared<CButton>( Point(744, 414), AnimationPath::builtin("IAM014"), CButton::tooltipLocalized("core.help.306"), 0, EShortcut::MOVE_UP));
+	townlist->setScrollDownButton( std::make_shared<CButton>( Point(744, 526), AnimationPath::builtin("IAM015"), CButton::tooltipLocalized("core.help.307"), 0, EShortcut::MOVE_DOWN));
 
 	if(from)
 		townlist->select(from);
@@ -1326,27 +1360,14 @@ void CCastleInterface::recreateIcons()
 	hall = std::make_shared<CTownInfo>(80, 413, town, true);
 	fort = std::make_shared<CTownInfo>(122, 413, town, false);
 
-	fastTownHall = std::make_shared<CButton>(Point(80, 413), AnimationPath::builtin("castleInterfaceQuickAccess"), CButton::tooltip(), [this](){ builds->enterTownHall(); });
+	fastTownHall = std::make_shared<CButton>(Point(80, 413), AnimationPath::builtin("castleInterfaceQuickAccess"), CButton::tooltip(), [this](){ builds->enterTownHall(); }, EShortcut::TOWN_OPEN_HALL);
 	fastTownHall->setOverlay(std::make_shared<CAnimImage>(AnimationPath::builtin("ITMTL"), town->hallLevel()));
 
 	int imageIndex = town->fortLevel() == CGTownInstance::EFortLevel::NONE ? 3 : town->fortLevel() - 1;
-	fastArmyPurchase = std::make_shared<CButton>(Point(122, 413), AnimationPath::builtin("castleInterfaceQuickAccess"), CButton::tooltip(), [this](){ builds->enterToTheQuickRecruitmentWindow(); });
+	fastArmyPurchase = std::make_shared<CButton>(Point(122, 413), AnimationPath::builtin("castleInterfaceQuickAccess"), CButton::tooltip(), [this](){ builds->enterToTheQuickRecruitmentWindow(); }, EShortcut::TOWN_OPEN_RECRUITMENT);
 	fastArmyPurchase->setOverlay(std::make_shared<CAnimImage>(AnimationPath::builtin("itmcl"), imageIndex));
 
-	fastMarket = std::make_shared<LRClickableArea>(Rect(163, 410, 64, 42), [&]()
-	{
-		std::vector<const CGTownInstance*> towns = LOCPLINT->cb->getTownsInfo(true);
-		for(auto & town : towns)
-		{
-			if(town->builtBuildings.count(BuildingID::MARKETPLACE))
-			{
-				GH.windows().createAndPushWindow<CMarketWindow>(town, nullptr, nullptr, EMarketMode::RESOURCE_RESOURCE);
-				return;
-			}
-		}
-		LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithMarket"));
-	});
-	
+	fastMarket = std::make_shared<LRClickableArea>(Rect(163, 410, 64, 42), [this]() { builds->enterAnyMarket(); });
 	fastTavern = std::make_shared<LRClickableArea>(Rect(15, 387, 58, 64), [&]()
 	{
 		if(town->builtBuildings.count(BuildingID::TAVERN))
@@ -1367,18 +1388,41 @@ void CCastleInterface::recreateIcons()
 
 	for(size_t i=0; i<4; i++)
 		creainfo.push_back(std::make_shared<CCreaInfo>(Point(14 + 55 * (int)i, 507), town, (int)i + 4, compactCreatureInfo, useAvailableCreaturesForLabel));
-
 }
 
 void CCastleInterface::keyPressed(EShortcut key)
 {
 	switch(key)
 	{
-	case EShortcut::MOVE_UP:
-		townlist->selectPrev();
+	case EShortcut::TOWN_OPEN_FORT:
+		GH.windows().createAndPushWindow<CFortScreen>(town);
+		break;
+	case EShortcut::TOWN_OPEN_MARKET:
+		builds->enterAnyMarket();
+		break;
+	case EShortcut::TOWN_OPEN_MAGE_GUILD:
+		if(town->hasBuilt(BuildingID::MAGES_GUILD_1))
+			builds->enterMagesGuild();
+		break;
+	case EShortcut::TOWN_OPEN_THIEVES_GUILD:
+		break;
+	case EShortcut::TOWN_OPEN_HERO_EXCHANGE:
+		if (town->visitingHero && town->garrisonHero)
+			LOCPLINT->showHeroExchange(town->visitingHero->id, town->garrisonHero->id);
+		break;
+	case EShortcut::TOWN_OPEN_HERO:
+		if (town->visitingHero)
+			LOCPLINT->openHeroWindow(town->visitingHero);
+		else if (town->garrisonHero)
+			LOCPLINT->openHeroWindow(town->garrisonHero);
+		break;
+	case EShortcut::TOWN_OPEN_VISITING_HERO:
+		if (town->visitingHero)
+			LOCPLINT->openHeroWindow(town->visitingHero);
 		break;
-	case EShortcut::MOVE_DOWN:
-		townlist->selectNext();
+	case EShortcut::TOWN_OPEN_GARRISONED_HERO:
+		if (town->garrisonHero)
+			LOCPLINT->openHeroWindow(town->garrisonHero);
 		break;
 	case EShortcut::TOWN_SWAP_ARMIES:
 		heroes->swapArmies();

+ 3 - 1
client/windows/CCastleInterface.h

@@ -154,7 +154,6 @@ class CCastleBuildings : public CIntObject
 	void enterBuilding(BuildingID building);//for buildings with simple description + pic left-click messages
 	void enterCastleGate();
 	void enterFountain(const BuildingID & building, BuildingSubID::EBuildingSubID subID, BuildingID upgrades);//Rampart's fountains
-	void enterMagesGuild();
 	
 	void openMagesGuild();
 	void openTownHall();
@@ -168,6 +167,9 @@ public:
 
 	void enterDwelling(int level);
 	void enterTownHall();
+	void enterMagesGuild();
+	void enterAnyMarket();
+	void enterAnyThievesGuild();
 	void enterToTheQuickRecruitmentWindow();
 
 	void buildingClicked(BuildingID building, BuildingSubID::EBuildingSubID subID = BuildingSubID::NONE, BuildingID upgrades = BuildingID::NONE);

+ 376 - 0
client/windows/CExchangeWindow.cpp

@@ -0,0 +1,376 @@
+/*
+ * CExchangeWindow.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#include "StdInc.h"
+#include "CExchangeWindow.h"
+
+#include "CHeroBackpackWindow.h"
+
+#include "../CGameInfo.h"
+#include "../CPlayerInterface.h"
+
+#include "../gui/CGuiHandler.h"
+#include "../gui/CursorHandler.h"
+#include "../gui/Shortcut.h"
+#include "../gui/WindowHandler.h"
+
+#include "../widgets/CGarrisonInt.h"
+#include "../widgets/Images.h"
+#include "../widgets/Buttons.h"
+#include "../widgets/TextControls.h"
+
+#include "../render/IRenderHandler.h"
+#include "../render/CAnimation.h"
+
+#include "../../CCallback.h"
+
+#include "../lib/mapObjects/CGHeroInstance.h"
+#include "../lib/CGeneralTextHandler.h"
+#include "../lib/CHeroHandler.h"
+#include "../lib/filesystem/Filesystem.h"
+#include "../lib/CSkillHandler.h"
+#include "../lib/TextOperations.h"
+
+static const std::string QUICK_EXCHANGE_BG = "quick-exchange/TRADEQE";
+
+static bool isQuickExchangeLayoutAvailable()
+{
+	return CResourceHandler::get()->existsResource(ImagePath::builtin("SPRITES/" + QUICK_EXCHANGE_BG));
+}
+
+CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID queryID)
+	: CWindowObject(PLAYER_COLORED | BORDERED, ImagePath::builtin(isQuickExchangeLayoutAvailable() ? QUICK_EXCHANGE_BG : "TRADE2")),
+	controller(hero1, hero2),
+	moveStackLeftButtons(),
+	moveStackRightButtons()
+{
+	const bool qeLayout = isQuickExchangeLayoutAvailable();
+
+	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
+	addUsedEvents(KEYBOARD);
+
+	heroInst[0] = LOCPLINT->cb->getHero(hero1);
+	heroInst[1] = LOCPLINT->cb->getHero(hero2);
+
+	auto genTitle = [](const CGHeroInstance * h)
+	{
+		boost::format fmt(CGI->generaltexth->allTexts[138]);
+		fmt % h->getNameTranslated() % h->level % h->getClassNameTranslated();
+		return boost::str(fmt);
+	};
+
+	titles[0] = std::make_shared<CLabel>(147, 25, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, genTitle(heroInst[0]));
+	titles[1] = std::make_shared<CLabel>(653, 25, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, genTitle(heroInst[1]));
+
+	auto PSKIL32 = GH.renderHandler().loadAnimation(AnimationPath::builtin("PSKIL32"));
+	PSKIL32->preload();
+
+	auto SECSK32 = GH.renderHandler().loadAnimation(AnimationPath::builtin("SECSK32"));
+
+	for(int g = 0; g < 4; ++g)
+	{
+		if (qeLayout)
+			primSkillImages.push_back(std::make_shared<CAnimImage>(PSKIL32, g, Rect(389, 12 + 26 * g, 22, 22)));
+		else
+			primSkillImages.push_back(std::make_shared<CAnimImage>(PSKIL32, g, 0, 385, 19 + 36 * g));
+	}
+
+	for(int leftRight : {0, 1})
+	{
+		const CGHeroInstance * hero = heroInst.at(leftRight);
+
+		for(int m=0; m<GameConstants::PRIMARY_SKILLS; ++m)
+			primSkillValues[leftRight].push_back(std::make_shared<CLabel>(352 + (qeLayout ? 96 : 93) * leftRight, (qeLayout ? 22 : 35) + (qeLayout ? 26 : 36) * m, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE));
+
+
+		for(int m=0; m < hero->secSkills.size(); ++m)
+			secSkillIcons[leftRight].push_back(std::make_shared<CAnimImage>(SECSK32, 0, 0, 32 + 36 * m + 454 * leftRight, qeLayout ? 83 : 88));
+
+		specImages[leftRight] = std::make_shared<CAnimImage>(AnimationPath::builtin("UN32"), hero->type->imageIndex, 0, 67 + 490 * leftRight, qeLayout ? 41 : 45);
+
+		expImages[leftRight] = std::make_shared<CAnimImage>(PSKIL32, 4, 0, 103 + 490 * leftRight, qeLayout ? 41 : 45);
+		expValues[leftRight] = std::make_shared<CLabel>(119 + 490 * leftRight, qeLayout ? 66 : 71, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
+
+		manaImages[leftRight] = std::make_shared<CAnimImage>(PSKIL32, 5, 0, 139 + 490 * leftRight, qeLayout ? 41 : 45);
+		manaValues[leftRight] = std::make_shared<CLabel>(155 + 490 * leftRight, qeLayout ? 66 : 71, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
+	}
+
+	artifs[0] = std::make_shared<CArtifactsOfHeroMain>(Point(-334, 151));
+	artifs[0]->setHero(heroInst[0]);
+	artifs[1] = std::make_shared<CArtifactsOfHeroMain>(Point(98, 151));
+	artifs[1]->setHero(heroInst[1]);
+
+	addSetAndCallbacks(artifs[0]);
+	addSetAndCallbacks(artifs[1]);
+
+	for(int g=0; g<4; ++g)
+	{
+		primSkillAreas.push_back(std::make_shared<LRClickableAreaWTextComp>());
+		if (qeLayout)
+			primSkillAreas[g]->pos = Rect(Point(pos.x + 324, pos.y + 12 + 26 * g), Point(152, 22));
+		else
+			primSkillAreas[g]->pos = Rect(Point(pos.x + 329, pos.y + 19 + 36 * g), Point(140, 32));
+		primSkillAreas[g]->text = CGI->generaltexth->arraytxt[2+g];
+		primSkillAreas[g]->component = Component( ComponentType::PRIM_SKILL, PrimarySkill(g));
+		primSkillAreas[g]->hoverText = CGI->generaltexth->heroscrn[1];
+		boost::replace_first(primSkillAreas[g]->hoverText, "%s", CGI->generaltexth->primarySkillNames[g]);
+	}
+
+	//heroes related thing
+	for(int b=0; b < heroInst.size(); b++)
+	{
+		const CGHeroInstance * hero = heroInst.at(b);
+
+		//secondary skill's clickable areas
+		for(int g=0; g<hero->secSkills.size(); ++g)
+		{
+			SecondarySkill skill = hero->secSkills[g].first;
+			int level = hero->secSkills[g].second; // <1, 3>
+			secSkillAreas[b].push_back(std::make_shared<LRClickableAreaWTextComp>());
+			secSkillAreas[b][g]->pos = Rect(Point(pos.x + 32 + g * 36 + b * 454 , pos.y + (qeLayout ? 83 : 88)), Point(32, 32) );
+			secSkillAreas[b][g]->component = Component(ComponentType::SEC_SKILL, skill, level);
+			secSkillAreas[b][g]->text = CGI->skillh->getByIndex(skill)->getDescriptionTranslated(level);
+
+			secSkillAreas[b][g]->hoverText = CGI->generaltexth->heroscrn[21];
+			boost::algorithm::replace_first(secSkillAreas[b][g]->hoverText, "%s", CGI->generaltexth->levels[level - 1]);
+			boost::algorithm::replace_first(secSkillAreas[b][g]->hoverText, "%s", CGI->skillh->getByIndex(skill)->getNameTranslated());
+		}
+
+		heroAreas[b] = std::make_shared<CHeroArea>(257 + 228 * b, 13, hero);
+		heroAreas[b]->addClickCallback([this, hero]() -> void
+									   {
+										   if(getPickedArtifact() == nullptr)
+											   LOCPLINT->openHeroWindow(hero);
+									   });
+
+		specialtyAreas[b] = std::make_shared<LRClickableAreaWText>();
+		specialtyAreas[b]->pos = Rect(Point(pos.x + 69 + 490 * b, pos.y + (qeLayout ? 41 : 45)), Point(32, 32));
+		specialtyAreas[b]->hoverText = CGI->generaltexth->heroscrn[27];
+		specialtyAreas[b]->text = hero->type->getSpecialtyDescriptionTranslated();
+
+		experienceAreas[b] = std::make_shared<LRClickableAreaWText>();
+		experienceAreas[b]->pos = Rect(Point(pos.x + 105 + 490 * b, pos.y + (qeLayout ? 41 : 45)), Point(32, 32));
+		experienceAreas[b]->hoverText = CGI->generaltexth->heroscrn[9];
+		experienceAreas[b]->text = CGI->generaltexth->allTexts[2];
+		boost::algorithm::replace_first(experienceAreas[b]->text, "%d", std::to_string(hero->level));
+		boost::algorithm::replace_first(experienceAreas[b]->text, "%d", std::to_string(CGI->heroh->reqExp(hero->level+1)));
+		boost::algorithm::replace_first(experienceAreas[b]->text, "%d", std::to_string(hero->exp));
+
+		spellPointsAreas[b] = std::make_shared<LRClickableAreaWText>();
+		spellPointsAreas[b]->pos = Rect(Point(pos.x + 141 + 490 * b, pos.y + (qeLayout ? 41 : 45)), Point(32, 32));
+		spellPointsAreas[b]->hoverText = CGI->generaltexth->heroscrn[22];
+		spellPointsAreas[b]->text = CGI->generaltexth->allTexts[205];
+		boost::algorithm::replace_first(spellPointsAreas[b]->text, "%s", hero->getNameTranslated());
+		boost::algorithm::replace_first(spellPointsAreas[b]->text, "%d", std::to_string(hero->mana));
+		boost::algorithm::replace_first(spellPointsAreas[b]->text, "%d", std::to_string(hero->manaLimit()));
+
+		morale[b] = std::make_shared<MoraleLuckBox>(true, Rect(Point(176 + 490 * b, 39), Point(32, 32)), true);
+		luck[b] = std::make_shared<MoraleLuckBox>(false,  Rect(Point(212 + 490 * b, 39), Point(32, 32)), true);
+	}
+
+	quit = std::make_shared<CButton>(Point(732, 567), AnimationPath::builtin("IOKAY.DEF"), CGI->generaltexth->zelp[600], std::bind(&CExchangeWindow::close, this), EShortcut::GLOBAL_ACCEPT);
+	if(queryID.getNum() > 0)
+		quit->addCallback([=](){ LOCPLINT->cb->selectionMade(0, queryID); });
+
+	questlogButton[0] = std::make_shared<CButton>(Point( 10, qeLayout ? 39 : 44), AnimationPath::builtin("hsbtns4.def"), CButton::tooltip(CGI->generaltexth->heroscrn[0]), std::bind(&CExchangeWindow::questLogShortcut, this), EShortcut::ADVENTURE_QUEST_LOG);
+	questlogButton[1] = std::make_shared<CButton>(Point(740, qeLayout ? 39 : 44), AnimationPath::builtin("hsbtns4.def"), CButton::tooltip(CGI->generaltexth->heroscrn[0]), std::bind(&CExchangeWindow::questLogShortcut, this), EShortcut::ADVENTURE_QUEST_LOG);
+
+	Rect barRect(5, 578, 725, 18);
+	statusbar = CGStatusBar::create(std::make_shared<CPicture>(background->getSurface(), barRect, 5, 578));
+
+	//garrison interface
+
+	garr = std::make_shared<CGarrisonInt>(Point(69, qeLayout ? 122 : 131), 4, Point(418,0), heroInst[0], heroInst[1], true, true);
+	auto splitButtonCallback = [&](){ garr->splitClick(); };
+	garr->addSplitBtn(std::make_shared<CButton>( Point( 10, qeLayout ? 122 : 132), AnimationPath::builtin("TSBTNS.DEF"), CButton::tooltip(CGI->generaltexth->tcommands[3]), splitButtonCallback, EShortcut::HERO_ARMY_SPLIT));
+	garr->addSplitBtn(std::make_shared<CButton>( Point(744, qeLayout ? 122 : 132), AnimationPath::builtin("TSBTNS.DEF"), CButton::tooltip(CGI->generaltexth->tcommands[3]), splitButtonCallback, EShortcut::HERO_ARMY_SPLIT));
+
+	if(qeLayout)
+	{
+		moveAllGarrButtonLeft    = std::make_shared<CButton>(Point(325, 118), AnimationPath::builtin("quick-exchange/armRight.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[1]),
+			[this](){ this->moveUnitsShortcut(false); });
+		exchangeGarrButton       = std::make_shared<CButton>(Point(377, 118), AnimationPath::builtin("quick-exchange/swapAll.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[2]),
+			[this](){ controller.swapArmy(); });
+		moveAllGarrButtonRight   = std::make_shared<CButton>(Point(425, 118), AnimationPath::builtin("quick-exchange/armLeft.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[1]),
+			[this](){ this->moveUnitsShortcut(true); });
+		moveArtifactsButtonLeft  = std::make_shared<CButton>(Point(325, 154), AnimationPath::builtin("quick-exchange/artRight.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[3]),
+			[this](){ this->moveArtifactsCallback(false);});
+		exchangeArtifactsButton  = std::make_shared<CButton>(Point(377, 154), AnimationPath::builtin("quick-exchange/swapAll.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[4]),
+			[this](){ this->swapArtifactsCallback(); });
+		moveArtifactsButtonRight = std::make_shared<CButton>(Point(425, 154), AnimationPath::builtin("quick-exchange/artLeft.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[3]),
+			[this](){ this->moveArtifactsCallback(true);});
+
+		backpackButtonLeft       = std::make_shared<CButton>(Point(325, 518), AnimationPath::builtin("heroBackpack"), CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"),
+			[this](){ this->backpackShortcut(true); });
+		backpackButtonRight      = std::make_shared<CButton>(Point(419, 518), AnimationPath::builtin("heroBackpack"), CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"),
+			[this](){ this->backpackShortcut(false); });
+		backpackButtonLeft->setOverlay(std::make_shared<CPicture>(ImagePath::builtin("heroWindow/backpackButtonIcon")));
+		backpackButtonRight->setOverlay(std::make_shared<CPicture>(ImagePath::builtin("heroWindow/backpackButtonIcon")));
+
+		auto leftHeroBlock = heroInst[0]->tempOwner != LOCPLINT->cb->getPlayerID();
+		auto rightHeroBlock = heroInst[1]->tempOwner != LOCPLINT->cb->getPlayerID();
+		moveAllGarrButtonLeft->block(leftHeroBlock);
+		exchangeGarrButton->block(leftHeroBlock || rightHeroBlock);
+		moveAllGarrButtonRight->block(rightHeroBlock);
+		moveArtifactsButtonLeft->block(leftHeroBlock);
+		exchangeArtifactsButton->block(leftHeroBlock || rightHeroBlock);
+		moveArtifactsButtonRight->block(rightHeroBlock);
+		backpackButtonLeft->block(leftHeroBlock);
+		backpackButtonRight->block(rightHeroBlock);
+
+		for(int i = 0; i < GameConstants::ARMY_SIZE; i++)
+		{
+			moveStackLeftButtons.push_back(
+				std::make_shared<CButton>(
+					Point(484 + 35 * i, 154),
+					AnimationPath::builtin("quick-exchange/unitLeft.DEF"),
+					CButton::tooltip(CGI->generaltexth->qeModCommands[1]),
+					std::bind(&CExchangeController::moveStack, &controller, false, SlotID(i))));
+			moveStackLeftButtons.back()->block(leftHeroBlock);
+
+			moveStackRightButtons.push_back(
+				std::make_shared<CButton>(
+					Point(66 + 35 * i, 154),
+					AnimationPath::builtin("quick-exchange/unitRight.DEF"),
+					CButton::tooltip(CGI->generaltexth->qeModCommands[1]),
+					std::bind(&CExchangeController::moveStack, &controller, true, SlotID(i))));
+			moveStackLeftButtons.back()->block(rightHeroBlock);
+		}
+	}
+
+	updateWidgets();
+}
+
+void CExchangeWindow::moveArtifactsCallback(bool leftToRight)
+{
+	bool moveEquipped = !GH.isKeyboardShiftDown();
+	bool moveBackpack = !GH.isKeyboardCmdDown();
+	controller.moveArtifacts(leftToRight, moveEquipped, moveBackpack);
+};
+
+void CExchangeWindow::swapArtifactsCallback()
+{
+	bool moveEquipped = !GH.isKeyboardShiftDown();
+	bool moveBackpack = !GH.isKeyboardCmdDown();
+	controller.swapArtifacts(moveEquipped, moveBackpack);
+}
+
+void CExchangeWindow::moveUnitsShortcut(bool leftToRight)
+{
+	std::optional<SlotID> slotId = std::nullopt;
+	if(const auto * slot = getSelectedSlotID())
+		slotId = slot->getSlot();
+	controller.moveArmy(leftToRight, slotId);
+};
+
+void CExchangeWindow::backpackShortcut(bool leftHero)
+{
+	GH.windows().createAndPushWindow<CHeroBackpackWindow>(heroInst[leftHero ? 0 : 1], artSets);
+};
+
+void CExchangeWindow::keyPressed(EShortcut key)
+{
+	switch (key)
+	{
+		case EShortcut::EXCHANGE_ARMY_TO_LEFT:
+			moveUnitsShortcut(false);
+		break;
+		case EShortcut::EXCHANGE_ARMY_TO_RIGHT:
+			moveUnitsShortcut(true);
+		break;
+		case EShortcut::EXCHANGE_ARMY_SWAP:
+			controller.swapArmy();
+		break;
+		case EShortcut::EXCHANGE_ARTIFACTS_TO_LEFT:
+			controller.moveArtifacts(false, true, true);
+		break;
+		case EShortcut::EXCHANGE_ARTIFACTS_TO_RIGHT:
+			controller.moveArtifacts(true, true, true);
+		break;
+		case EShortcut::EXCHANGE_ARTIFACTS_SWAP:
+			controller.swapArtifacts(true, true);
+		break;
+		case EShortcut::EXCHANGE_EQUIPPED_TO_LEFT:
+			controller.moveArtifacts(false, true, false);
+		break;
+		case EShortcut::EXCHANGE_EQUIPPED_TO_RIGHT:
+			controller.moveArtifacts(true, true, false);
+		break;
+		case EShortcut::EXCHANGE_EQUIPPED_SWAP:
+			controller.swapArtifacts(true, false);
+		break;
+		case EShortcut::EXCHANGE_BACKPACK_TO_LEFT:
+			controller.moveArtifacts(false, false, true);
+		break;
+		case EShortcut::EXCHANGE_BACKPACK_TO_RIGHT:
+			controller.moveArtifacts(true, false, true);
+		break;
+		case EShortcut::EXCHANGE_BACKPACK_SWAP:
+			controller.swapArtifacts(false, true);
+		break;
+		case EShortcut::EXCHANGE_BACKPACK_LEFT:
+			backpackShortcut(true);
+		break;
+		case EShortcut::EXCHANGE_BACKPACK_RIGHT:
+			backpackShortcut(false);
+		break;
+	}
+}
+
+const CGarrisonSlot * CExchangeWindow::getSelectedSlotID() const
+{
+	return garr->getSelection();
+}
+
+void CExchangeWindow::updateGarrisons()
+{
+	garr->recreateSlots();
+
+	updateWidgets();
+}
+
+bool CExchangeWindow::holdsGarrison(const CArmedInstance * army)
+{
+	return garr->upperArmy() == army || garr->lowerArmy() == army;
+}
+
+void CExchangeWindow::questLogShortcut()
+{
+	CCS->curh->dragAndDropCursor(nullptr);
+	LOCPLINT->showQuestLog();
+}
+
+void CExchangeWindow::updateWidgets()
+{
+	for(size_t leftRight : {0, 1})
+	{
+		const CGHeroInstance * hero = heroInst.at(leftRight);
+
+		for(int m=0; m<GameConstants::PRIMARY_SKILLS; ++m)
+		{
+			auto value = heroInst[leftRight]->getPrimSkillLevel(static_cast<PrimarySkill>(m));
+			primSkillValues[leftRight][m]->setText(std::to_string(value));
+		}
+
+		for(int m=0; m < hero->secSkills.size(); ++m)
+		{
+			int id = hero->secSkills[m].first;
+			int level = hero->secSkills[m].second;
+
+			secSkillIcons[leftRight][m]->setFrame(2 + id * 3 + level);
+		}
+
+		expValues[leftRight]->setText(TextOperations::formatMetric(hero->exp, 3));
+		manaValues[leftRight]->setText(TextOperations::formatMetric(hero->mana, 3));
+
+		morale[leftRight]->set(hero);
+		luck[leftRight]->set(hero);
+	}
+}
+

+ 78 - 0
client/windows/CExchangeWindow.h

@@ -0,0 +1,78 @@
+/*
+ * CExchangeWindow.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "CWindowWithArtifacts.h"
+#include "../widgets/CExchangeController.h"
+
+class CGarrisonSlot;
+
+class CExchangeWindow : public CStatusbarWindow, public IGarrisonHolder, public CWindowWithArtifacts
+{
+	std::array<std::shared_ptr<CLabel>, 2> titles;
+	std::vector<std::shared_ptr<CAnimImage>> primSkillImages;//shared for both heroes
+	std::array<std::vector<std::shared_ptr<CLabel>>, 2> primSkillValues;
+	std::array<std::vector<std::shared_ptr<CAnimImage>>, 2> secSkillIcons;
+	std::array<std::shared_ptr<CAnimImage>, 2> specImages;
+	std::array<std::shared_ptr<CAnimImage>, 2> expImages;
+	std::array<std::shared_ptr<CLabel>, 2> expValues;
+	std::array<std::shared_ptr<CAnimImage>, 2> manaImages;
+	std::array<std::shared_ptr<CLabel>, 2> manaValues;
+
+	std::vector<std::shared_ptr<LRClickableAreaWTextComp>> primSkillAreas;
+	std::array<std::vector<std::shared_ptr<LRClickableAreaWTextComp>>, 2> secSkillAreas;
+
+	std::array<std::shared_ptr<CHeroArea>, 2> heroAreas;
+	std::array<std::shared_ptr<LRClickableAreaWText>, 2> specialtyAreas;
+	std::array<std::shared_ptr<LRClickableAreaWText>, 2> experienceAreas;
+	std::array<std::shared_ptr<LRClickableAreaWText>, 2> spellPointsAreas;
+
+	std::array<std::shared_ptr<MoraleLuckBox>, 2> morale;
+	std::array<std::shared_ptr<MoraleLuckBox>, 2> luck;
+
+	std::shared_ptr<CButton> quit;
+	std::array<std::shared_ptr<CButton>, 2> questlogButton;
+
+	std::shared_ptr<CGarrisonInt> garr;
+	std::shared_ptr<CButton> moveAllGarrButtonLeft;
+	std::shared_ptr<CButton> exchangeGarrButton;
+	std::shared_ptr<CButton> moveAllGarrButtonRight;
+	std::shared_ptr<CButton> moveArtifactsButtonLeft;
+	std::shared_ptr<CButton> exchangeArtifactsButton;
+	std::shared_ptr<CButton> moveArtifactsButtonRight;
+	std::vector<std::shared_ptr<CButton>> moveStackLeftButtons;
+	std::vector<std::shared_ptr<CButton>> moveStackRightButtons;
+	std::shared_ptr<CButton> backpackButtonLeft;
+	std::shared_ptr<CButton> backpackButtonRight;
+	CExchangeController controller;
+
+	void moveArtifactsCallback(bool leftToRight);
+	void swapArtifactsCallback();
+	void moveUnitsShortcut(bool leftToRight);
+	void backpackShortcut(bool leftHero);
+	void questLogShortcut();
+
+	std::array<const CGHeroInstance *, 2> heroInst;
+	std::array<std::shared_ptr<CArtifactsOfHeroMain>, 2> artifs;
+
+	const CGarrisonSlot * getSelectedSlotID() const;
+
+public:
+	CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID queryID);
+
+	void keyPressed(EShortcut key) override;
+
+	void updateWidgets();
+
+	// IGarrisonHolder impl
+	void updateGarrisons() override;
+	bool holdsGarrison(const CArmedInstance * army) override;
+
+};

+ 5 - 5
client/windows/CHeroWindow.cpp

@@ -13,7 +13,7 @@
 #include "CCreatureWindow.h"
 #include "CHeroBackpackWindow.h"
 #include "CKingdomInterface.h"
-#include "GUIClasses.h"
+#include "CExchangeWindow.h"
 
 #include "../CGameInfo.h"
 #include "../CPlayerInterface.h"
@@ -22,6 +22,7 @@
 #include "../gui/TextAlignment.h"
 #include "../gui/Shortcut.h"
 #include "../gui/WindowHandler.h"
+#include "../widgets/Images.h"
 #include "../widgets/MiscWidgets.h"
 #include "../widgets/CComponent.h"
 #include "../widgets/CGarrisonInt.h"
@@ -213,7 +214,7 @@ void CHeroWindow::update(const CGHeroInstance * hero, bool redrawNeeded)
 			boost::algorithm::replace_first(helpBox, "%s", CGI->generaltexth->allTexts[43]);
 
 			garr = std::make_shared<CGarrisonInt>(Point(15, 485), 8, Point(), curHero);
-			auto split = std::make_shared<CButton>(Point(539, 519), AnimationPath::builtin("hsbtns9.def"), CButton::tooltip(CGI->generaltexth->allTexts[256], helpBox), [&](){ garr->splitClick(); });
+			auto split = std::make_shared<CButton>(Point(539, 519), AnimationPath::builtin("hsbtns9.def"), CButton::tooltip(CGI->generaltexth->allTexts[256], helpBox), [&](){ garr->splitClick(); }, EShortcut::HERO_ARMY_SPLIT);
 			garr->addSplitBtn(split);
 		}
 		if(!arts)
@@ -281,9 +282,8 @@ void CHeroWindow::update(const CGHeroInstance * hero, bool redrawNeeded)
 
 	for(auto cew : GH.windows().findWindows<CExchangeWindow>())
 	{
-		for(int g=0; g < cew->heroInst.size(); ++g)
-			if(cew->heroInst[g] == curHero)
-				noDismiss = true;
+		if (cew->holdsGarrison(curHero))
+			noDismiss = true;
 	}
 
 	//if player only have one hero and no towns

+ 9 - 9
client/windows/CMarketWindow.cpp

@@ -140,35 +140,35 @@ void CMarketWindow::createChangeModeButtons(EMarketMode currentMode, const IMark
 	auto buttonPos = Point(18, 520);
 
 	auto addButton = [this, &buttonPos](const AnimationPath & picPath, const std::pair<std::string, std::string> & buttonHelpContainer,
-		const std::function<void()> & pressButtonFunctor)
+		const std::function<void()> & pressButtonFunctor, EShortcut shortcut)
 	{
-		changeModeButtons.emplace_back(std::make_shared<CButton>(buttonPos, picPath, buttonHelpContainer, pressButtonFunctor));
+		changeModeButtons.emplace_back(std::make_shared<CButton>(buttonPos, picPath, buttonHelpContainer, pressButtonFunctor, shortcut));
 		buttonPos -= Point(0, buttonHeightWithMargin);
 	};
 
 	if(isButtonVisible(EMarketMode::RESOURCE_PLAYER))
-		addButton(AnimationPath::builtin("TPMRKBU1.DEF"), CGI->generaltexth->zelp[612], std::bind(&CMarketWindow::createTransferResources, this, market, hero));
+		addButton(AnimationPath::builtin("TPMRKBU1.DEF"), CGI->generaltexth->zelp[612], std::bind(&CMarketWindow::createTransferResources, this, market, hero), EShortcut::MARKET_RESOURCE_PLAYER);
 	if(isButtonVisible(EMarketMode::ARTIFACT_RESOURCE))
-		addButton(AnimationPath::builtin("TPMRKBU3.DEF"), CGI->generaltexth->zelp[613], std::bind(&CMarketWindow::createArtifactsSelling, this, market, hero));
+		addButton(AnimationPath::builtin("TPMRKBU3.DEF"), CGI->generaltexth->zelp[613], std::bind(&CMarketWindow::createArtifactsSelling, this, market, hero), EShortcut::MARKET_ARTIFACT_RESOURCE);
 	if(isButtonVisible(EMarketMode::RESOURCE_ARTIFACT))
-		addButton(AnimationPath::builtin("TPMRKBU2.DEF"), CGI->generaltexth->zelp[598], std::bind(&CMarketWindow::createArtifactsBuying, this, market, hero));
+		addButton(AnimationPath::builtin("TPMRKBU2.DEF"), CGI->generaltexth->zelp[598], std::bind(&CMarketWindow::createArtifactsBuying, this, market, hero), EShortcut::MARKET_RESOURCE_ARTIFACT);
 
 	buttonPos = Point(516, 520 - buttonHeightWithMargin);
 	if(isButtonVisible(EMarketMode::CREATURE_RESOURCE))
-		addButton(AnimationPath::builtin("TPMRKBU4.DEF"), CGI->generaltexth->zelp[599], std::bind(&CMarketWindow::createFreelancersGuild, this, market, hero));
+		addButton(AnimationPath::builtin("TPMRKBU4.DEF"), CGI->generaltexth->zelp[599], std::bind(&CMarketWindow::createFreelancersGuild, this, market, hero), EShortcut::MARKET_CREATURE_RESOURCE);
 	if(isButtonVisible(EMarketMode::RESOURCE_RESOURCE))
-		addButton(AnimationPath::builtin("TPMRKBU5.DEF"), CGI->generaltexth->zelp[605], std::bind(&CMarketWindow::createMarketResources, this, market, hero));
+		addButton(AnimationPath::builtin("TPMRKBU5.DEF"), CGI->generaltexth->zelp[605], std::bind(&CMarketWindow::createMarketResources, this, market, hero), EShortcut::MARKET_RESOURCE_RESOURCE);
 	
 	buttonPos = Point(516, 421);
 	if(isButtonVisible(EMarketMode::CREATURE_EXP))
 	{
-		addButton(AnimationPath::builtin("ALTSACC.DEF"), CGI->generaltexth->zelp[572], std::bind(&CMarketWindow::createAltarCreatures, this, market, hero));
+		addButton(AnimationPath::builtin("ALTSACC.DEF"), CGI->generaltexth->zelp[572], std::bind(&CMarketWindow::createAltarCreatures, this, market, hero), EShortcut::MARKET_CREATURE_EXPERIENCE);
 		if(marketWidget->hero->getAlignment() == EAlignment::GOOD)
 			changeModeButtons.back()->block(true);
 	}
 	if(isButtonVisible(EMarketMode::ARTIFACT_EXP))
 	{
-		addButton(AnimationPath::builtin("ALTART.DEF"), CGI->generaltexth->zelp[580], std::bind(&CMarketWindow::createAltarArtifacts, this, market, hero));
+		addButton(AnimationPath::builtin("ALTART.DEF"), CGI->generaltexth->zelp[580], std::bind(&CMarketWindow::createAltarArtifacts, this, market, hero), EShortcut::MARKET_ARTIFACT_EXPERIENCE);
 		if(marketWidget->hero->getAlignment() == EAlignment::EVIL)
 			changeModeButtons.back()->block(true);
 	}

+ 1 - 1
client/windows/CQuestLog.cpp

@@ -128,7 +128,7 @@ CQuestLog::CQuestLog (const std::vector<QuestInfo> & Quests)
 	minimap = std::make_shared<CQuestMinimap>(Rect(12, 12, 169, 169));
 	// TextBox have it's own 4 pixel padding from top at least for English. To achieve 10px from both left and top only add 6px margin
 	description = std::make_shared<CTextBox>("", Rect(205, 18, 385, DESCRIPTION_HEIGHT_MAX), CSlider::BROWN, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE);
-	ok = std::make_shared<CButton>(Point(539, 398), AnimationPath::builtin("IOKAY.DEF"), CGI->generaltexth->zelp[445], std::bind(&CQuestLog::close, this), EShortcut::GLOBAL_ACCEPT);
+	ok = std::make_shared<CButton>(Point(539, 398), AnimationPath::builtin("IOKAY.DEF"), CGI->generaltexth->zelp[445], std::bind(&CQuestLog::close, this), EShortcut::GLOBAL_RETURN);
 	// Both button and lable are shifted to -2px by x and y to not make them actually look like they're on same line with quests list and ok button
 	hideCompleteButton = std::make_shared<CToggleButton>(Point(10, 396), AnimationPath::builtin("sysopchk.def"), CButton::tooltipLocalized("vcmi.questLog.hideComplete"), std::bind(&CQuestLog::toggleComplete, this, _1));
 	hideCompleteLabel = std::make_shared<CLabel>(46, 398, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("vcmi.questLog.hideComplete.hover"));

+ 1 - 1
client/windows/CTutorialWindow.cpp

@@ -45,7 +45,7 @@ CTutorialWindow::CTutorialWindow(const TutorialMode & m)
 
 	labelTitle = std::make_shared<CLabel>(190, 15, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.tutorialWindow.title"));
 	labelInformation = std::make_shared<CMultiLineLabel>(Rect(5, 40, 370, 60), EFonts::FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, "");
-	buttonOk = std::make_shared<CButton>(Point(159, 367), AnimationPath::builtin("IOKAY"), CButton::tooltip(), std::bind(&CTutorialWindow::exit, this), EShortcut::GLOBAL_ACCEPT); //62x28
+	buttonOk = std::make_shared<CButton>(Point(159, 367), AnimationPath::builtin("IOKAY"), CButton::tooltip(), std::bind(&CTutorialWindow::exit, this), EShortcut::GLOBAL_RETURN); //62x28
 	buttonLeft = std::make_shared<CButton>(Point(5, 217), AnimationPath::builtin("HSBTNS3"), CButton::tooltip(), std::bind(&CTutorialWindow::previous, this), EShortcut::MOVE_LEFT); //22x46
 	buttonRight = std::make_shared<CButton>(Point(352, 217), AnimationPath::builtin("HSBTNS5"), CButton::tooltip(), std::bind(&CTutorialWindow::next, this), EShortcut::MOVE_RIGHT); //22x46
 

+ 5 - 4
client/windows/CWindowWithArtifacts.cpp

@@ -10,6 +10,11 @@
 #include "StdInc.h"
 #include "CWindowWithArtifacts.h"
 
+#include "CHeroWindow.h"
+#include "CSpellWindow.h"
+#include "CExchangeWindow.h"
+#include "CHeroBackpackWindow.h"
+
 #include "../gui/CGuiHandler.h"
 #include "../gui/CursorHandler.h"
 #include "../gui/WindowHandler.h"
@@ -20,10 +25,6 @@
 
 #include "../widgets/CComponent.h"
 
-#include "../windows/CHeroWindow.h"
-#include "../windows/CSpellWindow.h"
-#include "../windows/GUIClasses.h"
-#include "../windows/CHeroBackpackWindow.h"
 #include "../CPlayerInterface.h"
 #include "../CGameInfo.h"
 

+ 1 - 1
client/windows/CreaturePurchaseCard.cpp

@@ -45,7 +45,7 @@ void CreaturePurchaseCard::initMinButton()
 
 void CreaturePurchaseCard::initCreatureSwitcherButton()
 {
-	creatureSwitcher = std::make_shared<CButton>(Point(pos.x + 18, pos.y-37), AnimationPath::builtin("iDv6432.def"), CButton::tooltip(), [&](){ switchCreatureLevel(); });
+	creatureSwitcher = std::make_shared<CButton>(Point(pos.x + 18, pos.y-37), AnimationPath::builtin("iDv6432.def"), CButton::tooltip(), [&](){ switchCreatureLevel(); }, EShortcut::RECRUITMENT_SWITCH_LEVEL);
 }
 
 void CreaturePurchaseCard::switchCreatureLevel()

+ 1 - 290
client/windows/GUIClasses.cpp

@@ -12,7 +12,6 @@
 
 #include "CCastleInterface.h"
 #include "CCreatureWindow.h"
-#include "CHeroBackpackWindow.h"
 #include "CHeroWindow.h"
 #include "InfoWindows.h"
 
@@ -49,7 +48,6 @@
 #include "../lib/mapObjects/CGHeroInstance.h"
 #include "../lib/mapObjects/CGMarket.h"
 #include "../lib/mapObjects/CGTownInstance.h"
-#include "../lib/mapObjects/ObjectTemplate.h"
 #include "../lib/gameState/CGameState.h"
 #include "../lib/gameState/SThievesGuildInfo.h"
 #include "../lib/gameState/TavernHeroesPool.h"
@@ -58,8 +56,6 @@
 #include "../lib/GameSettings.h"
 #include "ConditionalWait.h"
 #include "../lib/CSkillHandler.h"
-#include "../lib/filesystem/Filesystem.h"
-#include "../lib/TextOperations.h"
 
 CRecruitmentWindow::CCreatureCard::CCreatureCard(CRecruitmentWindow * window, const CCreature * crea, int totalAmount)
 	: CIntObject(LCLICK | SHOW_POPUP),
@@ -694,291 +690,6 @@ CTavernWindow::HeroSelector::HeroSelector(std::map<HeroTypeID, CGHeroInstance*>
 	center();
 }
 
-static const std::string QUICK_EXCHANGE_MOD_PREFIX = "quick-exchange";
-static const std::string QUICK_EXCHANGE_BG = QUICK_EXCHANGE_MOD_PREFIX + "/TRADEQE";
-
-static bool isQuickExchangeLayoutAvailable()
-{
-	return CResourceHandler::get()->existsResource(ImagePath::builtin("SPRITES/" + QUICK_EXCHANGE_BG));
-}
-
-CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID queryID)
-	: CWindowObject(PLAYER_COLORED | BORDERED, ImagePath::builtin(isQuickExchangeLayoutAvailable() ? QUICK_EXCHANGE_BG : "TRADE2")),
-	controller(hero1, hero2),
-	moveStackLeftButtons(),
-	moveStackRightButtons()
-{
-	const bool qeLayout = isQuickExchangeLayoutAvailable();
-
-	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
-
-	heroInst[0] = LOCPLINT->cb->getHero(hero1);
-	heroInst[1] = LOCPLINT->cb->getHero(hero2);
-
-	auto genTitle = [](const CGHeroInstance * h)
-	{
-		boost::format fmt(CGI->generaltexth->allTexts[138]);
-		fmt % h->getNameTranslated() % h->level % h->getClassNameTranslated();
-		return boost::str(fmt);
-	};
-
-	titles[0] = std::make_shared<CLabel>(147, 25, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, genTitle(heroInst[0]));
-	titles[1] = std::make_shared<CLabel>(653, 25, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, genTitle(heroInst[1]));
-
-	auto PSKIL32 = GH.renderHandler().loadAnimation(AnimationPath::builtin("PSKIL32"));
-	PSKIL32->preload();
-
-	auto SECSK32 = GH.renderHandler().loadAnimation(AnimationPath::builtin("SECSK32"));
-
-	for(int g = 0; g < 4; ++g) 
-	{
-		if (qeLayout)
-			primSkillImages.push_back(std::make_shared<CAnimImage>(PSKIL32, g, Rect(389, 12 + 26 * g, 22, 22)));
-		else
-			primSkillImages.push_back(std::make_shared<CAnimImage>(PSKIL32, g, 0, 385, 19 + 36 * g));
-	}
-
-	for(int leftRight : {0, 1})
-	{
-		const CGHeroInstance * hero = heroInst.at(leftRight);
-
-		for(int m=0; m<GameConstants::PRIMARY_SKILLS; ++m)
-			primSkillValues[leftRight].push_back(std::make_shared<CLabel>(352 + (qeLayout ? 96 : 93) * leftRight, (qeLayout ? 22 : 35) + (qeLayout ? 26 : 36) * m, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE));
-
-
-		for(int m=0; m < hero->secSkills.size(); ++m)
-			secSkillIcons[leftRight].push_back(std::make_shared<CAnimImage>(SECSK32, 0, 0, 32 + 36 * m + 454 * leftRight, qeLayout ? 83 : 88));
-
-		specImages[leftRight] = std::make_shared<CAnimImage>(AnimationPath::builtin("UN32"), hero->type->imageIndex, 0, 67 + 490 * leftRight, qeLayout ? 41 : 45);
-
-		expImages[leftRight] = std::make_shared<CAnimImage>(PSKIL32, 4, 0, 103 + 490 * leftRight, qeLayout ? 41 : 45);
-		expValues[leftRight] = std::make_shared<CLabel>(119 + 490 * leftRight, qeLayout ? 66 : 71, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
-
-		manaImages[leftRight] = std::make_shared<CAnimImage>(PSKIL32, 5, 0, 139 + 490 * leftRight, qeLayout ? 41 : 45);
-		manaValues[leftRight] = std::make_shared<CLabel>(155 + 490 * leftRight, qeLayout ? 66 : 71, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
-	}
-
-	artifs[0] = std::make_shared<CArtifactsOfHeroMain>(Point(-334, 151));
-	artifs[0]->setHero(heroInst[0]);
-	artifs[1] = std::make_shared<CArtifactsOfHeroMain>(Point(98, 151));
-	artifs[1]->setHero(heroInst[1]);
-
-	addSetAndCallbacks(artifs[0]);
-	addSetAndCallbacks(artifs[1]);
-
-	for(int g=0; g<4; ++g)
-	{
-		primSkillAreas.push_back(std::make_shared<LRClickableAreaWTextComp>());
-		if (qeLayout)
-			primSkillAreas[g]->pos = Rect(Point(pos.x + 324, pos.y + 12 + 26 * g), Point(152, 22));
-		else
-			primSkillAreas[g]->pos = Rect(Point(pos.x + 329, pos.y + 19 + 36 * g), Point(140, 32));
-		primSkillAreas[g]->text = CGI->generaltexth->arraytxt[2+g];
-		primSkillAreas[g]->component = Component( ComponentType::PRIM_SKILL, PrimarySkill(g));
-		primSkillAreas[g]->hoverText = CGI->generaltexth->heroscrn[1];
-		boost::replace_first(primSkillAreas[g]->hoverText, "%s", CGI->generaltexth->primarySkillNames[g]);
-	}
-
-	//heroes related thing
-	for(int b=0; b < heroInst.size(); b++)
-	{
-		const CGHeroInstance * hero = heroInst.at(b);
-
-		//secondary skill's clickable areas
-		for(int g=0; g<hero->secSkills.size(); ++g)
-		{
-			SecondarySkill skill = hero->secSkills[g].first;
-			int level = hero->secSkills[g].second; // <1, 3>
-			secSkillAreas[b].push_back(std::make_shared<LRClickableAreaWTextComp>());
-			secSkillAreas[b][g]->pos = Rect(Point(pos.x + 32 + g * 36 + b * 454 , pos.y + (qeLayout ? 83 : 88)), Point(32, 32) );
-			secSkillAreas[b][g]->component = Component(ComponentType::SEC_SKILL, skill, level);
-			secSkillAreas[b][g]->text = CGI->skillh->getByIndex(skill)->getDescriptionTranslated(level);
-
-			secSkillAreas[b][g]->hoverText = CGI->generaltexth->heroscrn[21];
-			boost::algorithm::replace_first(secSkillAreas[b][g]->hoverText, "%s", CGI->generaltexth->levels[level - 1]);
-			boost::algorithm::replace_first(secSkillAreas[b][g]->hoverText, "%s", CGI->skillh->getByIndex(skill)->getNameTranslated());
-		}
-
-		heroAreas[b] = std::make_shared<CHeroArea>(257 + 228 * b, 13, hero);
-		heroAreas[b]->addClickCallback([this, hero]() -> void
-			{
-				if(getPickedArtifact() == nullptr)
-					LOCPLINT->openHeroWindow(hero);
-			});
-
-		specialtyAreas[b] = std::make_shared<LRClickableAreaWText>();
-		specialtyAreas[b]->pos = Rect(Point(pos.x + 69 + 490 * b, pos.y + (qeLayout ? 41 : 45)), Point(32, 32));
-		specialtyAreas[b]->hoverText = CGI->generaltexth->heroscrn[27];
-		specialtyAreas[b]->text = hero->type->getSpecialtyDescriptionTranslated();
-
-		experienceAreas[b] = std::make_shared<LRClickableAreaWText>();
-		experienceAreas[b]->pos = Rect(Point(pos.x + 105 + 490 * b, pos.y + (qeLayout ? 41 : 45)), Point(32, 32));
-		experienceAreas[b]->hoverText = CGI->generaltexth->heroscrn[9];
-		experienceAreas[b]->text = CGI->generaltexth->allTexts[2];
-		boost::algorithm::replace_first(experienceAreas[b]->text, "%d", std::to_string(hero->level));
-		boost::algorithm::replace_first(experienceAreas[b]->text, "%d", std::to_string(CGI->heroh->reqExp(hero->level+1)));
-		boost::algorithm::replace_first(experienceAreas[b]->text, "%d", std::to_string(hero->exp));
-
-		spellPointsAreas[b] = std::make_shared<LRClickableAreaWText>();
-		spellPointsAreas[b]->pos = Rect(Point(pos.x + 141 + 490 * b, pos.y + (qeLayout ? 41 : 45)), Point(32, 32));
-		spellPointsAreas[b]->hoverText = CGI->generaltexth->heroscrn[22];
-		spellPointsAreas[b]->text = CGI->generaltexth->allTexts[205];
-		boost::algorithm::replace_first(spellPointsAreas[b]->text, "%s", hero->getNameTranslated());
-		boost::algorithm::replace_first(spellPointsAreas[b]->text, "%d", std::to_string(hero->mana));
-		boost::algorithm::replace_first(spellPointsAreas[b]->text, "%d", std::to_string(hero->manaLimit()));
-
-		morale[b] = std::make_shared<MoraleLuckBox>(true, Rect(Point(176 + 490 * b, 39), Point(32, 32)), true);
-		luck[b] = std::make_shared<MoraleLuckBox>(false,  Rect(Point(212 + 490 * b, 39), Point(32, 32)), true);
-	}
-
-	quit = std::make_shared<CButton>(Point(732, 567), AnimationPath::builtin("IOKAY.DEF"), CGI->generaltexth->zelp[600], std::bind(&CExchangeWindow::close, this), EShortcut::GLOBAL_ACCEPT);
-	if(queryID.getNum() > 0)
-		quit->addCallback([=](){ LOCPLINT->cb->selectionMade(0, queryID); });
-
-	questlogButton[0] = std::make_shared<CButton>(Point( 10, qeLayout ? 39 : 44), AnimationPath::builtin("hsbtns4.def"), CButton::tooltip(CGI->generaltexth->heroscrn[0]), std::bind(&CExchangeWindow::questlog, this, 0));
-	questlogButton[1] = std::make_shared<CButton>(Point(740, qeLayout ? 39 : 44), AnimationPath::builtin("hsbtns4.def"), CButton::tooltip(CGI->generaltexth->heroscrn[0]), std::bind(&CExchangeWindow::questlog, this, 1));
-
-	Rect barRect(5, 578, 725, 18);
-	statusbar = CGStatusBar::create(std::make_shared<CPicture>(background->getSurface(), barRect, 5, 578));
-
-	//garrison interface
-
-	garr = std::make_shared<CGarrisonInt>(Point(69, qeLayout ? 122 : 131), 4, Point(418,0), heroInst[0], heroInst[1], true, true);
-	auto splitButtonCallback = [&](){ garr->splitClick(); };
-	garr->addSplitBtn(std::make_shared<CButton>( Point( 10, qeLayout ? 122 : 132), AnimationPath::builtin("TSBTNS.DEF"), CButton::tooltip(CGI->generaltexth->tcommands[3]), splitButtonCallback));
-	garr->addSplitBtn(std::make_shared<CButton>( Point(744, qeLayout ? 122 : 132), AnimationPath::builtin("TSBTNS.DEF"), CButton::tooltip(CGI->generaltexth->tcommands[3]), splitButtonCallback));
-
-	if(qeLayout)
-	{
-		auto moveArtifacts = [](const std::function<void(bool, bool)> moveRoutine) -> void
-		{
-			bool moveEquipped = true;
-			bool moveBackpack = true;
-
-			if(GH.isKeyboardCmdDown())
-				moveBackpack = false;
-			else if(GH.isKeyboardShiftDown())
-				moveEquipped = false;
-			moveRoutine(moveEquipped, moveBackpack);
-		};
-
-		auto moveArmy = [this](const bool leftToRight) -> void
-		{
-			std::optional<SlotID> slotId = std::nullopt;
-			if(auto slot = getSelectedSlotID())
-				slotId = slot->getSlot();
-			controller.moveArmy(leftToRight, slotId);
-		};
-
-		auto openBackpack = [this](const CGHeroInstance * hero) -> void
-		{
-			GH.windows().createAndPushWindow<CHeroBackpackWindow>(hero, artSets);
-		};
-
-		moveAllGarrButtonLeft    = std::make_shared<CButton>(Point(325, 118), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/armRight.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[1]),
-			std::bind(moveArmy, true));
-		exchangeGarrButton       = std::make_shared<CButton>(Point(377, 118), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/swapAll.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[2]),
-			std::bind(&CExchangeController::swapArmy, &controller));
-		moveAllGarrButtonRight   = std::make_shared<CButton>(Point(425, 118), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/armLeft.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[1]),
-			std::bind(moveArmy, false));
-		moveArtifactsButtonLeft  = std::make_shared<CButton>(Point(325, 154), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/artRight.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[3]),
-			std::bind(moveArtifacts, [this](bool equipped, bool baclpack) -> void {controller.moveArtifacts(true, equipped, baclpack);}));
-		exchangeArtifactsButton  = std::make_shared<CButton>(Point(377, 154), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/swapAll.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[4]),
-			std::bind(moveArtifacts, [this](bool equipped, bool baclpack) -> void {controller.swapArtifacts(equipped, baclpack);}));
-		moveArtifactsButtonRight = std::make_shared<CButton>(Point(425, 154), AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/artLeft.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[3]),
-			std::bind(moveArtifacts, [this](bool equipped, bool baclpack) -> void {controller.moveArtifacts(false, equipped, baclpack);}));
-		backpackButtonLeft       = std::make_shared<CButton>(Point(325, 518), AnimationPath::builtin("heroBackpack"), CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"),
-			std::bind(openBackpack, heroInst[0]));
-		backpackButtonLeft->setOverlay(std::make_shared<CPicture>(ImagePath::builtin("heroWindow/backpackButtonIcon")));
-		backpackButtonRight      = std::make_shared<CButton>(Point(419, 518), AnimationPath::builtin("heroBackpack"), CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"),
-			std::bind(openBackpack, heroInst[1]));
-		backpackButtonRight->setOverlay(std::make_shared<CPicture>(ImagePath::builtin("heroWindow/backpackButtonIcon")));
-
-		auto leftHeroBlock = heroInst[0]->tempOwner != LOCPLINT->cb->getPlayerID();
-		auto rightHeroBlock = heroInst[1]->tempOwner != LOCPLINT->cb->getPlayerID();
-		moveAllGarrButtonLeft->block(leftHeroBlock);
-		exchangeGarrButton->block(leftHeroBlock || rightHeroBlock);
-		moveAllGarrButtonRight->block(rightHeroBlock);
-		moveArtifactsButtonLeft->block(leftHeroBlock);
-		exchangeArtifactsButton->block(leftHeroBlock || rightHeroBlock);
-		moveArtifactsButtonRight->block(rightHeroBlock);
-		backpackButtonLeft->block(leftHeroBlock);
-		backpackButtonRight->block(rightHeroBlock);
-
-		for(int i = 0; i < GameConstants::ARMY_SIZE; i++)
-		{
-			moveStackLeftButtons.push_back(
-				std::make_shared<CButton>(
-					Point(484 + 35 * i, 154),
-					AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/unitLeft.DEF"),
-					CButton::tooltip(CGI->generaltexth->qeModCommands[1]),
-					std::bind(&CExchangeController::moveStack, &controller, false, SlotID(i))));
-			moveStackLeftButtons.back()->block(leftHeroBlock);
-
-			moveStackRightButtons.push_back(
-				std::make_shared<CButton>(
-					Point(66 + 35 * i, 154),
-					AnimationPath::builtin(QUICK_EXCHANGE_MOD_PREFIX + "/unitRight.DEF"),
-					CButton::tooltip(CGI->generaltexth->qeModCommands[1]),
-					std::bind(&CExchangeController::moveStack, &controller, true, SlotID(i))));
-			moveStackLeftButtons.back()->block(rightHeroBlock);
-		}
-	}
-
-	updateWidgets();
-}
-
-const CGarrisonSlot * CExchangeWindow::getSelectedSlotID() const
-{
-	return garr->getSelection();
-}
-
-void CExchangeWindow::updateGarrisons()
-{
-	garr->recreateSlots();
-
-	updateWidgets();
-}
-
-bool CExchangeWindow::holdsGarrison(const CArmedInstance * army)
-{
-	return garr->upperArmy() == army || garr->lowerArmy() == army;
-}
-
-void CExchangeWindow::questlog(int whichHero)
-{
-	CCS->curh->dragAndDropCursor(nullptr);
-	LOCPLINT->showQuestLog();
-}
-
-void CExchangeWindow::updateWidgets()
-{
-	for(size_t leftRight : {0, 1})
-	{
-		const CGHeroInstance * hero = heroInst.at(leftRight);
-
-		for(int m=0; m<GameConstants::PRIMARY_SKILLS; ++m)
-		{
-			auto value = heroInst[leftRight]->getPrimSkillLevel(static_cast<PrimarySkill>(m));
-			primSkillValues[leftRight][m]->setText(std::to_string(value));
-		}
-
-		for(int m=0; m < hero->secSkills.size(); ++m)
-		{
-			int id = hero->secSkills[m].first;
-			int level = hero->secSkills[m].second;
-
-			secSkillIcons[leftRight][m]->setFrame(2 + id * 3 + level);
-		}
-
-		expValues[leftRight]->setText(TextOperations::formatMetric(hero->exp, 3));
-		manaValues[leftRight]->setText(TextOperations::formatMetric(hero->mana, 3));
-
-		morale[leftRight]->set(hero);
-		luck[leftRight]->set(hero);
-	}
-}
-
 CShipyardWindow::CShipyardWindow(const TResources & cost, int state, BoatId boatType, const std::function<void()> & onBuy)
 	: CWindowObject(PLAYER_COLORED, ImagePath::builtin("TPSHIP"))
 {
@@ -1306,7 +1017,7 @@ CGarrisonWindow::CGarrisonWindow(const CArmedInstance * up, const CGHeroInstance
 
 	garr = std::make_shared<CGarrisonInt>(Point(92, 127), 4, Point(0,96), up, down, removableUnits);
 	{
-		auto split = std::make_shared<CButton>(Point(88, 314), AnimationPath::builtin("IDV6432.DEF"), CButton::tooltip(CGI->generaltexth->tcommands[3], ""), [&](){ garr->splitClick(); } );
+		auto split = std::make_shared<CButton>(Point(88, 314), AnimationPath::builtin("IDV6432.DEF"), CButton::tooltip(CGI->generaltexth->tcommands[3], ""), [&](){ garr->splitClick(); }, EShortcut::HERO_ARMY_SPLIT );
 		garr->addSplitBtn(split);
 	}
 	quit = std::make_shared<CButton>(Point(399, 314), AnimationPath::builtin("IOK6432.DEF"), CButton::tooltip(CGI->generaltexth->tcommands[8], ""), [&](){ close(); }, EShortcut::GLOBAL_ACCEPT);

+ 5 - 57
client/windows/GUIClasses.h

@@ -9,21 +9,24 @@
  */
 #pragma once
 
+#include "CWindowObject.h"
 #include "../lib/ResourceSet.h"
-#include "../widgets/CExchangeController.h"
 #include "../widgets/Images.h"
-#include "CWindowWithArtifacts.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
+class CGHeroInstance;
 class CGObjectInstance;
 class CGDwelling;
 class IMarket;
 
 VCMI_LIB_NAMESPACE_END
 
+class CButton;
+class LRClickableArea;
 class CreatureCostBox;
 class CCreaturePic;
+class CMinorResDataBar;
 class MoraleLuckBox;
 class CHeroArea;
 class CSlider;
@@ -280,61 +283,6 @@ public:
 	void show(Canvas & to) override;
 };
 
-class CExchangeWindow : public CStatusbarWindow, public IGarrisonHolder, public CWindowWithArtifacts
-{
-	std::array<std::shared_ptr<CLabel>, 2> titles;
-	std::vector<std::shared_ptr<CAnimImage>> primSkillImages;//shared for both heroes
-	std::array<std::vector<std::shared_ptr<CLabel>>, 2> primSkillValues;
-	std::array<std::vector<std::shared_ptr<CAnimImage>>, 2> secSkillIcons;
-	std::array<std::shared_ptr<CAnimImage>, 2> specImages;
-	std::array<std::shared_ptr<CAnimImage>, 2> expImages;
-	std::array<std::shared_ptr<CLabel>, 2> expValues;
-	std::array<std::shared_ptr<CAnimImage>, 2> manaImages;
-	std::array<std::shared_ptr<CLabel>, 2> manaValues;
-
-	std::vector<std::shared_ptr<LRClickableAreaWTextComp>> primSkillAreas;
-	std::array<std::vector<std::shared_ptr<LRClickableAreaWTextComp>>, 2> secSkillAreas;
-
-	std::array<std::shared_ptr<CHeroArea>, 2> heroAreas;
-	std::array<std::shared_ptr<LRClickableAreaWText>, 2> specialtyAreas;
-	std::array<std::shared_ptr<LRClickableAreaWText>, 2> experienceAreas;
-	std::array<std::shared_ptr<LRClickableAreaWText>, 2> spellPointsAreas;
-
-	std::array<std::shared_ptr<MoraleLuckBox>, 2> morale;
-	std::array<std::shared_ptr<MoraleLuckBox>, 2> luck;
-
-	std::shared_ptr<CButton> quit;
-	std::array<std::shared_ptr<CButton>, 2> questlogButton;
-
-	std::shared_ptr<CGarrisonInt> garr;
-	std::shared_ptr<CButton> moveAllGarrButtonLeft;
-	std::shared_ptr<CButton> exchangeGarrButton;
-	std::shared_ptr<CButton> moveAllGarrButtonRight;
-	std::shared_ptr<CButton> moveArtifactsButtonLeft;
-	std::shared_ptr<CButton> exchangeArtifactsButton;
-	std::shared_ptr<CButton> moveArtifactsButtonRight;
-	std::vector<std::shared_ptr<CButton>> moveStackLeftButtons;
-	std::vector<std::shared_ptr<CButton>> moveStackRightButtons;
-	std::shared_ptr<CButton> backpackButtonLeft;
-	std::shared_ptr<CButton> backpackButtonRight;
-	CExchangeController controller;
-
-public:
-	std::array<const CGHeroInstance *, 2> heroInst;
-	std::array<std::shared_ptr<CArtifactsOfHeroMain>, 2> artifs;
-
-	void updateGarrisons() override;
-	bool holdsGarrison(const CArmedInstance * army) override;
-
-	void questlog(int whichHero); //questlog button callback; whichHero: 0 - left, 1 - right
-
-	void updateWidgets();
-
-	const CGarrisonSlot * getSelectedSlotID() const;
-
-	CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID queryID);
-};
-
 /// Here you can buy ships
 class CShipyardWindow : public CStatusbarWindow
 {

+ 190 - 107
config/shortcutsConfig.json

@@ -7,131 +7,95 @@
 // It is possible to add modifiers to keys: Ctrl, Shift, or Alt. For example, "Ctrl+Tab" hotkey will only activate if Ctrl is pressed
 {
 	"keyboard" : {
-		"globalAccept":             [ "Return", "Keypad Enter"],
-		"globalCancel":             "Escape",
-		"globalReturn":             [ "Escape", "Return", "Keypad Enter"],
-		"globalFullscreen":         "F4",
-		"globalOptions":            "O",
-		"globalBackspace":          "Backspace",
-		"globalMoveFocus":          "Tab",
-		"moveLeft":                 "Left",
-		"moveRight":                "Right",
-		"moveUp":                   "Up",
-		"moveDown":                 "Down",
-		"moveFirst":                "Home",
-		"moveLast":                 "End",
-		"movePageUp":               "PageUp",
-		"movePageDown":             "PageDown",
-		"selectIndex1":             "1",
-		"selectIndex2":             "2",
-		"selectIndex3":             "3",
-		"selectIndex4":             "4",
-		"selectIndex5":             "5",
-		"selectIndex6":             "6",
-		"selectIndex7":             "7",
-		"selectIndex8":             "8",
-		"mainMenuNewGame":          "N",
-		"mainMenuLoadGame":         "L",
-		"mainMenuHighScores":       "H",
-		"mainMenuCredits":          "C",
-		"mainMenuQuit":             "Q",
-		"mainMenuBack":             "B",
-		"mainMenuSingleplayer":     "S",
-		"mainMenuMultiplayer":      "M",
-		"mainMenuCampaign":         "C",
-		"mainMenuTutorial":         "T",
-		"mainMenuCampaignSod":      "S",
-		"mainMenuCampaignRoe":      "R",
-		"mainMenuCampaignAb":       "A",
-		"mainMenuCampaignCustom":   "C",
-		"lobbyBeginStandardGame":   "B",
-		"lobbyBeginCampaign":       [ "Return", "Keypad Enter"],
-		"lobbyLoadGame":            [ "L", "Return", "Keypad Enter"],
-		"lobbySaveGame":            [ "S", "Return", "Keypad Enter"],
-		"lobbyRandomMap":           "R",
-		"lobbyHideChat":            "H",
-		"lobbyAdditionalOptions":   "A",
-		"lobbySelectScenario":      "S",
-		"gameEndTurn":              "E",
-		"gameLoadGame":             "L",
-		"gameSaveGame":             "S",
-		"gameRestartGame":          "R",
-		"gameMainMenu":             "M",
-		"gameQuitGame":             "Q",
-		"gameOpenMarketplace":      "B",
-		"gameOpenThievesGuild":     "G",
-		"gameActivateConsole":      "Tab",
+		"adventureCastSpell":       "C",
+		"adventureDigGrail":        "D",
+		"adventureEndTurn":         "E",
+		"adventureExitWorldView":   [ "Escape", "Return", "Keypad Enter"],
+		"adventureFirstHero":       "Ctrl+H",
+		"adventureFirstTown":       "Ctrl+T",
 		"adventureGameOptions":     "O",
-		"adventureToggleGrid":      "F6",
-		"adventureToggleSleep":     [],
-		"adventureSetHeroAsleep":   "Z",
-		"adventureSetHeroAwake":    "W",
+		"adventureKingdomOverview": "K",
+		"adventureLoadGame":        "L",
+		"adventureMainMenu":        "M",
+		"adventureMarketplace":     "B",
 		"adventureMoveHero":        "M",
-		"adventureVisitObject":     "Space",
-		"adventureMoveHeroSW":      [ "Keypad 1" ],
-		"adventureMoveHeroSS":      [ "Keypad 2", "Down" ],
-		"adventureMoveHeroSE":      [ "Keypad 3" ],
-		"adventureMoveHeroWW":      [ "Keypad 4", "Left" ],
 		"adventureMoveHeroEE":      [ "Keypad 6", "Right" ],
-		"adventureMoveHeroNW":      [ "Keypad 7" ],
+		"adventureMoveHeroNE":      [ "Keypad 9", "PageUp" ],
 		"adventureMoveHeroNN":      [ "Keypad 8", "Up" ],
-		"adventureMoveHeroNE":      [ "Keypad 9" ],
-		"adventureViewSelected":    [ "Return", "Keypad Enter"],
-		"adventureNextObject":      [],
-		"adventureNextTown":        "T",
+		"adventureMoveHeroNW":      [ "Keypad 7", "Home" ],
+		"adventureMoveHeroSE":      [ "Keypad 3", "PageDown" ],
+		"adventureMoveHeroSS":      [ "Keypad 2", "Down" ],
+		"adventureMoveHeroSW":      [ "Keypad 1", "End" ],
+		"adventureMoveHeroWW":      [ "Keypad 4", "Left" ],
+		"adventureNewGame":         "Ctrl+N",
 		"adventureNextHero":        "H",
-		"adventureFirstTown":       [],
-		"adventureFirstHero":       [],
-		"adventureViewScenario":    "I",
-		"adventureDigGrail":        "D",
+		"adventureNextObject":      "N",
+		"adventureNextTown":        "T",
+		"adventureQuestLog":        "Q",
+		"adventureQuitGame":        "Ctrl+Q",
+		"adventureReplayTurn":      [], // NOTE: functionality not implemented
+		"adventureRestartGame":     "R",
+		"adventureSaveGame":        "S",
+		"adventureSetHeroAsleep":   "Z",
+		"adventureSetHeroAwake":    "W",
+		"adventureThievesGuild":    "G",
+		"adventureToggleGrid":      "F6",
+		"adventureToggleMapLevel":  "U",
+		"adventureToggleSleep":     [],
+		"adventureTrackHero":       "F5",
 		"adventureViewPuzzle":      "P",
+		"adventureViewScenario":    "I",
+		"adventureViewSelected":    [ "Return", "Keypad Enter"],
 		"adventureViewWorld":       "V",
 		"adventureViewWorld1":      "1",
 		"adventureViewWorld2":      "2",
 		"adventureViewWorld4":      "4",
-		"adventureToggleMapLevel":  "U",
-		"adventureKingdomOverview": "K",
-		"adventureQuestLog":        "Q",
-		"adventureCastSpell":       "C",
-		"adventureThievesGuild":    "G",
-		"adventureExitWorldView":   [ "Escape", "Return", "Keypad Enter"],
+		"adventureVisitObject":     "Space",
 		"adventureZoomIn":          "Keypad +",
 		"adventureZoomOut":         "Keypad -",
 		"adventureZoomReset":       "Backspace",
-		"battleToggleQueue":        "Q",
-		"battleUseCreatureSpell":   "F",
-		"battleSurrender":          "S",
-		"battleRetreat":            "R",
 		"battleAutocombat":         "A",
 		"battleAutocombatEnd":      "E",
 		"battleCastSpell":          "C",
-		"battleWait":               "W",
-		"battleDefend":             [ "D", "Space"],
-		"battleConsoleUp":          "Up",
 		"battleConsoleDown":        "Down",
-		"battleTacticsNext":        "Space",
+		"battleConsoleUp":          "Up",
+		"battleDefend":             [ "D", "Space"],
+		"battleOpenActiveUnit":     "I",
+		"battleOpenHoveredUnit":    "V",
+		"battleRetreat":            "R",
+		"battleSelectAction":       "S",
+		"battleSurrender":          "S",
 		"battleTacticsEnd":         [ "Return", "Keypad Enter"],
+		"battleTacticsNext":        "Space",
 		"battleToggleHeroesStats":  [],
-		"battleSelectAction":       "S",
-		"lobbyActivateInterface":   "Ctrl+Tab",
-		"spectateTrackHero":        "F5",
-		"spectateSkipBattle":       "F7",
-		"spectateSkipBattleResult": "F8",
-		"townOpenTavern":           "T",
-		"townSwapArmies":           "Space",
-		"recruitmentMax":           "End",
-		"recruitmentMin":           "Home",
-		"recruitmentUpgrade":       "U",
-		"recruitmentUpgradeAll":    [ "A", "U" ],
-		"kingdomHeroesTab":         "H",
-		"kingdomTownsTab":          "T",
-		"heroDismiss":              "D",
+		"battleToggleQueue":        "Q",
+		"battleUseCreatureSpell":   "F",
+		"battleWait":               "W",
+		"exchangeArmySwap":         "F10",
+		"exchangeArmyToLeft":       [],
+		"exchangeArmyToRight":      [],
+		"exchangeArtifactsSwap":    "F11",
+		"exchangeArtifactsToLeft":  [],
+		"exchangeArtifactsToRight": [],
+		"exchangeBackpackLeft":     [],
+		"exchangeBackpackRight":    [],
+		"exchangeBackpackSwap":     "Shift+F11",
+		"exchangeBackpackToLeft":   [],
+		"exchangeBackpackToRight":  [],
+		"exchangeEquippedSwap":     "Ctrl+F11",
+		"exchangeEquippedToLeft":   [],
+		"exchangeEquippedToRight":  [],
+		"gameActivateConsole":      "Tab",
+		"globalAccept":             [ "Return", "Keypad Enter"],
+		"globalBackspace":          "Backspace",
+		"globalCancel":             "Escape",
+		"globalFullscreen":         "F4",
+		"globalMoveFocus":          "Tab",
+		"globalOptions":            "O",
+		"globalReturn":             [ "Escape", "Return", "Keypad Enter"],
+		"heroArmySplit":            "S",
+		"heroBackpack":             "B",
 		"heroCommander":            "C",
-		"heroLooseFormation":       "L",
-		"heroTightFormation":       "T",
-		"heroToggleTactics":        "B",
-		"spellbookTabAdventure":    "A",
-		"spellbookTabCombat":       "C",
 		"heroCostumeLoad0":         "0",
 		"heroCostumeLoad1":         "1",
 		"heroCostumeLoad2":         "2",
@@ -151,7 +115,126 @@
 		"heroCostumeSave6":         "Ctrl+6",
 		"heroCostumeSave7":         "Ctrl+7",
 		"heroCostumeSave8":         "Ctrl+8",
-		"heroCostumeSave9":         "Ctrl+9"
+		"heroCostumeSave9":         "Ctrl+9",
+		"heroDismiss":              "D",
+		"heroLooseFormation":       "L",
+		"heroTightFormation":       "T",
+		"heroToggleTactics":        "B",
+		"highScoresCampaigns":      "C",
+		"highScoresReset":          "R",
+		"highScoresScenarios":      "S",
+		"kingdomHeroesTab":         "H",
+		"kingdomTownsTab":          "T",
+		"lobbyAdditionalOptions":   "A",
+		"lobbyBeginCampaign":       [ "Return", "Keypad Enter"],
+		"lobbyBeginStandardGame":   "B",
+		"lobbyExtraOptions":        "E",
+		"lobbyFlipCoin":            "F",
+		"lobbyInvitePlayers":       "I",
+		"lobbyLoadGame":            [ "L", "Return", "Keypad Enter"],
+		"lobbyRandomMap":           "R",
+		"lobbyRandomTown":          "T",
+		"lobbyRandomTownVs":        "V",
+		"lobbyReplayVideo":         "R",
+		"lobbySaveGame":            [ "S", "Return", "Keypad Enter"],
+		"lobbySelectScenario":      "S",
+		"lobbyToggleChat":          "H",
+		"lobbyTurnOptions":         "T",
+		"mainMenuBack":             [ "B", "Escape" ],
+		"mainMenuCampaign":         "C",
+		"mainMenuCampaignAb":       "A",
+		"mainMenuCampaignCustom":   "C",
+		"mainMenuCampaignRoe":      "R",
+		"mainMenuCampaignSod":      "S",
+		"mainMenuCredits":          "C",
+		"mainMenuHighScores":       "H",
+		"mainMenuHostGame":         "C",
+		"mainMenuHotseat":          "H",
+		"mainMenuJoinGame":         "J",
+		"mainMenuLoadGame":         "L",
+		"mainMenuLobby":            "Ctrl+Tab",
+		"mainMenuMultiplayer":      "M",
+		"mainMenuNewGame":          "N",
+		"mainMenuQuit":             [ "Q", "Escape" ],
+		"mainMenuSingleplayer":     "S",
+		"mainMenuTutorial":         "T",
+		"mapsSizeAll":              [],
+		"mapsSizeL":                [],
+		"mapsSizeM":                [],
+		"mapsSizeS":                [],
+		"mapsSizeXl":               [],
+		"mapsSortChangedate":       [],
+		"mapsSortDefeat":           [],
+		"mapsSortFormat":           [],
+		"mapsSortMaps":             [],
+		"mapsSortName":             [],
+		"mapsSortPlayers":          [],
+		"mapsSortSize":             [],
+		"mapsSortVictory":          [],
+		"marketArtifactExperience": [],
+		"marketArtifactResource":   [],
+		"marketCreatureExperience": [],
+		"marketCreatureResource":   [],
+		"marketDeal":               "Space",
+		"marketMaxAmount":          "M",
+		"marketResourceArtifact":   [],
+		"marketResourcePlayer":     [],
+		"marketResourceResource":   [],
+		"marketSacrificeAll":       "A",
+		"marketSacrificeBackpack":  "B",
+		"moveDown":                 "Down",
+		"moveFirst":                "Home",
+		"moveLast":                 "End",
+		"moveLeft":                 "Left",
+		"movePageDown":             "PageDown",
+		"movePageUp":               "PageUp",
+		"moveRight":                "Right",
+		"moveUp":                   "Up",
+		"recruitmentMax":           [ "End", "M" ],
+		"recruitmentMin":           "Home",
+		"recruitmentSwitchLevel":   "Tab",
+		"recruitmentUpgrade":       "U",
+		"recruitmentUpgradeAll":    [ "A", "U" ],
+		"selectIndex1":             "1",
+		"selectIndex2":             "2",
+		"selectIndex3":             "3",
+		"selectIndex4":             "4",
+		"selectIndex5":             "5",
+		"selectIndex6":             "6",
+		"selectIndex7":             "7",
+		"selectIndex8":             "8",
+		"settingsLoadGame":         "L",
+		"settingsNewGame":          "N",
+		"settingsQuitGame":         "Q",
+		"settingsRestartGame":      "R",
+		"settingsSaveGame":         "S",
+		"settingsToMainMenu":       "M",
+		"spectateSkipBattle":       [],
+		"spectateSkipBattleResult": [],
+		"spectateTrackHero":        [],
+		"spellbookTabAdventure":    "A",
+		"spellbookTabCombat":       "C",
+		"townOpenFort":             "F",
+		"townOpenGarrisonedHero":   "Shift+H",
+		"townOpenHall":             "B",
+		"townOpenHero":             "H",
+		"townOpenHeroExchange":     "E",
+		"townOpenMageGuild":        "G",
+		"townOpenMarket":           "M",
+		"townOpenRecruitment":      "R",
+		"townOpenTavern":           "T",
+		"townOpenThievesGuild":     "G",
+		"townOpenVisitingHero":     "Ctrl+H",
+		"townSwapArmies":           "Space",
+		
+		// Controller-specific
+		"mouseCursorX":             [],
+		"mouseCursorY":             [],
+		"mouseClickLeft":           [],
+		"mouseClickRight":          [],
+		"mouseSwipeX":              [],
+		"mouseSwipeY":              [],
+
 	},
 	
 	"joystickAxes":
@@ -191,7 +274,7 @@
 		"adventureCastSpell" : "righttrigger",
 		"battleCastSpell" : "righttrigger",
 		
-		"gameEndTurn" : "back",
+		"adventureEndTurn" : "back",
 		"battleAutocombatEnd" : "back",
 		
 		"globalOptions" : "start",

+ 5 - 5
config/widgets/settings/settingsMainContainer.json

@@ -104,7 +104,7 @@
 			"imageOrder": [1, 0, 2, 3],
 			"help": "core.help.321",
 			"callback": "loadGame",
-			"hotkey": "gameLoadGame"
+			"hotkey": "settingsLoadGame"
 		},
 		{
 			"name": "saveButton",
@@ -114,7 +114,7 @@
 			"imageOrder": [1, 0, 2, 3],
 			"help": "core.help.322",
 			"callback": "saveGame",
-			"hotkey": "gameSaveGame"
+			"hotkey": "settingsSaveGame"
 		},
 		{
 			"name": "restartButton",
@@ -124,7 +124,7 @@
 			"imageOrder": [1, 0, 2, 3],
 			"help": "core.help.323",
 			"callback": "restartGame",
-			"hotkey": "gameRestartGame"
+			"hotkey": "settingsRestartGame"
 		},
 		{
 			"name": "mainMenuButton",
@@ -134,7 +134,7 @@
 			"imageOrder": [1, 0, 2, 3],
 			"help": "core.help.320",
 			"callback": "returnToMainMenu",
-			"hotkey": "gameMainMenu"
+			"hotkey": "settingsMainMenu"
 		},
 		{
 			"name": "quitButton",
@@ -144,7 +144,7 @@
 			"imageOrder": [1, 0, 2, 3],
 			"help": "core.help.324",
 			"callback": "quitGame",
-			"hotkey": "gameQuitGame"
+			"hotkey": "settingsQuitGame"
 		},
 		{
 			"name": "closeSettingsButton",