mirror of
https://github.com/vcmi/vcmi.git
synced 2025-08-13 19:54:17 +02:00
Moved swipe handling to TerrainRect class
This commit is contained in:
@@ -89,10 +89,12 @@ CAdvMapInt::CAdvMapInt():
|
|||||||
resdatabar(new CResDataBar),
|
resdatabar(new CResDataBar),
|
||||||
terrain(new CTerrainRect),
|
terrain(new CTerrainRect),
|
||||||
state(NA),
|
state(NA),
|
||||||
spellBeingCasted(nullptr), selection(nullptr),
|
spellBeingCasted(nullptr),
|
||||||
activeMapPanel(nullptr), duringAITurn(false), scrollingDir(0), scrollingState(false),
|
selection(nullptr),
|
||||||
swipeEnabled(settings["general"]["swipe"].Bool()), swipeMovementRequested(false),
|
activeMapPanel(nullptr),
|
||||||
swipeTargetPosition(Point(0, 0))
|
duringAITurn(false),
|
||||||
|
scrollingDir(0),
|
||||||
|
scrollingState(false)
|
||||||
{
|
{
|
||||||
pos.x = pos.y = 0;
|
pos.x = pos.y = 0;
|
||||||
pos.w = GH.screenDimensions().x;
|
pos.w = GH.screenDimensions().x;
|
||||||
@@ -558,16 +560,7 @@ void CAdvMapInt::show(SDL_Surface * to)
|
|||||||
if(state != INGAME)
|
if(state != INGAME)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if(swipeEnabled)
|
handleMapScrollingUpdate();
|
||||||
{
|
|
||||||
handleSwipeUpdate();
|
|
||||||
}
|
|
||||||
#if defined(VCMI_MOBILE) // on mobile, map-moving mode is exclusive (TODO technically it might work with both enabled; to be checked)
|
|
||||||
else
|
|
||||||
#endif
|
|
||||||
{
|
|
||||||
handleMapScrollingUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
for(int i = 0; i < 4; i++)
|
for(int i = 0; i < 4; i++)
|
||||||
{
|
{
|
||||||
@@ -619,17 +612,6 @@ void CAdvMapInt::handleMapScrollingUpdate()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void CAdvMapInt::handleSwipeUpdate()
|
|
||||||
{
|
|
||||||
if(swipeMovementRequested)
|
|
||||||
{
|
|
||||||
terrain->setViewCenter(swipeTargetPosition, terrain->getLevel());
|
|
||||||
CCS->curh->set(Cursor::Map::POINTER);
|
|
||||||
minimap->redraw();
|
|
||||||
swipeMovementRequested = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void CAdvMapInt::selectionChanged()
|
void CAdvMapInt::selectionChanged()
|
||||||
{
|
{
|
||||||
const CGTownInstance *to = LOCPLINT->towns[townList->getSelectedIndex()];
|
const CGTownInstance *to = LOCPLINT->towns[townList->getSelectedIndex()];
|
||||||
@@ -637,29 +619,21 @@ void CAdvMapInt::selectionChanged()
|
|||||||
select(to);
|
select(to);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CAdvMapInt::centerOn(int3 on, bool fade)
|
void CAdvMapInt::centerOn(int3 on)
|
||||||
{
|
{
|
||||||
bool switchedLevels = on.z != terrain->getLevel();
|
|
||||||
|
|
||||||
if (fade)
|
|
||||||
{
|
|
||||||
terrain->fadeFromCurrentView();
|
|
||||||
}
|
|
||||||
|
|
||||||
terrain->setViewCenter(on);
|
terrain->setViewCenter(on);
|
||||||
|
|
||||||
underground->setIndex(on.z,true); //change underground switch button image
|
underground->setIndex(on.z,true); //change underground switch button image
|
||||||
underground->redraw();
|
underground->redraw();
|
||||||
worldViewUnderground->setIndex(on.z, true);
|
worldViewUnderground->setIndex(on.z, true);
|
||||||
worldViewUnderground->redraw();
|
worldViewUnderground->redraw();
|
||||||
if (switchedLevels)
|
minimap->setLevel(terrain->getLevel());
|
||||||
minimap->setLevel(terrain->getLevel());
|
|
||||||
minimap->redraw();
|
minimap->redraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CAdvMapInt::centerOn(const CGObjectInstance * obj, bool fade)
|
void CAdvMapInt::centerOn(const CGObjectInstance * obj)
|
||||||
{
|
{
|
||||||
centerOn(obj->getSightCenter(), fade);
|
centerOn(obj->getSightCenter());
|
||||||
}
|
}
|
||||||
|
|
||||||
void CAdvMapInt::keyReleased(const SDL_Keycode &key)
|
void CAdvMapInt::keyReleased(const SDL_Keycode &key)
|
||||||
@@ -929,13 +903,8 @@ void CAdvMapInt::select(const CArmedInstance *sel, bool centerView)
|
|||||||
|
|
||||||
void CAdvMapInt::mouseMoved( const Point & cursorPosition )
|
void CAdvMapInt::mouseMoved( const Point & cursorPosition )
|
||||||
{
|
{
|
||||||
#if defined(VCMI_MOBILE)
|
|
||||||
if(swipeEnabled)
|
|
||||||
return;
|
|
||||||
#endif
|
|
||||||
// adventure map scrolling with mouse
|
// adventure map scrolling with mouse
|
||||||
// currently disabled in world view mode (as it is in OH3), but should work correctly if mode check is removed
|
// currently disabled in world view mode (as it is in OH3), but should work correctly if mode check is removed
|
||||||
// don't scroll if there is no window in focus - these events don't seem to correspond to the actual mouse movement
|
|
||||||
if(!GH.isKeyboardCtrlDown() && isActive() && mode == EAdvMapMode::NORMAL)
|
if(!GH.isKeyboardCtrlDown() && isActive() && mode == EAdvMapMode::NORMAL)
|
||||||
{
|
{
|
||||||
if(cursorPosition.x<15)
|
if(cursorPosition.x<15)
|
||||||
|
@@ -61,10 +61,6 @@ private:
|
|||||||
enum EDirections {LEFT=1, RIGHT=2, UP=4, DOWN=8};
|
enum EDirections {LEFT=1, RIGHT=2, UP=4, DOWN=8};
|
||||||
enum EGameStates {NA, INGAME, WAITING};
|
enum EGameStates {NA, INGAME, WAITING};
|
||||||
|
|
||||||
bool swipeEnabled;
|
|
||||||
bool swipeMovementRequested;
|
|
||||||
Point swipeTargetPosition;
|
|
||||||
|
|
||||||
EGameStates state;
|
EGameStates state;
|
||||||
EAdvMapMode mode;
|
EAdvMapMode mode;
|
||||||
|
|
||||||
@@ -140,7 +136,6 @@ private:
|
|||||||
void updateSpellbook(const CGHeroInstance *h);
|
void updateSpellbook(const CGHeroInstance *h);
|
||||||
|
|
||||||
void handleMapScrollingUpdate();
|
void handleMapScrollingUpdate();
|
||||||
void handleSwipeUpdate();
|
|
||||||
|
|
||||||
void showMoveDetailsInStatusbar(const CGHeroInstance & hero, const CGPathNode & pathNode);
|
void showMoveDetailsInStatusbar(const CGHeroInstance & hero, const CGPathNode & pathNode);
|
||||||
|
|
||||||
@@ -166,8 +161,8 @@ public:
|
|||||||
// public interface
|
// public interface
|
||||||
|
|
||||||
void select(const CArmedInstance *sel, bool centerView = true);
|
void select(const CArmedInstance *sel, bool centerView = true);
|
||||||
void centerOn(int3 on, bool fade = false);
|
void centerOn(int3 on);
|
||||||
void centerOn(const CGObjectInstance *obj, bool fade = false);
|
void centerOn(const CGObjectInstance *obj);
|
||||||
|
|
||||||
bool isHeroSleeping(const CGHeroInstance *hero);
|
bool isHeroSleeping(const CGHeroInstance *hero);
|
||||||
void setHeroSleeping(const CGHeroInstance *hero, bool sleep);
|
void setHeroSleeping(const CGHeroInstance *hero, bool sleep);
|
||||||
|
@@ -31,13 +31,18 @@
|
|||||||
#include "../../lib/mapping/CMap.h"
|
#include "../../lib/mapping/CMap.h"
|
||||||
#include "../../lib/CPathfinder.h"
|
#include "../../lib/CPathfinder.h"
|
||||||
|
|
||||||
#include <SDL_surface.h>
|
|
||||||
|
|
||||||
#define ADVOPT (conf.go()->ac)
|
#define ADVOPT (conf.go()->ac)
|
||||||
|
|
||||||
CTerrainRect::CTerrainRect()
|
CTerrainRect::CTerrainRect()
|
||||||
: curHoveredTile(-1, -1, -1)
|
: curHoveredTile(-1, -1, -1)
|
||||||
, isSwiping(false)
|
, isSwiping(false)
|
||||||
|
#if defined(VCMI_ANDROID) || defined(VCMI_IOS)
|
||||||
|
, swipeEnabled(settings["general"]["swipe"].Bool())
|
||||||
|
#else
|
||||||
|
, swipeEnabled(settings["general"]["swipeDesktop"].Bool())
|
||||||
|
#endif
|
||||||
|
, swipeMovementRequested(false)
|
||||||
|
, swipeTargetPosition(Point(0, 0))
|
||||||
{
|
{
|
||||||
OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
|
OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
|
||||||
|
|
||||||
@@ -73,8 +78,7 @@ void CTerrainRect::clickLeft(tribool down, bool previousState)
|
|||||||
if(indeterminate(down))
|
if(indeterminate(down))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
#if defined(VCMI_MOBILE)
|
if(swipeEnabled)
|
||||||
if(adventureInt->swipeEnabled)
|
|
||||||
{
|
{
|
||||||
if(handleSwipeStateChange((bool)down == true))
|
if(handleSwipeStateChange((bool)down == true))
|
||||||
{
|
{
|
||||||
@@ -82,7 +86,6 @@ void CTerrainRect::clickLeft(tribool down, bool previousState)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
#endif
|
|
||||||
{
|
{
|
||||||
if(down == false)
|
if(down == false)
|
||||||
return;
|
return;
|
||||||
@@ -97,10 +100,9 @@ void CTerrainRect::clickLeft(tribool down, bool previousState)
|
|||||||
|
|
||||||
void CTerrainRect::clickRight(tribool down, bool previousState)
|
void CTerrainRect::clickRight(tribool down, bool previousState)
|
||||||
{
|
{
|
||||||
#if defined(VCMI_MOBILE)
|
if(isSwiping)
|
||||||
if(adventureInt->swipeEnabled && isSwiping)
|
|
||||||
return;
|
return;
|
||||||
#endif
|
|
||||||
if(adventureInt->mode == EAdvMapMode::WORLD_VIEW)
|
if(adventureInt->mode == EAdvMapMode::WORLD_VIEW)
|
||||||
return;
|
return;
|
||||||
int3 mp = whichTileIsIt();
|
int3 mp = whichTileIsIt();
|
||||||
@@ -118,27 +120,26 @@ void CTerrainRect::mouseMoved(const Point & cursorPosition)
|
|||||||
{
|
{
|
||||||
handleHover(cursorPosition);
|
handleHover(cursorPosition);
|
||||||
|
|
||||||
if(!adventureInt->swipeEnabled)
|
|
||||||
return;
|
|
||||||
|
|
||||||
handleSwipeMove(cursorPosition);
|
handleSwipeMove(cursorPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CTerrainRect::handleSwipeMove(const Point & cursorPosition)
|
void CTerrainRect::handleSwipeMove(const Point & cursorPosition)
|
||||||
{
|
{
|
||||||
#if defined(VCMI_MOBILE)
|
// unless swipe is enabled, swipe move only works with middle mouse button
|
||||||
if(!GH.isMouseButtonPressed() || GH.multifinger) // any "button" is enough on mobile
|
if(!swipeEnabled && !GH.isMouseButtonPressed(MouseButton::MIDDLE))
|
||||||
return;
|
return;
|
||||||
#else
|
|
||||||
if(!GH.isMouseButtonPressed(MouseButton::MIDDLE)) // swipe only works with middle mouse on other platforms
|
// on mobile platforms with enabled swipe any button is enough
|
||||||
|
if(swipeEnabled && (!GH.isMouseButtonPressed() || GH.multifinger))
|
||||||
return;
|
return;
|
||||||
#endif
|
|
||||||
|
|
||||||
if(!isSwiping)
|
if(!isSwiping)
|
||||||
{
|
{
|
||||||
|
static constexpr int touchSwipeSlop = 16;
|
||||||
|
|
||||||
// try to distinguish if this touch was meant to be a swipe or just fat-fingering press
|
// try to distinguish if this touch was meant to be a swipe or just fat-fingering press
|
||||||
if(std::abs(cursorPosition.x - swipeInitialRealPos.x) > SwipeTouchSlop ||
|
if(std::abs(cursorPosition.x - swipeInitialRealPos.x) > touchSwipeSlop ||
|
||||||
std::abs(cursorPosition.y - swipeInitialRealPos.y) > SwipeTouchSlop)
|
std::abs(cursorPosition.y - swipeInitialRealPos.y) > touchSwipeSlop)
|
||||||
{
|
{
|
||||||
isSwiping = true;
|
isSwiping = true;
|
||||||
}
|
}
|
||||||
@@ -146,9 +147,9 @@ void CTerrainRect::handleSwipeMove(const Point & cursorPosition)
|
|||||||
|
|
||||||
if(isSwiping)
|
if(isSwiping)
|
||||||
{
|
{
|
||||||
adventureInt->swipeTargetPosition.x = swipeInitialViewPos.x + swipeInitialRealPos.x - cursorPosition.x;
|
swipeTargetPosition.x = swipeInitialViewPos.x + swipeInitialRealPos.x - cursorPosition.x;
|
||||||
adventureInt->swipeTargetPosition.y = swipeInitialViewPos.y + swipeInitialRealPos.y - cursorPosition.y;
|
swipeTargetPosition.y = swipeInitialViewPos.y + swipeInitialRealPos.y - cursorPosition.y;
|
||||||
adventureInt->swipeMovementRequested = true;
|
swipeMovementRequested = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,11 +212,6 @@ Rect CTerrainRect::visibleTilesArea()
|
|||||||
return renderer->getModel()->getTilesTotalRect();
|
return renderer->getModel()->getTilesTotalRect();
|
||||||
}
|
}
|
||||||
|
|
||||||
void CTerrainRect::fadeFromCurrentView()
|
|
||||||
{
|
|
||||||
assert(0);//TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
void CTerrainRect::setLevel(int level)
|
void CTerrainRect::setLevel(int level)
|
||||||
{
|
{
|
||||||
renderer->getController()->setViewCenter(renderer->getModel()->getMapViewCenter(), level);
|
renderer->getController()->setViewCenter(renderer->getModel()->getMapViewCenter(), level);
|
||||||
@@ -223,6 +219,10 @@ void CTerrainRect::setLevel(int level)
|
|||||||
|
|
||||||
void CTerrainRect::moveViewBy(const Point & delta)
|
void CTerrainRect::moveViewBy(const Point & delta)
|
||||||
{
|
{
|
||||||
|
// ignore scrolling attempts while we are swiping
|
||||||
|
if (isSwiping || swipeMovementRequested)
|
||||||
|
return;
|
||||||
|
|
||||||
renderer->getController()->setViewCenter(renderer->getModel()->getMapViewCenter() + delta, getLevel());
|
renderer->getController()->setViewCenter(renderer->getModel()->getMapViewCenter() + delta, getLevel());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,7 +241,6 @@ void CTerrainRect::setTileSize(int sizePixels)
|
|||||||
renderer->getController()->setTileSize(Point(sizePixels, sizePixels));
|
renderer->getController()->setTileSize(Point(sizePixels, sizePixels));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void CTerrainRect::setTerrainVisibility(bool showAllTerrain)
|
void CTerrainRect::setTerrainVisibility(bool showAllTerrain)
|
||||||
{
|
{
|
||||||
renderer->getController()->setTerrainVisibility(showAllTerrain);
|
renderer->getController()->setTerrainVisibility(showAllTerrain);
|
||||||
@@ -251,3 +250,14 @@ void CTerrainRect::setOverlayVisibility(const std::vector<ObjectPosInfo> & objec
|
|||||||
{
|
{
|
||||||
renderer->getController()->setOverlayVisibility(objectPositions);
|
renderer->getController()->setOverlayVisibility(objectPositions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CTerrainRect::show(SDL_Surface * to)
|
||||||
|
{
|
||||||
|
if(swipeMovementRequested)
|
||||||
|
{
|
||||||
|
setViewCenter(swipeTargetPosition, getLevel());
|
||||||
|
CCS->curh->set(Cursor::Map::POINTER);
|
||||||
|
swipeMovementRequested = false;
|
||||||
|
}
|
||||||
|
CIntObject::show(to);
|
||||||
|
}
|
||||||
|
@@ -24,16 +24,13 @@ class CTerrainRect : public CIntObject
|
|||||||
{
|
{
|
||||||
std::shared_ptr<MapView> renderer;
|
std::shared_ptr<MapView> renderer;
|
||||||
|
|
||||||
|
bool swipeEnabled;
|
||||||
|
bool swipeMovementRequested;
|
||||||
|
Point swipeTargetPosition;
|
||||||
Point swipeInitialViewPos;
|
Point swipeInitialViewPos;
|
||||||
Point swipeInitialRealPos;
|
Point swipeInitialRealPos;
|
||||||
bool isSwiping;
|
bool isSwiping;
|
||||||
|
|
||||||
#if defined(VCMI_ANDROID) || defined(VCMI_IOS)
|
|
||||||
static constexpr float SwipeTouchSlop = 16.0f; // touch UI
|
|
||||||
#else
|
|
||||||
static constexpr float SwipeTouchSlop = 1.0f; // mouse UI
|
|
||||||
#endif
|
|
||||||
|
|
||||||
void handleHover(const Point & cursorPosition);
|
void handleHover(const Point & cursorPosition);
|
||||||
void handleSwipeMove(const Point & cursorPosition);
|
void handleSwipeMove(const Point & cursorPosition);
|
||||||
/// handles start/finish of swipe (press/release of corresponding button); returns true if state change was handled
|
/// handles start/finish of swipe (press/release of corresponding button); returns true if state change was handled
|
||||||
@@ -43,20 +40,30 @@ class CTerrainRect : public CIntObject
|
|||||||
int3 whichTileIsIt(const Point & position); //x,y are cursor position
|
int3 whichTileIsIt(const Point & position); //x,y are cursor position
|
||||||
int3 whichTileIsIt(); //uses current cursor pos
|
int3 whichTileIsIt(); //uses current cursor pos
|
||||||
|
|
||||||
|
Point getViewCenter();
|
||||||
|
|
||||||
public:
|
public:
|
||||||
CTerrainRect();
|
CTerrainRect();
|
||||||
|
|
||||||
void moveViewBy(const Point & delta);
|
/// Handle swipe & selection of object
|
||||||
void setViewCenter(const int3 & coordinates);
|
void setViewCenter(const int3 & coordinates);
|
||||||
void setViewCenter(const Point & position, int level);
|
void setViewCenter(const Point & position, int level);
|
||||||
void setLevel(int level);
|
|
||||||
void setTileSize(int sizePixels);
|
|
||||||
|
|
||||||
|
/// Edge scrolling
|
||||||
|
void moveViewBy(const Point & delta);
|
||||||
|
|
||||||
|
/// Toggle undeground view button
|
||||||
|
void setLevel(int level);
|
||||||
|
int getLevel();
|
||||||
|
|
||||||
|
/// World view & View Earth/Air spells
|
||||||
void setTerrainVisibility(bool showAllTerrain);
|
void setTerrainVisibility(bool showAllTerrain);
|
||||||
void setOverlayVisibility(const std::vector<ObjectPosInfo> & objectPositions);
|
void setOverlayVisibility(const std::vector<ObjectPosInfo> & objectPositions);
|
||||||
|
void setTileSize(int sizePixels);
|
||||||
|
|
||||||
Point getViewCenter();
|
/// Minimap access
|
||||||
int getLevel();
|
/// @returns number of visible tiles on screen respecting current map scaling
|
||||||
|
Rect visibleTilesArea();
|
||||||
|
|
||||||
// CIntObject interface implementation
|
// CIntObject interface implementation
|
||||||
void deactivate() override;
|
void deactivate() override;
|
||||||
@@ -65,14 +72,8 @@ public:
|
|||||||
void clickMiddle(tribool down, bool previousState) override;
|
void clickMiddle(tribool down, bool previousState) override;
|
||||||
void hover(bool on) override;
|
void hover(bool on) override;
|
||||||
void mouseMoved (const Point & cursorPosition) override;
|
void mouseMoved (const Point & cursorPosition) override;
|
||||||
//void show(SDL_Surface * to) override;
|
void show(SDL_Surface * to) override;
|
||||||
//void showAll(SDL_Surface * to) override;
|
//void showAll(SDL_Surface * to) override;
|
||||||
|
|
||||||
//void showAnim(SDL_Surface * to);
|
//void showAnim(SDL_Surface * to);
|
||||||
|
|
||||||
/// @returns number of visible tiles on screen respecting current map scaling
|
|
||||||
Rect visibleTilesArea();
|
|
||||||
|
|
||||||
/// animates view by caching current surface and crossfading it with normal screen
|
|
||||||
void fadeFromCurrentView();
|
|
||||||
};
|
};
|
||||||
|
@@ -154,11 +154,7 @@ bool MapRendererContext::showOverlay() const
|
|||||||
|
|
||||||
bool MapRendererContext::showGrid() const
|
bool MapRendererContext::showGrid() const
|
||||||
{
|
{
|
||||||
<<<<<<< HEAD
|
|
||||||
return settings["gameTweaks"]["showGrid"].Bool();
|
|
||||||
=======
|
|
||||||
return settingsSessionShowGrid;
|
return settingsSessionShowGrid;
|
||||||
>>>>>>> 9a847b520 (Working version of image caching)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MapRendererContext::showVisitable() const
|
bool MapRendererContext::showVisitable() const
|
||||||
|
@@ -173,6 +173,7 @@ void MapViewController::onHeroMoved(const CGHeroInstance * obj, const int3 & fro
|
|||||||
{
|
{
|
||||||
// instant movement
|
// instant movement
|
||||||
context->addObject(movingObject);
|
context->addObject(movingObject);
|
||||||
|
setViewCenter(movingObject->visitablePos());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -25,6 +25,7 @@
|
|||||||
"encoding",
|
"encoding",
|
||||||
"language",
|
"language",
|
||||||
"swipe",
|
"swipe",
|
||||||
|
"swipeDesktop",
|
||||||
"saveRandomMaps",
|
"saveRandomMaps",
|
||||||
"saveFrequency",
|
"saveFrequency",
|
||||||
"notifications",
|
"notifications",
|
||||||
@@ -58,6 +59,10 @@
|
|||||||
"type" : "boolean",
|
"type" : "boolean",
|
||||||
"default" : true
|
"default" : true
|
||||||
},
|
},
|
||||||
|
"swipeDesktop" : {
|
||||||
|
"type" : "boolean",
|
||||||
|
"default" : false
|
||||||
|
},
|
||||||
"saveRandomMaps" : {
|
"saveRandomMaps" : {
|
||||||
"type" : "boolean",
|
"type" : "boolean",
|
||||||
"default" : false
|
"default" : false
|
||||||
|
Reference in New Issue
Block a user