Переглянути джерело

Merge pull request #2185 from IvanSavenko/touch_input_gestures

Touch input gestures
Ivan Savenko 2 роки тому
батько
коміт
ee8c8dca7b
77 змінених файлів з 1469 додано та 1090 видалено
  1. 0 4
      Mods/vcmi/config/vcmi/chinese.json
  2. 2 4
      Mods/vcmi/config/vcmi/english.json
  3. 0 2
      Mods/vcmi/config/vcmi/german.json
  4. 0 2
      Mods/vcmi/config/vcmi/polish.json
  5. 0 2
      Mods/vcmi/config/vcmi/russian.json
  6. 0 4
      Mods/vcmi/config/vcmi/spanish.json
  7. 8 2
      Mods/vcmi/config/vcmi/ukrainian.json
  8. 4 0
      client/CMakeLists.txt
  9. 1 1
      client/CServerHandler.cpp
  10. 2 2
      client/CServerHandler.h
  11. 1 2
      client/adventureMap/AdventureMapInterface.cpp
  12. 26 8
      client/adventureMap/CList.cpp
  13. 6 2
      client/adventureMap/CList.h
  14. 11 5
      client/adventureMap/CMinimap.cpp
  15. 2 1
      client/adventureMap/CMinimap.h
  16. 77 69
      client/battle/BattleActionsController.cpp
  17. 0 5
      client/battle/BattleActionsController.h
  18. 57 39
      client/battle/BattleFieldController.cpp
  19. 20 10
      client/battle/BattleFieldController.h
  20. 1 7
      client/battle/BattleWindow.cpp
  21. 0 1
      client/battle/BattleWindow.h
  22. 44 51
      client/eventsSDL/InputHandler.cpp
  23. 8 3
      client/eventsSDL/InputHandler.h
  24. 19 0
      client/eventsSDL/InputSourceKeyboard.cpp
  25. 4 0
      client/eventsSDL/InputSourceKeyboard.h
  26. 26 8
      client/eventsSDL/InputSourceMouse.cpp
  27. 8 0
      client/eventsSDL/InputSourceMouse.h
  28. 216 66
      client/eventsSDL/InputSourceTouch.cpp
  29. 86 8
      client/eventsSDL/InputSourceTouch.h
  30. 4 0
      client/eventsSDL/UserEventHandler.cpp
  31. 1 1
      client/gui/CGuiHandler.cpp
  32. 1 0
      client/gui/CGuiHandler.h
  33. 1 1
      client/gui/CIntObject.cpp
  34. 1 4
      client/gui/CIntObject.h
  35. 5 5
      client/gui/CursorHandler.cpp
  36. 4 6
      client/gui/CursorHandler.h
  37. 84 19
      client/gui/EventDispatcher.cpp
  38. 7 2
      client/gui/EventDispatcher.h
  39. 6 23
      client/gui/EventsReceiver.cpp
  40. 25 15
      client/gui/EventsReceiver.h
  41. 1 0
      client/gui/InterfaceObjectConfigurable.cpp
  42. 4 3
      client/lobby/CSelectionBase.cpp
  43. 31 2
      client/lobby/OptionsTab.cpp
  44. 4 1
      client/lobby/OptionsTab.h
  45. 1 0
      client/lobby/RandomMapTab.cpp
  46. 9 6
      client/lobby/SelectionTab.cpp
  47. 1 1
      client/lobby/SelectionTab.h
  48. 1 1
      client/mapView/MapView.cpp
  49. 22 71
      client/mapView/MapViewActions.cpp
  50. 6 10
      client/mapView/MapViewActions.h
  51. 2 2
      client/mapView/MapViewController.cpp
  52. 0 383
      client/widgets/Buttons.cpp
  53. 0 104
      client/widgets/Buttons.h
  54. 4 2
      client/widgets/ObjectLists.cpp
  55. 94 0
      client/widgets/Scrollable.cpp
  56. 61 0
      client/widgets/Scrollable.h
  57. 296 0
      client/widgets/Slider.cpp
  58. 86 0
      client/widgets/Slider.h
  59. 1 1
      client/widgets/TextControls.cpp
  60. 1 0
      client/windows/CMessage.cpp
  61. 5 4
      client/windows/CQuestLog.cpp
  62. 8 7
      client/windows/CTradeWindow.cpp
  63. 3 2
      client/windows/CreaturePurchaseCard.cpp
  64. 6 6
      client/windows/GUIClasses.cpp
  65. 1 2
      client/windows/GUIClasses.h
  66. 4 3
      client/windows/QuickRecruitmentWindow.cpp
  67. 7 14
      client/windows/settings/AdventureOptionsTab.cpp
  68. 4 18
      client/windows/settings/BattleOptionsTab.cpp
  69. 0 1
      client/windows/settings/BattleOptionsTab.h
  70. 6 4
      client/windows/settings/GeneralOptionsTab.cpp
  71. 12 17
      config/schemas/settings.json
  72. 7 7
      config/widgets/settings/adventureOptionsTab.json
  73. 1 9
      config/widgets/settings/battleOptionsTab.json
  74. 2 8
      config/widgets/settings/generalOptionsTab.json
  75. 0 14
      launcher/firstLaunch/firstlaunch_moc.cpp
  76. 0 3
      launcher/firstLaunch/firstlaunch_moc.h
  77. 10 0
      lib/Point.h

+ 0 - 4
Mods/vcmi/config/vcmi/chinese.json

@@ -64,8 +64,6 @@
 	"vcmi.adventureOptions.forceMovementInfo.help" : "{在状态栏中显示移动力}\n\n不需要按ALT就可以显示移动力。",
 	"vcmi.adventureOptions.showGrid.hover" : "显示网格",
 	"vcmi.adventureOptions.showGrid.help" : "{显示网格}\n\n显示网格覆盖层,高亮冒险地图物件的边沿。",
-	"vcmi.adventureOptions.mapSwipe.hover" : "地图拖动/镜头",
-	"vcmi.adventureOptions.mapSwipe.help" : "{地图拖动/镜头}\n\n在触摸屏设备上,你可以用手指轻扫来移动地图。使用鼠标时,按住鼠标左键或中键移动地图。",
 	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",
@@ -88,8 +86,6 @@
 	"vcmi.battleOptions.animationsSpeed1.help": "设置动画速度为非常慢",
 	"vcmi.battleOptions.animationsSpeed5.help": "设置动画速度为非常快",
 	"vcmi.battleOptions.animationsSpeed6.help": "设置动画速度为即刻",
-	"vcmi.battleOptions.touchscreenMode.hover": "触屏模式",
-	"vcmi.battleOptions.touchscreenMode.help": "{触屏模式}\n\n当启用时,需要进行双击进行确认和执行动作。减少触屏设备误触。",
 	"vcmi.battleOptions.movementHighlightOnHover.hover": "鼠标悬停高亮单位移动范围",
 	"vcmi.battleOptions.movementHighlightOnHover.help": "{鼠标悬停高亮单位移动范围}\n\n当你的鼠标悬停在单位上时高亮他的行动范围。",
 	"vcmi.battleOptions.skipBattleIntroMusic.hover": "跳过战斗开始音乐",

+ 2 - 4
Mods/vcmi/config/vcmi/english.json

@@ -78,8 +78,8 @@
 	"vcmi.adventureOptions.forceMovementInfo.help" : "{Always Show Movement Cost}\n\nAlways show movement points data in status bar information. (Instead of viewing it only while you hold down ALT key)",
 	"vcmi.adventureOptions.showGrid.hover" : "Show Grid",
 	"vcmi.adventureOptions.showGrid.help" : "{Show Grid}\n\nShow the grid overlay, highlighting the borders between adventure map tiles.",
-	"vcmi.adventureOptions.mapSwipe.hover" : "Map Swipe/Panning",
-	"vcmi.adventureOptions.mapSwipe.help" : "{Map Swipe/Panning}\n\nOn touchscreen devices, you can move the map by swiping with your finger. To pan the map using the mouse, hold down the left or middle mouse button and move the mouse.",
+	"vcmi.adventureOptions.borderScroll.hover" : "Border Scrolling",
+	"vcmi.adventureOptions.borderScroll.help" : "{Border Scrolling}\n\nScroll adventure map when cursor is adjacent to window edge. Can be disabled by holding down CTRL key.",
 	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",
@@ -102,8 +102,6 @@
 	"vcmi.battleOptions.animationsSpeed1.help": "Set animation speed to very slow",
 	"vcmi.battleOptions.animationsSpeed5.help": "Set animation speed to very fast",
 	"vcmi.battleOptions.animationsSpeed6.help": "Set animation speed to instantaneous",
-	"vcmi.battleOptions.touchscreenMode.hover": "Touchscreen mode",
-	"vcmi.battleOptions.touchscreenMode.help": "{Touchscreen mode}\n\nIf enabled, second click is required to confirm and execute action. This is more suitable for touchscreen devices.",
 	"vcmi.battleOptions.movementHighlightOnHover.hover": "Movement Highlight on Hover",
 	"vcmi.battleOptions.movementHighlightOnHover.help": "{Movement Highlight on Hover}\n\nHighlight unit's movement range when you hover over it.",
 	"vcmi.battleOptions.skipBattleIntroMusic.hover": "Skip Intro Music",

+ 0 - 2
Mods/vcmi/config/vcmi/german.json

@@ -78,8 +78,6 @@
 	"vcmi.adventureOptions.forceMovementInfo.help" : "{Bewegungskosten immer anzeigen}\n\n Ersetzt die Standardinformationen in der Statusleiste durch die Daten der Bewegungspunkte, ohne dass die ALT-Taste gedrückt werden muss.",
 	"vcmi.adventureOptions.showGrid.hover" : "Raster anzeigen",
 	"vcmi.adventureOptions.showGrid.help" : "{Raster anzeigen}\n\n Zeigt eine Rasterüberlagerung, die die Grenzen zwischen den Kacheln der Abenteuerkarte anzeigt.",
-	"vcmi.adventureOptions.mapSwipe.hover" : "Karte wischen",
-	"vcmi.adventureOptions.mapSwipe.help" : "{Karte wischen}\n\n Ermöglicht auf Systemen mit Touchscreen das Verschieben der Karte per Fingerwisch-Geste. Ab sofort kann auch über die linke Maustaste zugegriffen werden.",
 	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",

+ 0 - 2
Mods/vcmi/config/vcmi/polish.json

@@ -52,8 +52,6 @@
 	"vcmi.adventureOptions.forceMovementInfo.help" : "{Zawsze pokazuj koszt ruchu}\n\n Zastępuje domyślne informacje paska statusu danymi o ruchu bez potrzeby przytrzymywania klawisza ALT.",
 	"vcmi.adventureOptions.showGrid.hover" : "Pokaż siatkę",
 	"vcmi.adventureOptions.showGrid.help" : "{Pokaż siatkę}\n\n Włącza siatkę pokazującą brzegi pól mapy przygody.",
-	"vcmi.adventureOptions.mapSwipe.hover" : "Przeciąganie mapy",
-	"vcmi.adventureOptions.mapSwipe.help" : "{Przeciąganie mapy}\n\n Pozwala przesuwać mapę przygody palcem dla systemów z ekranami dotykowymi. Obecnie pozwala też przesuwać mapę lewym przyciskiem myszy.",
 	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",

+ 0 - 2
Mods/vcmi/config/vcmi/russian.json

@@ -52,8 +52,6 @@
 	"vcmi.adventureOptions.forceMovementInfo.help" : "{Всегда показывать стоимость перемещения}\n\n Заменить информацию в статусной строке на информацию о перемещении без необходимости нажатия {ALT}",
 	"vcmi.adventureOptions.showGrid.hover" : "Сетка",
 	"vcmi.adventureOptions.showGrid.help" : "{Сетка}\n\n Показывать сетку на видимой части карты.",
-	"vcmi.adventureOptions.mapSwipe.hover" : "Перемещение карты жестами",
-	"vcmi.adventureOptions.mapSwipe.help" : "{Перемещение карты жестами}\n\n Включает перемещение карты жестами на системах с сенсорным экраном. Сейчас также активируется левой кнопкой мыши.",
 	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",

+ 0 - 4
Mods/vcmi/config/vcmi/spanish.json

@@ -61,8 +61,6 @@
 	"vcmi.adventureOptions.forceMovementInfo.help": "{Mostrar siempre el coste de movimiento}\n\n Reemplaza la información predeterminada de la barra de estado con datos de puntos de movimiento sin necesidad de mantener presionado el botón ALT.",
 	"vcmi.adventureOptions.showGrid.hover": "Mostrar cuadrícula",
 	"vcmi.adventureOptions.showGrid.help": "{Mostrar cuadrícula}\n\n Muestra una superposición de cuadrícula que muestra las fronteras entre las casillas del mapa de aventuras.",
-	"vcmi.adventureOptions.mapSwipe.hover": "Deslizamiento de mapa",
-	"vcmi.adventureOptions.mapSwipe.help": "{Deslizamiento de mapa}\n\n Permite el movimiento del mapa mediante el gesto de deslizamiento con el dedo en sistemas con pantalla táctil. En este momento, también se puede acceder mediante el botón izquierdo del mouse.",
 	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",
@@ -85,8 +83,6 @@
 	"vcmi.battleOptions.animationsSpeed1.help": "Establece la velocidad de animación como muy lenta.",
 	"vcmi.battleOptions.animationsSpeed5.help": "Establece la velocidad de animación como muy rápida.",
 	"vcmi.battleOptions.animationsSpeed6.help": "Establece la velocidad de animación como instantánea.",
-	"vcmi.battleOptions.touchscreenMode.hover": "Modo pantalla táctil",
-	"vcmi.battleOptions.touchscreenMode.help": "{Modo pantalla táctil}\n\nSi está habilitado, se requiere de un segundo clic para confirmar y ejecutar la acción. Adecuado para dispositivos con pantalla táctil.",
 	"vcmi.battleOptions.movementHighlightOnHover.hover": "Resaltado de movimiento al pasar el ratón",
 	"vcmi.battleOptions.movementHighlightOnHover.help": "{Resaltado de movimiento al pasar el ratón}\n\nResalta el rango de movimiento de la unidad cuando el cursor esta sobre esta.",
 	"vcmi.battleOptions.skipBattleIntroMusic.hover": "Omitir música de introducción",

+ 8 - 2
Mods/vcmi/config/vcmi/ukrainian.json

@@ -50,6 +50,10 @@
 	"vcmi.systemOptions.resolutionButton.help"  : "{Роздільна здатність}\n\n Зміна розширення екрану в грі. Аби зміни набули чинності необхідно перезавантажити гру.",
 	"vcmi.systemOptions.resolutionMenu.hover"   : "Обрати роздільну здатність",
 	"vcmi.systemOptions.resolutionMenu.help"    : "Змінити роздільну здатність екрану в грі.",
+	"vcmi.systemOptions.scalingButton.hover"   : "Масштабування інтерфейсу: %p%",
+	"vcmi.systemOptions.scalingButton.help"    : "{Масштабування інтерфейсу}\n\nЗмінити масштабування ігрового інтерфейсу",
+	"vcmi.systemOptions.scalingMenu.hover"     : "Виберіть масштабування інтерфейсу",
+	"vcmi.systemOptions.scalingMenu.help"      : "Змінити масштабування ігрового інтерфейсу.",
 	"vcmi.systemOptions.framerateButton.hover"  : "Лічильник кадрів",
 	"vcmi.systemOptions.framerateButton.help"   : "{Лічильник кадрів}\n\n Перемикає видимість лічильника кадрів на секунду у кутку ігрового вікна",
 
@@ -59,8 +63,8 @@
 	"vcmi.adventureOptions.forceMovementInfo.help" : "{Завжди показувати вартість руху}\n\n Замінює стандартну інформацію в рядку стану на вартість переміщення без необхідності утримувати клавішу ALT.",
 	"vcmi.adventureOptions.showGrid.hover" : "Показувати сітку",
 	"vcmi.adventureOptions.showGrid.help" : "{Показувати сітку}\n\n Відображає сітку, що показує межі між клітинками на мапі пригод.",
-	"vcmi.adventureOptions.mapSwipe.hover" : "Прокрутка мапи жестом",
-	"vcmi.adventureOptions.mapSwipe.help" : "{Прокрутка мапи жестом}\n\n Дозволяє переміщати мапу пальцем на системах з сенсорним екраном. Станом на зараз, також доступний за допомогою лівої кнопки миші.",
+	"vcmi.adventureOptions.borderScroll.hover" : "Прокрутка по краю",
+	"vcmi.adventureOptions.borderScroll.help" : "{{Прокрутка по краю}\n\nПрокручувати мапу пригод, коли курсор знаходиться біля краю вікна. Цю функцію можна вимкнути, утримуючи клавішу CTRL.",
 	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",
@@ -83,6 +87,8 @@
 	"vcmi.battleOptions.animationsSpeed1.help": "Встановити дуже низьку швидкість анімації",
 	"vcmi.battleOptions.animationsSpeed5.help": "Встановити дуже високу швидкість анімації",
 	"vcmi.battleOptions.animationsSpeed6.help": "Встановити миттєву швидкість анімації",
+	"vcmi.battleOptions.movementHighlightOnHover.hover": "Підсвічувати зону руху істоти",
+	"vcmi.battleOptions.movementHighlightOnHover.help": "{Підсвічувати зону руху істоти}\n\nПідсвічувати можливу зону руху істоти при наведенні курсора миші на неї",
 	"vcmi.battleOptions.skipBattleIntroMusic.hover": "Пропускати вступну музику",
 	"vcmi.battleOptions.skipBattleIntroMusic.help": "{Пропускати вступну музику}\n\n Пропускати коротку музику, яка грає на початку кожної битви перед початком дії. Також можна пропустити, натиснувши клавішу ESC.",
 	

+ 4 - 0
client/CMakeLists.txt

@@ -99,6 +99,8 @@ set(client_SRCS
 	widgets/MiscWidgets.cpp
 	widgets/ObjectLists.cpp
 	widgets/TextControls.cpp
+	widgets/Scrollable.cpp
+	widgets/Slider.cpp
 	widgets/CArtifactsOfHeroBase.cpp
 	widgets/CArtifactsOfHeroMain.cpp
 	widgets/CArtifactsOfHeroKingdom.cpp
@@ -251,6 +253,8 @@ set(client_HEADERS
 	widgets/MiscWidgets.h
 	widgets/ObjectLists.h
 	widgets/TextControls.h
+	widgets/Scrollable.h
+	widgets/Slider.h
 	widgets/CArtifactsOfHeroBase.h
 	widgets/CArtifactsOfHeroMain.h
 	widgets/CArtifactsOfHeroKingdom.h

+ 1 - 1
client/CServerHandler.cpp

@@ -485,7 +485,7 @@ void CServerHandler::setPlayer(PlayerColor color) const
 	sendLobbyPack(lsp);
 }
 
-void CServerHandler::setPlayerOption(ui8 what, ui8 dir, PlayerColor player) const
+void CServerHandler::setPlayerOption(ui8 what, si8 dir, PlayerColor player) const
 {
 	LobbyChangePlayerOption lcpo;
 	lcpo.what = what;

+ 2 - 2
client/CServerHandler.h

@@ -62,7 +62,7 @@ public:
 	virtual void setCampaignBonus(int bonusId) const = 0;
 	virtual void setMapInfo(std::shared_ptr<CMapInfo> to, std::shared_ptr<CMapGenOptions> mapGenOpts = {}) const = 0;
 	virtual void setPlayer(PlayerColor color) const = 0;
-	virtual void setPlayerOption(ui8 what, ui8 dir, PlayerColor player) const = 0;
+	virtual void setPlayerOption(ui8 what, si8 dir, PlayerColor player) const = 0;
 	virtual void setDifficulty(int to) const = 0;
 	virtual void setTurnLength(int npos) const = 0;
 	virtual void sendMessage(const std::string & txt) const = 0;
@@ -140,7 +140,7 @@ public:
 	void setCampaignBonus(int bonusId) const override;
 	void setMapInfo(std::shared_ptr<CMapInfo> to, std::shared_ptr<CMapGenOptions> mapGenOpts = {}) const override;
 	void setPlayer(PlayerColor color) const override;
-	void setPlayerOption(ui8 what, ui8 dir, PlayerColor player) const override;
+	void setPlayerOption(ui8 what, si8 dir, PlayerColor player) const override;
 	void setDifficulty(int to) const override;
 	void setTurnLength(int npos) const override;
 	void sendMessage(const std::string & txt) const override;

+ 1 - 2
client/adventureMap/AdventureMapInterface.cpp

@@ -54,7 +54,6 @@ AdventureMapInterface::AdventureMapInterface():
 	pos.x = pos.y = 0;
 	pos.w = GH.screenDimensions().x;
 	pos.h = GH.screenDimensions().y;
-	setMoveEventStrongInterest(true); // handle all mouse move events to prevent dead mouse move space in fullscreen mode
 
 	shortcuts = std::make_shared<AdventureMapShortcuts>(*this);
 
@@ -182,7 +181,7 @@ void AdventureMapInterface::handleMapScrollingUpdate(uint32_t timePassed)
 
 	bool cursorInScrollArea = scrollDelta != Point(0,0);
 	bool scrollingActive = cursorInScrollArea && isActive() && shortcuts->optionSidePanelActive() && !scrollingWasBlocked;
-	bool scrollingBlocked = GH.isKeyboardCtrlDown();
+	bool scrollingBlocked = GH.isKeyboardCtrlDown() || !settings["adventure"]["borderScroll"].Bool();
 
 	if (!scrollingWasActive && scrollingBlocked)
 	{

+ 26 - 8
client/adventureMap/CList.cpp

@@ -38,9 +38,7 @@ CList::CListItem::CListItem(CList * Parent)
 	defActions = 255-DISPOSE;
 }
 
-CList::CListItem::~CListItem()
-{
-}
+CList::CListItem::~CListItem() = default;
 
 void CList::CListItem::clickRight(tribool down, bool previousState)
 {
@@ -84,7 +82,7 @@ void CList::CListItem::onSelect(bool on)
 }
 
 CList::CList(int Size, Rect widgetDimensions)
-	: CIntObject(0, widgetDimensions.topLeft()),
+	: Scrollable(0, widgetDimensions.topLeft(), Orientation::VERTICAL),
 	size(Size),
 	selected(nullptr)
 {
@@ -109,8 +107,7 @@ void CList::setScrollUpButton(std::shared_ptr<CButton> button)
 	addChild(button.get());
 
 	scrollUp = button;
-	scrollUp->addCallback(std::bind(&CListBox::moveToPrev, listBox));
-	scrollUp->addCallback(std::bind(&CList::update, this));
+	scrollUp->addCallback(std::bind(&CList::scrollPrev, this));
 	update();
 }
 
@@ -119,8 +116,29 @@ void CList::setScrollDownButton(std::shared_ptr<CButton> button)
 	addChild(button.get());
 
 	scrollDown = button;
-	scrollDown->addCallback(std::bind(&CList::update, this));
-	scrollDown->addCallback(std::bind(&CListBox::moveToNext, listBox));
+	scrollDown->addCallback(std::bind(&CList::scrollNext, this));
+	update();
+}
+
+void CList::scrollBy(int distance)
+{
+	if (distance < 0 && listBox->getPos() < -distance)
+		listBox->moveToPos(0);
+	else
+		listBox->moveToPos(static_cast<int>(listBox->getPos()) + distance);
+
+	update();
+}
+
+void CList::scrollPrev()
+{
+	listBox->moveToPrev();
+	update();
+}
+
+void CList::scrollNext()
+{
+	listBox->moveToNext();
 	update();
 }
 

+ 6 - 2
client/adventureMap/CList.h

@@ -9,7 +9,7 @@
  */
 #pragma once
 
-#include "../gui/CIntObject.h"
+#include "../widgets/Scrollable.h"
 #include "../../lib/FunctionList.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
@@ -24,7 +24,7 @@ class CButton;
 class CAnimImage;
 
 /// Base UI Element for hero\town lists
-class CList : public CIntObject
+class CList : public Scrollable
 {
 protected:
 	class CListItem : public CIntObject, public std::enable_shared_from_this<CListItem>
@@ -64,6 +64,10 @@ private:
 	std::shared_ptr<CButton> scrollUp;
 	std::shared_ptr<CButton> scrollDown;
 
+	void scrollBy(int distance) override;
+	void scrollPrev() override;
+	void scrollNext() override;
+
 protected:
 	std::shared_ptr<CListBox> listBox;
 

+ 11 - 5
client/adventureMap/CMinimap.cpp

@@ -88,7 +88,7 @@ void CMinimapInstance::showAll(Canvas & to)
 }
 
 CMinimap::CMinimap(const Rect & position)
-	: CIntObject(LCLICK | RCLICK | HOVER | MOVE, position.topLeft()),
+	: CIntObject(LCLICK | RCLICK | HOVER | MOVE | GESTURE_PANNING, position.topLeft()),
 	level(0)
 {
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
@@ -126,9 +126,9 @@ Point CMinimap::tileToPixels(const int3 &tile) const
 	return Point(x,y);
 }
 
-void CMinimap::moveAdvMapSelection()
+void CMinimap::moveAdvMapSelection(const Point & positionGlobal)
 {
-	int3 newLocation = pixelToTile(GH.getCursorPosition() - pos.topLeft());
+	int3 newLocation = pixelToTile(positionGlobal - pos.topLeft());
 	adventureInt->centerOnTile(newLocation);
 
 	if (!(adventureInt->isActive()))
@@ -137,10 +137,16 @@ void CMinimap::moveAdvMapSelection()
 		redraw();//redraw only this
 }
 
+void CMinimap::gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance)
+{
+	if (pos.isInside(currentPosition))
+		moveAdvMapSelection(currentPosition);
+}
+
 void CMinimap::clickLeft(tribool down, bool previousState)
 {
 	if(down)
-		moveAdvMapSelection();
+		moveAdvMapSelection(GH.getCursorPosition());
 }
 
 void CMinimap::clickRight(tribool down, bool previousState)
