Browse Source

Add pixel & time threshold for sending a drag begin UI event. Closes #65. Removed code duplication between mouse & touch hover.

Lasse Öörni 12 năm trước cách đây
mục cha
commit
0d7d4826e5

+ 4 - 0
Docs/AngelScriptAPI.h

@@ -7360,11 +7360,15 @@ UIElement focusElement;
 /* (readonly) */
 UIElement frontElement;
 /* (readonly) */
+UIElement dragElement;
+/* (readonly) */
 UIElement root;
 /* (readonly) */
 UIElement modalRoot;
 String clipBoardText;
 float doubleClickInterval;
+float dragBeginInterval;
+int dragBeginDistance;
 int maxFontTextureSize;
 bool nonFocusedMouseWheel;
 bool useSystemClipBoard;

+ 8 - 0
Docs/LuaScriptAPI.dox

@@ -4646,6 +4646,8 @@ Methods:
 - bool SaveLayout(Serializer& dest, UIElement* element)
 - void SetClipBoardText(const String text)
 - void SetDoubleClickInterval(float interval)
+- void SetDragBeginInterval(float interval)
+- void SetDragBeginDistance(int pixels)
 - void SetMaxFontTextureSize(int size)
 - void SetNonFocusedMouseWheel(bool nonFocusedMouseWheel)
 - void SetUseSystemClipBoard(bool enable)
@@ -4659,8 +4661,11 @@ Methods:
 - UIElement* GetElementAt(int x, int y, bool enabledOnly = true)
 - UIElement* GetFocusElement() const
 - UIElement* GetFrontElement() const
+- UIElement* GetDragElement() const
 - const String GetClipBoardText() const
 - float GetDoubleClickInterval() const
+- float GetDragBeginInterval() const
+- int GetDragBeginDistance() const
 - int GetMaxFontTextureSize() const
 - bool IsNonFocusedMouseWheel() const
 - bool GetUseSystemClipBoard() const
@@ -4676,8 +4681,11 @@ Properties:
 - IntVector2 cursorPosition (readonly)
 - UIElement* focusElement (readonly)
 - UIElement* frontElement (readonly)
+- UIElement* dragElement (readonly)
 - String& clipBoardText
 - float doubleClickInterval
+- float dragBeginInterval
+- int dragBeginDistance
 - int maxFontTextureSize
 - bool nonFocusedMouseWheel
 - bool useSystemClipBoard

+ 3 - 0
Docs/ScriptAPI.dox

@@ -6266,10 +6266,13 @@ Properties:
 - IntVector2 cursorPosition (readonly)
 - UIElement@ focusElement
 - UIElement@ frontElement (readonly)
+- UIElement@ dragElement (readonly)
 - UIElement@ root (readonly)
 - UIElement@ modalRoot (readonly)
 - String clipBoardText
 - float doubleClickInterval
+- float dragBeginInterval
+- int dragBeginDistance
 - int maxFontTextureSize
 - bool nonFocusedMouseWheel
 - bool useSystemClipBoard

+ 8 - 0
Source/Engine/LuaScript/pkgs/UI/UI.pkg

@@ -15,6 +15,8 @@ class UI : public Object
     void SetClipBoardText(const String text);
 
     void SetDoubleClickInterval(float interval);
+    void SetDragBeginInterval(float interval);
+    void SetDragBeginDistance(int pixels);
     void SetMaxFontTextureSize(int size);
     void SetNonFocusedMouseWheel(bool nonFocusedMouseWheel);
     void SetUseSystemClipBoard(bool enable);
@@ -30,8 +32,11 @@ class UI : public Object
 
     UIElement* GetFocusElement() const;
     UIElement* GetFrontElement() const;
+    UIElement* GetDragElement() const;
     const String GetClipBoardText() const;
     float GetDoubleClickInterval() const;
+    float GetDragBeginInterval() const;
+    int GetDragBeginDistance() const;
     int GetMaxFontTextureSize() const;
     bool IsNonFocusedMouseWheel() const;
     bool GetUseSystemClipBoard() const;
@@ -45,9 +50,12 @@ class UI : public Object
     tolua_readonly tolua_property__get_set IntVector2 cursorPosition;
     tolua_readonly tolua_property__get_set UIElement* focusElement;
     tolua_readonly tolua_property__get_set UIElement* frontElement;
