simplifying the inputcontext stuff
This commit is contained in:
parent
d947fb3999
commit
0a0acb5e27
@ -10,9 +10,7 @@
|
|||||||
#include <QTextCharFormat>
|
#include <QTextCharFormat>
|
||||||
#include <QWindow>
|
#include <QWindow>
|
||||||
|
|
||||||
#include <unordered_map>
|
static inline QString qskKeyString( int code )
|
||||||
|
|
||||||
static QString qskKeyString( int code )
|
|
||||||
{
|
{
|
||||||
// Special case entry codes here, else default to the symbol
|
// Special case entry codes here, else default to the symbol
|
||||||
switch ( code )
|
switch ( code )
|
||||||
@ -38,6 +36,15 @@ static QString qskKeyString( int code )
|
|||||||
return QChar( code );
|
return QChar( code );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static inline void qskSendKeyEvents( QObject* receiver, int key )
|
||||||
|
{
|
||||||
|
QKeyEvent keyPress( QEvent::KeyPress, key, Qt::NoModifier );
|
||||||
|
QCoreApplication::sendEvent( receiver, &keyPress );
|
||||||
|
|
||||||
|
QKeyEvent keyRelease( QEvent::KeyRelease, key, Qt::NoModifier );
|
||||||
|
QCoreApplication::sendEvent( receiver, &keyRelease );
|
||||||
|
}
|
||||||
|
|
||||||
class QskInputCompositionModel::PrivateData
|
class QskInputCompositionModel::PrivateData
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@ -55,14 +62,6 @@ public:
|
|||||||
int groupIndex;
|
int groupIndex;
|
||||||
};
|
};
|
||||||
|
|
||||||
static inline void sendCompositionEvent( QInputMethodEvent* e )
|
|
||||||
{
|
|
||||||
if( auto focusObject = QGuiApplication::focusObject() )
|
|
||||||
{
|
|
||||||
QCoreApplication::sendEvent( focusObject, e );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
QskInputCompositionModel::QskInputCompositionModel():
|
QskInputCompositionModel::QskInputCompositionModel():
|
||||||
m_data( new PrivateData )
|
m_data( new PrivateData )
|
||||||
{
|
{
|
||||||
@ -148,20 +147,12 @@ void QskInputCompositionModel::composeKey( Qt::Key key )
|
|||||||
{
|
{
|
||||||
commit( qskKeyString( key ) );
|
commit( qskKeyString( key ) );
|
||||||
}
|
}
|
||||||
#if 0
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
auto focusWindow = QGuiApplication::focusWindow();
|
if( auto focusWindow = QGuiApplication::focusWindow() )
|
||||||
|
qskSendKeyEvents( focusWindow, Qt::Key_Return );
|
||||||
if( focusWindow )
|
|
||||||
{
|
|
||||||
QKeyEvent keyPress( QEvent::KeyPress, Qt::Key_Return, Qt::NoModifier );
|
|
||||||
QKeyEvent keyRelease( QEvent::KeyRelease, Qt::Key_Return, Qt::NoModifier ); QCoreApplication::sendEvent( focusWindow, &keyPress );
|
|
||||||
QCoreApplication::sendEvent( focusWindow, &keyRelease );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,8 +209,9 @@ void QskInputCompositionModel::composeKey( Qt::Key key )
|
|||||||
}
|
}
|
||||||
|
|
||||||
m_data->preeditAttributes.first().length = displayPreedit.length();
|
m_data->preeditAttributes.first().length = displayPreedit.length();
|
||||||
QInputMethodEvent e( displayPreedit, m_data->preeditAttributes );
|
|
||||||
sendCompositionEvent( &e );
|
QInputMethodEvent event( displayPreedit, m_data->preeditAttributes );
|
||||||
|
sendCompositionEvent( &event );
|
||||||
}
|
}
|
||||||
|
|
||||||
void QskInputCompositionModel::clearPreedit()
|
void QskInputCompositionModel::clearPreedit()
|
||||||
@ -250,9 +242,10 @@ QString QskInputCompositionModel::polishPreedit( const QString& preedit )
|
|||||||
|
|
||||||
void QskInputCompositionModel::commit( const QString& text )
|
void QskInputCompositionModel::commit( const QString& text )
|
||||||
{
|
{
|
||||||
QInputMethodEvent e( QString(), { } );
|
QInputMethodEvent event( QString(), { } );
|
||||||
e.setCommitString( text );
|
event.setCommitString( text );
|
||||||
sendCompositionEvent( &e );
|
sendCompositionEvent( &event );
|
||||||
|
|
||||||
m_data->preedit.clear();
|
m_data->preedit.clear();
|
||||||
polishPreedit( m_data->preedit );
|
polishPreedit( m_data->preedit );
|
||||||
}
|
}
|
||||||
@ -264,27 +257,24 @@ void QskInputCompositionModel::commitCandidate( int index )
|
|||||||
|
|
||||||
void QskInputCompositionModel::backspace()
|
void QskInputCompositionModel::backspace()
|
||||||
{
|
{
|
||||||
if ( !m_data->inputItem )
|
if ( m_data->inputItem == nullptr )
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if ( !m_data->preedit.isEmpty() )
|
if ( !m_data->preedit.isEmpty() )
|
||||||
{
|
{
|
||||||
m_data->preedit.chop( 1 );
|
m_data->preedit.chop( 1 );
|
||||||
|
|
||||||
|
const QString displayText = polishPreedit( m_data->preedit );
|
||||||
|
m_data->preeditAttributes.first().length = displayText.length();
|
||||||
|
|
||||||
|
QInputMethodEvent event( displayText, m_data->preeditAttributes );
|
||||||
|
sendCompositionEvent( &event );
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Backspace one character only if preedit was inactive
|
// Backspace one character only if preedit was inactive
|
||||||
QKeyEvent keyPress( QEvent::KeyPress, Qt::Key_Backspace, Qt::NoModifier );
|
qskSendKeyEvents( m_data->inputItem, Qt::Key_Backspace );
|
||||||
QKeyEvent keyRelease( QEvent::KeyRelease, Qt::Key_Backspace, Qt::NoModifier );
|
|
||||||
QCoreApplication::sendEvent( m_data->inputItem, &keyPress );
|
|
||||||
QCoreApplication::sendEvent( m_data->inputItem, &keyRelease );
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const QString displayText = polishPreedit( m_data->preedit );
|
|
||||||
m_data->preeditAttributes.first().length = displayText.length();
|
|
||||||
QInputMethodEvent e( displayText, m_data->preeditAttributes );
|
|
||||||
sendCompositionEvent( &e );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void QskInputCompositionModel::moveCursor( Qt::Key key )
|
void QskInputCompositionModel::moveCursor( Qt::Key key )
|
||||||
@ -339,8 +329,9 @@ void QskInputCompositionModel::setGroupIndex( int groupIndex )
|
|||||||
m_data->groupIndex = groupIndex;
|
m_data->groupIndex = groupIndex;
|
||||||
const QString displayText = polishPreedit( m_data->preedit );
|
const QString displayText = polishPreedit( m_data->preedit );
|
||||||
m_data->preeditAttributes.first().length = displayText.length();
|
m_data->preeditAttributes.first().length = displayText.length();
|
||||||
QInputMethodEvent e( displayText, m_data->preeditAttributes );
|
|
||||||
sendCompositionEvent( &e );
|
QInputMethodEvent event( displayText, m_data->preeditAttributes );
|
||||||
|
sendCompositionEvent( &event );
|
||||||
}
|
}
|
||||||
|
|
||||||
QVector< Qt::Key > QskInputCompositionModel::groups() const
|
QVector< Qt::Key > QskInputCompositionModel::groups() const
|
||||||
|
@ -23,7 +23,7 @@ public:
|
|||||||
QPointer< QQuickItem > inputItem;
|
QPointer< QQuickItem > inputItem;
|
||||||
QPointer< QskVirtualKeyboard > inputPanel;
|
QPointer< QskVirtualKeyboard > inputPanel;
|
||||||
QskInputCompositionModel* compositionModel;
|
QskInputCompositionModel* compositionModel;
|
||||||
QHash< QLocale, QskInputCompositionModel* > inputModels;
|
QHash< QLocale, QskInputCompositionModel* > compositionModels;
|
||||||
};
|
};
|
||||||
|
|
||||||
QskInputContext::QskInputContext() :
|
QskInputContext::QskInputContext() :
|
||||||
@ -31,17 +31,17 @@ QskInputContext::QskInputContext() :
|
|||||||
{
|
{
|
||||||
m_data->compositionModel = new QskInputCompositionModel();
|
m_data->compositionModel = new QskInputCompositionModel();
|
||||||
|
|
||||||
|
connect( m_data->compositionModel, &QskInputCompositionModel::candidatesChanged,
|
||||||
|
this, &QskInputContext::handleCandidatesChanged );
|
||||||
|
|
||||||
connect( qskSetup, &QskSetup::inputPanelChanged,
|
connect( qskSetup, &QskSetup::inputPanelChanged,
|
||||||
this, &QskInputContext::setInputPanel );
|
this, &QskInputContext::setInputPanel );
|
||||||
|
|
||||||
|
#if 1
|
||||||
|
setCompositionModel( QLocale::Chinese, new QskPinyinCompositionModel() );
|
||||||
|
#endif
|
||||||
|
|
||||||
setInputPanel( qskSetup->inputPanel() );
|
setInputPanel( qskSetup->inputPanel() );
|
||||||
|
|
||||||
QskPinyinCompositionModel* pinyinModel = new QskPinyinCompositionModel;
|
|
||||||
// see also: QskVirtualKeyboard::registerCompositionModelForLocale()
|
|
||||||
inputMethodRegistered( QLocale::Chinese, pinyinModel );
|
|
||||||
|
|
||||||
// We could connect candidatesChanged() here, but we don't emit
|
|
||||||
// the signal in the normal composition model anyhow
|
|
||||||
}
|
}
|
||||||
|
|
||||||
QskInputContext::~QskInputContext()
|
QskInputContext::~QskInputContext()
|
||||||
@ -49,7 +49,7 @@ QskInputContext::~QskInputContext()
|
|||||||
if ( m_data->inputPanel )
|
if ( m_data->inputPanel )
|
||||||
delete m_data->inputPanel;
|
delete m_data->inputPanel;
|
||||||
|
|
||||||
qDeleteAll( m_data->inputModels );
|
qDeleteAll( m_data->compositionModels );
|
||||||
}
|
}
|
||||||
|
|
||||||
bool QskInputContext::isValid() const
|
bool QskInputContext::isValid() const
|
||||||
@ -249,18 +249,44 @@ void QskInputContext::setFocusObject( QObject* focusObject )
|
|||||||
update( Qt::InputMethodQuery( Qt::ImQueryAll & ~Qt::ImEnabled ) );
|
update( Qt::InputMethodQuery( Qt::ImQueryAll & ~Qt::ImEnabled ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
void QskInputContext::inputMethodRegistered(
|
void QskInputContext::setCompositionModel(
|
||||||
const QLocale& locale, QskInputCompositionModel* model )
|
const QLocale& locale, QskInputCompositionModel* model )
|
||||||
{
|
{
|
||||||
if ( auto oldModel = m_data->inputModels.value( locale, nullptr ) )
|
auto& models = m_data->compositionModels;
|
||||||
oldModel->deleteLater();
|
|
||||||
|
|
||||||
m_data->inputModels.insert( locale, model );
|
if ( model )
|
||||||
|
{
|
||||||
|
const auto it = models.find( locale );
|
||||||
|
if ( it != models.end() )
|
||||||
|
{
|
||||||
|
if ( it.value() == model )
|
||||||
|
return;
|
||||||
|
|
||||||
|
delete it.value();
|
||||||
|
*it = model;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
models.insert( locale, model );
|
||||||
|
}
|
||||||
|
|
||||||
|
connect( model, &QskInputCompositionModel::candidatesChanged,
|
||||||
|
this, &QskInputContext::handleCandidatesChanged );
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const auto it = models.find( locale );
|
||||||
|
if ( it != models.end() )
|
||||||
|
{
|
||||||
|
delete it.value();
|
||||||
|
models.erase( it );
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
QskInputCompositionModel* QskInputContext::compositionModel() const
|
QskInputCompositionModel* QskInputContext::compositionModel() const
|
||||||
{
|
{
|
||||||
return m_data->inputModels.value( locale(), m_data->compositionModel );
|
return m_data->compositionModels.value( locale(), m_data->compositionModel );
|
||||||
}
|
}
|
||||||
|
|
||||||
void QskInputContext::invokeAction( QInputMethod::Action action, int cursorPosition )
|
void QskInputContext::invokeAction( QInputMethod::Action action, int cursorPosition )
|
||||||
@ -282,6 +308,7 @@ void QskInputContext::invokeAction( QInputMethod::Action action, int cursorPosit
|
|||||||
case QskVirtualKeyboard::SelectCandidate:
|
case QskVirtualKeyboard::SelectCandidate:
|
||||||
{
|
{
|
||||||
model->commitCandidate( cursorPosition );
|
model->commitCandidate( cursorPosition );
|
||||||
|
|
||||||
if ( m_data->inputPanel )
|
if ( m_data->inputPanel )
|
||||||
m_data->inputPanel->setPreeditCandidates( QVector< QString >() );
|
m_data->inputPanel->setPreeditCandidates( QVector< QString >() );
|
||||||
|
|
||||||
@ -290,19 +317,19 @@ void QskInputContext::invokeAction( QInputMethod::Action action, int cursorPosit
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void QskInputContext::emitAnimatingChanged()
|
|
||||||
{
|
|
||||||
QPlatformInputContext::emitAnimatingChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
void QskInputContext::handleCandidatesChanged()
|
void QskInputContext::handleCandidatesChanged()
|
||||||
{
|
{
|
||||||
const auto model = compositionModel();
|
const auto model = compositionModel();
|
||||||
|
if ( model == nullptr || m_data->inputPanel == nullptr )
|
||||||
|
return;
|
||||||
|
|
||||||
QVector< QString > candidates( model->candidateCount() );
|
const auto count = model->candidateCount();
|
||||||
|
|
||||||
for( int i = 0; i < candidates.length(); ++i )
|
QVector< QString > candidates;
|
||||||
candidates[i] = model->candidate( i );
|
candidates.reserve( count );
|
||||||
|
|
||||||
|
for( int i = 0; i < count; i++ )
|
||||||
|
candidates += model->candidate( i );
|
||||||
|
|
||||||
m_data->inputPanel->setPreeditCandidates( candidates );
|
m_data->inputPanel->setPreeditCandidates( candidates );
|
||||||
}
|
}
|
||||||
|
@ -38,11 +38,11 @@ public:
|
|||||||
|
|
||||||
virtual QLocale locale() const override;
|
virtual QLocale locale() const override;
|
||||||
|
|
||||||
|
void setCompositionModel( const QLocale&, QskInputCompositionModel* );
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
void emitAnimatingChanged();
|
|
||||||
void handleCandidatesChanged();
|
void handleCandidatesChanged();
|
||||||
void setInputPanel( QskVirtualKeyboard* );
|
void setInputPanel( QskVirtualKeyboard* );
|
||||||
void inputMethodRegistered( const QLocale&, QskInputCompositionModel* );
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QskInputCompositionModel* compositionModel() const;
|
QskInputCompositionModel* compositionModel() const;
|
||||||
|
@ -174,7 +174,6 @@ QSK_SUBCONTROL( QskVirtualKeyboard, Panel )
|
|||||||
|
|
||||||
QSK_SUBCONTROL( QskVirtualKeyboardButton, Panel )
|
QSK_SUBCONTROL( QskVirtualKeyboardButton, Panel )
|
||||||
QSK_SUBCONTROL( QskVirtualKeyboardButton, Text )
|
QSK_SUBCONTROL( QskVirtualKeyboardButton, Text )
|
||||||
QSK_SUBCONTROL( QskVirtualKeyboardButton, TextCancelButton )
|
|
||||||
|
|
||||||
QskVirtualKeyboardButton::QskVirtualKeyboardButton(
|
QskVirtualKeyboardButton::QskVirtualKeyboardButton(
|
||||||
int keyIndex, QskVirtualKeyboard* inputPanel, QQuickItem* parent ) :
|
int keyIndex, QskVirtualKeyboard* inputPanel, QQuickItem* parent ) :
|
||||||
@ -214,7 +213,7 @@ QskAspect::Subcontrol QskVirtualKeyboardButton::effectiveSubcontrol(
|
|||||||
return QskVirtualKeyboardButton::Panel;
|
return QskVirtualKeyboardButton::Panel;
|
||||||
|
|
||||||
if( subControl == QskPushButton::Text )
|
if( subControl == QskPushButton::Text )
|
||||||
return isCancelButton() ? TextCancelButton : Text;
|
return QskVirtualKeyboardButton::Text;
|
||||||
|
|
||||||
return subControl;
|
return subControl;
|
||||||
}
|
}
|
||||||
@ -239,13 +238,6 @@ void QskVirtualKeyboardButton::updateText()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool QskVirtualKeyboardButton::isCancelButton() const
|
|
||||||
{
|
|
||||||
auto keyData = m_inputPanel->keyDataAt( m_keyIndex );
|
|
||||||
bool isCancel = ( keyData.key == 0x2716 );
|
|
||||||
return isCancel;
|
|
||||||
}
|
|
||||||
|
|
||||||
class QskVirtualKeyboard::PrivateData
|
class QskVirtualKeyboard::PrivateData
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@ -567,12 +559,6 @@ void QskVirtualKeyboard::setCandidateOffset( int candidateOffset )
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void QskVirtualKeyboard::registerCompositionModelForLocale(
|
|
||||||
const QLocale& locale, QskInputCompositionModel* model )
|
|
||||||
{
|
|
||||||
Q_EMIT inputMethodRegistered( locale, model );
|
|
||||||
}
|
|
||||||
|
|
||||||
void QskVirtualKeyboard::geometryChanged(
|
void QskVirtualKeyboard::geometryChanged(
|
||||||
const QRectF& newGeometry, const QRectF& oldGeometry )
|
const QRectF& newGeometry, const QRectF& oldGeometry )
|
||||||
{
|
{
|
||||||
@ -685,11 +671,6 @@ void QskVirtualKeyboard::handleKey( int keyIndex )
|
|||||||
: SpecialCharacterMode ) );
|
: SpecialCharacterMode ) );
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// This is (one of) the cancel symbol, not Qt::Key_Cancel:
|
|
||||||
case Qt::Key( 10006 ):
|
|
||||||
Q_EMIT cancelPressed();
|
|
||||||
return;
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -44,11 +44,13 @@ class QSK_EXPORT QskVirtualKeyboardButton : public QskPushButton
|
|||||||
using Inherited = QskPushButton;
|
using Inherited = QskPushButton;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
QSK_SUBCONTROLS( Panel, Text, TextCancelButton )
|
QSK_SUBCONTROLS( Panel, Text )
|
||||||
|
|
||||||
QskVirtualKeyboardButton( int keyIndex, QskVirtualKeyboard* inputPanel, QQuickItem* parent = nullptr );
|
QskVirtualKeyboardButton( int keyIndex,
|
||||||
|
QskVirtualKeyboard*, QQuickItem* parent = nullptr );
|
||||||
|
|
||||||
virtual QskAspect::Subcontrol effectiveSubcontrol( QskAspect::Subcontrol subControl ) const override;
|
virtual QskAspect::Subcontrol effectiveSubcontrol(
|
||||||
|
QskAspect::Subcontrol ) const override;
|
||||||
|
|
||||||
int keyIndex() const;
|
int keyIndex() const;
|
||||||
|
|
||||||
@ -56,8 +58,6 @@ public Q_SLOTS:
|
|||||||
void updateText();
|
void updateText();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
bool isCancelButton() const;
|
|
||||||
|
|
||||||
const int m_keyIndex;
|
const int m_keyIndex;
|
||||||
QskVirtualKeyboard* m_inputPanel;
|
QskVirtualKeyboard* m_inputPanel;
|
||||||
};
|
};
|
||||||
@ -112,11 +112,12 @@ public:
|
|||||||
QskVirtualKeyboard( QQuickItem* parent = nullptr );
|
QskVirtualKeyboard( QQuickItem* parent = nullptr );
|
||||||
virtual ~QskVirtualKeyboard() override;
|
virtual ~QskVirtualKeyboard() override;
|
||||||
|
|
||||||
virtual QskAspect::Subcontrol effectiveSubcontrol( QskAspect::Subcontrol subControl ) const override;
|
virtual QskAspect::Subcontrol effectiveSubcontrol(
|
||||||
|
QskAspect::Subcontrol ) const override;
|
||||||
|
|
||||||
void updateLocale( const QLocale& locale );
|
void updateLocale( const QLocale& );
|
||||||
|
|
||||||
void setMode( QskVirtualKeyboard::Mode index );
|
void setMode( QskVirtualKeyboard::Mode );
|
||||||
Mode mode() const;
|
Mode mode() const;
|
||||||
|
|
||||||
const KeyDataSet& keyData( QskVirtualKeyboard::Mode = CurrentMode ) const;
|
const KeyDataSet& keyData( QskVirtualKeyboard::Mode = CurrentMode ) const;
|
||||||
@ -124,9 +125,6 @@ public:
|
|||||||
QString textForKey( int ) const;
|
QString textForKey( int ) const;
|
||||||
QString displayLanguageName() const;
|
QString displayLanguageName() const;
|
||||||
|
|
||||||
// takes ownership:
|
|
||||||
void registerCompositionModelForLocale( const QLocale&, QskInputCompositionModel* );
|
|
||||||
|
|
||||||
void handleKey( int keyIndex );
|
void handleKey( int keyIndex );
|
||||||
KeyData& keyDataAt( int ) const;
|
KeyData& keyDataAt( int ) const;
|
||||||
QString currentTextForKeyIndex( int keyIndex ) const;
|
QString currentTextForKeyIndex( int keyIndex ) const;
|
||||||
@ -156,11 +154,7 @@ private:
|
|||||||
Q_SIGNALS:
|
Q_SIGNALS:
|
||||||
void keyboardRectChanged();
|
void keyboardRectChanged();
|
||||||
void displayLanguageNameChanged();
|
void displayLanguageNameChanged();
|
||||||
void inputMethodRegistered( const QLocale& locale, QskInputCompositionModel* model );
|
void modeChanged( QskVirtualKeyboard::Mode );
|
||||||
void inputMethodEventReceived( QInputMethodEvent* inputMethodEvent );
|
|
||||||
void keyEventReceived( QKeyEvent* keyEvent );
|
|
||||||
void modeChanged( QskVirtualKeyboard::Mode mode );
|
|
||||||
void cancelPressed();
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
class PrivateData;
|
class PrivateData;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user