@@ -160,7 +166,7 @@ void CMinimap::hover(bool on)
 void CMinimap::mouseMoved(const Point & cursorPosition)
 {
 	if(isMouseButtonPressed(MouseButton::LEFT))
-		moveAdvMapSelection();
+		moveAdvMapSelection(cursorPosition);
 }
 
 void CMinimap::showAll(Canvas & to)

+ 2 - 1
client/adventureMap/CMinimap.h

@@ -43,13 +43,14 @@ class CMinimap : public CIntObject
 	Rect screenArea;
 	int level;
 
+	void gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) override;
 	void clickLeft(tribool down, bool previousState) override;
 	void clickRight(tribool down, bool previousState) override;
 	void hover (bool on) override;
 	void mouseMoved (const Point & cursorPosition) override;
 
 	/// relocates center of adventure map screen to currently hovered tile
-	void moveAdvMapSelection();
+	void moveAdvMapSelection(const Point & positionGlobal);
 
 protected:
 	/// computes coordinates of tile below cursor pos

+ 77 - 69
client/battle/BattleActionsController.cpp

@@ -117,7 +117,6 @@ BattleActionsController::BattleActionsController(BattleInterface & owner):
 	selectedStack(nullptr),
 	heroSpellToCast(nullptr)
 {
-	touchscreenMode = settings["battle"]["touchscreenMode"].Bool();
 }
 
 void BattleActionsController::endCastingSpell()
@@ -161,7 +160,7 @@ void BattleActionsController::enterCreatureCastingMode()
 	if (!isActiveStackSpellcaster())
 		return;
 
-	for (auto const & action : possibleActions)
+	for(const auto & action : possibleActions)
 	{
 		if (action.get() != PossiblePlayerBattleAction::NO_LOCATION)
 			continue;
@@ -204,7 +203,7 @@ std::vector<PossiblePlayerBattleAction> BattleActionsController::getPossibleActi
 {
 	BattleClientInterfaceData data; //hard to get rid of these things so for now they're required data to pass
 
-	for (auto const & spell : creatureSpells)
+	for(const auto & spell : creatureSpells)
 		data.creatureSpellsToCast.push_back(spell->id);
 
 	data.tacticsMode = owner.tacticsMode;
@@ -220,49 +219,62 @@ void BattleActionsController::reorderPossibleActionsPriority(const CStack * stac
 {
 	if(owner.tacticsMode || possibleActions.empty()) return; //this function is not supposed to be called in tactics mode or before getPossibleActionsForStack
 
-	auto assignPriority = [&](PossiblePlayerBattleAction const & item) -> uint8_t //large lambda assigning priority which would have to be part of possibleActions without it
+	auto assignPriority = [&](const PossiblePlayerBattleAction & item
+						  ) -> uint8_t //large lambda assigning priority which would have to be part of possibleActions without it
 	{
 		switch(item.get())
 		{
-		case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
-		case PossiblePlayerBattleAction::ANY_LOCATION:
-		case PossiblePlayerBattleAction::NO_LOCATION:
-		case PossiblePlayerBattleAction::FREE_LOCATION:
-		case PossiblePlayerBattleAction::OBSTACLE:
-			if(!stack->hasBonusOfType(BonusType::NO_SPELLCAST_BY_DEFAULT) && context == MouseHoveredHexContext::OCCUPIED_HEX)
-				return 1;
-			else
-				return 100;//bottom priority
-			break;
-		case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
-			return 2; break;
-		case PossiblePlayerBattleAction::SHOOT:
-			return 4; break;
-		case PossiblePlayerBattleAction::ATTACK_AND_RETURN:
-			return 5; break;
-		case PossiblePlayerBattleAction::ATTACK:
-			return 6; break;
-		case PossiblePlayerBattleAction::WALK_AND_ATTACK:
-			return 7; break;
-		case PossiblePlayerBattleAction::MOVE_STACK:
-			return 8; break;
-		case PossiblePlayerBattleAction::CATAPULT:
-			return 9; break;
-		case PossiblePlayerBattleAction::HEAL:
-			return 10; break;
-		case PossiblePlayerBattleAction::CREATURE_INFO:
-			return 11; break;
-		case PossiblePlayerBattleAction::HERO_INFO:
-			return 12; break;
-		case PossiblePlayerBattleAction::TELEPORT:
-			return 13; break;
-		default:
-			assert(0);
-			return 200; break;
+			case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
+			case PossiblePlayerBattleAction::ANY_LOCATION:
+			case PossiblePlayerBattleAction::NO_LOCATION:
+			case PossiblePlayerBattleAction::FREE_LOCATION:
+			case PossiblePlayerBattleAction::OBSTACLE:
+				if(!stack->hasBonusOfType(BonusType::NO_SPELLCAST_BY_DEFAULT) && context == MouseHoveredHexContext::OCCUPIED_HEX)
+					return 1;
+				else
+					return 100; //bottom priority
+				break;
+			case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
+				return 2;
+				break;
+			case PossiblePlayerBattleAction::SHOOT:
+				return 4;
+				break;
+			case PossiblePlayerBattleAction::ATTACK_AND_RETURN:
+				return 5;
+				break;
+			case PossiblePlayerBattleAction::ATTACK:
+				return 6;
+				break;
+			case PossiblePlayerBattleAction::WALK_AND_ATTACK:
+				return 7;
+				break;
+			case PossiblePlayerBattleAction::MOVE_STACK:
+				return 8;
+				break;
+			case PossiblePlayerBattleAction::CATAPULT:
+				return 9;
+				break;
+			case PossiblePlayerBattleAction::HEAL:
+				return 10;
+				break;
+			case PossiblePlayerBattleAction::CREATURE_INFO:
+				return 11;
+				break;
+			case PossiblePlayerBattleAction::HERO_INFO:
+				return 12;
+				break;
+			case PossiblePlayerBattleAction::TELEPORT:
+				return 13;
+				break;
+			default:
+				assert(0);
+				return 200;
+				break;
 		}
 	};
 
-	auto comparer = [&](PossiblePlayerBattleAction const & lhs, PossiblePlayerBattleAction const & rhs)
+	auto comparer = [&](const PossiblePlayerBattleAction & lhs, const PossiblePlayerBattleAction & rhs)
 	{
 		return assignPriority(lhs) < assignPriority(rhs);
 	};
@@ -359,8 +371,26 @@ void BattleActionsController::actionSetCursor(PossiblePlayerBattleAction action,
 		case PossiblePlayerBattleAction::ATTACK:
 		case PossiblePlayerBattleAction::WALK_AND_ATTACK:
 		case PossiblePlayerBattleAction::ATTACK_AND_RETURN:
-			owner.fieldController->setBattleCursor(targetHex);
+		{
+			static const std::map<BattleHex::EDir, Cursor::Combat> sectorCursor = {
+				{BattleHex::TOP_LEFT,     Cursor::Combat::HIT_SOUTHEAST},
+				{BattleHex::TOP_RIGHT,    Cursor::Combat::HIT_SOUTHWEST},
+				{BattleHex::RIGHT,        Cursor::Combat::HIT_WEST     },
+				{BattleHex::BOTTOM_RIGHT, Cursor::Combat::HIT_NORTHWEST},
+				{BattleHex::BOTTOM_LEFT,  Cursor::Combat::HIT_NORTHEAST},
+				{BattleHex::LEFT,         Cursor::Combat::HIT_EAST     },
+				{BattleHex::TOP,          Cursor::Combat::HIT_SOUTH    },
+				{BattleHex::BOTTOM,       Cursor::Combat::HIT_NORTH    }
+			};
+
+			auto direction = owner.fieldController->selectAttackDirection(targetHex);
+
+			assert(sectorCursor.count(direction) > 0);
+			if (sectorCursor.count(direction))
+				CCS->curh->set(sectorCursor.at(direction));
+
 			return;
+		}
 
 		case PossiblePlayerBattleAction::SHOOT:
 			if (owner.curInt->cb->battleHasShootingPenalty(owner.stacksController->getActiveStack(), targetHex))
@@ -826,10 +856,6 @@ void BattleActionsController::onHoverEnded()
 
 void BattleActionsController::onHexLeftClicked(BattleHex clickedHex)
 {
-	static BattleHex lastSelectedHex;
-	static BattleHex lastDirectionalHex;
-	static PossiblePlayerBattleAction::Actions lastSelectedAction;
-	
 	if (owner.stacksController->getActiveStack() == nullptr)
 		return;
 
@@ -840,24 +866,8 @@ void BattleActionsController::onHexLeftClicked(BattleHex clickedHex)
 	if (!actionIsLegal(action, clickedHex))
 		return;
 	
-	auto directionalHex = lastDirectionalHex;
-	if(action.get() == PossiblePlayerBattleAction::ATTACK
-	   || action.get() == PossiblePlayerBattleAction::WALK_AND_ATTACK
-	   || action.get() == PossiblePlayerBattleAction::ATTACK_AND_RETURN)
-		directionalHex = owner.fieldController->fromWhichHexAttack(clickedHex);
-
-	if(!touchscreenMode || (lastSelectedAction == action.get() && lastSelectedHex == clickedHex && lastDirectionalHex == directionalHex))
-	{
-		actionRealize(action, clickedHex);
-
-		GH.statusbar()->clear();
-	}
-	else
-	{
-		lastSelectedAction = action.get();
-		lastSelectedHex = clickedHex;
-		lastDirectionalHex = directionalHex;
-	}
+	actionRealize(action, clickedHex);
+	GH.statusbar()->clear();
 }
 
 void BattleActionsController::tryActivateStackSpellcasting(const CStack *casterStack)
@@ -877,7 +887,7 @@ void BattleActionsController::tryActivateStackSpellcasting(const CStack *casterS
 
 	TConstBonusListPtr bl = casterStack->getBonuses(Selector::type()(BonusType::SPELLCASTER));
 
-	for (auto const & bonus : *bl)
+	for(const auto & bonus : *bl)
 	{
 		if (bonus->additionalInfo[0] <= 0)
 			creatureSpells.push_back(SpellID(bonus->subtype).toSpell());
@@ -971,6 +981,9 @@ void BattleActionsController::activateStack()
 
 void BattleActionsController::onHexRightClicked(BattleHex clickedHex)
 {
+	if (spellcastingModeActive())
+		endCastingSpell();
+
 	auto selectedStack = owner.curInt->cb->battleGetStackByPos(clickedHex, true);
 
 	if (selectedStack != nullptr)
@@ -1015,8 +1028,3 @@ void BattleActionsController::pushFrontPossibleAction(PossiblePlayerBattleAction
 {
 	possibleActions.insert(possibleActions.begin(), action);
 }
-
-void BattleActionsController::setTouchScreenMode(bool enabled)
-{
-	touchscreenMode = enabled;
-}

+ 0 - 5
client/battle/BattleActionsController.h

@@ -35,9 +35,6 @@ class BattleActionsController
 {
 	BattleInterface & owner;
 	
-	/// mouse or touchscreen click mode
-	bool touchscreenMode = false;
-	
 	/// all actions possible to call at the moment by player
 	std::vector<PossiblePlayerBattleAction> possibleActions;
 
@@ -131,6 +128,4 @@ public:
 	
 	/// inserts possible action in the beggining in order to prioritize it
 	void pushFrontPossibleAction(PossiblePlayerBattleAction);
-
-	void setTouchScreenMode(bool enabled);
 };

+ 57 - 39
client/battle/BattleFieldController.cpp

@@ -22,6 +22,7 @@
 
 #include "../CGameInfo.h"
 #include "../CPlayerInterface.h"
+#include "../render/CAnimation.h"
 #include "../render/Canvas.h"
 #include "../render/IImage.h"
 #include "../renderSDL/SDL_Extensions.h"
@@ -39,7 +40,6 @@ BattleFieldController::BattleFieldController(BattleInterface & owner):
 	owner(owner)
 {
 	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
-	setMoveEventStrongInterest(true);
 
 	//preparing cells and hexes
 	cellBorder = IImage::createFromFile("CCELLGRD.BMP", EImageBlitMode::COLORKEY);
@@ -47,6 +47,9 @@ BattleFieldController::BattleFieldController(BattleInterface & owner):
 	cellUnitMovementHighlight = IImage::createFromFile("UnitMovementHighlight.PNG", EImageBlitMode::COLORKEY);
 	cellUnitMaxMovementHighlight = IImage::createFromFile("UnitMaxMovementHighlight.PNG", EImageBlitMode::COLORKEY);
 
+	attackCursors = std::make_shared<CAnimation>("CRCOMBAT");
+	attackCursors->preload();
+
 	if(!owner.siegeController)
 	{
 		auto bfieldType = owner.curInt->cb->battleGetBattlefieldType();
@@ -68,7 +71,7 @@ BattleFieldController::BattleFieldController(BattleInterface & owner):
 	backgroundWithHexes = std::make_unique<Canvas>(Point(background->width(), background->height()));
 
 	updateAccessibleHexes();
-	addUsedEvents(LCLICK | RCLICK | MOVE | TIME);
+	addUsedEvents(LCLICK | RCLICK | MOVE | TIME | GESTURE_PANNING);
 }
 
 void BattleFieldController::activate()
@@ -89,16 +92,36 @@ void BattleFieldController::createHeroes()
 		owner.defendingHero = std::make_shared<BattleHero>(owner, owner.defendingHeroInstance, true);
 }
 
+void BattleFieldController::panning(bool on, const Point & initialPosition, const Point & finalPosition)
+{
+	if (!on && pos.isInside(finalPosition))
+		clickLeft(false, false);
+}
+
+void BattleFieldController::gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance)
+{
+	Point distance = currentPosition - initialPosition;
+
+	if (distance.length() < settings["battle"]["swipeAttackDistance"].Float())
+		hoveredHex = getHexAtPosition(initialPosition);
+	else
+		hoveredHex = BattleHex::INVALID;
+
+	currentAttackOriginPoint = currentPosition;
+
+	if (pos.isInside(initialPosition))
+		owner.actionsController->onHexHovered(getHoveredHex());
+}
+
 void BattleFieldController::mouseMoved(const Point & cursorPosition)
 {
-	if (!pos.isInside(cursorPosition))
-	{
-		owner.actionsController->onHoverEnded();
-		return;
-	}
+	hoveredHex = getHexAtPosition(cursorPosition);
+	currentAttackOriginPoint = cursorPosition;
 
-	BattleHex selectedHex = getHoveredHex();
-	owner.actionsController->onHexHovered(selectedHex);
+	if (pos.isInside(cursorPosition))
+		owner.actionsController->onHexHovered(getHoveredHex());
+	else
+		owner.actionsController->onHoverEnded();
 }
 
 void BattleFieldController::clickLeft(tribool down, bool previousState)
@@ -139,7 +162,7 @@ void BattleFieldController::renderBattlefield(Canvas & canvas)
 
 void BattleFieldController::showBackground(Canvas & canvas)
 {
-	if (owner.stacksController->getActiveStack() != nullptr ) //&& creAnims[stacksController->getActiveStack()->unitId()]->isIdle() //show everything with range
+	if (owner.stacksController->getActiveStack() != nullptr )
 		showBackgroundImageWithHexes(canvas);
 	else
 		showBackgroundImage(canvas);
@@ -171,7 +194,7 @@ void BattleFieldController::showBackgroundImage(Canvas & canvas)
 
 void BattleFieldController::showBackgroundImageWithHexes(Canvas & canvas)
 {
-	canvas.draw(*backgroundWithHexes.get(), Point(0, 0));
+	canvas.draw(*backgroundWithHexes, Point(0, 0));
 }
 
 void BattleFieldController::redrawBackgroundWithHexes()
@@ -234,7 +257,7 @@ std::set<BattleHex> BattleFieldController::getHighlightedHexesForActiveStack()
 
 	auto hoveredHex = getHoveredHex();
 
-	std::set<BattleHex> set = owner.curInt->cb->battleGetAttackedHexes(owner.stacksController->getActiveStack(), hoveredHex, attackingHex);
+	std::set<BattleHex> set = owner.curInt->cb->battleGetAttackedHexes(owner.stacksController->getActiveStack(), hoveredHex);
 	for(BattleHex hex : set)
 		result.insert(hex);
 
@@ -391,8 +414,11 @@ bool BattleFieldController::isPixelInHex(Point const & position)
 
 BattleHex BattleFieldController::getHoveredHex()
 {
-	Point hoverPos = GH.getCursorPosition();
+	return hoveredHex;
+}
 
+BattleHex BattleFieldController::getHexAtPosition(Point hoverPos)
+{
 	if (owner.attackingHero)
 	{
 		if (owner.attackingHero->pos.isInside(hoverPos))
@@ -405,7 +431,6 @@ BattleHex BattleFieldController::getHoveredHex()
 			return BattleHex::HERO_DEFENDER;
 	}
 
-
 	for (int h = 0; h < GameConstants::BFIELD_SIZE; ++h)
 	{
 		Rect hexPosition = hexPositionAbsolute(h);
@@ -420,29 +445,7 @@ BattleHex BattleFieldController::getHoveredHex()
 	return BattleHex::INVALID;
 }
 
-void BattleFieldController::setBattleCursor(BattleHex myNumber)
-{
-	Point cursorPos = CCS->curh->position();
-
-	std::vector<Cursor::Combat> sectorCursor = {
-		Cursor::Combat::HIT_SOUTHEAST,
-		Cursor::Combat::HIT_SOUTHWEST,
-		Cursor::Combat::HIT_WEST,
-		Cursor::Combat::HIT_NORTHWEST,
-		Cursor::Combat::HIT_NORTHEAST,
-		Cursor::Combat::HIT_EAST,
-		Cursor::Combat::HIT_SOUTH,
-		Cursor::Combat::HIT_NORTH,
-	};
-
-	auto direction = static_cast<size_t>(selectAttackDirection(myNumber, cursorPos));
-
-	assert(direction != -1);
-	if (direction != -1)
-		CCS->curh->set(sectorCursor[direction]);
-}
-
-BattleHex::EDir BattleFieldController::selectAttackDirection(BattleHex myNumber, const Point & cursorPos)
+BattleHex::EDir BattleFieldController::selectAttackDirection(BattleHex myNumber)
 {
 	const bool doubleWide = owner.stacksController->getActiveStack()->doubleWide();
 	auto neighbours = myNumber.allNeighbouringTiles();
@@ -505,7 +508,7 @@ BattleHex::EDir BattleFieldController::selectAttackDirection(BattleHex myNumber,
 
 	for (size_t i = 0; i < 8; ++i)
 		if (attackAvailability[i])
-			distance2[i] = (testPoint[i].y - cursorPos.y)*(testPoint[i].y - cursorPos.y) + (testPoint[i].x - cursorPos.x)*(testPoint[i].x - cursorPos.x);
+			distance2[i] = (testPoint[i].y - currentAttackOriginPoint.y)*(testPoint[i].y - currentAttackOriginPoint.y) + (testPoint[i].x - currentAttackOriginPoint.x)*(testPoint[i].x - currentAttackOriginPoint.x);
 
 	size_t nearest = -1;
 	for (size_t i = 0; i < 8; ++i)
@@ -518,7 +521,7 @@ BattleHex::EDir BattleFieldController::selectAttackDirection(BattleHex myNumber,
 
 BattleHex BattleFieldController::fromWhichHexAttack(BattleHex attackTarget)
 {
-	BattleHex::EDir direction = selectAttackDirection(attackTarget, CCS->curh->position());
+	BattleHex::EDir direction = selectAttackDirection(getHoveredHex());
 
 	const CStack * attacker = owner.stacksController->getActiveStack();
 
@@ -624,4 +627,19 @@ void BattleFieldController::show(Canvas & to)
 	CSDL_Ext::CClipRectGuard guard(to.getInternalSurface(), pos);
 
 	renderBattlefield(to);
+
+	if (isActive() && isPanning() && getHoveredHex() != BattleHex::INVALID)
+	{
+		auto cursorIndex = CCS->curh->get<Cursor::Combat>();
+		auto imageIndex = static_cast<size_t>(cursorIndex);
+
+		to.draw(attackCursors->getImage(imageIndex), hexPositionAbsolute(getHoveredHex()).center() - CCS->curh->getPivotOffsetCombat(imageIndex));
+	}
+}
+
+bool BattleFieldController::receiveEvent(const Point & position, int eventType) const
+{
+	if (eventType == HOVER)
+		return true;
+	return CIntObject::receiveEvent(position, eventType);
 }

+ 20 - 10
client/battle/BattleFieldController.h

@@ -10,14 +10,12 @@
 #pragma once
 
 #include "../../lib/battle/BattleHex.h"
+#include "../../lib/Point.h"
 #include "../gui/CIntObject.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
-
 class CStack;
 class Rect;
-class Point;
-
 VCMI_LIB_NAMESPACE_END
 
 class BattleHero;
@@ -36,11 +34,16 @@ class BattleFieldController : public CIntObject
 	std::shared_ptr<IImage> cellUnitMaxMovementHighlight;
 	std::shared_ptr<IImage> cellShade;
 
+	std::shared_ptr<CAnimation> attackCursors;
+
 	/// Canvas that contains background, hex grid (if enabled), absolute obstacles and movement range of active stack
 	std::unique_ptr<Canvas> backgroundWithHexes;
 
-	/// hex from which the stack would perform attack with current cursor
-	BattleHex attackingHex;
+	/// direction which will be used to perform attack with current cursor position
+	Point currentAttackOriginPoint;
+
+	/// hex currently under mouse hover
+	BattleHex hoveredHex;
 
 	/// hexes to which currently active stack can move
 	std::vector<BattleHex> occupiableHexes;
@@ -61,8 +64,14 @@ class BattleFieldController : public CIntObject
 	void showHighlightedHexes(Canvas & canvas);
 	void updateAccessibleHexes();
 
-	BattleHex::EDir selectAttackDirection(BattleHex myNumber, const Point & point);
+	BattleHex getHexAtPosition(Point hoverPosition);
+
+	/// Checks whether selected pixel is transparent, uses local coordinates of a hex
+	bool isPixelInHex(Point const & position);
+	size_t selectBattleCursor(BattleHex myNumber);
 
+	void panning(bool on, const Point & initialPosition, const Point & finalPosition) override;
+	void gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) override;
 	void mouseMoved(const Point & cursorPosition) override;
 	void clickLeft(tribool down, bool previousState) override;
 	void clickRight(tribool down, bool previousState) override;
@@ -71,6 +80,9 @@ class BattleFieldController : public CIntObject
 	void showAll(Canvas & to) override;
 	void show(Canvas & to) override;
 	void tick(uint32_t msPassed) override;
+
+	bool receiveEvent(const Point & position, int eventType) const override;
+
 public:
 	BattleFieldController(BattleInterface & owner);
 
@@ -85,9 +97,6 @@ public:
 	/// Returns position of hex relative to game window
 	Rect hexPositionAbsolute(BattleHex hex) const;
 
-	/// Checks whether selected pixel is transparent, uses local coordinates of a hex
-	bool isPixelInHex(Point const & position);
-
 	/// Returns ID of currently hovered hex or BattleHex::INVALID if none
 	BattleHex getHoveredHex();
 
@@ -97,6 +106,7 @@ public:
 	/// returns true if stack should render its stack count image in default position - outside own hex
 	bool stackCountOutsideHex(const BattleHex & number) const;
 
-	void setBattleCursor(BattleHex myNumber);
+	BattleHex::EDir selectAttackDirection(BattleHex myNumber);
+
 	BattleHex fromWhichHexAttack(BattleHex myNumber);
 };

+ 1 - 7
client/battle/BattleWindow.cpp

@@ -85,7 +85,7 @@ BattleWindow::BattleWindow(BattleInterface & owner):
 	else
 		tacticPhaseEnded();
 
-	addUsedEvents(RCLICK | KEYBOARD);
+	addUsedEvents(KEYBOARD);
 }
 
 void BattleWindow::createQueue()
@@ -203,12 +203,6 @@ void BattleWindow::keyPressed(EShortcut key)
 	InterfaceObjectConfigurable::keyPressed(key);
 }
 
-void BattleWindow::clickRight(tribool down, bool previousState)
-{
-	if (!down)
-		owner.actionsController->endCastingSpell();
-}
-
 void BattleWindow::tacticPhaseStarted()
 {
 	auto menuBattle = widget<CIntObject>("menuBattle");

+ 0 - 1
client/battle/BattleWindow.h

@@ -86,7 +86,6 @@ public:
 	void deactivate() override;
 	void keyPressed(EShortcut key) override;
 	bool captureThisKey(EShortcut key) override;
-	void clickRight(tribool down, bool previousState) override;
 	void show(Canvas & to) override;
 	void showAll(Canvas & to) override;
 

+ 44 - 51
client/eventsSDL/InputHandler.cpp

@@ -29,7 +29,6 @@
 #include "../../lib/CConfigHandler.h"
 
 #include <SDL_events.h>
-#include <SDL_hints.h>
 
 InputHandler::InputHandler()
 	: mouseHandler(std::make_unique<InputSourceMouse>())
@@ -37,8 +36,6 @@ InputHandler::InputHandler()
 	, fingerHandler(std::make_unique<InputSourceTouch>())
 	, textHandler(std::make_unique<InputSourceText>())
 	, userHandler(std::make_unique<UserEventHandler>())
-	, mouseButtonsMask(0)
-	, pointerSpeedMultiplier(settings["general"]["relativePointerSpeedMultiplier"].Float())
 {
 }
 
@@ -56,14 +53,14 @@ void InputHandler::handleCurrentEvent(const SDL_Event & current)
 			return mouseHandler->handleEventMouseMotion(current.motion);
 		case SDL_MOUSEBUTTONDOWN:
 			return mouseHandler->handleEventMouseButtonDown(current.button);
+		case SDL_MOUSEBUTTONUP:
+			return mouseHandler->handleEventMouseButtonUp(current.button);
 		case SDL_MOUSEWHEEL:
 			return mouseHandler->handleEventMouseWheel(current.wheel);
 		case SDL_TEXTINPUT:
 			return textHandler->handleEventTextInput(current.text);
 		case SDL_TEXTEDITING:
 			return textHandler->handleEventTextEditing(current.edit);
-		case SDL_MOUSEBUTTONUP:
-			return mouseHandler->handleEventMouseButtonUp(current.button);
 		case SDL_FINGERMOTION:
 			return fingerHandler->handleEventFingerMotion(current.tfinger);
 		case SDL_FINGERDOWN:
@@ -77,15 +74,10 @@ void InputHandler::processEvents()
 {
 	boost::unique_lock<boost::mutex> lock(eventsMutex);
 	for (auto const & currentEvent : eventsQueue)
-	{
-		if (currentEvent.type == SDL_MOUSEMOTION)
-		{
-			cursorPosition = Point(currentEvent.motion.x, currentEvent.motion.y);
-			mouseButtonsMask = currentEvent.motion.state;
-		}
 		handleCurrentEvent(currentEvent);
-	}
+
 	eventsQueue.clear();
+	fingerHandler->handleUpdate();
 }
 
 bool InputHandler::ignoreEventsUntilInput()
@@ -169,14 +161,31 @@ void InputHandler::preprocessEvent(const SDL_Event & ev)
 	{
 		boost::unique_lock<boost::mutex> lock(eventsMutex);
 
-		if(ev.type == SDL_MOUSEMOTION && !eventsQueue.empty() && eventsQueue.back().type == SDL_MOUSEMOTION)
+		// In a sequence of motion events, skip all but the last one.
+		// This prevents freezes when every motion event takes longer to handle than interval at which
+		// the events arrive (like dragging on the minimap in world view, with redraw at every event)
+		// so that the events would start piling up faster than they can be processed.
+		if (!eventsQueue.empty())
 		{
-			// In a sequence of mouse motion events, skip all but the last one.
-			// This prevents freezes when every motion event takes longer to handle than interval at which
-			// the events arrive (like dragging on the minimap in world view, with redraw at every event)
-			// so that the events would start piling up faster than they can be processed.
-			eventsQueue.back() = ev;
-			return;
+			const SDL_Event & prev = eventsQueue.back();
+
+			if(ev.type == SDL_MOUSEMOTION && prev.type == SDL_MOUSEMOTION)
+			{
+				SDL_Event accumulated = ev;
+				accumulated.motion.xrel += prev.motion.xrel;
+				accumulated.motion.yrel += prev.motion.yrel;
+				eventsQueue.back() = accumulated;
+				return;
+			}
+
+			if(ev.type == SDL_FINGERMOTION && prev.type == SDL_FINGERMOTION && ev.tfinger.fingerId == prev.tfinger.fingerId)
+			{
+				SDL_Event accumulated = ev;
+				accumulated.tfinger.dx += prev.tfinger.dx;
+				accumulated.tfinger.dy += prev.tfinger.dy;
+				eventsQueue.back() = accumulated;
+				return;
+			}
 		}
 		eventsQueue.push_back(ev);
 	}
@@ -194,42 +203,28 @@ void InputHandler::fetchEvents()
 
 bool InputHandler::isKeyboardCtrlDown() const
 {
-#ifdef VCMI_MAC
-	return SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_LGUI] || SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_RGUI];
-#else
-	return SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_LCTRL] || SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_RCTRL];
-#endif
+	return keyboardHandler->isKeyboardCtrlDown();
 }
 
 bool InputHandler::isKeyboardAltDown() const
 {
-	return SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_LALT] || SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_RALT];
+	return keyboardHandler->isKeyboardAltDown();
 }
 
 bool InputHandler::isKeyboardShiftDown() const
 {
-	return SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_LSHIFT] || SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_RSHIFT];
+	return keyboardHandler->isKeyboardShiftDown();
 }
 
-
-void InputHandler::fakeMoveCursor(float dx, float dy)
+void InputHandler::moveCursorPosition(const Point & distance)
 {
-	int x, y, w, h;
-
-	SDL_Event event;
-	SDL_MouseMotionEvent sme = {SDL_MOUSEMOTION, 0, 0, 0, 0, 0, 0, 0, 0};
-
-	sme.state = SDL_GetMouseState(&x, &y);
-	SDL_GetWindowSize(mainWindow, &w, &h);
-
-	sme.x = GH.getCursorPosition().x + (int)(pointerSpeedMultiplier * w * dx);
-	sme.y = GH.getCursorPosition().y + (int)(pointerSpeedMultiplier * h * dy);
-
-	vstd::abetween(sme.x, 0, w);
-	vstd::abetween(sme.y, 0, h);
+	setCursorPosition(getCursorPosition() + distance);
+}
 
-	event.motion = sme;
-	SDL_PushEvent(&event);
+void InputHandler::setCursorPosition(const Point & position)
+{
+	cursorPosition = position;
+	GH.events().dispatchMouseMoved(position);
 }
 
 void InputHandler::startTextInput(const Rect & where)
@@ -242,16 +237,14 @@ void InputHandler::stopTextInput()
 	textHandler->stopTextInput();
 }
 
+bool InputHandler::hasTouchInputDevice() const
+{
+	return fingerHandler->hasTouchInputDevice();
+}
+
 bool InputHandler::isMouseButtonPressed(MouseButton button) const
 {
-	static_assert(static_cast<uint32_t>(MouseButton::LEFT)   == SDL_BUTTON_LEFT,   "mismatch between VCMI and SDL enum!");
-	static_assert(static_cast<uint32_t>(MouseButton::MIDDLE) == SDL_BUTTON_MIDDLE, "mismatch between VCMI and SDL enum!");
-	static_assert(static_cast<uint32_t>(MouseButton::RIGHT)  == SDL_BUTTON_RIGHT,  "mismatch between VCMI and SDL enum!");
-	static_assert(static_cast<uint32_t>(MouseButton::EXTRA1) == SDL_BUTTON_X1,     "mismatch between VCMI and SDL enum!");
-	static_assert(static_cast<uint32_t>(MouseButton::EXTRA2) == SDL_BUTTON_X2,     "mismatch between VCMI and SDL enum!");
-
-	uint32_t index = static_cast<uint32_t>(button);
-	return mouseButtonsMask & SDL_BUTTON(index);
+	return mouseHandler->isMouseButtonPressed(button) || fingerHandler->isMouseButtonPressed(button);
 }
 
 void InputHandler::pushUserEvent(EUserEvent usercode, void * userdata)

+ 8 - 3
client/eventsSDL/InputHandler.h

@@ -28,8 +28,6 @@ class InputHandler
 	boost::mutex eventsMutex;
 
 	Point cursorPosition;
-	float pointerSpeedMultiplier;
-	int mouseButtonsMask;
 
 	void preprocessEvent(const SDL_Event & event);
 	void handleCurrentEvent(const SDL_Event & current);
@@ -53,7 +51,11 @@ public:
 	/// returns true if input event has been found
 	bool ignoreEventsUntilInput();
 
-	void fakeMoveCursor(float dx, float dy);
+	/// Moves cursor by specified distance
+	void moveCursorPosition(const Point & distance);
+
+	/// Moves cursor to a specified position
+	void setCursorPosition(const Point & position);
 
 	/// Initiates text input in selected area, potentially creating IME popup (mobile systems only at the moment)
 	void startTextInput(const Rect & where);
@@ -61,6 +63,9 @@ public:
 	/// Ends any existing text input state
 	void stopTextInput();
 
+	/// returns true if system has active touchscreen
+	bool hasTouchInputDevice() const;
+
 	/// Returns true if selected mouse button is pressed at the moment
 	bool isMouseButtonPressed(MouseButton button) const;
 

+ 19 - 0
client/eventsSDL/InputSourceKeyboard.cpp

@@ -83,3 +83,22 @@ void InputSourceKeyboard::handleEventKeyUp(const SDL_KeyboardEvent & key)
 
 	GH.events().dispatchShortcutReleased(shortcutsVector);
 }