+    tolua_readonly tolua_property__get_set UIElement* dragElement;
 
     tolua_property__get_set String& clipBoardText;
     tolua_property__get_set float doubleClickInterval;
+    tolua_property__get_set float dragBeginInterval;
+    tolua_property__get_set int dragBeginDistance;
     tolua_property__get_set int maxFontTextureSize;
     tolua_property__is_set bool nonFocusedMouseWheel;
     tolua_property__get_set bool useSystemClipBoard;

+ 5 - 0
Source/Engine/Script/UIAPI.cpp

@@ -597,12 +597,17 @@ static void RegisterUI(asIScriptEngine* engine)
     engine->RegisterObjectMethod("UI", "void set_focusElement(UIElement@+)", asMETHOD(UI, SetFocusElement), asCALL_THISCALL);
     engine->RegisterObjectMethod("UI", "UIElement@+ get_focusElement() const", asMETHOD(UI, GetFocusElement), asCALL_THISCALL);
     engine->RegisterObjectMethod("UI", "UIElement@+ get_frontElement() const", asMETHOD(UI, GetFrontElement), asCALL_THISCALL);
+    engine->RegisterObjectMethod("UI", "UIElement@+ get_dragElement() const", asMETHOD(UI, GetDragElement), asCALL_THISCALL);
     engine->RegisterObjectMethod("UI", "UIElement@+ get_root() const", asMETHOD(UI, GetRoot), asCALL_THISCALL);
     engine->RegisterObjectMethod("UI", "UIElement@+ get_modalRoot() const", asMETHOD(UI, GetRootModalElement), asCALL_THISCALL);
     engine->RegisterObjectMethod("UI", "void set_clipBoardText(const String&in)", asMETHOD(UI, SetClipBoardText), asCALL_THISCALL);
     engine->RegisterObjectMethod("UI", "const String& get_clipBoardText() const", asMETHOD(UI, GetClipBoardText), asCALL_THISCALL);
     engine->RegisterObjectMethod("UI", "void set_doubleClickInterval(float)", asMETHOD(UI, SetDoubleClickInterval), asCALL_THISCALL);
     engine->RegisterObjectMethod("UI", "float get_doubleClickInterval() const", asMETHOD(UI, GetDoubleClickInterval), asCALL_THISCALL);
+    engine->RegisterObjectMethod("UI", "void set_dragBeginInterval(float)", asMETHOD(UI, SetDragBeginInterval), asCALL_THISCALL);
+    engine->RegisterObjectMethod("UI", "float get_dragBeginInterval() const", asMETHOD(UI, GetDragBeginInterval), asCALL_THISCALL);
+    engine->RegisterObjectMethod("UI", "void set_dragBeginDistance(int)", asMETHOD(UI, SetDragBeginDistance), asCALL_THISCALL);
+    engine->RegisterObjectMethod("UI", "int get_dragBeginDistance() const", asMETHOD(UI, GetDragBeginDistance), asCALL_THISCALL);
     engine->RegisterObjectMethod("UI", "void set_maxFontTextureSize(int)", asMETHOD(UI, SetMaxFontTextureSize), asCALL_THISCALL);
     engine->RegisterObjectMethod("UI", "int get_maxFontTextureSize() const", asMETHOD(UI, GetMaxFontTextureSize), asCALL_THISCALL);
     engine->RegisterObjectMethod("UI", "void set_nonFocusedMouseWheel(bool)", asMETHOD(UI, SetNonFocusedMouseWheel), asCALL_THISCALL);

+ 112 - 51
Source/Engine/UI/UI.cpp

@@ -67,6 +67,8 @@ const ShortStringHash VAR_ORIGINAL_CHILD_INDEX("OriginalChildIndex");
 const ShortStringHash VAR_PARENT_CHANGED("ParentChanged");
 
 const float DEFAULT_DOUBLECLICK_INTERVAL = 0.5f;
+const float DEFAULT_DRAGBEGIN_INTERVAL = 0.5f;
+const int DEFAULT_DRAGBEGIN_DISTANCE = 5;
 const int DEFAULT_FONT_TEXTURE_MAX_SIZE = 2048;
 
 const char* UI_CATEGORY = "UI";
