summaryrefslogtreecommitdiffstats
path: root/xbmc/windowing/X11/WinEventsX11.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'xbmc/windowing/X11/WinEventsX11.cpp')
-rw-r--r--xbmc/windowing/X11/WinEventsX11.cpp673
1 files changed, 673 insertions, 0 deletions
diff --git a/xbmc/windowing/X11/WinEventsX11.cpp b/xbmc/windowing/X11/WinEventsX11.cpp
new file mode 100644
index 0000000..faffd99
--- /dev/null
+++ b/xbmc/windowing/X11/WinEventsX11.cpp
@@ -0,0 +1,673 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "WinEventsX11.h"
+
+#include "ServiceBroker.h"
+#include "application/AppInboundProtocol.h"
+#include "application/Application.h"
+#include "cores/AudioEngine/Interfaces/AE.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "input/InputManager.h"
+#include "input/mouse/MouseStat.h"
+#include "messaging/ApplicationMessenger.h"
+#include "utils/CharsetConverter.h"
+#include "utils/log.h"
+#include "windowing/WinEvents.h"
+#include "windowing/X11/WinSystemX11.h"
+
+#include <stdexcept>
+
+#include <X11/XF86keysym.h>
+#include <X11/Xlib.h>
+#include <X11/extensions/Xrandr.h>
+#include <X11/keysymdef.h>
+
+using namespace KODI::WINDOWING::X11;
+
+static uint32_t SymMappingsX11[][2] =
+{
+ {XK_BackSpace, XBMCK_BACKSPACE}
+, {XK_Tab, XBMCK_TAB}
+, {XK_Clear, XBMCK_CLEAR}
+, {XK_Return, XBMCK_RETURN}
+, {XK_Pause, XBMCK_PAUSE}
+, {XK_Escape, XBMCK_ESCAPE}
+, {XK_Delete, XBMCK_DELETE}
+// multi-media keys
+, {XF86XK_Back, XBMCK_BROWSER_BACK}
+, {XF86XK_Forward, XBMCK_BROWSER_FORWARD}
+, {XF86XK_Refresh, XBMCK_BROWSER_REFRESH}
+, {XF86XK_Stop, XBMCK_BROWSER_STOP}
+, {XF86XK_Search, XBMCK_BROWSER_SEARCH}
+, {XF86XK_Favorites, XBMCK_BROWSER_FAVORITES}
+, {XF86XK_HomePage, XBMCK_BROWSER_HOME}
+, {XF86XK_AudioMute, XBMCK_VOLUME_MUTE}
+, {XF86XK_AudioLowerVolume, XBMCK_VOLUME_DOWN}
+, {XF86XK_AudioRaiseVolume, XBMCK_VOLUME_UP}
+, {XF86XK_AudioNext, XBMCK_MEDIA_NEXT_TRACK}
+, {XF86XK_AudioPrev, XBMCK_MEDIA_PREV_TRACK}
+, {XF86XK_AudioStop, XBMCK_MEDIA_STOP}
+, {XF86XK_AudioPause, XBMCK_MEDIA_PLAY_PAUSE}
+, {XF86XK_Mail, XBMCK_LAUNCH_MAIL}
+, {XF86XK_Select, XBMCK_LAUNCH_MEDIA_SELECT}
+, {XF86XK_Launch0, XBMCK_LAUNCH_APP1}
+, {XF86XK_Launch1, XBMCK_LAUNCH_APP2}
+, {XF86XK_WWW, XBMCK_LAUNCH_FILE_BROWSER}
+, {XF86XK_AudioMedia, XBMCK_LAUNCH_MEDIA_CENTER }
+ // Numeric keypad
+, {XK_KP_0, XBMCK_KP0}
+, {XK_KP_1, XBMCK_KP1}
+, {XK_KP_2, XBMCK_KP2}
+, {XK_KP_3, XBMCK_KP3}
+, {XK_KP_4, XBMCK_KP4}
+, {XK_KP_5, XBMCK_KP5}
+, {XK_KP_6, XBMCK_KP6}
+, {XK_KP_7, XBMCK_KP7}
+, {XK_KP_8, XBMCK_KP8}
+, {XK_KP_9, XBMCK_KP9}
+, {XK_KP_Separator, XBMCK_KP_PERIOD}
+, {XK_KP_Divide, XBMCK_KP_DIVIDE}
+, {XK_KP_Multiply, XBMCK_KP_MULTIPLY}
+, {XK_KP_Subtract, XBMCK_KP_MINUS}
+, {XK_KP_Add, XBMCK_KP_PLUS}
+, {XK_KP_Enter, XBMCK_KP_ENTER}
+, {XK_KP_Equal, XBMCK_KP_EQUALS}
+ // Arrows + Home/End pad
+, {XK_Up, XBMCK_UP}
+, {XK_Down, XBMCK_DOWN}
+, {XK_Right, XBMCK_RIGHT}
+, {XK_Left, XBMCK_LEFT}
+, {XK_Insert, XBMCK_INSERT}
+, {XK_Home, XBMCK_HOME}
+, {XK_End, XBMCK_END}
+, {XK_Page_Up, XBMCK_PAGEUP}
+, {XK_Page_Down, XBMCK_PAGEDOWN}
+ // Function keys
+, {XK_F1, XBMCK_F1}
+, {XK_F2, XBMCK_F2}
+, {XK_F3, XBMCK_F3}
+, {XK_F4, XBMCK_F4}
+, {XK_F5, XBMCK_F5}
+, {XK_F6, XBMCK_F6}
+, {XK_F7, XBMCK_F7}
+, {XK_F8, XBMCK_F8}
+, {XK_F9, XBMCK_F9}
+, {XK_F10, XBMCK_F10}
+, {XK_F11, XBMCK_F11}
+, {XK_F12, XBMCK_F12}
+, {XK_F13, XBMCK_F13}
+, {XK_F14, XBMCK_F14}
+, {XK_F15, XBMCK_F15}
+ // Key state modifier keys
+, {XK_Num_Lock, XBMCK_NUMLOCK}
+, {XK_Caps_Lock, XBMCK_CAPSLOCK}
+, {XK_Scroll_Lock, XBMCK_SCROLLOCK}
+, {XK_Shift_R, XBMCK_RSHIFT}
+, {XK_Shift_L, XBMCK_LSHIFT}
+, {XK_Control_R, XBMCK_RCTRL}
+, {XK_Control_L, XBMCK_LCTRL}
+, {XK_Alt_R, XBMCK_RALT}
+, {XK_Alt_L, XBMCK_LALT}
+, {XK_Meta_R, XBMCK_RMETA}
+, {XK_Meta_L, XBMCK_LMETA}
+, {XK_Super_L, XBMCK_LSUPER}
+, {XK_Super_R, XBMCK_RSUPER}
+, {XK_Mode_switch, XBMCK_MODE}
+, {XK_Multi_key, XBMCK_COMPOSE}
+ // Miscellaneous function keys
+, {XK_Help, XBMCK_HELP}
+, {XK_Print, XBMCK_PRINT}
+//, {0, XBMCK_SYSREQ}
+, {XK_Break, XBMCK_BREAK}
+, {XK_Menu, XBMCK_MENU}
+, {XF86XK_PowerOff, XBMCK_POWER}
+, {XF86XK_Sleep, XBMCK_SLEEP}
+, {XK_EcuSign, XBMCK_EURO}
+, {XK_Undo, XBMCK_UNDO}
+ /* Media keys */
+, {XF86XK_Eject, XBMCK_EJECT}
+, {XF86XK_Stop, XBMCK_STOP}
+, {XF86XK_AudioRecord, XBMCK_RECORD}
+, {XF86XK_AudioRewind, XBMCK_REWIND}
+, {XF86XK_Phone, XBMCK_PHONE}
+, {XF86XK_AudioPlay, XBMCK_PLAY}
+, {XF86XK_AudioRandomPlay, XBMCK_SHUFFLE}
+, {XF86XK_AudioForward, XBMCK_FASTFORWARD}
+};
+
+CWinEventsX11::CWinEventsX11(CWinSystemX11& winSystem) : m_winSystem(winSystem)
+{
+}
+
+CWinEventsX11::~CWinEventsX11()
+{
+ Quit();
+}
+
+bool CWinEventsX11::Init(Display *dpy, Window win)
+{
+ if (m_display)
+ return true;
+
+ m_display = dpy;
+ m_window = win;
+ m_keybuf_len = 32*sizeof(char);
+ m_keybuf = (char*)malloc(m_keybuf_len);
+ m_keymodState = 0;
+ m_wmDeleteMessage = XInternAtom(dpy, "WM_DELETE_WINDOW", False);
+ m_structureChanged = false;
+ m_xrrEventPending = false;
+
+ // open input method
+ char *old_locale = NULL, *old_modifiers = NULL;
+ char res_name[8];
+ const char *p;
+
+ // set resource name to xbmc, not used
+ strcpy(res_name, "xbmc");
+
+ // save current locale, this should be "C"
+ p = setlocale(LC_ALL, NULL);
+ if (p)
+ {
+ old_locale = (char*)malloc(strlen(p) +1);
+ strcpy(old_locale, p);
+ }
+ p = XSetLocaleModifiers(NULL);
+ if (p)
+ {
+ old_modifiers = (char*)malloc(strlen(p) +1);
+ strcpy(old_modifiers, p);
+ }
+
+ // set users preferences and open input method
+ p = setlocale(LC_ALL, "");
+ XSetLocaleModifiers("");
+ m_xim = XOpenIM(m_display, NULL, res_name, res_name);
+
+ // restore old locale
+ if (old_locale)
+ {
+ setlocale(LC_ALL, old_locale);
+ free(old_locale);
+ }
+ if (old_modifiers)
+ {
+ XSetLocaleModifiers(old_modifiers);
+ free(old_modifiers);
+ }
+
+ m_xic = NULL;
+ if (m_xim)
+ {
+ m_xic = XCreateIC(m_xim,
+ XNClientWindow, m_window,
+ XNFocusWindow, m_window,
+ XNInputStyle, XIMPreeditNothing | XIMStatusNothing,
+ XNResourceName, res_name,
+ XNResourceClass, res_name,
+ nullptr);
+ }
+
+ if (!m_xic)
+ CLog::Log(LOGWARNING,"CWinEventsX11::Init - no input method found");
+
+ // build Keysym lookup table
+ for (const auto& symMapping : SymMappingsX11)
+ {
+ m_symLookupTable[symMapping[0]] = symMapping[1];
+ }
+
+ // register for xrandr events
+ int iReturn;
+ XRRQueryExtension(m_display, &m_RREventBase, &iReturn);
+ int numScreens = XScreenCount(m_display);
+ for (int i = 0; i < numScreens; i++)
+ {
+ XRRSelectInput(m_display, RootWindow(m_display, i), RRScreenChangeNotifyMask | RRCrtcChangeNotifyMask | RROutputChangeNotifyMask | RROutputPropertyNotifyMask);
+ }
+
+ return true;
+}
+
+void CWinEventsX11::Quit()
+{
+ free(m_keybuf);
+ m_keybuf = nullptr;
+
+ if (m_xic)
+ {
+ XUnsetICFocus(m_xic);
+ XDestroyIC(m_xic);
+ m_xic = nullptr;
+ }
+
+ if (m_xim)
+ {
+ XCloseIM(m_xim);
+ m_xim = nullptr;
+ }
+
+ m_symLookupTable.clear();
+
+ m_display = nullptr;
+}
+
+bool CWinEventsX11::HasStructureChanged()
+{
+ if (!m_display)
+ return false;
+
+ bool ret = m_structureChanged;
+ m_structureChanged = false;
+ return ret;
+}
+
+void CWinEventsX11::SetXRRFailSafeTimer(std::chrono::milliseconds duration)
+{
+ if (!m_display)
+ return;
+
+ m_xrrFailSafeTimer.Set(duration);
+ m_xrrEventPending = true;
+}
+
+bool CWinEventsX11::MessagePump()
+{
+ if (!m_display)
+ return false;
+
+ bool ret = false;
+ XEvent xevent;
+ unsigned long serial = 0;
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+
+ while (m_display && XPending(m_display))
+ {
+ memset(&xevent, 0, sizeof (XEvent));
+ XNextEvent(m_display, &xevent);
+
+ if (m_display && (xevent.type == m_RREventBase + RRScreenChangeNotify))
+ {
+ if (xevent.xgeneric.serial == serial)
+ continue;
+
+ if (m_xrrEventPending)
+ {
+ m_winSystem.NotifyXRREvent();
+ m_xrrEventPending = false;
+ serial = xevent.xgeneric.serial;
+ }
+
+ continue;
+ }
+ else if (m_display && (xevent.type == m_RREventBase + RRNotify))
+ {
+ if (xevent.xgeneric.serial == serial)
+ continue;
+
+ XRRNotifyEvent* rrEvent = reinterpret_cast<XRRNotifyEvent*>(&xevent);
+ if (rrEvent->subtype == RRNotify_OutputChange)
+ {
+ XRROutputChangeNotifyEvent* changeEvent = reinterpret_cast<XRROutputChangeNotifyEvent*>(&xevent);
+ if (changeEvent->connection == RR_Connected ||
+ changeEvent->connection == RR_Disconnected)
+ {
+ m_winSystem.NotifyXRREvent();
+ CServiceBroker::GetActiveAE()->DeviceChange();
+ serial = xevent.xgeneric.serial;
+ }
+ }
+
+ continue;
+ }
+
+ if (XFilterEvent(&xevent, m_window))
+ continue;
+
+ switch (xevent.type)
+ {
+ case MapNotify:
+ {
+ if (appPort)
+ appPort->SetRenderGUI(true);
+ break;
+ }
+
+ case UnmapNotify:
+ {
+ if (appPort)
+ appPort->SetRenderGUI(false);
+ break;
+ }
+
+ case FocusIn:
+ {
+ if (m_xic)
+ XSetICFocus(m_xic);
+ g_application.m_AppFocused = true;
+ m_keymodState = 0;
+ if (serial == xevent.xfocus.serial)
+ break;
+ m_winSystem.NotifyAppFocusChange(g_application.m_AppFocused);
+ break;
+ }
+
+ case FocusOut:
+ {
+ if (m_xic)
+ XUnsetICFocus(m_xic);
+ g_application.m_AppFocused = false;
+ m_winSystem.NotifyAppFocusChange(g_application.m_AppFocused);
+ serial = xevent.xfocus.serial;
+ break;
+ }
+
+ case Expose:
+ {
+ CServiceBroker::GetGUI()->GetWindowManager().MarkDirty();
+ break;
+ }
+
+ case ConfigureNotify:
+ {
+ if (xevent.xconfigure.window != m_window)
+ break;
+
+ m_structureChanged = true;
+ XBMC_Event newEvent = {};
+ newEvent.type = XBMC_VIDEORESIZE;
+ newEvent.resize.w = xevent.xconfigure.width;
+ newEvent.resize.h = xevent.xconfigure.height;
+ if (appPort)
+ ret |= appPort->OnEvent(newEvent);
+ CServiceBroker::GetGUI()->GetWindowManager().MarkDirty();
+ break;
+ }
+
+ case ClientMessage:
+ {
+ if ((unsigned int)xevent.xclient.data.l[0] == m_wmDeleteMessage)
+ if (!g_application.m_bStop)
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_QUIT);
+ break;
+ }
+
+ case KeyPress:
+ {
+ XBMC_Event newEvent = {};
+ newEvent.type = XBMC_KEYDOWN;
+ KeySym xkeysym;
+
+ // fallback if we have no IM
+ if (!m_xic)
+ {
+ static XComposeStatus state;
+ char keybuf[32];
+ XLookupString(&xevent.xkey, NULL, 0, &xkeysym, NULL);
+ newEvent.key.keysym.sym = LookupXbmcKeySym(xkeysym);
+ newEvent.key.keysym.scancode = xevent.xkey.keycode;
+ if (XLookupString(&xevent.xkey, keybuf, sizeof(keybuf), NULL, &state))
+ {
+ newEvent.key.keysym.unicode = keybuf[0];
+ }
+ ret |= ProcessKey(newEvent);
+ break;
+ }
+
+ Status status;
+ int len;
+ len = Xutf8LookupString(m_xic, &xevent.xkey,
+ m_keybuf, m_keybuf_len,
+ &xkeysym, &status);
+ if (status == XBufferOverflow)
+ {
+ m_keybuf_len = len;
+ m_keybuf = (char*)realloc(m_keybuf, m_keybuf_len);
+ if (m_keybuf == nullptr)
+ throw std::runtime_error("Failed to realloc memory, insufficient memory available");
+ len = Xutf8LookupString(m_xic, &xevent.xkey,
+ m_keybuf, m_keybuf_len,
+ &xkeysym, &status);
+ }
+ switch (status)
+ {
+ case XLookupNone:
+ break;
+ case XLookupChars:
+ case XLookupBoth:
+ {
+ std::string data(m_keybuf, len);
+ std::wstring keys;
+ g_charsetConverter.utf8ToW(data, keys, false);
+
+ if (keys.length() == 0)
+ {
+ break;
+ }
+
+ for (unsigned int i = 0; i < keys.length() - 1; i++)
+ {
+ newEvent.key.keysym.sym = XBMCK_UNKNOWN;
+ newEvent.key.keysym.unicode = keys[i];
+ ret |= ProcessKey(newEvent);
+ }
+ if (keys.length() > 0)
+ {
+ newEvent.key.keysym.scancode = xevent.xkey.keycode;
+ XLookupString(&xevent.xkey, NULL, 0, &xkeysym, NULL);
+ newEvent.key.keysym.sym = LookupXbmcKeySym(xkeysym);
+ newEvent.key.keysym.unicode = keys[keys.length() - 1];
+
+ ret |= ProcessKey(newEvent);
+ }
+ break;
+ }
+
+ case XLookupKeySym:
+ {
+ newEvent.key.keysym.scancode = xevent.xkey.keycode;
+ newEvent.key.keysym.sym = LookupXbmcKeySym(xkeysym);
+ ret |= ProcessKey(newEvent);
+ break;
+ }
+
+ }// switch status
+ break;
+ } //KeyPress
+
+ case KeyRelease:
+ {
+ // if we have a queued press directly after, this is a repeat
+ if (XEventsQueued(m_display, QueuedAfterReading))
+ {
+ XEvent next_event;
+ XPeekEvent(m_display, &next_event);
+ if (next_event.type == KeyPress &&
+ next_event.xkey.window == xevent.xkey.window &&
+ next_event.xkey.keycode == xevent.xkey.keycode &&
+ (next_event.xkey.time - xevent.xkey.time < 2))
+ continue;
+ }
+
+ XBMC_Event newEvent = {};
+ KeySym xkeysym;
+ newEvent.type = XBMC_KEYUP;
+ xkeysym = XLookupKeysym(&xevent.xkey, 0);
+ newEvent.key.keysym.scancode = xevent.xkey.keycode;
+ newEvent.key.keysym.sym = LookupXbmcKeySym(xkeysym);
+ ret |= ProcessKey(newEvent);
+ break;
+ }
+
+ case EnterNotify:
+ {
+ break;
+ }
+
+ // lose mouse coverage
+ case LeaveNotify:
+ {
+ CServiceBroker::GetInputManager().SetMouseActive(false);
+ break;
+ }
+
+ case MotionNotify:
+ {
+ if (xevent.xmotion.window != m_window)
+ break;
+ XBMC_Event newEvent = {};
+ newEvent.type = XBMC_MOUSEMOTION;
+ newEvent.motion.x = (int16_t)xevent.xmotion.x;
+ newEvent.motion.y = (int16_t)xevent.xmotion.y;
+ if (appPort)
+ ret |= appPort->OnEvent(newEvent);
+ break;
+ }
+
+ case ButtonPress:
+ {
+ XBMC_Event newEvent = {};
+ newEvent.type = XBMC_MOUSEBUTTONDOWN;
+ newEvent.button.button = (unsigned char)xevent.xbutton.button;
+ newEvent.button.x = (int16_t)xevent.xbutton.x;
+ newEvent.button.y = (int16_t)xevent.xbutton.y;
+ if (appPort)
+ ret |= appPort->OnEvent(newEvent);
+ break;
+ }
+
+ case ButtonRelease:
+ {
+ XBMC_Event newEvent = {};
+ newEvent.type = XBMC_MOUSEBUTTONUP;
+ newEvent.button.button = (unsigned char)xevent.xbutton.button;
+ newEvent.button.x = (int16_t)xevent.xbutton.x;
+ newEvent.button.y = (int16_t)xevent.xbutton.y;
+ if (appPort)
+ ret |= appPort->OnEvent(newEvent);
+ break;
+ }
+
+ default:
+ {
+ break;
+ }
+ }// switch event.type
+ }// while
+
+ if (m_display && m_xrrEventPending && m_xrrFailSafeTimer.IsTimePast())
+ {
+ CLog::Log(LOGERROR,"CWinEventsX11::MessagePump - missed XRR Events");
+ m_winSystem.NotifyXRREvent();
+ m_xrrEventPending = false;
+ }
+
+ return ret;
+}
+
+bool CWinEventsX11::ProcessKey(XBMC_Event &event)
+{
+ if (event.type == XBMC_KEYDOWN)
+ {
+ // check key modifiers
+ switch(event.key.keysym.sym)
+ {
+ case XBMCK_LSHIFT:
+ m_keymodState |= XBMCKMOD_LSHIFT;
+ break;
+ case XBMCK_RSHIFT:
+ m_keymodState |= XBMCKMOD_RSHIFT;
+ break;
+ case XBMCK_LCTRL:
+ m_keymodState |= XBMCKMOD_LCTRL;
+ break;
+ case XBMCK_RCTRL:
+ m_keymodState |= XBMCKMOD_RCTRL;
+ break;
+ case XBMCK_LALT:
+ m_keymodState |= XBMCKMOD_LALT;
+ break;
+ case XBMCK_RALT:
+ m_keymodState |= XBMCKMOD_RCTRL;
+ break;
+ case XBMCK_LMETA:
+ m_keymodState |= XBMCKMOD_LMETA;
+ break;
+ case XBMCK_RMETA:
+ m_keymodState |= XBMCKMOD_RMETA;
+ break;
+ case XBMCK_MODE:
+ m_keymodState |= XBMCKMOD_MODE;
+ break;
+ default:
+ break;
+ }
+ event.key.keysym.mod = (XBMCMod)m_keymodState;
+ }
+ else if (event.type == XBMC_KEYUP)
+ {
+ switch(event.key.keysym.sym)
+ {
+ case XBMCK_LSHIFT:
+ m_keymodState &= ~XBMCKMOD_LSHIFT;
+ break;
+ case XBMCK_RSHIFT:
+ m_keymodState &= ~XBMCKMOD_RSHIFT;
+ break;
+ case XBMCK_LCTRL:
+ m_keymodState &= ~XBMCKMOD_LCTRL;
+ break;
+ case XBMCK_RCTRL:
+ m_keymodState &= ~XBMCKMOD_RCTRL;
+ break;
+ case XBMCK_LALT:
+ m_keymodState &= ~XBMCKMOD_LALT;
+ break;
+ case XBMCK_RALT:
+ m_keymodState &= ~XBMCKMOD_RCTRL;
+ break;
+ case XBMCK_LMETA:
+ m_keymodState &= ~XBMCKMOD_LMETA;
+ break;
+ case XBMCK_RMETA:
+ m_keymodState &= ~XBMCKMOD_RMETA;
+ break;
+ case XBMCK_MODE:
+ m_keymodState &= ~XBMCKMOD_MODE;
+ break;
+ default:
+ break;
+ }
+ event.key.keysym.mod = (XBMCMod)m_keymodState;
+ }
+
+ std::shared_ptr<CAppInboundProtocol> appPort = CServiceBroker::GetAppPort();
+ if (appPort)
+ appPort->OnEvent(event);
+ return true;
+}
+
+XBMCKey CWinEventsX11::LookupXbmcKeySym(KeySym keysym)
+{
+ // try direct mapping first
+ std::map<uint32_t, uint32_t>::iterator it;
+ it = m_symLookupTable.find(keysym);
+ if (it != m_symLookupTable.end())
+ {
+ return (XBMCKey)(it->second);
+ }
+
+ // try ascii mappings
+ if (keysym>>8 == 0x00)
+ return (XBMCKey)tolower(keysym & 0xFF);
+
+ return (XBMCKey)keysym;
+}