+
+bool InputSourceKeyboard::isKeyboardCtrlDown() const
+{
+#ifdef VCMI_MAC
+	return SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_LGUI] || SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_RGUI];
+#else
+	return SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_LCTRL] || SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_RCTRL];
+#endif
+}
+
+bool InputSourceKeyboard::isKeyboardAltDown() const
+{
+	return SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_LALT] || SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_RALT];
+}
+
+bool InputSourceKeyboard::isKeyboardShiftDown() const
+{
+	return SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_LSHIFT] || SDL_GetKeyboardState(nullptr)[SDL_SCANCODE_RSHIFT];
+}

+ 4 - 0
client/eventsSDL/InputSourceKeyboard.h

@@ -20,4 +20,8 @@ public:
 
 	void handleEventKeyDown(const SDL_KeyboardEvent & current);
 	void handleEventKeyUp(const SDL_KeyboardEvent & current);
+
+	bool isKeyboardAltDown() const;
+	bool isKeyboardCtrlDown() const;
+	bool isKeyboardShiftDown() const;
 };

+ 26 - 8
client/eventsSDL/InputSourceMouse.cpp

@@ -10,6 +10,7 @@
 
 #include "StdInc.h"
 #include "InputSourceMouse.h"
+#include "InputHandler.h"
 
 #include "../../lib/Point.h"
 #include "../gui/CGuiHandler.h"
@@ -20,7 +21,15 @@
 
 void InputSourceMouse::handleEventMouseMotion(const SDL_MouseMotionEvent & motion)
 {
-	GH.events().dispatchMouseMoved(Point(motion.x, motion.y));
+	Point newPosition(motion.x, motion.y);
+	Point distance(-motion.xrel, -motion.yrel);
+
+	if (mouseButtonsMask & SDL_BUTTON(SDL_BUTTON_MIDDLE))
+		GH.events().dispatchGesturePanning(middleClickPosition, newPosition, distance);
+	else
+		GH.input().setCursorPosition(newPosition);
+
+	mouseButtonsMask = motion.state;
 }
 
 void InputSourceMouse::handleEventMouseButtonDown(const SDL_MouseButtonEvent & button)
@@ -39,18 +48,15 @@ void InputSourceMouse::handleEventMouseButtonDown(const SDL_MouseButtonEvent & b
 			GH.events().dispatchMouseButtonPressed(MouseButton::RIGHT, position);
 			break;
 		case SDL_BUTTON_MIDDLE:
-			GH.events().dispatchMouseButtonPressed(MouseButton::MIDDLE, position);
+			middleClickPosition = position;
+			GH.events().dispatchGesturePanningStarted(position);
 			break;
 	}
 }
 
 void InputSourceMouse::handleEventMouseWheel(const SDL_MouseWheelEvent & wheel)
 {
-	// SDL doesn't have the proper values for mouse positions on SDL_MOUSEWHEEL, refetch them
-	int x = 0, y = 0;
-	SDL_GetMouseState(&x, &y);
-
-	GH.events().dispatchMouseScrolled(Point(wheel.x, wheel.y), Point(x, y));
+	GH.events().dispatchMouseScrolled(Point(wheel.x, wheel.y), GH.getCursorPosition());
 }
 
 void InputSourceMouse::handleEventMouseButtonUp(const SDL_MouseButtonEvent & button)
@@ -66,7 +72,19 @@ void InputSourceMouse::handleEventMouseButtonUp(const SDL_MouseButtonEvent & but
 			GH.events().dispatchMouseButtonReleased(MouseButton::RIGHT, position);
 			break;
 		case SDL_BUTTON_MIDDLE:
-			GH.events().dispatchMouseButtonReleased(MouseButton::MIDDLE, position);
+			GH.events().dispatchGesturePanningEnded(middleClickPosition, position);
 			break;
 	}
 }
+
+bool InputSourceMouse::isMouseButtonPressed(MouseButton button) const
+{
+	static_assert(static_cast<uint32_t>(MouseButton::LEFT)   == SDL_BUTTON_LEFT,   "mismatch between VCMI and SDL enum!");
+	static_assert(static_cast<uint32_t>(MouseButton::MIDDLE) == SDL_BUTTON_MIDDLE, "mismatch between VCMI and SDL enum!");
+	static_assert(static_cast<uint32_t>(MouseButton::RIGHT)  == SDL_BUTTON_RIGHT,  "mismatch between VCMI and SDL enum!");
+	static_assert(static_cast<uint32_t>(MouseButton::EXTRA1) == SDL_BUTTON_X1,     "mismatch between VCMI and SDL enum!");
+	static_assert(static_cast<uint32_t>(MouseButton::EXTRA2) == SDL_BUTTON_X2,     "mismatch between VCMI and SDL enum!");
+
+	uint32_t index = static_cast<uint32_t>(button);
+	return mouseButtonsMask & SDL_BUTTON(index);
+}

+ 8 - 0
client/eventsSDL/InputSourceMouse.h

@@ -10,16 +10,24 @@
 
 #pragma once
 
+#include "../../lib/Point.h"
+
 struct SDL_MouseWheelEvent;
 struct SDL_MouseMotionEvent;
 struct SDL_MouseButtonEvent;
 
+enum class MouseButton;
+
 /// Class that handles mouse input from SDL events
 class InputSourceMouse
 {
+	Point middleClickPosition;
+	int mouseButtonsMask = 0;
 public:
 	void handleEventMouseMotion(const SDL_MouseMotionEvent & current);
 	void handleEventMouseButtonDown(const SDL_MouseButtonEvent & current);
 	void handleEventMouseWheel(const SDL_MouseWheelEvent & current);
 	void handleEventMouseButtonUp(const SDL_MouseButtonEvent & current);
+
+	bool isMouseButtonPressed(MouseButton button) const;
 };

+ 216 - 66
client/eventsSDL/InputSourceTouch.cpp

@@ -15,121 +15,271 @@
 
 #include "../../lib/CConfigHandler.h"
 #include "../CMT.h"
+#include "../CGameInfo.h"
+#include "../gui/CursorHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/EventDispatcher.h"
 #include "../gui/MouseButton.h"
 
 #include <SDL_events.h>
-#include <SDL_render.h>
 #include <SDL_hints.h>
+#include <SDL_timer.h>
 
 InputSourceTouch::InputSourceTouch()
