Changes to support remapping modifier keys on clients.
This commit is contained in:
parent
dce445a83a
commit
5f164375d4
|
@ -39,6 +39,10 @@ CServerProxy::CServerProxy(IClient* client,
|
||||||
assert(m_client != NULL);
|
assert(m_client != NULL);
|
||||||
assert(m_input != NULL);
|
assert(m_input != NULL);
|
||||||
assert(m_output != NULL);
|
assert(m_output != NULL);
|
||||||
|
|
||||||
|
// initialize modifier translation table
|
||||||
|
for (KeyModifierID id = 0; id < kKeyModifierIDLast; ++id)
|
||||||
|
m_modifierTranslationTable[id] = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
CServerProxy::~CServerProxy()
|
CServerProxy::~CServerProxy()
|
||||||
|
@ -313,6 +317,110 @@ CServerProxy::sendInfo(const CClientInfo& info)
|
||||||
info.m_mx, info.m_my);
|
info.m_mx, info.m_my);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
KeyID
|
||||||
|
CServerProxy::translateKey(KeyID id) const
|
||||||
|
{
|
||||||
|
static const KeyID s_translationTable[kKeyModifierIDLast][2] = {
|
||||||
|
{ kKeyNone, kKeyNone },
|
||||||
|
{ kKeyShift_L, kKeyShift_R },
|
||||||
|
{ kKeyControl_L, kKeyControl_R },
|
||||||
|
{ kKeyAlt_L, kKeyAlt_R },
|
||||||
|
{ kKeyMeta_L, kKeyMeta_R },
|
||||||
|
{ kKeySuper_L, kKeySuper_R }
|
||||||
|
};
|
||||||
|
|
||||||
|
KeyModifierID id2 = kKeyModifierIDNull;
|
||||||
|
UInt32 side = 0;
|
||||||
|
switch (id) {
|
||||||
|
case kKeyShift_L:
|
||||||
|
id2 = kKeyModifierIDShift;
|
||||||
|
side = 0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case kKeyShift_R:
|
||||||
|
id2 = kKeyModifierIDShift;
|
||||||
|
side = 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case kKeyControl_L:
|
||||||
|
id2 = kKeyModifierIDControl;
|
||||||
|
side = 0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case kKeyControl_R:
|
||||||
|
id2 = kKeyModifierIDControl;
|
||||||
|
side = 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case kKeyAlt_L:
|
||||||
|
id2 = kKeyModifierIDAlt;
|
||||||
|
side = 0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case kKeyAlt_R:
|
||||||
|
id2 = kKeyModifierIDAlt;
|
||||||
|
side = 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case kKeyMeta_L:
|
||||||
|
id2 = kKeyModifierIDMeta;
|
||||||
|
side = 0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case kKeyMeta_R:
|
||||||
|
id2 = kKeyModifierIDMeta;
|
||||||
|
side = 1;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case kKeySuper_L:
|
||||||
|
id2 = kKeyModifierIDSuper;
|
||||||
|
side = 0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case kKeySuper_R:
|
||||||
|
id2 = kKeyModifierIDSuper;
|
||||||
|
side = 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (id2 != kKeyModifierIDNull) {
|
||||||
|
return s_translationTable[m_modifierTranslationTable[id2]][side];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyModifierMask
|
||||||
|
CServerProxy::translateModifierMask(KeyModifierMask mask) const
|
||||||
|
{
|
||||||
|
static const KeyModifierMask s_masks[kKeyModifierIDLast] = {
|
||||||
|
0x0000,
|
||||||
|
KeyModifierShift,
|
||||||
|
KeyModifierControl,
|
||||||
|
KeyModifierAlt,
|
||||||
|
KeyModifierMeta,
|
||||||
|
KeyModifierSuper
|
||||||
|
};
|
||||||
|
|
||||||
|
KeyModifierMask newMask = 0;
|
||||||
|
if ((mask & KeyModifierShift) != 0) {
|
||||||
|
newMask |= s_masks[m_modifierTranslationTable[kKeyModifierIDShift]];
|
||||||
|
}
|
||||||
|
if ((mask & KeyModifierControl) != 0) {
|
||||||
|
newMask |= s_masks[m_modifierTranslationTable[kKeyModifierIDControl]];
|
||||||
|
}
|
||||||
|
if ((mask & KeyModifierAlt) != 0) {
|
||||||
|
newMask |= s_masks[m_modifierTranslationTable[kKeyModifierIDAlt]];
|
||||||
|
}
|
||||||
|
if ((mask & KeyModifierMeta) != 0) {
|
||||||
|
newMask |= s_masks[m_modifierTranslationTable[kKeyModifierIDMeta]];
|
||||||
|
}
|
||||||
|
if ((mask & KeyModifierSuper) != 0) {
|
||||||
|
newMask |= s_masks[m_modifierTranslationTable[kKeyModifierIDSuper]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
CServerProxy::enter()
|
CServerProxy::enter()
|
||||||
{
|
{
|
||||||
|
@ -397,9 +505,16 @@ CServerProxy::keyDown()
|
||||||
CProtocolUtil::readf(getInputStream(), kMsgDKeyDown + 4, &id, &mask);
|
CProtocolUtil::readf(getInputStream(), kMsgDKeyDown + 4, &id, &mask);
|
||||||
LOG((CLOG_DEBUG1 "recv key down id=%d, mask=0x%04x", id, mask));
|
LOG((CLOG_DEBUG1 "recv key down id=%d, mask=0x%04x", id, mask));
|
||||||
|
|
||||||
// forward
|
// translate
|
||||||
getClient()->keyDown(static_cast<KeyID>(id),
|
KeyID id2 = translateKey(static_cast<KeyID>(id));
|
||||||
|
KeyModifierMask mask2 = translateModifierMask(
|
||||||
static_cast<KeyModifierMask>(mask));
|
static_cast<KeyModifierMask>(mask));
|
||||||
|
if (id2 != static_cast<KeyID>(id) ||
|
||||||
|
mask2 != static_cast<KeyModifierMask>(mask))
|
||||||
|
LOG((CLOG_DEBUG1 "key down translated to id=%d, mask=0x%04x", id2, mask2));
|
||||||
|
|
||||||
|
// forward
|
||||||
|
getClient()->keyDown(id2, mask2);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
@ -414,10 +529,16 @@ CServerProxy::keyRepeat()
|
||||||
kMsgDKeyRepeat + 4, &id, &mask, &count);
|
kMsgDKeyRepeat + 4, &id, &mask, &count);
|
||||||
LOG((CLOG_DEBUG1 "recv key repeat id=%d, mask=0x%04x, count=%d", id, mask, count));
|
LOG((CLOG_DEBUG1 "recv key repeat id=%d, mask=0x%04x, count=%d", id, mask, count));
|
||||||
|
|
||||||
|
// translate
|
||||||
|
KeyID id2 = translateKey(static_cast<KeyID>(id));
|
||||||
|
KeyModifierMask mask2 = translateModifierMask(
|
||||||
|
static_cast<KeyModifierMask>(mask));
|
||||||
|
if (id2 != static_cast<KeyID>(id) ||
|
||||||
|
mask2 != static_cast<KeyModifierMask>(mask))
|
||||||
|
LOG((CLOG_DEBUG1 "key down translated to id=%d, mask=0x%04x", id2, mask2));
|
||||||
|
|
||||||
// forward
|
// forward
|
||||||
getClient()->keyRepeat(static_cast<KeyID>(id),
|
getClient()->keyRepeat(id2, mask2, count);
|
||||||
static_cast<KeyModifierMask>(mask),
|
|
||||||
count);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
@ -431,9 +552,16 @@ CServerProxy::keyUp()
|
||||||
CProtocolUtil::readf(getInputStream(), kMsgDKeyUp + 4, &id, &mask);
|
CProtocolUtil::readf(getInputStream(), kMsgDKeyUp + 4, &id, &mask);
|
||||||
LOG((CLOG_DEBUG1 "recv key up id=%d, mask=0x%04x", id, mask));
|
LOG((CLOG_DEBUG1 "recv key up id=%d, mask=0x%04x", id, mask));
|
||||||
|
|
||||||
// forward
|
// translate
|
||||||
getClient()->keyUp(static_cast<KeyID>(id),
|
KeyID id2 = translateKey(static_cast<KeyID>(id));
|
||||||
|
KeyModifierMask mask2 = translateModifierMask(
|
||||||
static_cast<KeyModifierMask>(mask));
|
static_cast<KeyModifierMask>(mask));
|
||||||
|
if (id2 != static_cast<KeyID>(id) ||
|
||||||
|
mask2 != static_cast<KeyModifierMask>(mask))
|
||||||
|
LOG((CLOG_DEBUG1 "key down translated to id=%d, mask=0x%04x", id2, mask2));
|
||||||
|
|
||||||
|
// forward
|
||||||
|
getClient()->keyUp(id2, mask2);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
@ -534,6 +662,10 @@ CServerProxy::resetOptions()
|
||||||
|
|
||||||
// forward
|
// forward
|
||||||
getClient()->resetOptions();
|
getClient()->resetOptions();
|
||||||
|
|
||||||
|
// reset modifier translation table
|
||||||
|
for (KeyModifierID id = 0; id < kKeyModifierIDLast; ++id)
|
||||||
|
m_modifierTranslationTable[id] = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
@ -546,6 +678,31 @@ CServerProxy::setOptions()
|
||||||
|
|
||||||
// forward
|
// forward
|
||||||
getClient()->setOptions(options);
|
getClient()->setOptions(options);
|
||||||
|
|
||||||
|
// update modifier table
|
||||||
|
for (UInt32 i = 0, n = options.size(); i < n; i += 2) {
|
||||||
|
KeyModifierID id = kKeyModifierIDNull;
|
||||||
|
if (options[i] == kOptionModifierMapForShift) {
|
||||||
|
id = kKeyModifierIDShift;
|
||||||
|
}
|
||||||
|
else if (options[i] == kOptionModifierMapForControl) {
|
||||||
|
id = kKeyModifierIDControl;
|
||||||
|
}
|
||||||
|
else if (options[i] == kOptionModifierMapForAlt) {
|
||||||
|
id = kKeyModifierIDAlt;
|
||||||
|
}
|
||||||
|
else if (options[i] == kOptionModifierMapForMeta) {
|
||||||
|
id = kKeyModifierIDMeta;
|
||||||
|
}
|
||||||
|
else if (options[i] == kOptionModifierMapForSuper) {
|
||||||
|
id = kKeyModifierIDSuper;
|
||||||
|
}
|
||||||
|
if (id != kKeyModifierIDNull) {
|
||||||
|
m_modifierTranslationTable[id] =
|
||||||
|
static_cast<KeyModifierID>(options[i + 1]);
|
||||||
|
LOG((CLOG_DEBUG1 "modifier %d mapped to %d", id, m_modifierTranslationTable[id]));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
#define CSERVERPROXY_H
|
#define CSERVERPROXY_H
|
||||||
|
|
||||||
#include "IScreenReceiver.h"
|
#include "IScreenReceiver.h"
|
||||||
|
#include "KeyTypes.h"
|
||||||
#include "CMutex.h"
|
#include "CMutex.h"
|
||||||
|
|
||||||
class IClient;
|
class IClient;
|
||||||
|
@ -92,6 +93,10 @@ private:
|
||||||
|
|
||||||
void sendInfo(const CClientInfo&);
|
void sendInfo(const CClientInfo&);
|
||||||
|
|
||||||
|
// modifier key translation
|
||||||
|
KeyID translateKey(KeyID) const;
|
||||||
|
KeyModifierMask translateModifierMask(KeyModifierMask) const;
|
||||||
|
|
||||||
// message handlers
|
// message handlers
|
||||||
void enter();
|
void enter();
|
||||||
void leave();
|
void leave();
|
||||||
|
@ -123,6 +128,8 @@ private:
|
||||||
SInt32 m_xMouse, m_yMouse;
|
SInt32 m_xMouse, m_yMouse;
|
||||||
|
|
||||||
bool m_ignoreMouse;
|
bool m_ignoreMouse;
|
||||||
|
|
||||||
|
KeyModifierID m_modifierTranslationTable[kKeyModifierIDLast];
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "CConfig.h"
|
#include "CConfig.h"
|
||||||
#include "ProtocolTypes.h"
|
#include "KeyTypes.h"
|
||||||
#include "XSocket.h"
|
#include "XSocket.h"
|
||||||
#include "stdistream.h"
|
#include "stdistream.h"
|
||||||
#include "stdostream.h"
|
#include "stdostream.h"
|
||||||
|
@ -514,6 +514,24 @@ CConfig::parseBoolean(const CString& arg)
|
||||||
throw XConfigRead("invalid argument");
|
throw XConfigRead("invalid argument");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OptionValue
|
||||||
|
CConfig::parseModifierKey(const CString& arg)
|
||||||
|
{
|
||||||
|
if (CStringUtil::CaselessCmp::equal(arg, "shift"))
|
||||||
|
return static_cast<OptionValue>(kKeyModifierIDShift);
|
||||||
|
if (CStringUtil::CaselessCmp::equal(arg, "ctrl"))
|
||||||
|
return static_cast<OptionValue>(kKeyModifierIDControl);
|
||||||
|
if (CStringUtil::CaselessCmp::equal(arg, "alt"))
|
||||||
|
return static_cast<OptionValue>(kKeyModifierIDAlt);
|
||||||
|
if (CStringUtil::CaselessCmp::equal(arg, "meta"))
|
||||||
|
return static_cast<OptionValue>(kKeyModifierIDMeta);
|
||||||
|
if (CStringUtil::CaselessCmp::equal(arg, "super"))
|
||||||
|
return static_cast<OptionValue>(kKeyModifierIDSuper);
|
||||||
|
if (CStringUtil::CaselessCmp::equal(arg, "none"))
|
||||||
|
return static_cast<OptionValue>(kKeyModifierIDNull);
|
||||||
|
throw XConfigRead("invalid argument");
|
||||||
|
}
|
||||||
|
|
||||||
const char*
|
const char*
|
||||||
CConfig::getOptionName(OptionID id)
|
CConfig::getOptionName(OptionID id)
|
||||||
{
|
{
|
||||||
|
@ -523,6 +541,21 @@ CConfig::getOptionName(OptionID id)
|
||||||
if (id == kOptionHalfDuplexNumLock) {
|
if (id == kOptionHalfDuplexNumLock) {
|
||||||
return "halfDuplexNumLock";
|
return "halfDuplexNumLock";
|
||||||
}
|
}
|
||||||
|
if (id == kOptionModifierMapForShift) {
|
||||||
|
return "shift";
|
||||||
|
}
|
||||||
|
if (id == kOptionModifierMapForControl) {
|
||||||
|
return "ctrl";
|
||||||
|
}
|
||||||
|
if (id == kOptionModifierMapForAlt) {
|
||||||
|
return "alt";
|
||||||
|
}
|
||||||
|
if (id == kOptionModifierMapForMeta) {
|
||||||
|
return "meta";
|
||||||
|
}
|
||||||
|
if (id == kOptionModifierMapForSuper) {
|
||||||
|
return "super";
|
||||||
|
}
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -533,6 +566,31 @@ CConfig::getOptionValue(OptionID id, OptionValue value)
|
||||||
id == kOptionHalfDuplexNumLock) {
|
id == kOptionHalfDuplexNumLock) {
|
||||||
return (value != 0) ? "true" : "false";
|
return (value != 0) ? "true" : "false";
|
||||||
}
|
}
|
||||||
|
if (id == kOptionModifierMapForShift ||
|
||||||
|
id == kOptionModifierMapForControl ||
|
||||||
|
id == kOptionModifierMapForAlt ||
|
||||||
|
id == kOptionModifierMapForMeta ||
|
||||||
|
id == kOptionModifierMapForSuper) {
|
||||||
|
switch (value) {
|
||||||
|
case kKeyModifierIDShift:
|
||||||
|
return "shift";
|
||||||
|
|
||||||
|
case kKeyModifierIDControl:
|
||||||
|
return "ctrl";
|
||||||
|
|
||||||
|
case kKeyModifierIDAlt:
|
||||||
|
return "alt";
|
||||||
|
|
||||||
|
case kKeyModifierIDMeta:
|
||||||
|
return "meta";
|
||||||
|
|
||||||
|
case kKeyModifierIDSuper:
|
||||||
|
return "super";
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
@ -700,6 +758,26 @@ CConfig::readSectionScreens(std::istream& s)
|
||||||
addOption(screen, kOptionHalfDuplexNumLock,
|
addOption(screen, kOptionHalfDuplexNumLock,
|
||||||
parseBoolean(value));
|
parseBoolean(value));
|
||||||
}
|
}
|
||||||
|
else if (name == "shift") {
|
||||||
|
addOption(screen, kOptionModifierMapForShift,
|
||||||
|
parseModifierKey(value));
|
||||||
|
}
|
||||||
|
else if (name == "ctrl") {
|
||||||
|
addOption(screen, kOptionModifierMapForControl,
|
||||||
|
parseModifierKey(value));
|
||||||
|
}
|
||||||
|
else if (name == "alt") {
|
||||||
|
addOption(screen, kOptionModifierMapForAlt,
|
||||||
|
parseModifierKey(value));
|
||||||
|
}
|
||||||
|
else if (name == "meta") {
|
||||||
|
addOption(screen, kOptionModifierMapForMeta,
|
||||||
|
parseModifierKey(value));
|
||||||
|
}
|
||||||
|
else if (name == "super") {
|
||||||
|
addOption(screen, kOptionModifierMapForSuper,
|
||||||
|
parseModifierKey(value));
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
// unknown argument
|
// unknown argument
|
||||||
throw XConfigRead("unknown argument");
|
throw XConfigRead("unknown argument");
|
||||||
|
|
|
@ -293,6 +293,7 @@ public:
|
||||||
private:
|
private:
|
||||||
static bool readLine(std::istream&, CString&);
|
static bool readLine(std::istream&, CString&);
|
||||||
static bool parseBoolean(const CString&);
|
static bool parseBoolean(const CString&);
|
||||||
|
static OptionValue parseModifierKey(const CString&);
|
||||||
static const char* getOptionName(OptionID);
|
static const char* getOptionName(OptionID);
|
||||||
static const char* getOptionValue(OptionID, OptionValue);
|
static const char* getOptionValue(OptionID, OptionValue);
|
||||||
void readSection(std::istream&);
|
void readSection(std::istream&);
|
||||||
|
|
|
@ -25,13 +25,19 @@ keys, function keys, modifier keys, etc).
|
||||||
*/
|
*/
|
||||||
typedef UInt32 KeyID;
|
typedef UInt32 KeyID;
|
||||||
|
|
||||||
//! Modifier key ID
|
//! Modifier key mask
|
||||||
/*!
|
/*!
|
||||||
Type to hold a bitmask of key modifiers (e.g. shift keys).
|
Type to hold a bitmask of key modifiers (e.g. shift keys).
|
||||||
*/
|
*/
|
||||||
typedef UInt32 KeyModifierMask;
|
typedef UInt32 KeyModifierMask;
|
||||||
|
|
||||||
//! @name Modifier key identifiers
|
//! Modifier key ID
|
||||||
|
/*!
|
||||||
|
Type to hold the id of a key modifier (e.g. a shift key).
|
||||||
|
*/
|
||||||
|
typedef UInt32 KeyModifierID;
|
||||||
|
|
||||||
|
//! @name Modifier key masks
|
||||||
//@{
|
//@{
|
||||||
static const KeyModifierMask KeyModifierShift = 0x0001;
|
static const KeyModifierMask KeyModifierShift = 0x0001;
|
||||||
static const KeyModifierMask KeyModifierControl = 0x0002;
|
static const KeyModifierMask KeyModifierControl = 0x0002;
|
||||||
|
@ -44,6 +50,17 @@ static const KeyModifierMask KeyModifierNumLock = 0x2000;
|
||||||
static const KeyModifierMask KeyModifierScrollLock = 0x4000;
|
static const KeyModifierMask KeyModifierScrollLock = 0x4000;
|
||||||
//@}
|
//@}
|
||||||
|
|
||||||
|
//! @name Modifier key identifiers
|
||||||
|
//@{
|
||||||
|
static const KeyModifierID kKeyModifierIDNull = 0;
|
||||||
|
static const KeyModifierID kKeyModifierIDShift = 1;
|
||||||
|
static const KeyModifierID kKeyModifierIDControl = 2;
|
||||||
|
static const KeyModifierID kKeyModifierIDAlt = 3;
|
||||||
|
static const KeyModifierID kKeyModifierIDMeta = 4;
|
||||||
|
static const KeyModifierID kKeyModifierIDSuper = 5;
|
||||||
|
static const KeyModifierID kKeyModifierIDLast = 6;
|
||||||
|
//@}
|
||||||
|
|
||||||
//! @name Key identifiers
|
//! @name Key identifiers
|
||||||
//@{
|
//@{
|
||||||
// all identifiers except kKeyNone are equal to the corresponding
|
// all identifiers except kKeyNone are equal to the corresponding
|
||||||
|
|
|
@ -42,9 +42,14 @@ typedef std::vector<UInt32> COptionsList;
|
||||||
|
|
||||||
//! @name Option identifiers
|
//! @name Option identifiers
|
||||||
//@{
|
//@{
|
||||||
static const OptionID kOptionHalfDuplexCapsLock = OPTION_CODE("HDCL");
|
static const OptionID kOptionHalfDuplexCapsLock = OPTION_CODE("HDCL");
|
||||||
static const OptionID kOptionHalfDuplexNumLock = OPTION_CODE("HDNL");
|
static const OptionID kOptionHalfDuplexNumLock = OPTION_CODE("HDNL");
|
||||||
static const OptionID kOptionHalfDuplexScrollLock = OPTION_CODE("HDSL");
|
static const OptionID kOptionHalfDuplexScrollLock = OPTION_CODE("HDSL");
|
||||||
|
static const OptionID kOptionModifierMapForShift = OPTION_CODE("MMFS");
|
||||||
|
static const OptionID kOptionModifierMapForControl = OPTION_CODE("MMFC");
|
||||||
|
static const OptionID kOptionModifierMapForAlt = OPTION_CODE("MMFA");
|
||||||
|
static const OptionID kOptionModifierMapForMeta = OPTION_CODE("MMFM");
|
||||||
|
static const OptionID kOptionModifierMapForSuper = OPTION_CODE("MMFR");
|
||||||
//@}
|
//@}
|
||||||
|
|
||||||
#undef OPTION_CODE
|
#undef OPTION_CODE
|
||||||
|
|
Loading…
Reference in New Issue