@@ -76,9 +78,12 @@ UI::UI(Context* context) :
     rootElement_(new UIElement(context)),
     rootModalElement_(new UIElement(context)),
     mouseButtons_(0),
+    lastMouseButtons_(0),
     qualifiers_(0),
     maxFontTextureSize_(DEFAULT_FONT_TEXTURE_MAX_SIZE),
     doubleClickInterval_(DEFAULT_DOUBLECLICK_INTERVAL),
+    dragBeginInterval_(DEFAULT_DRAGBEGIN_INTERVAL),
+    dragBeginDistance_(DEFAULT_DRAGBEGIN_DISTANCE),
     initialized_(false),
     usingTouchInput_(false),
     #ifdef WIN32
@@ -89,12 +94,12 @@ UI::UI(Context* context) :
     useSystemClipBoard_(false),
     useMutableGlyphs_(false),
     forceAutoHint_(false),
+    dragBeginPending_(false),
     nonModalBatchSize_(0)
 {
     rootElement_->SetTraversalMode(TM_DEPTH_FIRST);
     rootModalElement_->SetTraversalMode(TM_DEPTH_FIRST);
-    clickTimer_ = new Timer();
-
+    
     // Register UI library object factories
     RegisterUILibrary(context_);
 
@@ -116,7 +121,6 @@ UI::UI(Context* context) :
 
 UI::~UI()
 {
-    delete clickTimer_;
 }
 
 void UI::SetCursor(Cursor* cursor)
@@ -284,59 +288,36 @@ void UI::Update(float timeStep)
     bool cursorVisible;
     GetCursorPositionAndVisible(cursorPos, cursorVisible);
 
-    // Mouse hover
-    if (!usingTouchInput_ && cursorVisible)
+    // Drag begin based on time
+    if (dragElement_ && dragBeginPending_)
     {
-        WeakPtr<UIElement> element(GetElementAt(cursorPos));
-
-        bool dragSource = dragElement_ && (dragElement_->GetDragDropMode() & DD_SOURCE) != 0;
-        bool dragTarget = element && (element->GetDragDropMode() & DD_TARGET) != 0;
-        bool dragDropTest = dragSource && dragTarget && element != dragElement_;
-
-        // Hover effect
-        // If a drag is going on, transmit hover only to the element being dragged, unless it's a drop target
-        if (element && element->IsEnabled())
-        {
-            if (!dragElement_ || dragElement_ == element || dragDropTest)
-                element->OnHover(element->ScreenToElement(cursorPos), cursorPos, mouseButtons_, qualifiers_, cursor_);
-        }
-        else
-            SetCursorShape(CS_NORMAL);
-
-        // Drag and drop test
-        if (dragDropTest)
+        if (dragBeginTimer_.GetMSec(false) >= (unsigned)(dragBeginInterval_ * 1000))
         {
-            bool accept = element->OnDragDropTest(dragElement_);
-            if (accept)
+            dragBeginPending_ = false;
+            if (!usingTouchInput_)
             {
-                using namespace DragDropTest;
-
-                VariantMap eventData;
-                eventData[P_SOURCE] = (void*)dragElement_.Get();
-                eventData[P_TARGET] = (void*)element.Get();
-                eventData[P_ACCEPT] = accept;
-                SendEvent(E_DRAGDROPTEST, eventData);
-                accept = eventData[P_ACCEPT].GetBool();
+                dragElement_->OnDragBegin(dragElement_->ScreenToElement(dragBeginPos_), dragBeginPos_, mouseButtons_, qualifiers_,
+                    cursor_);
             }
-
-            SetCursorShape(accept ? CS_ACCEPTDROP : CS_REJECTDROP);
+            else
+                dragElement_->OnDragBegin(dragElement_->ScreenToElement(dragBeginPos_), dragBeginPos_, MOUSEB_LEFT, 0, 0);
+            SendDragEvent(E_DRAGBEGIN, dragElement_, dragBeginPos_);
         }
-        else if (dragSource)
-            SetCursorShape(dragElement_ == element ? CS_ACCEPTDROP : CS_REJECTDROP);
     }
-
+    
+    // Mouse hover
+    if (!usingTouchInput_ && cursorVisible)
+        ProcessHover(cursorPos, mouseButtons_, qualifiers_, cursor_);
+    
     // Touch hover
     Input* input = GetSubsystem<Input>();
     unsigned numTouches = input->GetNumTouches();
-
     for (unsigned i = 0; i < numTouches; ++i)
     {
         TouchState* touch = input->GetTouch(i);
-        UIElement* element = GetElementAt(touch->position_);
-        if (element && element->IsEnabled())
-            element->OnHover(element->ScreenToElement(touch->position_), touch->position_, MOUSEB_LEFT, 0, 0);
+        ProcessHover(touch->position_, MOUSEB_LEFT, 0, 0);
     }
-
+    
     Update(timeStep, rootElement_);
     Update(timeStep, rootModalElement_);
 }