-	: multifinger(false)
-	, isPointerRelativeMode(settings["general"]["userRelativePointer"].Bool())
+	: lastTapTimeTicks(0)
 {
-	if(isPointerRelativeMode)
-	{
-		SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0");
-		SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0");
-	}
+	params.useRelativeMode = settings["general"]["userRelativePointer"].Bool();
+	params.relativeModeSpeedFactor = settings["general"]["relativePointerSpeedMultiplier"].Float();
+
+	if (params.useRelativeMode)
+		state = TouchState::RELATIVE_MODE;
+	else
+		state = TouchState::IDLE;
+
+	SDL_SetHint(SDL_HINT_MOUSE_TOUCH_EVENTS, "0");
+	SDL_SetHint(SDL_HINT_TOUCH_MOUSE_EVENTS, "0");
 }
 
 void InputSourceTouch::handleEventFingerMotion(const SDL_TouchFingerEvent & tfinger)
 {
-	if(isPointerRelativeMode)
+	switch(state)
 	{
-		GH.input().fakeMoveCursor(tfinger.dx, tfinger.dy);
+		case TouchState::RELATIVE_MODE:
+		{
+			Point screenSize = GH.screenDimensions();
+
+			Point moveDistance {
+				static_cast<int>(screenSize.x * params.relativeModeSpeedFactor * tfinger.dx),
+				static_cast<int>(screenSize.y * params.relativeModeSpeedFactor * tfinger.dy)
+			};
+
+			GH.input().moveCursorPosition(moveDistance);
+			if (CCS && CCS->curh)
+				CCS->curh->cursorMove(GH.getCursorPosition().x, GH.getCursorPosition().y);
+
+			break;
+		}
+		case TouchState::IDLE:
+		{
+			// no-op, might happen in some edge cases, e.g. when fingerdown event was ignored
+			break;
+		}
+		case TouchState::TAP_DOWN_SHORT:
+		{
+			Point distance = convertTouchToMouse(tfinger) - lastTapPosition;
+			if ( std::abs(distance.x) > params.panningSensitivityThreshold || std::abs(distance.y) > params.panningSensitivityThreshold)
+				state = TouchState::TAP_DOWN_PANNING;
+			break;
+		}
+		case TouchState::TAP_DOWN_PANNING:
+		{
+			emitPanningEvent(tfinger);
+			break;
+		}
+		case TouchState::TAP_DOWN_DOUBLE:
+		{
+			emitPinchEvent(tfinger);
+			break;
+		}
+		case TouchState::TAP_DOWN_LONG:
+		case TouchState::TAP_DOWN_LONG_AWAIT:
+		{
+			// no-op
+			break;
+		}
 	}
 }
 
 void InputSourceTouch::handleEventFingerDown(const SDL_TouchFingerEvent & tfinger)
 {
-	auto fingerCount = SDL_GetNumTouchFingers(tfinger.touchId);
-
-	multifinger = fingerCount > 1;
+	lastTapTimeTicks = tfinger.timestamp;
 
-	if(isPointerRelativeMode)
+	switch(state)
 	{
-		if(tfinger.x > 0.5)
+		case TouchState::RELATIVE_MODE:
 		{
-			bool isRightClick = tfinger.y < 0.5;
-
-			fakeMouseButtonEventRelativeMode(true, isRightClick);
+			if(tfinger.x > 0.5)
+			{
+				MouseButton button =  tfinger.y < 0.5 ? MouseButton::RIGHT : MouseButton::LEFT;
+				GH.events().dispatchMouseButtonPressed(button, GH.getCursorPosition());
+			}
+			break;
+		}
+		case TouchState::IDLE:
+		{
+			lastTapPosition = convertTouchToMouse(tfinger);
+			GH.input().setCursorPosition(lastTapPosition);
+			GH.events().dispatchGesturePanningStarted(lastTapPosition);
+			state = TouchState::TAP_DOWN_SHORT;
+			break;
+		}
+		case TouchState::TAP_DOWN_SHORT:
+		case TouchState::TAP_DOWN_PANNING:
+		{
+			GH.input().setCursorPosition(convertTouchToMouse(tfinger));
+			state = TouchState::TAP_DOWN_DOUBLE;
+			break;
+		}
+		case TouchState::TAP_DOWN_DOUBLE:
+		case TouchState::TAP_DOWN_LONG:
+		case TouchState::TAP_DOWN_LONG_AWAIT:
+		{
+			// no-op
+			break;
 		}
 	}
-#ifndef VCMI_IOS
-	else if(fingerCount == 2)
-	{
-		Point position = convertTouchToMouse(tfinger);
-
-		GH.events().dispatchMouseMoved(position);
-		GH.events().dispatchMouseButtonPressed(MouseButton::RIGHT, position);
-	}
-#endif //VCMI_IOS
 }
 
 void InputSourceTouch::handleEventFingerUp(const SDL_TouchFingerEvent & tfinger)
 {
-#ifndef VCMI_IOS
-	auto fingerCount = SDL_GetNumTouchFingers(tfinger.touchId);
-#endif //VCMI_IOS
-
-	if(isPointerRelativeMode)
+	switch(state)
 	{
-		if(tfinger.x > 0.5)
+		case TouchState::RELATIVE_MODE:
 		{
-			bool isRightClick = tfinger.y < 0.5;
-
-			fakeMouseButtonEventRelativeMode(false, isRightClick);
+			if(tfinger.x > 0.5)
+			{
+				MouseButton button =  tfinger.y < 0.5 ? MouseButton::RIGHT : MouseButton::LEFT;
+				GH.events().dispatchMouseButtonReleased(button, GH.getCursorPosition());
+			}
+			break;
+		}
+		case TouchState::IDLE:
+		{
+			// no-op, might happen in some edge cases, e.g. when fingerdown event was ignored
+			break;
+		}
+		case TouchState::TAP_DOWN_SHORT:
+		{
+			GH.input().setCursorPosition(convertTouchToMouse(tfinger));
+			GH.events().dispatchMouseButtonPressed(MouseButton::LEFT, convertTouchToMouse(tfinger));
+			GH.events().dispatchMouseButtonReleased(MouseButton::LEFT, convertTouchToMouse(tfinger));
+			state = TouchState::IDLE;
+			break;
+		}
+		case TouchState::TAP_DOWN_PANNING:
+		{
+			GH.events().dispatchGesturePanningEnded(lastTapPosition, convertTouchToMouse(tfinger));
+			state = TouchState::IDLE;
+			break;
+		}
+		case TouchState::TAP_DOWN_DOUBLE:
+		{
+			if (SDL_GetNumTouchFingers(tfinger.touchId) == 1)
+				state = TouchState::TAP_DOWN_PANNING;
+			if (SDL_GetNumTouchFingers(tfinger.touchId) == 0)
+			{
+				GH.events().dispatchGesturePanningEnded(lastTapPosition, convertTouchToMouse(tfinger));
+				state = TouchState::IDLE;
+			}
+			break;
+		}
+		case TouchState::TAP_DOWN_LONG:
+		{
+			if (SDL_GetNumTouchFingers(tfinger.touchId) == 0)
+			{
+				state = TouchState::TAP_DOWN_LONG_AWAIT;
+			}
+			break;
+		}
+		case TouchState::TAP_DOWN_LONG_AWAIT:
+		{
+			if (SDL_GetNumTouchFingers(tfinger.touchId) == 0)
+			{
+				GH.input().setCursorPosition(convertTouchToMouse(tfinger));
+				GH.events().dispatchMouseButtonReleased(MouseButton::RIGHT, convertTouchToMouse(tfinger));
+				state = TouchState::IDLE;
+			}
+			break;
 		}
 	}
-#ifndef VCMI_IOS
-	else if(multifinger)
+}
+
+void InputSourceTouch::handleUpdate()
+{
+	if ( state == TouchState::TAP_DOWN_SHORT)
 	{
-		Point position = convertTouchToMouse(tfinger);
-		GH.events().dispatchMouseMoved(position);
-		GH.events().dispatchMouseButtonReleased(MouseButton::RIGHT, position);
-		multifinger = fingerCount != 0;
+		uint32_t currentTime = SDL_GetTicks();
+		if (currentTime > lastTapTimeTicks + params.longPressTimeMilliseconds)
+		{
+			state = TouchState::TAP_DOWN_LONG;
+			GH.events().dispatchMouseButtonPressed(MouseButton::RIGHT, GH.getCursorPosition());
+		}
 	}
-#endif //VCMI_IOS
 }
 
 Point InputSourceTouch::convertTouchToMouse(const SDL_TouchFingerEvent & tfinger)
 {
-	return Point(tfinger.x * GH.screenDimensions().x, tfinger.y * GH.screenDimensions().y);
+	return convertTouchToMouse(tfinger.x, tfinger.y);
 }
 
-void InputSourceTouch::fakeMouseButtonEventRelativeMode(bool down, bool right)
+Point InputSourceTouch::convertTouchToMouse(float x, float y)
 {
-	SDL_Event event;
-	SDL_MouseButtonEvent sme = {SDL_MOUSEBUTTONDOWN, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+	return Point(x * GH.screenDimensions().x, y * GH.screenDimensions().y);
+}
+
+bool InputSourceTouch::hasTouchInputDevice() const
+{
+	return SDL_GetNumTouchDevices() > 0;
+}
 
-	if(!down)
+bool InputSourceTouch::isMouseButtonPressed(MouseButton button) const
+{
+	if (state == TouchState::TAP_DOWN_LONG)
 	{
-		sme.type = SDL_MOUSEBUTTONUP;
+		if (button == MouseButton::RIGHT)
+			return true;
 	}
 
-	sme.button = right ? SDL_BUTTON_RIGHT : SDL_BUTTON_LEFT;
+	return false;
+}
 
-	sme.x = GH.getCursorPosition().x;
-	sme.y = GH.getCursorPosition().y;
+void InputSourceTouch::emitPanningEvent(const SDL_TouchFingerEvent & tfinger)
+{
+	Point distance = convertTouchToMouse(-tfinger.dx, -tfinger.dy);
 
-	float xScale, yScale;
-	int w, h, rLogicalWidth, rLogicalHeight;
+	GH.events().dispatchGesturePanning(lastTapPosition, convertTouchToMouse(tfinger), distance);
+}
 
-	SDL_GetWindowSize(mainWindow, &w, &h);
-	SDL_RenderGetLogicalSize(mainRenderer, &rLogicalWidth, &rLogicalHeight);
-	SDL_RenderGetScale(mainRenderer, &xScale, &yScale);
+void InputSourceTouch::emitPinchEvent(const SDL_TouchFingerEvent & tfinger)
+{
+	int fingers = SDL_GetNumTouchFingers(tfinger.touchId);
 
-	SDL_EventState(SDL_MOUSEMOTION, SDL_IGNORE);
-	moveCursorToPosition(Point((int)(sme.x * xScale) + (w - rLogicalWidth * xScale) / 2, (int)(sme.y * yScale + (h - rLogicalHeight * yScale) / 2)));
-	SDL_EventState(SDL_MOUSEMOTION, SDL_ENABLE);
+	if (fingers < 2)
+		return;
 
-	event.button = sme;
-	SDL_PushEvent(&event);
-}
+	bool otherFingerFound = false;
+	double otherX;
+	double otherY;
 
-void InputSourceTouch::moveCursorToPosition(const Point & position)
-{
-	SDL_WarpMouseInWindow(mainWindow, position.x, position.y);
+	for (int i = 0; i < fingers; ++i)
+	{
+		SDL_Finger * finger = SDL_GetTouchFinger(tfinger.touchId, i);
+
+		if (finger && finger->id != tfinger.fingerId)
+		{
+			otherX = finger->x * GH.screenDimensions().x;
+			otherY = finger->y * GH.screenDimensions().y;
+			otherFingerFound = true;
+			break;
+		}
+	}
+
+	if (!otherFingerFound)
+		return; // should be impossible, but better to avoid weird edge cases
+
+	float thisX = tfinger.x * GH.screenDimensions().x;
+	float thisY = tfinger.y * GH.screenDimensions().y;
+	float deltaX = tfinger.dx * GH.screenDimensions().x;
+	float deltaY = tfinger.dy * GH.screenDimensions().y;
+
+	float oldX = thisX - deltaX - otherX;
+	float oldY = thisY - deltaY - otherY;
+	float newX = thisX - otherX;
+	float newY = thisY - otherY;
+
+	double distanceOld = std::sqrt(oldX * oldX + oldY + oldY);
+	double distanceNew = std::sqrt(newX * newX + newY + newY);
+
+	if (distanceOld > params.pinchSensitivityThreshold)
+		GH.events().dispatchGesturePinch(lastTapPosition, distanceNew / distanceOld);
 }

+ 86 - 8
client/eventsSDL/InputSourceTouch.h

@@ -10,23 +10,96 @@
 
 #pragma once
 
-VCMI_LIB_NAMESPACE_BEGIN
-class Point;
-VCMI_LIB_NAMESPACE_END
+#include "../../lib/Point.h"
 
+enum class MouseButton;
 struct SDL_TouchFingerEvent;
 
+/// Enumeration that describes current state of gesture recognition
+enum class TouchState
+{
+	// special state that allows no transitions
+	// used when player selects "relative mode" in Launcher
+	// in this mode touchscreen acts like touchpad, moving cursor at certains speed
+	// and generates events for positions below cursor instead of positions below touch events
+	RELATIVE_MODE,
+
+	// no active touch events
+	// DOWN -> transition to TAP_DOWN_SHORT
+	// MOTION / UP -> not expected
+	IDLE,
+
+	// single finger is touching the screen for a short time
+	// DOWN -> transition to TAP_DOWN_DOUBLE
+	// MOTION -> transition to TAP_DOWN_PANNING
+	// UP -> transition to IDLE, emit onLeftClickDown and onLeftClickUp
+	// on timer -> transition to TAP_DOWN_LONG, emit onRightClickDown event
+	TAP_DOWN_SHORT,
+
+	// single finger is moving across screen
+	// DOWN -> transition to TAP_DOWN_DOUBLE
+	// MOTION -> emit panning event
+	// UP -> transition to IDLE
+	TAP_DOWN_PANNING,
+
+	// two fingers are touching the screen
+	// DOWN -> ??? how to handle 3rd finger? Ignore?
+	// MOTION -> emit pinch event
+	// UP -> transition to TAP_DOWN
+	TAP_DOWN_DOUBLE,
+
+	// single finger is down for long period of time
+	// DOWN -> ignored
+	// MOTION -> ignored
+	// UP -> transition to TAP_DOWN_LONG_AWAIT
+	TAP_DOWN_LONG,
+
+	// right-click popup is active, waiting for new tap to hide popup
+	// DOWN -> ignored
+	// MOTION -> ignored
+	// UP -> transition to IDLE, generate onRightClickUp() event
+	TAP_DOWN_LONG_AWAIT,
+
+
+	// Possible transitions:
+	//                               -> DOUBLE
+	//                    -> PANNING -> IDLE
+	// IDLE -> DOWN_SHORT -> IDLE
+	//                    -> LONG -> IDLE
+	//                    -> DOUBLE -> PANNING
+	//                              -> IDLE
+};
+
+struct TouchInputParameters
+{
+	/// Speed factor of mouse pointer when relative mode is used
+	double relativeModeSpeedFactor = 1.0;
+
+	/// tap for period longer than specified here will be qualified as "long tap", triggering corresponding gesture
+	uint32_t longPressTimeMilliseconds = 750;
+
+	/// moving finger for distance larger than specified will be qualified as panning gesture instead of long press
+	uint32_t panningSensitivityThreshold = 10;
+
+	/// gesture will be qualified as pinch if distance between fingers is at least specified here
+	uint32_t pinchSensitivityThreshold = 10;
+
+	bool useRelativeMode = false;
+};
+
 /// Class that handles touchscreen input from SDL events
 class InputSourceTouch
 {
-	bool multifinger;
-	bool isPointerRelativeMode;
+	TouchInputParameters params;
+	TouchState state;
+	uint32_t lastTapTimeTicks;
+	Point lastTapPosition;
 
-	/// moves mouse pointer into specified position inside vcmi window
-	void moveCursorToPosition(const Point & position);
 	Point convertTouchToMouse(const SDL_TouchFingerEvent & current);
+	Point convertTouchToMouse(float x, float y);
 
-	void fakeMouseButtonEventRelativeMode(bool down, bool right);
+	void emitPanningEvent(const SDL_TouchFingerEvent & tfinger);
+	void emitPinchEvent(const SDL_TouchFingerEvent & tfinger);
 
 public:
 	InputSourceTouch();
@@ -34,4 +107,9 @@ public:
 	void handleEventFingerMotion(const SDL_TouchFingerEvent & current);
 	void handleEventFingerDown(const SDL_TouchFingerEvent & current);
 	void handleEventFingerUp(const SDL_TouchFingerEvent & current);
+
+	void handleUpdate();
+
+	bool hasTouchInputDevice() const;
+	bool isMouseButtonPressed(MouseButton button) const;
 };

+ 4 - 0
client/eventsSDL/UserEventHandler.cpp

@@ -18,6 +18,7 @@
 #include "../gui/WindowHandler.h"
 #include "../mainmenu/CMainMenu.h"
 #include "../mainmenu/CPrologEpilogVideo.h"
+#include "../gui/EventDispatcher.h"
 
 #include <SDL_events.h>
 
@@ -80,6 +81,9 @@ void UserEventHandler::handleUserEvent(const SDL_UserEvent & user)
 			GH.onScreenResize();
 			break;
 		}
+		case EUserEvent::FAKE_MOUSE_MOVE:
+			GH.events().dispatchMouseMoved(GH.getCursorPosition());
+			break;
 		default:
 			logGlobal->error("Unknown user event. Code %d", user.code);
 			break;

+ 1 - 1
client/gui/CGuiHandler.cpp

@@ -87,7 +87,7 @@ void CGuiHandler::handleEvents()
 
 void CGuiHandler::fakeMouseMove()
 {
-	input().fakeMoveCursor(0, 0);
+	pushUserEvent(EUserEvent::FAKE_MOUSE_MOVE);
 }
 
 void CGuiHandler::startTextInput(const Rect & whereInput)

+ 1 - 0
client/gui/CGuiHandler.h

@@ -36,6 +36,7 @@ enum class EUserEvent
 	FULLSCREEN_TOGGLED,
 	CAMPAIGN_START_SCENARIO,
 	FORCE_QUIT,
+	FAKE_MOUSE_MOVE,
 };
 
 // Handles GUI logic and drawing

+ 1 - 1
client/gui/CIntObject.cpp

@@ -229,7 +229,7 @@ void CIntObject::redraw()
 	}
 }
 
-bool CIntObject::isInside(const Point & position)
+bool CIntObject::receiveEvent(const Point & position, int eventType) const
 {
 	return pos.isInside(position);
 }

+ 1 - 4
client/gui/CIntObject.h

@@ -62,9 +62,6 @@ public:
 	CIntObject(int used=0, Point offset=Point());
 	virtual ~CIntObject();
 
-	//hover handling
-	void hover (bool on) override{}
-
 	//keyboard handling
 	bool captureAllKeys; //if true, only this object should get info about pressed keys
 
@@ -100,7 +97,7 @@ public:
 	/// default behavior is to re-center, can be overriden
 	void onScreenResize() override;
 
-	bool isInside(const Point & position) override;
+	bool receiveEvent(const Point & position, int eventType) const override;
 
 	const Rect & center(const Rect &r, bool propagate = true); //sets pos so that r will be in the center of screen, assigns sizes of r to pos, returns new position
 	const Rect & center(const Point &p, bool propagate = true);  //moves object so that point p will be in its center

+ 5 - 5
client/gui/CursorHandler.cpp

@@ -25,7 +25,10 @@ std::unique_ptr<ICursor> CursorHandler::createCursor()
 	if (settings["video"]["cursor"].String() == "auto")
 	{
 #if defined(VCMI_MOBILE)
-		return std::make_unique<CursorSoftware>();
+		if (settings["general"]["userRelativePointer"].Bool())
+			return std::make_unique<CursorSoftware>();
+		else
+			return std::make_unique<CursorHardware>();
 #else
 		return std::make_unique<CursorHardware>();
 #endif
@@ -62,10 +65,7 @@ CursorHandler::CursorHandler()
 	set(Cursor::Map::POINTER);
 }
 
-Point CursorHandler::position() const
-{
-	return pos;
-}
+CursorHandler::~CursorHandler() = default;
 
 void CursorHandler::changeGraphic(Cursor::Type type, size_t index)
 {

+ 4 - 6
client/gui/CursorHandler.h

@@ -127,9 +127,6 @@ class CursorHandler final
 
 	void changeGraphic(Cursor::Type type, size_t index);
 
-	Point getPivotOffsetDefault(size_t index);
-	Point getPivotOffsetMap(size_t index);
-	Point getPivotOffsetCombat(size_t index);
 	Point getPivotOffsetSpellcast();
 	Point getPivotOffset();
 
@@ -150,9 +147,6 @@ public:
 
 	void dragAndDropCursor(std::string path, size_t index);
 
-	/// Returns current position of the cursor
-	Point position() const;
-
 	/// Changes cursor to specified index
 	void set(Cursor::Default index);
 	void set(Cursor::Map index);
@@ -171,6 +165,10 @@ public:
 		return static_cast<Index>(frame);
 	}
 
+	Point getPivotOffsetDefault(size_t index);
+	Point getPivotOffsetMap(size_t index);
+	Point getPivotOffsetCombat(size_t index);
+
 	void render();
 
 	void hide();

+ 84 - 19
client/gui/EventDispatcher.cpp

@@ -28,7 +28,6 @@ void EventDispatcher::processLists(ui16 activityFlag, const Functor & cb)
 
 	processList(AEventsReceiver::LCLICK, lclickable);
 	processList(AEventsReceiver::RCLICK, rclickable);
-	processList(AEventsReceiver::MCLICK, mclickable);
 	processList(AEventsReceiver::HOVER, hoverable);
 	processList(AEventsReceiver::MOVE, motioninterested);
 	processList(AEventsReceiver::KEYBOARD, keyinterested);
@@ -36,6 +35,7 @@ void EventDispatcher::processLists(ui16 activityFlag, const Functor & cb)
 	processList(AEventsReceiver::WHEEL, wheelInterested);
 	processList(AEventsReceiver::DOUBLECLICK, doubleClickInterested);
 	processList(AEventsReceiver::TEXTINPUT, textInterested);
+	processList(AEventsReceiver::GESTURE_PANNING, panningInterested);
 }
 
 void EventDispatcher::activateElement(AEventsReceiver * elem, ui16 activityFlag)
@@ -120,8 +120,6 @@ EventDispatcher::EventReceiversList & EventDispatcher::getListForMouseButton(Mou
 			return lclickable;
 		case MouseButton::RIGHT:
 			return rclickable;
-		case MouseButton::MIDDLE:
-			return mclickable;
 	}
 	throw std::runtime_error("Invalid mouse button in getListForMouseButton");
 }
@@ -136,9 +134,9 @@ void EventDispatcher::dispatchMouseDoubleClick(const Point & position)
 		if(!vstd::contains(doubleClickInterested, i))
 			continue;
 
-		if(i->isInside(position))
+		if(i->receiveEvent(position, AEventsReceiver::DOUBLECLICK))
 		{
-			i->onDoubleClick();
+			i->clickDouble();
 			doubleClicked = true;
 		}
 	}
