mirror of
https://github.com/codewriter-packages/Tri-Inspector.git
synced 2025-01-22 00:08:51 -05:00
Rework property extensions
This commit is contained in:
parent
ffb0e8f4c5
commit
6038aa2d13
@ -14,21 +14,15 @@ namespace TriInspector.Drawers
|
|||||||
{
|
{
|
||||||
private ValueResolver<string> _nameResolver;
|
private ValueResolver<string> _nameResolver;
|
||||||
|
|
||||||
public override void Initialize(TriPropertyDefinition propertyDefinition)
|
public override string Initialize(TriPropertyDefinition propertyDefinition)
|
||||||
{
|
{
|
||||||
base.Initialize(propertyDefinition);
|
var isValidMethod = propertyDefinition.MemberInfo is MethodInfo mi && mi.GetParameters().Length == 0;
|
||||||
|
|
||||||
_nameResolver = ValueResolver.ResolveString(propertyDefinition, Attribute.Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string CanDraw(TriProperty property)
|
|
||||||
{
|
|
||||||
var isValidMethod = property.MemberInfo is MethodInfo mi && mi.GetParameters().Length == 0;
|
|
||||||
if (!isValidMethod)
|
if (!isValidMethod)
|
||||||
{
|
{
|
||||||
return "[Button] valid only on methods without parameters";
|
return "[Button] valid only on methods without parameters";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_nameResolver = ValueResolver.ResolveString(propertyDefinition, Attribute.Name);
|
||||||
if (_nameResolver.TryGetErrorString(out var error))
|
if (_nameResolver.TryGetErrorString(out var error))
|
||||||
{
|
{
|
||||||
return error;
|
return error;
|
||||||
|
@ -14,14 +14,14 @@ namespace TriInspector.Drawers
|
|||||||
{
|
{
|
||||||
public class EnumToggleButtonsDrawer : TriAttributeDrawer<EnumToggleButtonsAttribute>
|
public class EnumToggleButtonsDrawer : TriAttributeDrawer<EnumToggleButtonsAttribute>
|
||||||
{
|
{
|
||||||
public override string CanDraw(TriProperty property)
|
public override string Initialize(TriPropertyDefinition propertyDefinition)
|
||||||
{
|
{
|
||||||
if (!property.FieldType.IsEnum)
|
if (!propertyDefinition.FieldType.IsEnum)
|
||||||
{
|
{
|
||||||
return "EnumToggleButtons attribute can be used only on enums";
|
return "EnumToggleButtons attribute can be used only on enums";
|
||||||
}
|
}
|
||||||
|
|
||||||
return base.CanDraw(property);
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override TriElement CreateElement(TriProperty property, TriElement next)
|
public override TriElement CreateElement(TriProperty property, TriElement next)
|
||||||
|
@ -13,9 +13,9 @@ namespace TriInspector.Drawers
|
|||||||
{
|
{
|
||||||
public class InlineEditorDrawer : TriAttributeDrawer<InlineEditorAttribute>
|
public class InlineEditorDrawer : TriAttributeDrawer<InlineEditorAttribute>
|
||||||
{
|
{
|
||||||
public override string CanDraw(TriProperty property)
|
public override string Initialize(TriPropertyDefinition propertyDefinition)
|
||||||
{
|
{
|
||||||
if (!typeof(Object).IsAssignableFrom(property.FieldType))
|
if (!typeof(Object).IsAssignableFrom(propertyDefinition.FieldType))
|
||||||
{
|
{
|
||||||
return "[InlineEditor] valid only on Object fields";
|
return "[InlineEditor] valid only on Object fields";
|
||||||
}
|
}
|
||||||
|
@ -10,21 +10,17 @@ namespace TriInspector.Drawers
|
|||||||
{
|
{
|
||||||
private ActionResolver _actionResolver;
|
private ActionResolver _actionResolver;
|
||||||
|
|
||||||
public override void Initialize(TriPropertyDefinition propertyDefinition)
|
public override string Initialize(TriPropertyDefinition propertyDefinition)
|
||||||
{
|
{
|
||||||
base.Initialize(propertyDefinition);
|
base.Initialize(propertyDefinition);
|
||||||
|
|
||||||
_actionResolver = ActionResolver.Resolve(propertyDefinition, Attribute.Method);
|
_actionResolver = ActionResolver.Resolve(propertyDefinition, Attribute.Method);
|
||||||
}
|
|
||||||
|
|
||||||
public override string CanDraw(TriProperty property)
|
|
||||||
{
|
|
||||||
if (_actionResolver.TryGetErrorString(out var error))
|
if (_actionResolver.TryGetErrorString(out var error))
|
||||||
{
|
{
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return base.CanDraw(property);
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override TriElement CreateElement(TriProperty property, TriElement next)
|
public override TriElement CreateElement(TriProperty property, TriElement next)
|
||||||
|
@ -16,9 +16,9 @@ namespace TriInspector.Drawers
|
|||||||
{
|
{
|
||||||
public class TableListDrawer : TriAttributeDrawer<TableListAttribute>
|
public class TableListDrawer : TriAttributeDrawer<TableListAttribute>
|
||||||
{
|
{
|
||||||
public override string CanDraw(TriProperty property)
|
public override string Initialize(TriPropertyDefinition propertyDefinition)
|
||||||
{
|
{
|
||||||
if (property.PropertyType != TriPropertyType.Array)
|
if (!propertyDefinition.IsArray)
|
||||||
{
|
{
|
||||||
return "[TableList] valid only on lists";
|
return "[TableList] valid only on lists";
|
||||||
}
|
}
|
||||||
|
@ -17,21 +17,18 @@ namespace TriInspector.Drawers
|
|||||||
|
|
||||||
private ValueResolver<string> _titleResolver;
|
private ValueResolver<string> _titleResolver;
|
||||||
|
|
||||||
public override void Initialize(TriPropertyDefinition propertyDefinition)
|
public override string Initialize(TriPropertyDefinition propertyDefinition)
|
||||||
{
|
{
|
||||||
base.Initialize(propertyDefinition);
|
base.Initialize(propertyDefinition);
|
||||||
|
|
||||||
_titleResolver = ValueResolver.ResolveString(propertyDefinition, Attribute.Title);
|
_titleResolver = ValueResolver.ResolveString(propertyDefinition, Attribute.Title);
|
||||||
}
|
|
||||||
|
|
||||||
public override string CanDraw(TriProperty property)
|
|
||||||
{
|
|
||||||
if (_titleResolver.TryGetErrorString(out var error))
|
if (_titleResolver.TryGetErrorString(out var error))
|
||||||
{
|
{
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return base.CanDraw(property);
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override float GetHeight(float width, TriProperty property, TriElement next)
|
public override float GetHeight(float width, TriProperty property, TriElement next)
|
||||||
|
@ -19,11 +19,17 @@ namespace TriInspector.Processors
|
|||||||
_inverse = inverse;
|
_inverse = inverse;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Initialize(TriPropertyDefinition propertyDefinition)
|
public override string Initialize(TriPropertyDefinition propertyDefinition)
|
||||||
{
|
{
|
||||||
base.Initialize(propertyDefinition);
|
base.Initialize(propertyDefinition);
|
||||||
|
|
||||||
_conditionResolver = ValueResolver.Resolve<object>(propertyDefinition, Attribute.Condition);
|
_conditionResolver = ValueResolver.Resolve<object>(propertyDefinition, Attribute.Condition);
|
||||||
|
if (_conditionResolver.TryGetErrorString(out var error))
|
||||||
|
{
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed override bool IsDisabled(TriProperty property)
|
public sealed override bool IsDisabled(TriProperty property)
|
||||||
|
@ -19,11 +19,18 @@ namespace TriInspector.Processors
|
|||||||
_inverse = inverse;
|
_inverse = inverse;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void Initialize(TriPropertyDefinition propertyDefinition)
|
public override string Initialize(TriPropertyDefinition propertyDefinition)
|
||||||
{
|
{
|
||||||
base.Initialize(propertyDefinition);
|
base.Initialize(propertyDefinition);
|
||||||
|
|
||||||
_conditionResolver = ValueResolver.Resolve<object>(propertyDefinition, Attribute.Condition);
|
_conditionResolver = ValueResolver.Resolve<object>(propertyDefinition, Attribute.Condition);
|
||||||
|
|
||||||
|
if (_conditionResolver.TryGetErrorString(out var error))
|
||||||
|
{
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed override bool IsHidden(TriProperty property)
|
public sealed override bool IsHidden(TriProperty property)
|
||||||
|
@ -11,14 +11,25 @@ namespace TriInspector.Validators
|
|||||||
private ValueResolver<string> _resolver;
|
private ValueResolver<string> _resolver;
|
||||||
private ValueResolver<bool> _visibleIfResolver;
|
private ValueResolver<bool> _visibleIfResolver;
|
||||||
|
|
||||||
public override void Initialize(TriPropertyDefinition propertyDefinition)
|
public override string Initialize(TriPropertyDefinition propertyDefinition)
|
||||||
{
|
{
|
||||||
base.Initialize(propertyDefinition);
|
|
||||||
|
|
||||||
_resolver = ValueResolver.ResolveString(propertyDefinition, Attribute.Text);
|
_resolver = ValueResolver.ResolveString(propertyDefinition, Attribute.Text);
|
||||||
_visibleIfResolver = Attribute.VisibleIf != null
|
_visibleIfResolver = Attribute.VisibleIf != null
|
||||||
? ValueResolver.Resolve<bool>(propertyDefinition, Attribute.VisibleIf)
|
? ValueResolver.Resolve<bool>(propertyDefinition, Attribute.VisibleIf)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
if (_resolver.TryGetErrorString(out var error))
|
||||||
|
{
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_visibleIfResolver != null &&
|
||||||
|
_visibleIfResolver.TryGetErrorString(out var error2))
|
||||||
|
{
|
||||||
|
return error2;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override TriValidationResult Validate(TriProperty property)
|
public override TriValidationResult Validate(TriProperty property)
|
||||||
|
@ -10,11 +10,18 @@ namespace TriInspector.Validators
|
|||||||
{
|
{
|
||||||
private ValueResolver<TriValidationResult> _resolver;
|
private ValueResolver<TriValidationResult> _resolver;
|
||||||
|
|
||||||
public override void Initialize(TriPropertyDefinition propertyDefinition)
|
public override string Initialize(TriPropertyDefinition propertyDefinition)
|
||||||
{
|
{
|
||||||
base.Initialize(propertyDefinition);
|
base.Initialize(propertyDefinition);
|
||||||
|
|
||||||
_resolver = ValueResolver.Resolve<TriValidationResult>(propertyDefinition, Attribute.Method);
|
_resolver = ValueResolver.Resolve<TriValidationResult>(propertyDefinition, Attribute.Method);
|
||||||
|
|
||||||
|
if (_resolver.TryGetErrorString(out var error))
|
||||||
|
{
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override TriValidationResult Validate(TriProperty property)
|
public override TriValidationResult Validate(TriProperty property)
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using TriInspector.Utilities;
|
|
||||||
using TriInspectorUnityInternalBridge;
|
using TriInspectorUnityInternalBridge;
|
||||||
using UnityEditor;
|
using UnityEditor;
|
||||||
using UnityEngine;
|
using UnityEngine;
|
||||||
@ -25,16 +24,7 @@ namespace TriInspector.Elements
|
|||||||
var drawers = property.AllDrawers;
|
var drawers = property.AllDrawers;
|
||||||
for (var index = drawers.Count - 1; index >= 0; index--)
|
for (var index = drawers.Count - 1; index >= 0; index--)
|
||||||
{
|
{
|
||||||
var drawer = drawers[index];
|
element = drawers[index].CreateElementInternal(property, element);
|
||||||
|
|
||||||
var canDrawMessage = drawer.CanDraw(_property);
|
|
||||||
if (!string.IsNullOrEmpty(canDrawMessage))
|
|
||||||
{
|
|
||||||
AddChild(new TriInfoBoxElement(canDrawMessage, TriMessageType.Error));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
element = drawer.CreateElementInternal(property, element);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
AddChild(element);
|
AddChild(element);
|
||||||
|
@ -1,22 +1,8 @@
|
|||||||
using JetBrains.Annotations;
|
namespace TriInspector
|
||||||
|
|
||||||
namespace TriInspector
|
|
||||||
{
|
{
|
||||||
public abstract class TriCustomDrawer
|
public abstract class TriCustomDrawer : TriPropertyExtension
|
||||||
{
|
{
|
||||||
internal int Order { get; set; }
|
internal int Order { get; set; }
|
||||||
internal bool? ApplyOnArrayElement { get; set; }
|
|
||||||
|
|
||||||
[PublicAPI]
|
|
||||||
public virtual void Initialize(TriPropertyDefinition propertyDefinition)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
[PublicAPI]
|
|
||||||
public virtual string CanDraw(TriProperty property)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract TriElement CreateElementInternal(TriProperty property, TriElement next);
|
public abstract TriElement CreateElementInternal(TriProperty property, TriElement next);
|
||||||
}
|
}
|
||||||
|
@ -103,13 +103,8 @@ namespace TriInspector
|
|||||||
{
|
{
|
||||||
_hideProcessorsBackingField =
|
_hideProcessorsBackingField =
|
||||||
TriDrawersUtilities.CreateHideProcessorsFor(Attributes)
|
TriDrawersUtilities.CreateHideProcessorsFor(Attributes)
|
||||||
.Where(it => CanApplyOn(this, applyOnArrayElement: false))
|
.Where(CanApplyExtensionOnSelf)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
foreach (var processor in _hideProcessorsBackingField)
|
|
||||||
{
|
|
||||||
processor.Initialize(this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return _hideProcessorsBackingField;
|
return _hideProcessorsBackingField;
|
||||||
@ -124,13 +119,8 @@ namespace TriInspector
|
|||||||
{
|
{
|
||||||
_disableProcessorsBackingField =
|
_disableProcessorsBackingField =
|
||||||
TriDrawersUtilities.CreateDisableProcessorsFor(Attributes)
|
TriDrawersUtilities.CreateDisableProcessorsFor(Attributes)
|
||||||
.Where(it => CanApplyOn(this, applyOnArrayElement: false))
|
.Where(CanApplyExtensionOnSelf)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
foreach (var processor in _disableProcessorsBackingField)
|
|
||||||
{
|
|
||||||
processor.Initialize(this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return _disableProcessorsBackingField;
|
return _disableProcessorsBackingField;
|
||||||
@ -150,14 +140,9 @@ namespace TriInspector
|
|||||||
{
|
{
|
||||||
new ValidatorsDrawer {Order = TriDrawerOrder.Validator,},
|
new ValidatorsDrawer {Order = TriDrawerOrder.Validator,},
|
||||||
})
|
})
|
||||||
.Where(it => CanApplyOn(this, it.ApplyOnArrayElement))
|
.Where(CanApplyExtensionOnSelf)
|
||||||
.OrderBy(it => it.Order)
|
.OrderBy(it => it.Order)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
foreach (var drawer in _drawersBackingField)
|
|
||||||
{
|
|
||||||
drawer.Initialize(this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return _drawersBackingField;
|
return _drawersBackingField;
|
||||||
@ -173,13 +158,8 @@ namespace TriInspector
|
|||||||
_validatorsBackingField = Enumerable.Empty<TriValidator>()
|
_validatorsBackingField = Enumerable.Empty<TriValidator>()
|
||||||
.Concat(TriDrawersUtilities.CreateValueValidatorsFor(FieldType))
|
.Concat(TriDrawersUtilities.CreateValueValidatorsFor(FieldType))
|
||||||
.Concat(TriDrawersUtilities.CreateAttributeValidatorsFor(Attributes))
|
.Concat(TriDrawersUtilities.CreateAttributeValidatorsFor(Attributes))
|
||||||
.Where(it => CanApplyOn(this, it.ApplyOnArrayElement))
|
.Where(CanApplyExtensionOnSelf)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
foreach (var validator in _validatorsBackingField)
|
|
||||||
{
|
|
||||||
validator.Initialize(this);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return _validatorsBackingField;
|
return _validatorsBackingField;
|
||||||
@ -296,16 +276,23 @@ namespace TriInspector
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool CanApplyOn(TriPropertyDefinition definition, bool? applyOnArrayElement)
|
private bool CanApplyExtensionOnSelf(TriPropertyExtension propertyExtension)
|
||||||
{
|
{
|
||||||
if (!applyOnArrayElement.HasValue)
|
if (propertyExtension.ApplyOnArrayElement.HasValue)
|
||||||
{
|
{
|
||||||
return true;
|
if (IsArrayElement && !propertyExtension.ApplyOnArrayElement.Value ||
|
||||||
|
IsArray && propertyExtension.ApplyOnArrayElement.Value)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (definition.IsArrayElement && !applyOnArrayElement.Value ||
|
var error = propertyExtension.Initialize(this);
|
||||||
definition.IsArray && applyOnArrayElement.Value)
|
if (error != null)
|
||||||
{
|
{
|
||||||
|
var message = $"Extension {propertyExtension.GetType()} cannot be applied " +
|
||||||
|
$"on property '{MemberInfo?.DeclaringType}.{Name}': {error}";
|
||||||
|
Debug.LogError(message);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,15 +3,10 @@ using JetBrains.Annotations;
|
|||||||
|
|
||||||
namespace TriInspector
|
namespace TriInspector
|
||||||
{
|
{
|
||||||
public abstract class TriPropertyDisableProcessor
|
public abstract class TriPropertyDisableProcessor : TriPropertyExtension
|
||||||
{
|
{
|
||||||
internal Attribute RawAttribute { get; set; }
|
internal Attribute RawAttribute { get; set; }
|
||||||
|
|
||||||
[PublicAPI]
|
|
||||||
public virtual void Initialize(TriPropertyDefinition propertyDefinition)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public abstract bool IsDisabled(TriProperty property);
|
public abstract bool IsDisabled(TriProperty property);
|
||||||
}
|
}
|
||||||
|
15
Editor/TriPropertyExtension.cs
Normal file
15
Editor/TriPropertyExtension.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using JetBrains.Annotations;
|
||||||
|
|
||||||
|
namespace TriInspector
|
||||||
|
{
|
||||||
|
public abstract class TriPropertyExtension
|
||||||
|
{
|
||||||
|
public bool? ApplyOnArrayElement { get; internal set; }
|
||||||
|
|
||||||
|
[PublicAPI]
|
||||||
|
public virtual string Initialize(TriPropertyDefinition propertyDefinition)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
Editor/TriPropertyExtension.cs.meta
Normal file
3
Editor/TriPropertyExtension.cs.meta
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4da7122af7f84f1e8c0f2986e064e161
|
||||||
|
timeCreated: 1654255038
|
@ -3,15 +3,10 @@ using JetBrains.Annotations;
|
|||||||
|
|
||||||
namespace TriInspector
|
namespace TriInspector
|
||||||
{
|
{
|
||||||
public abstract class TriPropertyHideProcessor
|
public abstract class TriPropertyHideProcessor : TriPropertyExtension
|
||||||
{
|
{
|
||||||
internal Attribute RawAttribute { get; set; }
|
internal Attribute RawAttribute { get; set; }
|
||||||
|
|
||||||
[PublicAPI]
|
|
||||||
public virtual void Initialize(TriPropertyDefinition propertyDefinition)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public abstract bool IsHidden(TriProperty property);
|
public abstract bool IsHidden(TriProperty property);
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using UnityEditor;
|
|
||||||
|
|
||||||
namespace TriInspector
|
namespace TriInspector
|
||||||
{
|
{
|
||||||
public abstract class TriValidator
|
public abstract class TriValidator : TriPropertyExtension
|
||||||
{
|
{
|
||||||
internal bool ApplyOnArrayElement { get; set; }
|
|
||||||
|
|
||||||
[PublicAPI]
|
|
||||||
public virtual void Initialize(TriPropertyDefinition propertyDefinition)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
[PublicAPI]
|
[PublicAPI]
|
||||||
public abstract TriValidationResult Validate(TriProperty property);
|
public abstract TriValidationResult Validate(TriProperty property);
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
"name": "com.codewriter.triinspector",
|
"name": "com.codewriter.triinspector",
|
||||||
"displayName": "Tri Inspector",
|
"displayName": "Tri Inspector",
|
||||||
"description": "Advanced inspector attributes for Unity",
|
"description": "Advanced inspector attributes for Unity",
|
||||||
"version": "1.2.0",
|
"version": "1.3.0",
|
||||||
"unity": "2020.3",
|
"unity": "2020.3",
|
||||||
"author": "CodeWriter (https://github.com/orgs/codewriter-packages)",
|
"author": "CodeWriter (https://github.com/orgs/codewriter-packages)",
|
||||||
"homepage": "https://github.com/codewriter-packages/Tri-Inspector#readme",
|
"homepage": "https://github.com/codewriter-packages/Tri-Inspector#readme",
|
||||||
|
Loading…
Reference in New Issue
Block a user