@@ -471,6 +452,16 @@ void UI::SetDoubleClickInterval(float interval)
     doubleClickInterval_ = Max(interval, 0.0f);
 }
 
+void UI::SetDragBeginInterval(float interval)
+{
+    dragBeginInterval_ = Max(interval, 0.0f);
+}
+
+void UI::SetDragBeginDistance(int pixels)
+{
+    dragBeginDistance_ = Max(pixels, 0);
+}
+
 void UI::SetMaxFontTextureSize(int size)
 {
     if (IsPowerOfTwo(size) && size >= FONT_TEXTURE_MIN_SIZE)
@@ -551,6 +542,12 @@ UIElement* UI::GetFrontElement() const
     return front;
 }
 
+UIElement* UI::GetDragElement() const
+{
+    // Do not return the element until drag begin event has actually been posted
+    return dragBeginPending_ ? (UIElement*)0 : dragElement_;
+}
+
 const String& UI::GetClipBoardText() const
 {
     if (useSystemClipBoard_)
@@ -864,6 +861,50 @@ void UI::ReleaseFontFaces()
         fonts[i]->ReleaseFaces();
 }
 
+void UI::ProcessHover(const IntVector2& cursorPos, int buttons, int qualifiers, Cursor* cursor)
+{
+    WeakPtr<UIElement> element(GetElementAt(cursorPos));
+
+    bool dragSource = dragElement_ && (dragElement_->GetDragDropMode() & DD_SOURCE) != 0;
+    bool dragTarget = element && (element->GetDragDropMode() & DD_TARGET) != 0;
+    bool dragDropTest = dragSource && dragTarget && element != dragElement_;
+
+    // Hover effect
+    // If a drag is going on, transmit hover only to the element being dragged, unless it's a drop target
+    if (element && element->IsEnabled())
+    {
+        if (!dragElement_ || dragElement_ == element || dragDropTest)
+            element->OnHover(element->ScreenToElement(cursorPos), cursorPos, buttons, qualifiers, cursor);
+    }
+    else if (cursor)
+        cursor->SetShape(CS_NORMAL);
+
+    if (!dragBeginPending_)
+    {
+        // Drag and drop test
+        if (dragDropTest)
+        {
+            bool accept = element->OnDragDropTest(dragElement_);
+            if (accept)
+            {
+                using namespace DragDropTest;
+
+                VariantMap eventData;
+                eventData[P_SOURCE] = (void*)dragElement_.Get();
+                eventData[P_TARGET] = (void*)element.Get();
+                eventData[P_ACCEPT] = accept;
+                SendEvent(E_DRAGDROPTEST, eventData);
+                accept = eventData[P_ACCEPT].GetBool();
+            }
+
+            if (cursor)
+                cursor->SetShape(accept ? CS_ACCEPTDROP : CS_REJECTDROP);
+        }
+        else if (dragSource && cursor)
+            cursor->SetShape(dragElement_ == element ? CS_ACCEPTDROP : CS_REJECTDROP);
+    }
+}
+
 void UI::ProcessClickBegin(const IntVector2& cursorPos, int button, int buttons, int qualifiers, Cursor* cursor, bool cursorVisible)
 {
     if (cursorVisible)
@@ -887,7 +928,7 @@ void UI::ProcessClickBegin(const IntVector2& cursorPos, int button, int buttons,
             clickElement_ = element;
             
             // Fire double click event if element matches and is in time
-            if (doubleClickElement_ && element == doubleClickElement_ && clickTimer_->GetMSec(true) <
+            if (doubleClickElement_ && element == doubleClickElement_ && clickTimer_.GetMSec(true) <
                 (unsigned)(doubleClickInterval_ * 1000) && lastMouseButtons_ == buttons)
             {
                 element->OnDoubleClick(element->ScreenToElement(cursorPos), cursorPos, button, buttons, qualifiers, cursor);
@@ -897,15 +938,16 @@ void UI::ProcessClickBegin(const IntVector2& cursorPos, int button, int buttons,
             else
             {
                 doubleClickElement_ = element;
-                clickTimer_->Reset();
+                clickTimer_.Reset();
             }
             
             // Handle start of drag. Click handling may have caused destruction of the element, so check the pointer again
             if (element && !dragElement_ && buttons == MOUSEB_LEFT)
             {
                 dragElement_ = element;
-                element->OnDragBegin(element->ScreenToElement(cursorPos), cursorPos, buttons, qualifiers, cursor);
-                SendDragEvent(E_DRAGBEGIN, element, cursorPos);
+                dragBeginPending_ = true;
+                dragBeginPos_ = cursorPos;
+                dragBeginTimer_.Reset();
             }
         }
         else
@@ -934,10 +976,12 @@ void UI::ProcessClickEnd(const IntVector2& cursorPos, int button, int buttons, i
         // Handle end of drag
         if (dragElement_ && !buttons)
         {
-            if (dragElement_->IsEnabled() && dragElement_->IsVisible())
+            if (dragElement_->IsEnabled() && dragElement_->IsVisible() && !dragBeginPending_)
             {
                 dragElement_->OnDragEnd(dragElement_->ScreenToElement(cursorPos), cursorPos, cursor);
                 SendDragEvent(E_DRAGEND, dragElement_, cursorPos);
+                if (cursor)
+                    cursor->SetShape(CS_NORMAL);
 
                 bool dragSource = dragElement_ && (dragElement_->GetDragDropMode() & DD_SOURCE) != 0;
                 if (dragSource)
@@ -965,6 +1009,7 @@ void UI::ProcessClickEnd(const IntVector2& cursorPos, int button, int buttons, i
             }
 
             dragElement_.Reset();
+            dragBeginPending_ = false;
         }
         
         clickElement_.Reset();
@@ -977,8 +1022,23 @@ void UI::ProcessMove(const IntVector2& cursorPos, int buttons, int qualifiers, C
     {
         if (dragElement_->IsEnabled() && dragElement_->IsVisible())
         {
-            dragElement_->OnDragMove(dragElement_->ScreenToElement(cursorPos), cursorPos, buttons, qualifiers, cursor);
-            SendDragEvent(E_DRAGMOVE, dragElement_, cursorPos);
+            // Signal drag begin if distance threshold was exceeded
+            if (dragBeginPending_)
+            {
+                IntVector2 offset = cursorPos - dragBeginPos_;
+                if (Abs(offset.x_) >= dragBeginDistance_ || Abs(offset.y_) >= dragBeginDistance_)
+                {
+                    dragBeginPending_ = false;
+                    dragElement_->OnDragBegin(dragElement_->ScreenToElement(dragBeginPos_), dragBeginPos_, buttons, qualifiers, cursor);
+                    SendDragEvent(E_DRAGBEGIN, dragElement_, dragBeginPos_);
+                }
+            }
+            
+            if (!dragBeginPending_)
+            {
+                dragElement_->OnDragMove(dragElement_->ScreenToElement(cursorPos), cursorPos, buttons, qualifiers, cursor);
+                SendDragEvent(E_DRAGMOVE, dragElement_, cursorPos);
+            }
         }
         else
         {
@@ -1048,7 +1108,8 @@ void UI::HandleMouseButtonDown(StringHash eventType, VariantMap& eventData)
     bool cursorVisible;
     GetCursorPositionAndVisible(cursorPos, cursorVisible);
 
-    ProcessClickBegin(cursorPos, eventData[MouseButtonDown::P_BUTTON].GetInt(), mouseButtons_, qualifiers_, cursor_, cursorVisible);
+    ProcessClickBegin(cursorPos, eventData[MouseButtonDown::P_BUTTON].GetInt(), mouseButtons_, qualifiers_, cursor_,
+        cursorVisible);
 }
 
 void UI::HandleMouseButtonUp(StringHash eventType, VariantMap& eventData)

+ 28 - 5
Source/Engine/UI/UI.h

@@ -79,6 +79,10 @@ public:
     void SetClipBoardText(const String& text);
     /// Set UI element double click interval in seconds.
     void SetDoubleClickInterval(float interval);
+    /// Set UI drag event start interval in seconds.
+    void SetDragBeginInterval(float interval);
+    /// Set UI drag event start distance threshold in pixels.
+    void SetDragBeginDistance(int pixels);
     /// Set maximum font face texture size. Must be a power of two. Default is 2048.
     void SetMaxFontTextureSize(int size);
     /// Set whether mouse wheel can control also a non-focused element.
@@ -106,10 +110,16 @@ public:
     UIElement* GetFocusElement() const { return focusElement_; }
     /// Return topmost enabled root-level non-modal element.
     UIElement* GetFrontElement() const;
+    /// Return currently dragged element.
+    UIElement* GetDragElement() const;
     /// Return clipboard text.
     const String& GetClipBoardText() const;
     /// Return UI element double click interval in seconds.
     float GetDoubleClickInterval() const { return doubleClickInterval_; }
+    /// Return UI drag start event interval in seconds.
+    float GetDragBeginInterval() const { return dragBeginInterval_; }
+    /// Return UI drag start event distance threshold in pixels.
+    int GetDragBeginDistance() const { return dragBeginDistance_; }
     /// Return font texture maximum size.
     int GetMaxFontTextureSize() const { return maxFontTextureSize_; }
     /// Return whether mouse wheel can control also a non-focused element.
@@ -144,6 +154,8 @@ private:
     void SetCursorShape(CursorShape shape);
     /// Force release of font faces when global font properties change.
     void ReleaseFontFaces();
+    /// Handle button or touch hover
+    void ProcessHover(const IntVector2& cursorPos, int buttons, int qualifiers, Cursor* cursor);
     /// Handle button or touch begin.
     void ProcessClickBegin(const IntVector2& cursorPos, int button, int buttons, int qualifiers, Cursor* cursor, bool cursorVisible);
     /// Handle button or touch end.
@@ -221,8 +233,16 @@ private:
     PODVector<UIElement*> tempElements_;
     /// Clipboard text.
     mutable String clipBoard_;
+    /// Seconds between clicks to register a double click.
+    float doubleClickInterval_;
+    /// Seconds from mouse button down to begin a drag if there has been no movement exceeding pixel threshold.
+    float dragBeginInterval_;
+    /// Drag begin event distance threshold in pixels.
+    int dragBeginDistance_;
     /// Mouse buttons held down.
     int mouseButtons_;
+    /// Last mouse button pressed.
+    int lastMouseButtons_;
     /// Qualifier keys held down.
     int qualifiers_;
     /// Font texture maximum size.
@@ -239,18 +259,21 @@ private:
     bool useMutableGlyphs_;
     /// Flag for forcing FreeType autohinting.
     bool forceAutoHint_;
+    /// Flag for a drag start event pending.
+    bool dragBeginPending_;
     /// Non-modal batch size (used internally for rendering).
     unsigned nonModalBatchSize_;
     /// Timer used to trigger double click.
-    Timer* clickTimer_;
+    Timer clickTimer_;
+    /// Timer used to trigger drag begin event.
+    Timer dragBeginTimer_;
+    /// Drag start position.
+    IntVector2 dragBeginPos_;
+    /// Timer used for drag begin.
     /// UI element last clicked for tracking click end.
     WeakPtr<UIElement> clickElement_;
     /// UI element last clicked for tracking double clicks.
     WeakPtr<UIElement> doubleClickElement_;
-    /// Last mouse button pressed.
-    int lastMouseButtons_;
-    /// Seconds between clicks to register a double click.
-    float doubleClickInterval_;
 };
 
 /// Register UI library objects.

+ 2 - 1
Source/Engine/UI/UIElement.cpp

@@ -482,7 +482,8 @@ const IntVector2& UIElement::GetScreenPosition() const
 
 void UIElement::OnHover(const IntVector2& position, const IntVector2& screenPosition, int buttons, int qualifiers, Cursor* cursor)
 {
-    if (cursor && cursor->IsVisible())
+    // Reset cursor shape on hover, but do not do that unnecessarily during drag
+    if (cursor && cursor->IsVisible() && !GetSubsystem<UI>()->GetDragElement())
         cursor->SetShape(CS_NORMAL);
     hovering_ = true;
 }