@@ -166,16 +164,29 @@ void EventDispatcher::handleMouseButtonClick(EventReceiversList & interestedObjs
 			continue;
 
 		auto prev = i->isMouseButtonPressed(btn);
+
 		if(!isPressed)
 			i->currentMouseState[btn] = isPressed;
-		if(i->isInside(GH.getCursorPosition()))
+
+		if( btn == MouseButton::LEFT && i->receiveEvent(GH.getCursorPosition(), AEventsReceiver::LCLICK))
+		{
+			if(isPressed)
+				i->currentMouseState[btn] = isPressed;
+			i->clickLeft(isPressed, prev);
+		}
+		else if( btn == MouseButton::RIGHT && i->receiveEvent(GH.getCursorPosition(), AEventsReceiver::RCLICK))
 		{
 			if(isPressed)
 				i->currentMouseState[btn] = isPressed;
-			i->click(btn, isPressed, prev);
+			i->clickRight(isPressed, prev);
 		}
 		else if(!isPressed)
-			i->click(btn, boost::logic::indeterminate, prev);
+		{
+			if (btn == MouseButton::LEFT)
+				i->clickLeft(boost::logic::indeterminate, prev);
+			if (btn == MouseButton::RIGHT)
+				i->clickRight(boost::logic::indeterminate, prev);
+		}
 	}
 }
 
@@ -186,7 +197,9 @@ void EventDispatcher::dispatchMouseScrolled(const Point & distance, const Point
 	{
 		if(!vstd::contains(wheelInterested,i))
 			continue;
-		i->wheelScrolled(distance.y < 0, i->isInside(position));
+
+		if (i->receiveEvent(position, AEventsReceiver::WHEEL))
+			i->wheelScrolled(distance.y);
 	}
 }
 
@@ -206,27 +219,79 @@ void EventDispatcher::dispatchTextEditing(const std::string & text)
 	}
 }
 
+void EventDispatcher::dispatchGesturePanningStarted(const Point & initialPosition)
+{
+	auto copied = panningInterested;
+
+	for(auto it : copied)
+	{
+		if (it->receiveEvent(initialPosition, AEventsReceiver::GESTURE_PANNING))
+		{
+			it->panning(true, initialPosition, initialPosition);
+			it->panningState = true;
+		}
+	}
+}
+
+void EventDispatcher::dispatchGesturePanningEnded(const Point & initialPosition, const Point & finalPosition)
+{
+	auto copied = panningInterested;
+
+	for(auto it : copied)
+	{
+		if (it->isPanning())
+		{
+			it->panning(false, initialPosition, finalPosition);
+			it->panningState = false;
+		}
+	}
+}
+
+void EventDispatcher::dispatchGesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance)
+{
+	auto copied = panningInterested;
+
+	for(auto it : copied)
+	{
+		if (it->isPanning())
+			it->gesturePanning(initialPosition, currentPosition, lastUpdateDistance);
+	}
+}
+
+void EventDispatcher::dispatchGesturePinch(const Point & initialPosition, double distance)
+{
+	for(auto it : panningInterested)
+	{
+		if (it->isPanning())
+			it->gesturePinch(initialPosition, distance);
+	}
+}
+
 void EventDispatcher::dispatchMouseMoved(const Point & position)
 {
-	//sending active, hovered hoverable objects hover() call
-	EventReceiversList hlp;
+	EventReceiversList newlyHovered;
 
 	auto hoverableCopy = hoverable;
 	for(auto & elem : hoverableCopy)
 	{
-		if(elem->isInside(GH.getCursorPosition()))
+		if(elem->receiveEvent(position, AEventsReceiver::HOVER))
 		{
-			if (!(elem)->isHovered())
-				hlp.push_back((elem));
+			if (!elem->isHovered())
+			{
+				newlyHovered.push_back((elem));
+			}
 		}
-		else if ((elem)->isHovered())
+		else
 		{
-			(elem)->hover(false);
-			(elem)->hoveredState = false;
+			if (elem->isHovered())
+			{
+				(elem)->hover(false);
+				(elem)->hoveredState = false;
+			}
 		}
 	}
 
-	for(auto & elem : hlp)
+	for(auto & elem : newlyHovered)
 	{
 		elem->hover(true);
 		elem->hoveredState = true;
@@ -236,7 +301,7 @@ void EventDispatcher::dispatchMouseMoved(const Point & position)
 	EventReceiversList miCopy = motioninterested;
 	for(auto & elem : miCopy)
 	{
-		if(elem->strongInterestState || elem->isInside(position)) //checking bounds including border fixes bug #2476
+		if(elem->receiveEvent(position, AEventsReceiver::HOVER))
 		{
 			(elem)->mouseMoved(position);
 		}

+ 7 - 2
client/gui/EventDispatcher.h

@@ -25,7 +25,6 @@ class EventDispatcher
 	/// list of UI elements that are interested in particular event
 	EventReceiversList lclickable;
 	EventReceiversList rclickable;
-	EventReceiversList mclickable;
 	EventReceiversList hoverable;
 	EventReceiversList keyinterested;
 	EventReceiversList motioninterested;
@@ -33,6 +32,7 @@ class EventDispatcher
 	EventReceiversList wheelInterested;
 	EventReceiversList doubleClickInterested;
 	EventReceiversList textInterested;
+	EventReceiversList panningInterested;
 
 	EventReceiversList & getListForMouseButton(MouseButton button);
 
@@ -60,7 +60,12 @@ public:
 	void dispatchMouseButtonReleased(const MouseButton & button, const Point & position);
 	void dispatchMouseScrolled(const Point & distance, const Point & position);
 	void dispatchMouseDoubleClick(const Point & position);
-	void dispatchMouseMoved(const Point & position);
+	void dispatchMouseMoved(const Point & distance);
+
+	void dispatchGesturePanningStarted(const Point & initialPosition);
+	void dispatchGesturePanningEnded(const Point & initialPosition, const Point & finalPosition);
+	void dispatchGesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance);
+	void dispatchGesturePinch(const Point & initialPosition, double distance);
 
 	/// Text input events
 	void dispatchTextInput(const std::string & text);

+ 6 - 23
client/gui/EventsReceiver.cpp

@@ -17,7 +17,7 @@
 AEventsReceiver::AEventsReceiver()
 	: activeState(0)
 	, hoveredState(false)
-	, strongInterestState(false)
+	, panningState(false)
 {
 }
 
@@ -26,6 +26,11 @@ bool AEventsReceiver::isHovered() const
 	return hoveredState;
 }
 
+bool AEventsReceiver::isPanning() const
+{
+	return panningState;
+}
+
 bool AEventsReceiver::isActive() const
 {
 	return activeState;
@@ -36,11 +41,6 @@ bool AEventsReceiver::isMouseButtonPressed(MouseButton btn) const
 	return currentMouseState.count(btn) ? currentMouseState.at(btn) : false;
 }
 
-void AEventsReceiver::setMoveEventStrongInterest(bool on)
-{
-	strongInterestState = on;
-}
-
 void AEventsReceiver::activateEvents(ui16 what)
 {
 	assert((what & GENERAL) || (activeState & GENERAL));
@@ -55,20 +55,3 @@ void AEventsReceiver::deactivateEvents(ui16 what)
 		activeState &= ~GENERAL;
 	GH.events().deactivateElement(this, what & activeState);
 }
-
-void AEventsReceiver::click(MouseButton btn, tribool down, bool previousState)
-{
-	switch(btn)
-	{
-	default:
-	case MouseButton::LEFT:
-		clickLeft(down, previousState);
-		break;
-	case MouseButton::MIDDLE:
-		clickMiddle(down, previousState);
-		break;
-	case MouseButton::RIGHT:
-		clickRight(down, previousState);
-		break;
-	}
-}

+ 25 - 15
client/gui/EventsReceiver.h

@@ -24,17 +24,12 @@ class AEventsReceiver
 {
 	friend class EventDispatcher;
 
+	std::map<MouseButton, bool> currentMouseState;
 	ui16 activeState;
 	bool hoveredState;
-	bool strongInterestState;
-	std::map<MouseButton, bool> currentMouseState;
+	bool panningState;
 
-	void click(MouseButton btn, tribool down, bool previousState);
 protected:
-
-	/// If set, UI element will receive all mouse movement events, even those outside this element
-	void setMoveEventStrongInterest(bool on);
-
 	/// Activates particular events for this UI element. Uses unnamed enum from this class
 	void activateEvents(ui16 what);
 	/// Deactivates particular events for this UI element. Uses unnamed enum from this class
@@ -42,33 +37,48 @@ protected:
 
 	virtual void clickLeft(tribool down, bool previousState) {}
 	virtual void clickRight(tribool down, bool previousState) {}
-	virtual void clickMiddle(tribool down, bool previousState) {}
+	virtual void clickDouble() {}
 
-	virtual void textInputed(const std::string & enteredText) {}
-	virtual void textEdited(const std::string & enteredText) {}
+	/// Called when user pans screen by specified distance
+	virtual void gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) {}
 
-	virtual void tick(uint32_t msPassed) {}
-	virtual void wheelScrolled(bool down, bool in) {}
+	virtual void gesturePinch(const Point & centerPosition, double lastUpdateFactor) {}
+
+	virtual void wheelScrolled(int distance) {}
 	virtual void mouseMoved(const Point & cursorPosition) {}
+
+	/// Called when UI element hover status changes
 	virtual void hover(bool on) {}
-	virtual void onDoubleClick() {}
+
+	/// Called when UI element panning gesture status changes
+	virtual void panning(bool on, const Point & initialPosition, const Point & finalPosition) {}
+
+	virtual void textInputed(const std::string & enteredText) {}
+	virtual void textEdited(const std::string & enteredText) {}
 
 	virtual void keyPressed(EShortcut key) {}
 	virtual void keyReleased(EShortcut key) {}
 
+	virtual void tick(uint32_t msPassed) {}
+
 	virtual bool captureThisKey(EShortcut key) = 0;
-	virtual bool isInside(const Point & position) = 0;
+
+	/// If true, event of selected type in selected position will be processed by this element
+	virtual bool receiveEvent(const Point & position, int eventType) const= 0;
 
 public:
 	AEventsReceiver();
 	virtual ~AEventsReceiver() = default;
 
 	/// These are the arguments that can be used to determine what kind of input UI element will receive
-	enum {LCLICK=1, RCLICK=2, HOVER=4, MOVE=8, KEYBOARD=16, TIME=32, GENERAL=64, WHEEL=128, DOUBLECLICK=256, TEXTINPUT=512, MCLICK=1024, ALL=0xffff};
+	enum {LCLICK=1, RCLICK=2, HOVER=4, MOVE=8, KEYBOARD=16, TIME=32, GENERAL=64, WHEEL=128, DOUBLECLICK=256, TEXTINPUT=512, GESTURE_PANNING=1024, ALL=0xffff};
 
 	/// Returns true if element is currently hovered by mouse
 	bool isHovered() const;
 
+	/// Returns true if panning/swiping gesture is currently active
+	bool isPanning() const;
+
 	/// Returns true if element is currently active and may receive events
 	bool isActive() const;
 

+ 1 - 0
client/gui/InterfaceObjectConfigurable.cpp

@@ -21,6 +21,7 @@
 #include "../widgets/Buttons.h"
 #include "../widgets/MiscWidgets.h"
 #include "../widgets/ObjectLists.h"
+#include "../widgets/Slider.h"
 #include "../widgets/TextControls.h"
 #include "../windows/GUIClasses.h"
 #include "../windows/InfoWindows.h"

+ 4 - 3
client/lobby/CSelectionBase.cpp

@@ -28,10 +28,11 @@
 #include "../gui/Shortcut.h"
 #include "../gui/WindowHandler.h"
 #include "../mainmenu/CMainMenu.h"
-#include "../widgets/CComponent.h"
 #include "../widgets/Buttons.h"
+#include "../widgets/CComponent.h"
 #include "../widgets/MiscWidgets.h"
 #include "../widgets/ObjectLists.h"
+#include "../widgets/Slider.h"
 #include "../widgets/TextControls.h"
 #include "../windows/GUIClasses.h"
 #include "../windows/InfoWindows.h"
@@ -201,7 +202,7 @@ void InfoCard::changeSelection()
 
 	mapDescription->label->scrollTextTo(0, false);
 	if(mapDescription->slider)
-		mapDescription->slider->moveToMin();
+		mapDescription->slider->scrollToMin();
 
 	if(SEL->screenType == ESelectionScreen::campaignList)
 		return;
@@ -336,7 +337,7 @@ void CChatBox::addNewMessage(const std::string & text)
 	CCS->soundh->playSound("CHAT");
 	chatHistory->setText(chatHistory->label->getText() + text + "\n");
 	if(chatHistory->slider)
-		chatHistory->slider->moveToMax();
+		chatHistory->slider->scrollToMax();
 }
 
 CFlagBox::CFlagBox(const Rect & rect)

+ 31 - 2
client/lobby/OptionsTab.cpp

@@ -20,6 +20,7 @@
 #include "../widgets/Buttons.h"
 #include "../widgets/MiscWidgets.h"
 #include "../widgets/ObjectLists.h"
+#include "../widgets/Slider.h"
 #include "../widgets/TextControls.h"
 #include "../windows/GUIClasses.h"
 #include "../windows/InfoWindows.h"
@@ -48,6 +49,8 @@ OptionsTab::OptionsTab() : humanPlayers(0)
 	if(SEL->screenType == ESelectionScreen::newGame || SEL->screenType == ESelectionScreen::loadGame || SEL->screenType == ESelectionScreen::scenarioInfo)
 	{
 		sliderTurnDuration = std::make_shared<CSlider>(Point(55, 551), 194, std::bind(&IServerAPI::setTurnLength, CSH, _1), 1, (int)GameConstants::POSSIBLE_TURNTIME.size(), (int)GameConstants::POSSIBLE_TURNTIME.size(), true, CSlider::BLUE);
+		sliderTurnDuration->setScrollBounds(Rect(-3, -25, 337, 43));
+		sliderTurnDuration->setPanningStep(20);
 		labelPlayerTurnDuration = std::make_shared<CLabel>(222, 538, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[521]);
 		labelTurnDurationValue = std::make_shared<CLabel>(319, 559, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
 	}
@@ -69,7 +72,7 @@ void OptionsTab::recreate()
 
 	if(sliderTurnDuration)
 	{
-		sliderTurnDuration->moveTo(vstd::find_pos(GameConstants::POSSIBLE_TURNTIME, SEL->getStartInfo()->turnTime));
+		sliderTurnDuration->scrollTo(vstd::find_pos(GameConstants::POSSIBLE_TURNTIME, SEL->getStartInfo()->turnTime));
 		labelTurnDurationValue->setText(CGI->generaltexth->turnDurations[sliderTurnDuration->getValue()]);
 	}
 }
@@ -410,7 +413,8 @@ void OptionsTab::CPlayerOptionTooltipBox::genBonusWindow()
 }
 
 OptionsTab::SelectedBox::SelectedBox(Point position, PlayerSettings & settings, SelType type)
-	: CIntObject(RCLICK, position), CPlayerSettingsHelper(settings, type)
+	: Scrollable(RCLICK, position, Orientation::HORIZONTAL)
+	, CPlayerSettingsHelper(settings, type)
 {
 	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
 
@@ -418,6 +422,8 @@ OptionsTab::SelectedBox::SelectedBox(Point position, PlayerSettings & settings,
 	subtitle = std::make_shared<CLabel>(23, 39, FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, getName());
 
 	pos = image->pos;
+
+	setPanningStep(pos.w);
 }
 
 void OptionsTab::SelectedBox::update()
@@ -440,6 +446,29 @@ void OptionsTab::SelectedBox::clickRight(tribool down, bool previousState)
 	}
 }
 
+void OptionsTab::SelectedBox::scrollBy(int distance)
+{
+	// FIXME: currently options tab is completely recreacted from scratch whenever we receive any information from server
+	// because of that, panning event gets interrupted (due to destruction of element)
+	// so, currently, gesture will always move selection only by 1, and then wait for recreation from server info
+	distance = std::clamp(distance, -1, 1);
+
+	switch(CPlayerSettingsHelper::type)
+	{
+		case TOWN:
+			CSH->setPlayerOption(LobbyChangePlayerOption::TOWN, distance, settings.color);
+			break;
+		case HERO:
+			CSH->setPlayerOption(LobbyChangePlayerOption::HERO, distance, settings.color);
+			break;
+		case BONUS:
+			CSH->setPlayerOption(LobbyChangePlayerOption::BONUS, distance, settings.color);
+			break;
+	}
+
+	setScrollingEnabled(false);
+}
+
 OptionsTab::PlayerOptionsEntry::PlayerOptionsEntry(const PlayerSettings & S, const OptionsTab & parent)
 	: pi(std::make_unique<PlayerInfo>(SEL->getPlayerInfo(S.color.getNum())))
 	, s(std::make_unique<PlayerSettings>(S))

+ 4 - 1
client/lobby/OptionsTab.h

@@ -16,6 +16,8 @@ struct PlayerSettings;
 struct PlayerInfo;
 VCMI_LIB_NAMESPACE_END
 
+#include "../widgets/Scrollable.h"
+
 class CSlider;
 class CLabel;
 class CMultiLineLabel;
@@ -93,13 +95,14 @@ public:
 	};
 
 	/// Image with current town/hero/bonus
-	struct SelectedBox : public CIntObject, public CPlayerSettingsHelper
+	struct SelectedBox : public Scrollable, public CPlayerSettingsHelper
 	{
 		std::shared_ptr<CAnimImage> image;
 		std::shared_ptr<CLabel> subtitle;
 
 		SelectedBox(Point position, PlayerSettings & settings, SelType type);
 		void clickRight(tribool down, bool previousState) override;
+		void scrollBy(int distance) override;
 
 		void update();
 	};

+ 1 - 0
client/lobby/RandomMapTab.cpp

@@ -21,6 +21,7 @@
 #include "../widgets/Buttons.h"
 #include "../widgets/MiscWidgets.h"
 #include "../widgets/ObjectLists.h"
+#include "../widgets/Slider.h"
 #include "../widgets/TextControls.h"
 #include "../windows/GUIClasses.h"
 #include "../windows/InfoWindows.h"

+ 9 - 6
client/lobby/SelectionTab.cpp

@@ -22,6 +22,7 @@
 #include "../widgets/Buttons.h"
 #include "../widgets/MiscWidgets.h"
 #include "../widgets/ObjectLists.h"
+#include "../widgets/Slider.h"
 #include "../widgets/TextControls.h"
 #include "../windows/GUIClasses.h"
 #include "../windows/InfoWindows.h"
@@ -132,7 +133,7 @@ static ESortBy getSortBySelectionScreen(ESelectionScreen Type)
 }
 
 SelectionTab::SelectionTab(ESelectionScreen Type)
-	: CIntObject(LCLICK | WHEEL | KEYBOARD | DOUBLECLICK), callOnSelect(nullptr), tabType(Type), selectionPos(0), sortModeAscending(true), inputNameRect{32, 539, 350, 20}
+	: CIntObject(LCLICK | KEYBOARD | DOUBLECLICK), callOnSelect(nullptr), tabType(Type), selectionPos(0), sortModeAscending(true), inputNameRect{32, 539, 350, 20}
 {
 	OBJ_CONSTRUCTION;
 
@@ -205,6 +206,7 @@ SelectionTab::SelectionTab(ESelectionScreen Type)
 
 	labelTabTitle = std::make_shared<CLabel>(205, 28, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, tabTitle);
 	slider = std::make_shared<CSlider>(Point(372, 86), tabType != ESelectionScreen::saveGame ? 480 : 430, std::bind(&SelectionTab::sliderMove, this, _1), positionsToShow, (int)curItems.size(), 0, false, CSlider::BLUE);
+	slider->setPanningStep(24);
 	filter(0);
 }
 
@@ -313,10 +315,11 @@ void SelectionTab::keyPressed(EShortcut key)
 	select((int)selectionPos - slider->getValue() + moveBy);
 }
 
-void SelectionTab::onDoubleClick()
+void SelectionTab::clickDouble()
 {
 	if(getLine() != -1) //double clicked scenarios list
 	{
+		(static_cast<CLobbyScreen *>(parent))->buttonStart->clickLeft(true, false);
 		(static_cast<CLobbyScreen *>(parent))->buttonStart->clickLeft(false, true);
 	}
 }
@@ -348,7 +351,7 @@ void SelectionTab::filter(int size, bool selectFirst)
 		sort();
 		if(selectFirst)
 		{
-			slider->moveTo(0);
+			slider->scrollTo(0);
 			callOnSelect(curItems[0]);
 			selectAbs(0);
 		}
@@ -403,9 +406,9 @@ void SelectionTab::select(int position)
 	selectionPos = py;
 
 	if(position < 0)
-		slider->moveBy(position);
+		slider->scrollBy(position);
 	else if(position >= listItems.size())
-		slider->moveBy(position - (int)listItems.size() + 1);
+		slider->scrollBy(position - (int)listItems.size() + 1);
 
 	rememberCurrentSelection();
 
@@ -479,7 +482,7 @@ void SelectionTab::selectFileName(std::string fname)
 	{
 		if(curItems[i]->fileURI == fname)
 		{
-			slider->moveTo(i);
+			slider->scrollTo(i);
 			selectAbs(i);
 			return;
 		}

+ 1 - 1
client/lobby/SelectionTab.h

@@ -68,7 +68,7 @@ public:
 	void clickLeft(tribool down, bool previousState) override;
 	void keyPressed(EShortcut key) override;
 
-	void onDoubleClick() override;
+	void clickDouble() override;
 
 	void filter(int size, bool selectFirst = false); //0 - all
 	void sortBy(int criteria);

+ 1 - 1
client/mapView/MapView.cpp

@@ -117,7 +117,7 @@ void MapView::onMapScrolled(const Point & distance)
 void MapView::onMapSwiped(const Point & viewPosition)
 {
 	isSwiping = true;
-	controller->setViewCenter(viewPosition, model->getLevel());
+	controller->setViewCenter(model->getMapViewCenter() + viewPosition, model->getLevel());
 }
 
 void MapView::onMapSwipeEnded()

+ 22 - 71
client/mapView/MapViewActions.cpp

@@ -20,26 +20,20 @@
 #include "../gui/CursorHandler.h"
 #include "../gui/MouseButton.h"
 
+#include "../CPlayerInterface.h"
+#include "../adventureMap/CInGameConsole.h"
+
 #include "../../lib/CConfigHandler.h"
 
 MapViewActions::MapViewActions(MapView & owner, const std::shared_ptr<MapViewModel> & model)
 	: model(model)
 	, owner(owner)
-	, isSwiping(false)
+	, pinchZoomFactor(1.0)
 {
 	pos.w = model->getPixelsVisibleDimensions().x;
 	pos.h = model->getPixelsVisibleDimensions().y;
 
-	addUsedEvents(LCLICK | RCLICK | MCLICK | HOVER | MOVE | WHEEL);
-}
-
-bool MapViewActions::swipeEnabled() const
-{
-#if defined(VCMI_ANDROID) || defined(VCMI_IOS)
-	return settings["general"]["swipe"].Bool();
-#else
-	return settings["general"]["swipeDesktop"].Bool();
-#endif
+	addUsedEvents(LCLICK | RCLICK | GESTURE_PANNING | HOVER | MOVE | WHEEL);
 }
 
 void MapViewActions::setContext(const std::shared_ptr<IMapRendererContext> & context)
@@ -52,18 +46,8 @@ void MapViewActions::clickLeft(tribool down, bool previousState)
 	if(indeterminate(down))
 		return;
 
-	if(swipeEnabled())
-	{
-		if(handleSwipeStateChange(static_cast<bool>(down)))
-		{
-			return; // if swipe is enabled, we don't process "down" events and wait for "up" (to make sure this wasn't a swiping gesture)
-		}
-	}
-	else
-	{
-		if(down == false)
-			return;
-	}
+	if(down == false)
+		return;
 
 	int3 tile = model->getTileAtPoint(GH.getCursorPosition() - pos.topLeft());
 
@@ -73,76 +57,43 @@ void MapViewActions::clickLeft(tribool down, bool previousState)
 
 void MapViewActions::clickRight(tribool down, bool previousState)
 {
-	if(isSwiping)
-		return;
-
 	int3 tile = model->getTileAtPoint(GH.getCursorPosition() - pos.topLeft());
 
 	if(down && context->isInMap(tile))
 		adventureInt->onTileRightClicked(tile);
 }
 
-void MapViewActions::clickMiddle(tribool down, bool previousState)
-{
-	handleSwipeStateChange(static_cast<bool>(down));
-}
-
 void MapViewActions::mouseMoved(const Point & cursorPosition)
 {
 	handleHover(cursorPosition);
-	handleSwipeMove(cursorPosition);
 }
 
-void MapViewActions::wheelScrolled(bool down, bool in)
+void MapViewActions::wheelScrolled(int distance)
 {
-	if (!in)
-		return;
-	adventureInt->hotkeyZoom(down ? -1 : +1);
+	adventureInt->hotkeyZoom(distance * 4);
 }
 
-void MapViewActions::handleSwipeMove(const Point & cursorPosition)
+void MapViewActions::gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance)
 {
-	// unless swipe is enabled, swipe move only works with middle mouse button
-	if(!swipeEnabled() && !GH.isMouseButtonPressed(MouseButton::MIDDLE))
-		return;
+	owner.onMapSwiped(lastUpdateDistance);
+}
 
-	// on mobile platforms with enabled swipe we use left button
-	if(swipeEnabled() && !GH.isMouseButtonPressed(MouseButton::LEFT))
-		return;
+void MapViewActions::gesturePinch(const Point & centerPosition, double lastUpdateFactor)
+{
+	double newZoom = pinchZoomFactor * lastUpdateFactor;
 
-	if(!isSwiping)
-	{
-		static constexpr int touchSwipeSlop = 16;
-		Point distance = (cursorPosition - swipeInitialRealPos);
+	int newZoomSteps = std::round(std::log(newZoom) / std::log(1.01));
+	int oldZoomSteps = std::round(std::log(pinchZoomFactor) / std::log(1.01));
 
-		// try to distinguish if this touch was meant to be a swipe or just fat-fingering press
-		if(std::abs(distance.x) + std::abs(distance.y) > touchSwipeSlop)
-			isSwiping = true;
-	}
+	if (newZoomSteps != oldZoomSteps)
+		adventureInt->hotkeyZoom(newZoomSteps - oldZoomSteps);
 
-	if(isSwiping)
-	{
-		Point swipeTargetPosition = swipeInitialViewPos + swipeInitialRealPos - cursorPosition;
-		owner.onMapSwiped(swipeTargetPosition);
-	}
+	pinchZoomFactor = newZoom;
 }
 
-bool MapViewActions::handleSwipeStateChange(bool btnPressed)
+void MapViewActions::panning(bool on, const Point & initialPosition, const Point & finalPosition)
 {
-	if(btnPressed)
-	{
-		swipeInitialRealPos = GH.getCursorPosition();
-		swipeInitialViewPos = model->getMapViewCenter();
-		return true;
-	}
-
-	if(isSwiping) // only accept this touch if it wasn't a swipe
-	{
-		owner.onMapSwipeEnded();
-		isSwiping = false;
-		return true;
-	}
-	return false;
+	pinchZoomFactor = 1.0;
 }
 
 void MapViewActions::handleHover(const Point & cursorPosition)

+ 6 - 10
client/mapView/MapViewActions.h

@@ -18,19 +18,13 @@ class MapView;
 
 class MapViewActions : public CIntObject
 {
-	bool isSwiping;
-
-	Point swipeInitialViewPos;
-	Point swipeInitialRealPos;
-
 	MapView & owner;
 	std::shared_ptr<MapViewModel> model;
 	std::shared_ptr<IMapRendererContext> context;
 
+	double pinchZoomFactor;
+
 	void handleHover(const Point & cursorPosition);
-	void handleSwipeMove(const Point & cursorPosition);
-	bool handleSwipeStateChange(bool btnPressed);
-	bool swipeEnabled() const;
 
 public:
 	MapViewActions(MapView & owner, const std::shared_ptr<MapViewModel> & model);
@@ -39,8 +33,10 @@ public:
 
 	void clickLeft(tribool down, bool previousState) override;
 	void clickRight(tribool down, bool previousState) override;
-	void clickMiddle(tribool down, bool previousState) override;
+	void gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) override;
+	void gesturePinch(const Point & centerPosition, double lastUpdateFactor) override;
 	void hover(bool on) override;
+	void panning(bool on, const Point & initialPosition, const Point & finalPosition) override;
 	void mouseMoved(const Point & cursorPosition) override;
-	void wheelScrolled(bool down, bool in) override;
+	void wheelScrolled(int distance) override;
 };

+ 2 - 2
client/mapView/MapViewController.cpp

@@ -91,9 +91,9 @@ void MapViewController::modifyTileSize(int stepsChange)
 	// so, zooming in for 5 steps will put game at 1.1^5 = 1.61 scale
 	// try to determine current zooming level and change it by requested number of steps
 	double currentZoomFactor = model->getSingleTileSize().x / 32.0;
-	double currentZoomSteps = std::round(std::log(currentZoomFactor) / std::log(1.1));
+	double currentZoomSteps = std::round(std::log(currentZoomFactor) / std::log(1.01));
 	double newZoomSteps = stepsChange != 0 ? currentZoomSteps + stepsChange : stepsChange;
-	double newZoomFactor = std::pow(1.1, newZoomSteps);
+	double newZoomFactor = std::pow(1.01, newZoomSteps);
 
 	Point currentZoom = model->getSingleTileSize();
 	Point desiredZoom = Point(32,32) * newZoomFactor;

+ 0 - 383
client/widgets/Buttons.cpp

@@ -472,386 +472,3 @@ int CToggleGroup::getSelected() const
 {
 	return selectedID;
 }
-
-CVolumeSlider::CVolumeSlider(const Point & position, const std::string & defName, const int value, ETooltipMode mode)
-	: CIntObject(LCLICK | RCLICK | WHEEL),
-	value(value),
-	mode(mode)
-{
-	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
-	animImage = std::make_shared<CAnimImage>(std::make_shared<CAnimation>(defName), 0, 0, position.x, position.y),
-	pos.x += position.x;
-	pos.y += position.y;
-	pos.w = (animImage->pos.w + 1) * (int)animImage->size();
-	pos.h = animImage->pos.h;
-	type |= REDRAW_PARENT;
-	setVolume(value);
-}
-
-void CVolumeSlider::setVolume(int value_)
-{
-	value = value_;
-	moveTo((int)(value * static_cast<double>(animImage->size()) / 100.0));
-}
-
-void CVolumeSlider::moveTo(int id)
-{
-	vstd::abetween<int>(id, 0, animImage->size() - 1);
-	animImage->setFrame(id);
-	animImage->moveTo(Point(pos.x + (animImage->pos.w + 1) * id, pos.y));
-	if (isActive())
-		redraw();
-}
-
-void CVolumeSlider::addCallback(std::function<void(int)> callback)
-{
-	onChange += callback;
-}
-
-void CVolumeSlider::clickLeft(tribool down, bool previousState)
-{
-	if (down)
-	{
-		double px = GH.getCursorPosition().x - pos.x;
-		double rx = px / static_cast<double>(pos.w);
-		// setVolume is out of 100
-		setVolume((int)(rx * 100));
-		// Volume config is out of 100, set to increments of 5ish roughly based on the half point of the indicator
-		// 0.0 -> 0, 0.05 -> 5, 0.09 -> 5,...,
-		// 0.1 -> 10, ..., 0.19 -> 15, 0.2 -> 20, ...,
-		// 0.28 -> 25, 0.29 -> 30, 0.3 -> 30, ...,
-		// 0.85 -> 85, 0.86 -> 90, ..., 0.87 -> 90,...,
-		// 0.95 -> 95, 0.96 -> 100, 0.99 -> 100
-		int volume = 5 * int(rx * (2 * animImage->size() + 1));
-		onChange(volume);
-	}
-}
-
-void CVolumeSlider::clickRight(tribool down, bool previousState)
-{
-	if (down)
-	{
-		double px = GH.getCursorPosition().x - pos.x;
-		int index = static_cast<int>(px / static_cast<double>(pos.w) * animImage->size());
-
-		size_t helpIndex = index + (mode == MUSIC ? 326 : 336);
-		std::string helpBox = CGI->generaltexth->translate("core.help", helpIndex, "help" );
-
-		if(!helpBox.empty())
-			CRClickPopup::createAndPush(helpBox);
-
-		GH.statusbar()->write(helpBox);
-	}
-}
-
-void CVolumeSlider::wheelScrolled(bool down, bool in)
-{
-	if (in)
-	{
-		int volume = value + 3 * (down ? 1 : -1);
-		vstd::abetween(volume, 0, 100);
-		setVolume(volume);
-		onChange(volume);
-	}
-}
-
-void CSlider::sliderClicked()
-{
-	addUsedEvents(MOVE);
-}
-
-void CSlider::mouseMoved (const Point & cursorPosition)
-{
-	double v = 0;
-	if(horizontal)
-	{
-		if(	std::abs(cursorPosition.y-(pos.y+pos.h/2)) > pos.h/2+40  ||  std::abs(cursorPosition.x-(pos.x+pos.w/2)) > pos.w/2  )
-			return;
-		v = cursorPosition.x - pos.x - 24;
-		v *= positions;
-		v /= (pos.w - 48);
-	}
-	else
-	{
-		if(std::abs(cursorPosition.x-(pos.x+pos.w/2)) > pos.w/2+40  ||  std::abs(cursorPosition.y-(pos.y+pos.h/2)) > pos.h/2  )
-			return;
-		v = cursorPosition.y - pos.y - 24;
-		v *= positions;
-		v /= (pos.h - 48);
-	}
-	v += 0.5;
-	if(v!=value)
-	{
-		moveTo(static_cast<int>(v));
-	}
-}
-
-void CSlider::setScrollStep(int to)
-{
-	scrollStep = to;
-}
-
-void CSlider::setScrollBounds(const Rect & bounds )
-{
-	scrollBounds = bounds;
-}
-
-void CSlider::clearScrollBounds()
-{
-	scrollBounds = std::nullopt;
-}
-
-int CSlider::getAmount() const
-{
-	return amount;
-}
-
-int CSlider::getValue() const
-{
-	return value;
-}
-
-int CSlider::getCapacity() const
-{
-	return capacity;
-}
-
-void CSlider::moveLeft()
-{
-	moveTo(value-1);
-}
-
-void CSlider::moveRight()
-{
-	moveTo(value+1);
-}
-
-void CSlider::moveBy(int amount)
-{
-	moveTo(value + amount);
-}
-
-void CSlider::updateSliderPos()
-{
-	if(horizontal)
-	{
-		if(positions)
-		{
-			double part = static_cast<double>(value) / positions;
-			part*=(pos.w-48);
-			int newPos = static_cast<int>(part + pos.x + 16 - slider->pos.x);
-			slider->moveBy(Point(newPos, 0));
-		}
-		else
-			slider->moveTo(Point(pos.x+16, pos.y));
-	}
-	else
-	{
-		if(positions)
-		{
-			double part = static_cast<double>(value) / positions;
-			part*=(pos.h-48);
-			int newPos = static_cast<int>(part + pos.y + 16 - slider->pos.y);
-			slider->moveBy(Point(0, newPos));
-		}
-		else
-			slider->moveTo(Point(pos.x, pos.y+16));
-	}
-}
-
-void CSlider::moveTo(int to)
-{
-	vstd::amax(to, 0);
-	vstd::amin(to, positions);
-
-	//same, old position?
-	if(value == to)
-		return;
-	value = to;
-
-	updateSliderPos();
-
-	moved(to);
-}
-
-void CSlider::clickLeft(tribool down, bool previousState)
-{
-	if(down && !slider->isBlocked())
-	{
-		double pw = 0;
-		double rw = 0;
-		if(horizontal)
-		{
-			pw = GH.getCursorPosition().x-pos.x-25;
-			rw = pw / static_cast<double>(pos.w - 48);
-		}
-		else
-		{
-			pw = GH.getCursorPosition().y-pos.y-24;
-			rw = pw / (pos.h-48);
-		}
-		if(pw < -8  ||  pw > (horizontal ? pos.w : pos.h) - 40)
-			return;
-		// 		if (rw>1) return;
-		// 		if (rw<0) return;
-		slider->clickLeft(true, slider->isMouseButtonPressed(MouseButton::LEFT));
-		moveTo((int)(rw * positions  +  0.5));
-		return;
-	}
-	removeUsedEvents(MOVE);
-}
-
-CSlider::CSlider(Point position, int totalw, std::function<void(int)> Moved, int Capacity, int Amount, int Value, bool Horizontal, CSlider::EStyle style)
-	: CIntObject(LCLICK | RCLICK | WHEEL),
-	capacity(Capacity),
-	horizontal(Horizontal),
-	amount(Amount),
-	value(Value),
-	scrollStep(1),
-	moved(Moved)
-{
-	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
-	setAmount(amount);
-	vstd::amax(value, 0);
-	vstd::amin(value, positions);
-
-	setMoveEventStrongInterest(true);
-
-	pos.x += position.x;
-	pos.y += position.y;
-
-	if(style == BROWN)
-	{
-		std::string name = horizontal ? "IGPCRDIV.DEF" : "OVBUTN2.DEF";
-		//NOTE: this images do not have "blocked" frames. They should be implemented somehow (e.g. palette transform or something...)
-
-		left = std::make_shared<CButton>(Point(), name, CButton::tooltip());
-		right = std::make_shared<CButton>(Point(), name, CButton::tooltip());
-		slider = std::make_shared<CButton>(Point(), name, CButton::tooltip());
-
-		left->setImageOrder(0, 1, 1, 1);
-		right->setImageOrder(2, 3, 3, 3);
-		slider->setImageOrder(4, 4, 4, 4);
-	}
-	else
-	{
-		left = std::make_shared<CButton>(Point(), horizontal ? "SCNRBLF.DEF" : "SCNRBUP.DEF", CButton::tooltip());
-		right = std::make_shared<CButton>(Point(), horizontal ? "SCNRBRT.DEF" : "SCNRBDN.DEF", CButton::tooltip());
-		slider = std::make_shared<CButton>(Point(), "SCNRBSL.DEF", CButton::tooltip());
-	}
-	slider->actOnDown = true;
-	slider->soundDisabled = true;
-	left->soundDisabled = true;
-	right->soundDisabled = true;
-
-	if (horizontal)
-		right->moveBy(Point(totalw - right->pos.w, 0));
-	else
-		right->moveBy(Point(0, totalw - right->pos.h));
-
-	left->addCallback(std::bind(&CSlider::moveLeft,this));
-	right->addCallback(std::bind(&CSlider::moveRight,this));
-	slider->addCallback(std::bind(&CSlider::sliderClicked,this));
-
-	if(horizontal)
-	{
-		pos.h = slider->pos.h;
-		pos.w = totalw;
-	}
-	else
-	{
-		pos.w = slider->pos.w;
-		pos.h = totalw;
-	}
-
-	updateSliderPos();
-}
-
-CSlider::~CSlider() = default;
-
-void CSlider::block( bool on )
-{
-	left->block(on);
-	right->block(on);
-	slider->block(on);
-}
-
-void CSlider::setAmount( int to )
-{
-	amount = to;
-	positions = to - capacity;
-	vstd::amax(positions, 0);
-}
-
-void CSlider::showAll(Canvas & to)
-{
-	to.drawColor(pos, Colors::BLACK);
-	CIntObject::showAll(to);
-}
-
-void CSlider::wheelScrolled(bool down, bool in)
-{
-	if (scrollBounds)
-	{
-		Rect testTarget = *scrollBounds + pos.topLeft();
-
-		if (!testTarget.isInside(GH.getCursorPosition()))
-			return;
-	}
-
-	// vertical slider -> scrolling up move slider upwards
-	// horizontal slider -> scrolling up moves slider towards right
-	bool positive = (down != horizontal);
-
-	moveTo(value + 3 * (positive ? +scrollStep : -scrollStep));
-}
-
-void CSlider::keyPressed(EShortcut key)
-{
-	int moveDest = value;
-	switch(key)
-	{
-	case EShortcut::MOVE_UP:
-		if (!horizontal)
-			moveDest = value - scrollStep;
-		break;
-	case EShortcut::MOVE_LEFT:
-		if (horizontal)
-			moveDest = value - scrollStep;
-		break;
-	case EShortcut::MOVE_DOWN:
-		if (!horizontal)
-			moveDest = value + scrollStep;
-		break;
-	case EShortcut::MOVE_RIGHT:
-		if (horizontal)
-			moveDest = value + scrollStep;
-		break;
-	case EShortcut::MOVE_PAGE_UP:
-		moveDest = value - capacity + scrollStep;
-		break;
-	case EShortcut::MOVE_PAGE_DOWN:
-		moveDest = value + capacity - scrollStep;
-		break;
-	case EShortcut::MOVE_FIRST:
-		moveDest = 0;
-		break;
-	case EShortcut::MOVE_LAST:
-		moveDest = amount - capacity;
-		break;
-	default:
-		return;
-	}
-
-	moveTo(moveDest);
-}
-
-void CSlider::moveToMin()
-{
-	moveTo(0);
-}
-
-void CSlider::moveToMax()
-{
-	moveTo(amount);
-}

+ 0 - 104
client/widgets/Buttons.h

@@ -181,107 +181,3 @@ public:
 	void setSelectedOnly(int id);
 	int getSelected() const;
 };
-
-/// A typical slider for volume with an animated indicator
-class CVolumeSlider : public CIntObject
-{
-public:
-	enum ETooltipMode
-	{
-		MUSIC,
-		SOUND
-	};
-
-private:
-	int value;
-	CFunctionList<void(int)> onChange;
-	std::shared_ptr<CAnimImage> animImage;
-	ETooltipMode mode;
-	void setVolume(const int v);
-public:
-	/// @param position coordinates of slider
-	/// @param defName name of def animation for slider
-	/// @param value initial value for volume
-	/// @param mode that determines tooltip texts
-	CVolumeSlider(const Point & position, const std::string & defName, const int value, ETooltipMode mode);
-
-	void moveTo(int id);
-	void addCallback(std::function<void(int)> callback);
-
-
-	void clickLeft(tribool down, bool previousState) override;
-	void clickRight(tribool down, bool previousState) override;
-	void wheelScrolled(bool down, bool in) override;
-};
-
-/// A typical slider which can be orientated horizontally/vertically.
-class CSlider : public CIntObject
-{
-	//if vertical then left=up
-	std::shared_ptr<CButton> left;
-	std::shared_ptr<CButton> right;
-	std::shared_ptr<CButton> slider;
-
-	std::optional<Rect> scrollBounds;
-
-	int capacity;//how many elements can be active at same time (e.g. hero list = 5)
-	int positions; //number of highest position (0 if there is only one)
-	bool horizontal;
-	int amount; //total amount of elements (e.g. hero list = 0-8)
-	int value; //first active element
-	int scrollStep; // how many elements will be scrolled via one click, default = 1
-	CFunctionList<void(int)> moved;
-
-	void updateSliderPos();
-	void sliderClicked();
-
-public:
-	enum EStyle
-	{
-		BROWN,
-		BLUE
-	};
-
-	void block(bool on);
-
-	/// Controls how many items wil be scrolled via one click
-	void setScrollStep(int to);
-
-	/// If set, mouse scroll will only scroll slider when inside of this area
-	void setScrollBounds(const Rect & bounds );
-	void clearScrollBounds();
-
-	/// Value modifiers
-	void moveLeft();
-	void moveRight();
-	void moveTo(int value);
-	void moveBy(int amount);
-	void moveToMin();
-	void moveToMax();
-
-	/// Amount modifier
-	void setAmount(int to);
-
-	/// Accessors
-	int getAmount() const;
-	int getValue() const;
-	int getCapacity() const;
-
-	void addCallback(std::function<void(int)> callback);
-
-	void keyPressed(EShortcut key) override;
-	void wheelScrolled(bool down, bool in) override;
-	void clickLeft(tribool down, bool previousState) override;
-	void mouseMoved (const Point & cursorPosition) override;
-	void showAll(Canvas & to) override;
-
-	 /// @param position coordinates of slider
-	 /// @param length length of slider ribbon, including left/right buttons
-	 /// @param Moved function that will be called whenever slider moves
-	 /// @param Capacity maximal number of visible at once elements
-	 /// @param Amount total amount of elements, including not visible
-	 /// @param Value starting position
-	CSlider(Point position, int length, std::function<void(int)> Moved, int Capacity, int Amount,
-		int Value=0, bool Horizontal=true, EStyle style = BROWN);
-	~CSlider();
-};

+ 4 - 2
client/widgets/ObjectLists.cpp

@@ -11,7 +11,7 @@
 #include "ObjectLists.h"
 
 #include "../gui/CGuiHandler.h"
-#include "Buttons.h"
+#include "Slider.h"
 
 CObjectList::CObjectList(CreateFunc create)
 	: createObject(create)
@@ -94,6 +94,8 @@ CListBox::CListBox(CreateFunc create, Point Pos, Point ItemOffset, size_t Visibl
 		OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
 		slider = std::make_shared<CSlider>(SliderPos.topLeft(), SliderPos.w, std::bind(&CListBox::moveToPos, this, _1),
 			(int)VisibleSize, (int)TotalSize, (int)InitialPos, Slider & 2, Slider & 4 ? CSlider::BLUE : CSlider::BROWN);
+
+		slider->setPanningStep(itemOffset.x + itemOffset.y);
 	}
 	reset();
 }
@@ -111,7 +113,7 @@ void CListBox::updatePositions()
 	{
 		redraw();
 		if (slider)
-			slider->moveTo((int)first);
+			slider->scrollTo((int)first);
 	}
 }
 

+ 94 - 0
client/widgets/Scrollable.cpp

@@ -0,0 +1,94 @@
+/*
+ * Scrollable.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 "Scrollable.h"
+
+Scrollable::Scrollable(int used, Point position, Orientation orientation)
+	: CIntObject(used | WHEEL | GESTURE_PANNING, position)
+	, scrollStep(1)
+	, panningDistanceSingle(32)
+	, panningDistanceAccumulated(0)
+	, orientation(orientation)
+{
+}
+
+void Scrollable::panning(bool on, const Point & initialPosition, const Point & finalPosition)
+{
+	panningDistanceAccumulated = 0;
+}
+
+void Scrollable::wheelScrolled(int distance)
+{
+	if (orientation == Orientation::HORIZONTAL)
+		scrollBy(distance * scrollStep);
+	else
+		scrollBy(-distance * scrollStep);
+}
+
+void Scrollable::gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance)
+{
+	if (orientation == Orientation::HORIZONTAL)
+		panningDistanceAccumulated += -lastUpdateDistance.x;
+	else
+		panningDistanceAccumulated += lastUpdateDistance.y;
+
+	if (-panningDistanceAccumulated > panningDistanceSingle )
+	{
+		int scrollAmount = (-panningDistanceAccumulated) / panningDistanceSingle;
+		scrollBy(-scrollAmount);
+		panningDistanceAccumulated += scrollAmount * panningDistanceSingle;
+	}
+
+	if (panningDistanceAccumulated > panningDistanceSingle )
+	{
+		int scrollAmount = panningDistanceAccumulated / panningDistanceSingle;
+		scrollBy(scrollAmount);
+		panningDistanceAccumulated += -scrollAmount * panningDistanceSingle;
+	}
+}
+
+int Scrollable::getScrollStep() const
+{
+	return scrollStep;
+}
+
+Orientation Scrollable::getOrientation() const
+{
+	return orientation;
+}
+
+void Scrollable::scrollNext()
+{
+	scrollBy(+1);
+}
+
+void Scrollable::scrollPrev()
+{
+	scrollBy(-1);
+}
+
+void Scrollable::setScrollStep(int to)
+{
+	scrollStep = to;
+}
+
+void Scrollable::setPanningStep(int to)
+{
+	panningDistanceSingle = to;
+}
+
+void Scrollable::setScrollingEnabled(bool on)
+{
+	if (on)
+		addUsedEvents(WHEEL | GESTURE_PANNING);
+	else
+		removeUsedEvents(WHEEL | GESTURE_PANNING);
+}

+ 61 - 0
client/widgets/Scrollable.h

@@ -0,0 +1,61 @@
+/*
+ * Scrollable.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 "../gui/CIntObject.h"
+
+enum class Orientation
+{
+	HORIZONTAL,
+	VERTICAL
+};
+
+/// Simple class that provides scrolling functionality via either mouse wheel or touchscreen gesture
+class Scrollable : public CIntObject
+{
+	/// how many elements will be scrolled via one wheel action, default = 1
+	int scrollStep;
+	/// How far player must move finger/mouse to move slider by 1 via gesture
+	int panningDistanceSingle;
+	/// How far have player moved finger/mouse via gesture so far.
+	int panningDistanceAccumulated;
+
+	Orientation orientation;
+
+protected:
+	Scrollable(int used, Point position, Orientation orientation);
+
+	void panning(bool on, const Point & initialPosition, const Point & finalPosition) override;
+	void wheelScrolled(int distance) override;
+	void gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) override;
+
+	int getScrollStep() const;
+	Orientation getOrientation() const;
+
+public:
+	/// Scrolls view by specified number of items
+	virtual void scrollBy(int distance) = 0;
+
+	/// Scrolls view by 1 item, identical to scrollBy(+1)
+	virtual void scrollNext();
+
+	/// Scrolls view by 1 item, identical to scrollBy(-1)
+	virtual void scrollPrev();
+
+	/// Controls how many items wil be scrolled via one click
+	void setScrollStep(int to);
+
+	/// Controls size of panning step needed to move list by 1 item
+	void setPanningStep(int to);
+
+	/// Enables or disabled scrolling
+	void setScrollingEnabled(bool on);
+};

+ 296 - 0
client/widgets/Slider.cpp

@@ -0,0 +1,296 @@
+/*
+ * Slider.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 "Slider.h"
+
+#include "Buttons.h"
+
+#include "../gui/MouseButton.h"
+#include "../gui/Shortcut.h"
+#include "../gui/CGuiHandler.h"
+#include "../render/Canvas.h"
+
+void CSlider::sliderClicked()
+{
+	addUsedEvents(MOVE);
+}
+
+void CSlider::mouseMoved (const Point & cursorPosition)
+{
+	double v = 0;
+	if(getOrientation() == Orientation::HORIZONTAL)
+	{
+		if(	std::abs(cursorPosition.y-(pos.y+pos.h/2)) > pos.h/2+40  ||  std::abs(cursorPosition.x-(pos.x+pos.w/2)) > pos.w/2  )
+			return;
+		v = cursorPosition.x - pos.x - 24;
+		v *= positions;
+		v /= (pos.w - 48);
+	}
+	else
+	{
+		if(std::abs(cursorPosition.x-(pos.x+pos.w/2)) > pos.w/2+40  ||  std::abs(cursorPosition.y-(pos.y+pos.h/2)) > pos.h/2  )
+			return;
+		v = cursorPosition.y - pos.y - 24;
+		v *= positions;
+		v /= (pos.h - 48);
+	}
+	v += 0.5;
+	if(v!=value)
+	{
+		scrollTo(static_cast<int>(v));
+	}
+}
+
+void CSlider::setScrollBounds(const Rect & bounds )
+{
+	scrollBounds = bounds;
+}
+
+void CSlider::clearScrollBounds()
+{
+	scrollBounds = std::nullopt;
+}
+
+int CSlider::getAmount() const
+{
+	return amount;
+}
+
+int CSlider::getValue() const
+{
+	return value;
+}
+
+int CSlider::getCapacity() const
+{
+	return capacity;
+}
+
+void CSlider::scrollBy(int amount)
+{
+	scrollTo(value + amount);
+}
+
+void CSlider::updateSliderPos()
+{
+	if(getOrientation() == Orientation::HORIZONTAL)
+	{
+		if(positions)
+		{
+			double part = static_cast<double>(value) / positions;
+			part*=(pos.w-48);
+			int newPos = static_cast<int>(part + pos.x + 16 - slider->pos.x);
+			slider->moveBy(Point(newPos, 0));
+		}
+		else
+			slider->moveTo(Point(pos.x+16, pos.y));
+	}
+	else
+	{
+		if(positions)
+		{
+			double part = static_cast<double>(value) / positions;
+			part*=(pos.h-48);
+			int newPos = static_cast<int>(part + pos.y + 16 - slider->pos.y);
+			slider->moveBy(Point(0, newPos));
+		}
+		else
+			slider->moveTo(Point(pos.x, pos.y+16));
+	}
+}
+
+void CSlider::scrollTo(int to)
+{
+	vstd::amax(to, 0);
+	vstd::amin(to, positions);
+
+	//same, old position?
+	if(value == to)
+		return;
+	value = to;
+
+	updateSliderPos();
+
+	moved(to);
+}
+
+void CSlider::clickLeft(tribool down, bool previousState)
+{
+	if(down && !slider->isBlocked())
+	{
+		double pw = 0;
+		double rw = 0;
+		if(getOrientation() == Orientation::HORIZONTAL)
+		{
+			pw = GH.getCursorPosition().x-pos.x-25;
+			rw = pw / static_cast<double>(pos.w - 48);
+		}
+		else
+		{
+			pw = GH.getCursorPosition().y-pos.y-24;
+			rw = pw / (pos.h-48);
+		}
+		if(pw < -8  ||  pw > (getOrientation() == Orientation::HORIZONTAL ? pos.w : pos.h) - 40)
+			return;
+		// 		if (rw>1) return;
+		// 		if (rw<0) return;
+		slider->clickLeft(true, slider->isMouseButtonPressed(MouseButton::LEFT));
+		scrollTo((int)(rw * positions  +  0.5));
+		return;
+	}
+	removeUsedEvents(MOVE);
+}
+
+bool CSlider::receiveEvent(const Point &position, int eventType) const
+{
+	if (eventType != WHEEL && eventType != GESTURE_PANNING)
+	{
+		return CIntObject::receiveEvent(position, eventType);
+	}
+
+	if (!scrollBounds)
+		return true;
+
+	Rect testTarget = *scrollBounds + pos.topLeft();
+
+	return testTarget.isInside(position);
+}
+
+CSlider::CSlider(Point position, int totalw, std::function<void(int)> Moved, int Capacity, int Amount, int Value, bool Horizontal, CSlider::EStyle style)
+	: Scrollable(LCLICK, position, Horizontal ? Orientation::HORIZONTAL : Orientation::VERTICAL ),
+	capacity(Capacity),
+	amount(Amount),
+	value(Value),
+	moved(Moved)
+{
+	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
+	setAmount(amount);
+	vstd::amax(value, 0);
+	vstd::amin(value, positions);
+
+	if(style == BROWN)
+	{
+		std::string name = getOrientation() == Orientation::HORIZONTAL ? "IGPCRDIV.DEF" : "OVBUTN2.DEF";
+		//NOTE: this images do not have "blocked" frames. They should be implemented somehow (e.g. palette transform or something...)
+
+		left = std::make_shared<CButton>(Point(), name, CButton::tooltip());
+		right = std::make_shared<CButton>(Point(), name, CButton::tooltip());
+		slider = std::make_shared<CButton>(Point(), name, CButton::tooltip());
+
+		left->setImageOrder(0, 1, 1, 1);
+		right->setImageOrder(2, 3, 3, 3);
+		slider->setImageOrder(4, 4, 4, 4);
+	}
+	else
+	{
+		left = std::make_shared<CButton>(Point(), getOrientation() == Orientation::HORIZONTAL ? "SCNRBLF.DEF" : "SCNRBUP.DEF", CButton::tooltip());
+		right = std::make_shared<CButton>(Point(), getOrientation() == Orientation::HORIZONTAL ? "SCNRBRT.DEF" : "SCNRBDN.DEF", CButton::tooltip());
+		slider = std::make_shared<CButton>(Point(), "SCNRBSL.DEF", CButton::tooltip());
+	}
+	slider->actOnDown = true;
+	slider->soundDisabled = true;
+	left->soundDisabled = true;
+	right->soundDisabled = true;
+
+	if (getOrientation() == Orientation::HORIZONTAL)
+		right->moveBy(Point(totalw - right->pos.w, 0));
+	else
+		right->moveBy(Point(0, totalw - right->pos.h));
+
+	left->addCallback(std::bind(&CSlider::scrollPrev,this));
+	right->addCallback(std::bind(&CSlider::scrollNext,this));
+	slider->addCallback(std::bind(&CSlider::sliderClicked,this));
+
+	if(getOrientation() == Orientation::HORIZONTAL)
+	{
+		pos.h = slider->pos.h;
+		pos.w = totalw;
+	}
+	else
+	{
+		pos.w = slider->pos.w;
+		pos.h = totalw;
+	}
+
+	updateSliderPos();
+}
+
+CSlider::~CSlider() = default;
+
+void CSlider::block( bool on )
+{
+	left->block(on);
+	right->block(on);
+	slider->block(on);
+}
+
+void CSlider::setAmount( int to )
+{
+	amount = to;
+	positions = to - capacity;
+	vstd::amax(positions, 0);
+}
+
+void CSlider::showAll(Canvas & to)
+{
+	to.drawColor(pos, Colors::BLACK);
+	CIntObject::showAll(to);
+}
+
+void CSlider::keyPressed(EShortcut key)
+{
+	int moveDest = value;
+	switch(key)
+	{
+	case EShortcut::MOVE_UP:
+		if (getOrientation() == Orientation::VERTICAL)
+			moveDest = value - getScrollStep();
+		break;
+	case EShortcut::MOVE_LEFT:
+		if (getOrientation() == Orientation::HORIZONTAL)
+			moveDest = value - getScrollStep();
+		break;
+	case EShortcut::MOVE_DOWN:
+		if (getOrientation() == Orientation::VERTICAL)
+			moveDest = value + getScrollStep();
+		break;
+	case EShortcut::MOVE_RIGHT:
+		if (getOrientation() == Orientation::HORIZONTAL)
+			moveDest = value + getScrollStep();
+		break;
+	case EShortcut::MOVE_PAGE_UP:
+		moveDest = value - capacity + getScrollStep();
+		break;
+	case EShortcut::MOVE_PAGE_DOWN:
+		moveDest = value + capacity - getScrollStep();
+		break;
+	case EShortcut::MOVE_FIRST:
+		moveDest = 0;
+		break;
+	case EShortcut::MOVE_LAST:
+		moveDest = amount - capacity;
+		break;
+	default:
+		return;
+	}
+
+	scrollTo(moveDest);
+}
+
+void CSlider::scrollToMin()
+{
+	scrollTo(0);
+}
+
+void CSlider::scrollToMax()
+{
+	scrollTo(amount);
+}

+ 86 - 0
client/widgets/Slider.h

@@ -0,0 +1,86 @@
+/*
+ * Slider.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 "Scrollable.h"
+#include "../../lib/FunctionList.h"
+
+class CButton;
+
+/// A typical slider which can be orientated horizontally/vertically.
+class CSlider : public Scrollable
+{
+	//if vertical then left=up
+	std::shared_ptr<CButton> left;
+	std::shared_ptr<CButton> right;
+	std::shared_ptr<CButton> slider;
+
+	std::optional<Rect> scrollBounds;
+
+	/// how many elements are visible simultaneously
+	int capacity;
+	/// number of highest position, or 0 if there is only one
+	int positions;
+	/// total amount of elements in the list
+	int amount;
+	/// topmost vislble (first active) element
+	int value;
+
+	CFunctionList<void(int)> moved;
+
+	void updateSliderPos();
+	void sliderClicked();
+
+public:
+	enum EStyle
+	{
+		BROWN,
+		BLUE
+	};
+
+	void block(bool on);
+
+	/// If set, mouse scroll will only scroll slider when inside of this area
+	void setScrollBounds(const Rect & bounds );
+	void clearScrollBounds();
+
+	/// Value modifiers
+	void scrollTo(int value);
+	void scrollBy(int amount) override;
+	void scrollToMin();
+	void scrollToMax();
+
+	/// Amount modifier
+	void setAmount(int to);
+
+	/// Accessors
+	int getAmount() const;
+	int getValue() const;
+	int getCapacity() const;
+
+	void addCallback(std::function<void(int)> callback);
+
+	bool receiveEvent(const Point & position, int eventType) const override;
+	void keyPressed(EShortcut key) override;
+	void clickLeft(tribool down, bool previousState) override;
+	void mouseMoved (const Point & cursorPosition) override;
+	void showAll(Canvas & to) override;
+
+	 /// @param position coordinates of slider
+	 /// @param length length of slider ribbon, including left/right buttons
+	 /// @param Moved function that will be called whenever slider moves
+	 /// @param Capacity maximal number of visible at once elements
+	 /// @param Amount total amount of elements, including not visible
+	 /// @param Value starting position
+	CSlider(Point position, int length, std::function<void(int)> Moved, int Capacity, int Amount,
+		int Value=0, bool Horizontal=true, EStyle style = BROWN);
+	~CSlider();
+};

+ 1 - 1
client/widgets/TextControls.cpp

@@ -10,7 +10,7 @@
 #include "StdInc.h"
 #include "TextControls.h"
 
-#include "Buttons.h"
+#include "Slider.h"
 #include "Images.h"
 
 #include "../CPlayerInterface.h"

+ 1 - 0
client/windows/CMessage.cpp

@@ -18,6 +18,7 @@
 #include "../windows/InfoWindows.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/CComponent.h"
+#include "../widgets/Slider.h"
 #include "../widgets/TextControls.h"
 #include "../gui/CGuiHandler.h"
 #include "../render/CAnimation.h"

+ 5 - 4
client/windows/CQuestLog.cpp

@@ -15,9 +15,10 @@
 
 #include "../gui/CGuiHandler.h"
 #include "../gui/Shortcut.h"
+#include "../widgets/Buttons.h"
 #include "../widgets/CComponent.h"
+#include "../widgets/Slider.h"
 #include "../adventureMap/AdventureMapInterface.h"
-#include "../widgets/Buttons.h"
 #include "../adventureMap/CMinimap.h"
 #include "../render/Canvas.h"
 #include "../renderSDL/SDL_Extensions.h"
@@ -193,12 +194,12 @@ void CQuestLog::recreateLabelList()
 	if (currentLabel > QUEST_COUNT)
 	{
 		slider->block(false);
-		slider->moveToMax();
+		slider->scrollToMax();
 	}
 	else
 	{
 		slider->block(true);
-		slider->moveToMin();
+		slider->scrollToMin();
 	}
 }
 
@@ -238,7 +239,7 @@ void CQuestLog::selectQuest(int which, int labelId)
 	std::vector<Component> components;
 	currentQuest->quest->getVisitText (text, components, currentQuest->quest->isCustomFirst, true);
 	if(description->slider)
-		description->slider->moveToMin(); // scroll text to start position
+		description->slider->scrollToMin(); // scroll text to start position
 	description->setText(text.toString()); //TODO: use special log entry text
 
 	componentsBox.reset();

+ 8 - 7
client/windows/CTradeWindow.cpp

@@ -18,6 +18,7 @@
 #include "../gui/Shortcut.h"
 #include "../gui/WindowHandler.h"
 #include "../widgets/Buttons.h"
+#include "../widgets/Slider.h"
 #include "../widgets/TextControls.h"
 #include "../windows/InfoWindows.h"
 
@@ -784,7 +785,7 @@ CMarketplaceWindow::~CMarketplaceWindow() = default;
 
 void CMarketplaceWindow::setMax()
 {
-	slider->moveToMax();
+	slider->scrollToMax();
 }
 
 void CMarketplaceWindow::makeDeal()
@@ -824,7 +825,7 @@ void CMarketplaceWindow::makeDeal()
 		if(slider)
 		{
 			LOCPLINT->cb->trade(market, mode, leftIdToSend, hRight->id, slider->getValue() * r1, hero);
-			slider->moveTo(0);
+			slider->scrollTo(0);
 		}
 		else
 		{
@@ -868,7 +869,7 @@ void CMarketplaceWindow::selectionChanged(bool side)
 				assert(0);
 
 			slider->setAmount(newAmount / r1);
-			slider->moveTo(0);
+			slider->scrollTo(0);
 			max->block(false);
 			deal->block(false);
 		}
@@ -885,7 +886,7 @@ void CMarketplaceWindow::selectionChanged(bool side)
 		{
 			max->block(true);
 			slider->setAmount(0);
-			slider->moveTo(0);
+			slider->scrollTo(0);
 		}
 		deal->block(true);
 	}
@@ -1120,7 +1121,7 @@ CAltarWindow::CAltarWindow(const IMarket * Market, const CGHeroInstance * Hero,
 		new CTextBox(CGI->generaltexth->allTexts[480], Rect(320, 56, 256, 40), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW);
 
 		slider = std::make_shared<CSlider>(Point(231,481),137,std::bind(&CAltarWindow::sliderMoved,this,_1),0,0);
-		max = std::make_shared<CButton>(Point(147, 520), "IRCBTNS.DEF", CGI->generaltexth->zelp[578], std::bind(&CSlider::moveToMax, slider));
+		max = std::make_shared<CButton>(Point(147, 520), "IRCBTNS.DEF", CGI->generaltexth->zelp[578], std::bind(&CSlider::scrollToMax, slider));
 
 		sacrificedUnits.resize(GameConstants::ARMY_SIZE, 0);
 		sacrificeAll = std::make_shared<CButton>(Point(393, 520), "ALTARMY.DEF", CGI->generaltexth->zelp[579], std::bind(&CAltarWindow::SacrificeAll,this));
@@ -1213,7 +1214,7 @@ void CAltarWindow::makeDeal()
 	if(mode == EMarketMode::CREATURE_EXP)
 	{
 		blockTrade();
-		slider->moveTo(0);
+		slider->scrollTo(0);
 
 		std::vector<ui32> ids;
 		std::vector<ui32> toSacrifice;
@@ -1310,7 +1311,7 @@ void CAltarWindow::selectionChanged(bool side)
 
 	slider->setAmount(hero->getStackCount(SlotID(hLeft->serial)) - (stackCount == 1));
 	slider->block(!slider->getAmount());
-	slider->moveTo(sacrificedUnits[hLeft->serial]);
+	slider->scrollTo(sacrificedUnits[hLeft->serial]);
 	max->block(!slider->getAmount());
 	selectOppositeItem(side);
 	readyToTrade = true;

+ 3 - 2
client/windows/CreaturePurchaseCard.cpp

@@ -19,6 +19,7 @@
 #include "../gui/TextAlignment.h"
 #include "../gui/WindowHandler.h"
 #include "../widgets/Buttons.h"
+#include "../widgets/Slider.h"
 #include "../widgets/TextControls.h"
 #include "../widgets/CreatureCostBox.h"
 
@@ -34,12 +35,12 @@ void CreaturePurchaseCard::initButtons()
 
 void CreaturePurchaseCard::initMaxButton()
 {
-	maxButton = std::make_shared<CButton>(Point(pos.x + 52, pos.y + 180), "QuickRecruitmentWindow/QuickRecruitmentAllButton.def", CButton::tooltip(), std::bind(&CSlider::moveToMax,slider), EShortcut::RECRUITMENT_MAX);
+	maxButton = std::make_shared<CButton>(Point(pos.x + 52, pos.y + 180), "QuickRecruitmentWindow/QuickRecruitmentAllButton.def", CButton::tooltip(), std::bind(&CSlider::scrollToMax,slider), EShortcut::RECRUITMENT_MAX);
 }
 
 void CreaturePurchaseCard::initMinButton()
 {
-	minButton = std::make_shared<CButton>(Point(pos.x, pos.y + 180), "QuickRecruitmentWindow/QuickRecruitmentNoneButton.def", CButton::tooltip(), std::bind(&CSlider::moveToMin,slider), EShortcut::RECRUITMENT_MIN);
+	minButton = std::make_shared<CButton>(Point(pos.x, pos.y + 180), "QuickRecruitmentWindow/QuickRecruitmentNoneButton.def", CButton::tooltip(), std::bind(&CSlider::scrollToMin,slider), EShortcut::RECRUITMENT_MIN);
 }
 
 void CreaturePurchaseCard::initCreatureSwitcherButton()

+ 6 - 6
client/windows/GUIClasses.cpp

@@ -21,7 +21,6 @@
 #include "../CVideoHandler.h"
 #include "../CServerHandler.h"
 
-//#include "../adventureMap/CResDataBar.h"
 #include "../battle/BattleInterfaceClasses.h"
 #include "../battle/BattleInterface.h"
 
@@ -35,6 +34,7 @@
 #include "../widgets/MiscWidgets.h"
 #include "../widgets/CreatureCostBox.h"
 #include "../widgets/Buttons.h"
+#include "../widgets/Slider.h"
 #include "../widgets/TextControls.h"
 #include "../widgets/ObjectLists.h"
 
@@ -134,7 +134,7 @@ void CRecruitmentWindow::select(std::shared_ptr<CCreatureCard> card)
 		slider->setAmount(maxAmount);
 
 		if(slider->getValue() != maxAmount)
-			slider->moveTo(maxAmount);
+			slider->scrollTo(maxAmount);
 		else // if slider already at 0 - emulate call to sliderMoved()
 			sliderMoved(maxAmount);
 
@@ -215,7 +215,7 @@ CRecruitmentWindow::CRecruitmentWindow(const CGDwelling * Dwelling, int Level, c
 
 	slider = std::make_shared<CSlider>(Point(176,279),135,std::bind(&CRecruitmentWindow::sliderMoved,this, _1),0,0,0,true);
 
-	maxButton = std::make_shared<CButton>(Point(134, 313), "IRCBTNS.DEF", CGI->generaltexth->zelp[553], std::bind(&CSlider::moveToMax, slider), EShortcut::RECRUITMENT_MAX);
+	maxButton = std::make_shared<CButton>(Point(134, 313), "IRCBTNS.DEF", CGI->generaltexth->zelp[553], std::bind(&CSlider::scrollToMax, slider), EShortcut::RECRUITMENT_MAX);
 	buyButton = std::make_shared<CButton>(Point(212, 313), "IBY6432.DEF", CGI->generaltexth->zelp[554], std::bind(&CRecruitmentWindow::buy, this), EShortcut::GLOBAL_ACCEPT);
 	cancelButton = std::make_shared<CButton>(Point(290, 313), "ICN6432.DEF", CGI->generaltexth->zelp[555], std::bind(&CRecruitmentWindow::close, this), EShortcut::GLOBAL_CANCEL);
 
@@ -287,7 +287,7 @@ void CRecruitmentWindow::availableCreaturesChanged()
 	select(cards[selectedIndex]);
 
 	if(slider->getValue() == slider->getAmount())
-		slider->moveToMax();
+		slider->scrollToMax();
 	else // if slider already at 0 - emulate call to sliderMoved()
 		sliderMoved(slider->getAmount());
 }
@@ -363,7 +363,7 @@ void CSplitWindow::setAmountText(std::string text, bool left)
 	}
 
 	setAmount(amount, left);
-	slider->moveTo(rightAmount - rightMin);
+	slider->scrollTo(rightAmount - rightMin);
 }
 
 void CSplitWindow::setAmount(int value, bool left)
@@ -1788,7 +1788,7 @@ void CObjectListWindow::CItem::clickLeft(tribool down, bool previousState)
 		parent->changeSelection(index);
 }
 
-void CObjectListWindow::CItem::onDoubleClick()
+void CObjectListWindow::CItem::clickDouble()
 {
 	parent->elementSelected();
 }

+ 1 - 2
client/windows/GUIClasses.h

@@ -35,7 +35,6 @@ class CTextInput;
 class CListBox;
 class CLabelGroup;
 class CToggleButton;
-class CVolumeSlider;
 class CGStatusBar;
 class CTextBox;
 class CResDataBar;
@@ -163,7 +162,7 @@ class CObjectListWindow : public CWindowObject
 
 		void select(bool on);
 		void clickLeft(tribool down, bool previousState) override;
-		void onDoubleClick() override;
+		void clickDouble() override;
 	};
 
 	std::function<void(int)> onSelect;//called when OK button is pressed, returns id of selected item.

+ 4 - 3
client/windows/QuickRecruitmentWindow.cpp

@@ -13,6 +13,7 @@
 #include "../CPlayerInterface.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/CreatureCostBox.h"
+#include "../widgets/Slider.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/Shortcut.h"
 #include "../../CCallback.h"
@@ -88,11 +89,11 @@ void QuickRecruitmentWindow::maxAllCards(std::vector<std::shared_ptr<CreaturePur
 		i->slider->setAmount(maxAmount);
 
 		if(i->slider->getValue() != maxAmount)
-			i->slider->moveTo(maxAmount);
+			i->slider->scrollTo(maxAmount);
 		else
 			i->sliderMoved(maxAmount);
 
-		i->slider->moveToMax();
+		i->slider->scrollToMax();
 		allAvailableResources -= (i->creatureOnTheCard->getFullRecruitCost() * maxAmount);
 	}
 	maxButton->block(allAvailableResources == LOCPLINT->cb->getResourceAmount());
@@ -140,7 +141,7 @@ void QuickRecruitmentWindow::updateAllSliders()
 			i->slider->setAmount(i->slider->getValue() + maxAmount);
 		else
 			i->slider->setAmount(i->maxAmount);
-		i->slider->moveTo(i->slider->getValue());
+		i->slider->scrollTo(i->slider->getValue());
 	}
 	totalCost->createItems(LOCPLINT->cb->getResourceAmount() - allAvailableResources);
 	totalCost->set(LOCPLINT->cb->getResourceAmount() - allAvailableResources);

+ 7 - 14
client/windows/settings/AdventureOptionsTab.cpp

@@ -102,18 +102,14 @@ AdventureOptionsTab::AdventureOptionsTab()
 	{
 		return setBoolSetting("gameTweaks", "showGrid", value);
 	});
-	addCallback("mapSwipeChanged", [](bool value)
-	{
-#if defined(VCMI_MOBILE)
-		return setBoolSetting("general", "swipe", value);
-#else
-		return setBoolSetting("general", "swipeDesktop", value);
-#endif
-	});
 	addCallback("infoBarPickChanged", [](bool value)
 	{
 		return setBoolSetting("gameTweaks", "infoBarPick", value);
 	});
+	addCallback("borderScrollChanged", [](bool value)
+	{
+		return setBoolSetting("adventure", "borderScroll", value);
+	});
 	build(config);
 
 	std::shared_ptr<CToggleGroup> playerHeroSpeedToggle = widget<CToggleGroup>("heroMovementSpeedPicker");
@@ -140,12 +136,9 @@ AdventureOptionsTab::AdventureOptionsTab()
 	std::shared_ptr<CToggleButton> showGridCheckbox = widget<CToggleButton>("showGridCheckbox");
 	showGridCheckbox->setSelected(settings["gameTweaks"]["showGrid"].Bool());
 
-	std::shared_ptr<CToggleButton> mapSwipeCheckbox = widget<CToggleButton>("mapSwipeCheckbox");
-#if defined(VCMI_MOBILE)
-	mapSwipeCheckbox->setSelected(settings["general"]["swipe"].Bool());
-#else
-	mapSwipeCheckbox->setSelected(settings["general"]["swipeDesktop"].Bool());
-#endif
 	std::shared_ptr<CToggleButton> infoBarPickCheckbox = widget<CToggleButton>("infoBarPickCheckbox");
 	infoBarPickCheckbox->setSelected(settings["gameTweaks"]["infoBarPick"].Bool());
+
+	std::shared_ptr<CToggleButton> borderScrollCheckbox = widget<CToggleButton>("borderScrollCheckbox");
+	borderScrollCheckbox->setSelected(settings["adventure"]["borderScroll"].Bool());
 }

+ 4 - 18
client/windows/settings/BattleOptionsTab.cpp

@@ -8,13 +8,12 @@
  *
  */
 #include "StdInc.h"
-
 #include "BattleOptionsTab.h"
-#include "CConfigHandler.h"
 
 #include "../../battle/BattleInterface.h"
-#include "../../battle/BattleActionsController.h"
 #include "../../gui/CGuiHandler.h"
+#include "../../eventsSDL/InputHandler.h"
+#include "../../../lib/CConfigHandler.h"
 #include "../../../lib/filesystem/ResourceID.h"
 #include "../../../lib/CGeneralTextHandler.h"
 #include "../../widgets/Buttons.h"
@@ -25,6 +24,8 @@ BattleOptionsTab::BattleOptionsTab(BattleInterface * owner)
 	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
 	type |= REDRAW_PARENT;
 
+	//addConditional("touchscreen", GH.input().hasTouchInputDevice());
+
 	const JsonNode config(ResourceID("config/widgets/settings/battleOptionsTab.json"));
 	addCallback("viewGridChanged", [this, owner](bool value)
 	{
@@ -58,10 +59,6 @@ BattleOptionsTab::BattleOptionsTab(BattleInterface * owner)
 	{
 		skipBattleIntroMusicChangedCallback(value);
 	});
-	addCallback("touchscreenModeChanged", [this, owner](bool value)
-	{
-		touchscreenModeChangedCallback(value, owner);
-	});
 	build(config);
 
 	std::shared_ptr<CToggleGroup> animationSpeedToggle = widget<CToggleGroup>("animationSpeedPicker");
@@ -81,9 +78,6 @@ BattleOptionsTab::BattleOptionsTab(BattleInterface * owner)
 
 	std::shared_ptr<CToggleButton> mouseShadowCheckbox = widget<CToggleButton>("mouseShadowCheckbox");
 	mouseShadowCheckbox->setSelected(settings["battle"]["mouseShadow"].Bool());
-	
-	std::shared_ptr<CToggleButton> touchscreenModeCheckbox = widget<CToggleButton>("touchscreenModeCheckbox");
-	touchscreenModeCheckbox->setSelected(settings["battle"]["touchscreenMode"].Bool());
 
 	std::shared_ptr<CToggleButton> skipBattleIntroMusicCheckbox = widget<CToggleButton>("skipBattleIntroMusicCheckbox");
 	skipBattleIntroMusicCheckbox->setSelected(settings["gameTweaks"]["skipBattleIntroMusic"].Bool());
@@ -164,14 +158,6 @@ void BattleOptionsTab::mouseShadowChangedCallback(bool value)
 	shadow->Bool() = value;
 }
 
-void BattleOptionsTab::touchscreenModeChangedCallback(bool value, BattleInterface * parentBattleInterface)
-{
-	Settings touchcreenMode = settings.write["battle"]["touchscreenMode"];
-	touchcreenMode->Bool() = value;
-	if(parentBattleInterface)
-		parentBattleInterface->actionsController->setTouchScreenMode(value);
-}
-
 void BattleOptionsTab::animationSpeedChangedCallback(int value)
 {
 	Settings speed = settings.write["battle"]["speedFactor"];

+ 0 - 1
client/windows/settings/BattleOptionsTab.h

@@ -30,7 +30,6 @@ private:
 	void showQueueChangedCallback(bool value, BattleInterface * parentBattleInterface);
 	void queueSizeChangedCallback(int value, BattleInterface * parentBattleInterface);
 	void skipBattleIntroMusicChangedCallback(bool value);
-	void touchscreenModeChangedCallback(bool value, BattleInterface * parentBattleInterface);
 public:
 	BattleOptionsTab(BattleInterface * owner = nullptr);
 };

+ 6 - 4
client/windows/settings/GeneralOptionsTab.cpp

@@ -16,6 +16,7 @@
 #include "../../gui/CGuiHandler.h"
 #include "../../gui/WindowHandler.h"
 #include "../../widgets/Buttons.h"
+#include "../../widgets/Slider.h"
 #include "../../widgets/TextControls.h"
 #include "../../widgets/Images.h"
 #include "CGameInfo.h"
@@ -78,11 +79,12 @@ GeneralOptionsTab::GeneralOptionsTab()
 	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
 	type |= REDRAW_PARENT;
 
-	addConditional("mobile", false);
-	addConditional("desktop", true);
 #ifdef VCMI_MOBILE
 	addConditional("mobile", true);
 	addConditional("desktop", false);
+#else
+	addConditional("mobile", false);
+	addConditional("desktop", true);
 #endif
 
 	const JsonNode config(ResourceID("config/widgets/settings/generalOptionsTab.json"));
@@ -163,10 +165,10 @@ GeneralOptionsTab::GeneralOptionsTab()
 	framerateCheckbox->setSelected(settings["video"]["showfps"].Bool());
 
 	std::shared_ptr<CSlider> musicSlider = widget<CSlider>("musicSlider");
-	musicSlider->moveTo(CCS->musich->getVolume());
+	musicSlider->scrollTo(CCS->musich->getVolume());
 
 	std::shared_ptr<CSlider> volumeSlider = widget<CSlider>("soundVolumeSlider");
-	volumeSlider->moveTo(CCS->soundh->getVolume());
+	volumeSlider->scrollTo(CCS->soundh->getVolume());
 
 	std::shared_ptr<CToggleGroup> creatureGrowthAsDwellingPicker = widget<CToggleGroup>("availableCreaturesAsDwellingPicker");
 	creatureGrowthAsDwellingPicker->setSelected(settings["gameTweaks"]["availableCreaturesAsDwellingLabel"].Bool());

+ 12 - 17
config/schemas/settings.json

@@ -23,8 +23,6 @@
 				"sound",
 				"language",
 				"gameDataLanguage",
-				"swipe",
-				"swipeDesktop",
 				"saveRandomMaps",
 				"saveFrequency",
 				"notifications",
@@ -46,14 +44,6 @@
 					"type" : "number",
 					"default" : 88
 				},
-				"swipe" : {
-					"type" : "boolean",
-					"default" : true
-				},
-				"swipeDesktop" : {
-					"type" : "boolean",
-					"default" : false
-				},
 				"saveRandomMaps" : {
 					"type" : "boolean",
 					"default" : false
@@ -183,7 +173,7 @@
 			"type" : "object",
 			"additionalProperties" : false,
 			"default" : {},
-			"required" : [ "heroMoveTime", "enemyMoveTime", "scrollSpeedPixels", "heroReminder", "quickCombat", "objectAnimation", "terrainAnimation", "alwaysSkipCombat" ],
+			"required" : [ "heroMoveTime", "enemyMoveTime", "scrollSpeedPixels", "heroReminder", "quickCombat", "objectAnimation", "terrainAnimation", "alwaysSkipCombat", "borderScroll" ],
 			"properties" : {
 				"heroMoveTime" : {
 					"type" : "number",
@@ -216,7 +206,12 @@
 				"alwaysSkipCombat" : {
 					"type" : "boolean",
 					"default" : false
-				}
+				},
+				"borderScroll" : 
+				{
+					"type" : "boolean",
+					"default" : true
+				},
 			}
 		},
 		"pathfinder" : {
@@ -291,7 +286,7 @@
 			"type" : "object",
 			"additionalProperties" : false,
 			"default" : {},
-			"required" : [ "speedFactor", "mouseShadow", "cellBorders", "stackRange", "movementHighlightOnHover", "showQueue", "queueSize", "touchscreenMode" ],
+			"required" : [ "speedFactor", "mouseShadow", "cellBorders", "stackRange", "movementHighlightOnHover", "showQueue", "swipeAttackDistance", "queueSize" ],
 			"properties" : {
 				"speedFactor" : {
 					"type" : "number",
@@ -305,10 +300,6 @@
 					"type" : "boolean",
 					"default" : false
 				},
-				"touchscreenMode" : {
-					"type" : "boolean",
-					"default" : false
-				},
 				"stackRange" : {
 					"type" : "boolean",
 					"default" : true
@@ -321,6 +312,10 @@
 					"type" : "boolean",
 					"default" : true
 				},
+				"swipeAttackDistance" : {
+					"type" : "number",
+					"default" : 250
+				},
 				"queueSize" : {
 					"type" : "string",
 					"default" : "auto",

+ 7 - 7
config/widgets/settings/adventureOptionsTab.json

@@ -336,10 +336,10 @@
 					"text": "vcmi.adventureOptions.showGrid.hover"
 				},
 				{
-					"text": "vcmi.adventureOptions.mapSwipe.hover"
+					"text": "vcmi.adventureOptions.infoBarPick.hover"
 				},
 				{
-					"text": "vcmi.adventureOptions.infoBarPick.hover"
+					"text": "vcmi.adventureOptions.borderScroll.hover"
 				}
 			]
 		},
@@ -364,15 +364,15 @@
 					"help": "vcmi.adventureOptions.showGrid",
 					"callback": "showGridChanged"
 				},
-				{
-					"name": "mapSwipeCheckbox",
-					"help": "vcmi.adventureOptions.mapSwipe",
-					"callback": "mapSwipeChanged"
-				},
 				{
 					"name": "infoBarPickCheckbox",
 					"help": "vcmi.adventureOptions.infoBarPick",
 					"callback": "infoBarPickChanged"
+				},
+				{
+					"name": "borderScrollCheckbox",
+					"help": "vcmi.adventureOptions.borderScroll",
+					"callback": "borderScrollChanged"
 				}
 			]
 		}

+ 1 - 9
config/widgets/settings/battleOptionsTab.json

@@ -112,9 +112,6 @@
 				},
 				{
 					"text": "vcmi.battleOptions.skipBattleIntroMusic.hover",
-				},
-				{
-					"text": "vcmi.battleOptions.touchscreenMode.hover",
 				}
 			]
 		},
@@ -148,12 +145,7 @@
 					"name": "skipBattleIntroMusicCheckbox",
 					"help": "vcmi.battleOptions.skipBattleIntroMusic",
 					"callback": "skipBattleIntroMusicChanged"
-				},
-				{
-					"name": "touchscreenModeCheckbox",
-					"help": "vcmi.battleOptions.touchscreenMode",
-					"callback": "touchscreenModeChanged"
-				},
+				}
 			]
 		},
 /////////////////////////////////////// Bottom section - Animation Speed and Turn Order

+ 2 - 8
config/widgets/settings/generalOptionsTab.json

@@ -9,12 +9,6 @@
 			"image": "settingsWindow/lineHorizontal",
 			"rect": { "x" : 5, "y" : 289, "w": 365, "h": 3}
 		},
-		{
-			"name": "lineCreatureNumbersToggleGroupEnd",
-			"type": "texture",
-			"image": "settingsWindow/lineHorizontal",
-			"rect": { "x" : 5, "y" : 383, "w": 220, "h": 3}
-		},
 		{
 			"type" : "labelTitle",
 			"position": {"x": 10, "y": 55},
@@ -187,7 +181,7 @@
 			"name": "compactTownCreatureInfoLabel",
 			"type" : "verticalLayout",
 			"customType" : "labelDescription",
-			"position": {"x": 45, "y": 391},
+			"position": {"x": 45, "y": 385},
 			"items" : [
 				{
 					"text": "vcmi.otherOptions.compactTownCreatureInfo.hover",
@@ -198,7 +192,7 @@
 			"name": "compactTownCreatureInfoCheckbox",
 			"type": "checkbox",
 			"help": "vcmi.otherOptions.compactTownCreatureInfo",
-			"position": {"x": 10, "y": 389},
+			"position": {"x": 10, "y": 383},
 			"callback": "compactTownCreatureInfoChanged"
 		}
 	]

+ 0 - 14
launcher/firstLaunch/firstlaunch_moc.cpp

@@ -102,7 +102,6 @@ void FirstLaunchView::on_comboBoxLanguage_currentIndexChanged(int index)
 
 void FirstLaunchView::enterSetup()
 {
-	setupPlatformSettings();
 	Languages::fillLanguages(ui->listWidgetLanguage, false);
 }
 
@@ -159,19 +158,6 @@ void FirstLaunchView::exitSetup()
 		mainWindow->exitSetup();
 }
 
-// Initial platform-dependend settings
-void FirstLaunchView::setupPlatformSettings()
-{
-#if defined(VCMI_MOBILE)
-	bool touchscreenMode = true;
-#else
-	bool touchscreenMode = false;
-#endif
-	
-	Settings node = settings.write["battle"]["touchscreenMode"];
-	node->Bool() = touchscreenMode;
-}
-
 // Tab Language
 void FirstLaunchView::languageSelected(const QString & selectedLanguage)
 {

+ 0 - 3
launcher/firstLaunch/firstlaunch_moc.h

@@ -38,9 +38,6 @@ class FirstLaunchView : public QWidget
 	void activateTabModPreset();
 	void exitSetup();
 	
-	// Initial platform-dependend settings
-	void setupPlatformSettings();
-
 	// Tab Language
 	void languageSelected(const QString & languageCode);
 

+ 10 - 0
lib/Point.h

@@ -101,6 +101,16 @@ public:
 		return x > std::numeric_limits<int>::min() && y > std::numeric_limits<int>::min();
 	}
 
+	constexpr int lengthSquared() const
+	{
+		return x * x + y * y;
+	}
+
+	int length() const
+	{
+		return std::sqrt(lengthSquared());
+	}
+
 	template <typename Handler>
 	void serialize(Handler &h, const int version)
 	{