Browse Source

Added LineEdit UI element.
Added getNumLogicalProcessors() to ProcessUtils.
Lose UI element focus when left-clicking over an inactive or nonexistent UI element.
Include cleanup.

Lasse Öörni 15 years ago
parent
commit
b0642caa78

BIN
Bin/Data/Textures/UI.png


+ 16 - 1
Bin/Data/UI/DefaultStyle.xml

@@ -22,6 +22,22 @@
         <imagerect value="0 0 16 24" />
         <hotspot value="0 0" />
     </element>
+    <element type="LineEdit">
+        <texture name="Textures/UI.png" />
+        <imagerect value="96 0 112 16" />
+        <border value="2 2 2 2" />
+        <clipborder value="1 1 1 1" />
+        <hoveroffset value="0 16" />
+        <text>
+            <position value="1 1" />
+            <font name="Cour.ttf" size="10" />
+        </text>
+        <cursor>
+            <size value="4 16" />
+            <texture name="Textures/UI.png" />
+            <imagerect value="112 0 116 16" />
+        </cursor>
+    </element>
     <element type="ScrollView">
         <size value="16 16" />
         <texture name="Textures/UI.png" />
@@ -52,5 +68,4 @@
     <element type="Text">
         <font name="Cour.ttf" size="10" />
     </element>
-
 </elements>

+ 5 - 1
Bin/Data/UI/TestLayout.xml

@@ -43,9 +43,13 @@
             <element type="Text">
                 <position value="1 1" />
                 <font name="times.ttf" size="15" />
-                <text value="Urho3D - a Win32/Direct3D9 rendering and game engine http://urho3d.googlecode.com Licensed under the MIT license, see License.txt for details." />
+                <text value="Urho3D - a Win32/Direct3D9 rendering and game engine\nhttp://urho3d.googlecode.com\nLicensed under the MIT license, see License.txt for details." />
                 <maxwidth value="200" />
             </element>
         </element>
+        <element type="LineEdit">
+            <position value="150 270" />
+            <size value="200 16" />
+        </element>
     </element>
 </layout>

+ 8 - 1
Engine/Common/ProcessUtils.cpp

@@ -187,7 +187,14 @@ std::string getUserDocumentsDirectory()
     pathName[0] = 0;
     
     SHGetSpecialFolderPath(0, pathName, CSIDL_PERSONAL, 0);
-    return fixPath(replace(std::string(pathName), '\\', '/'));
+    return fixPath(std::string(pathName));
+}
+
+unsigned getNumLogicalProcessors()
+{
+    SYSTEM_INFO info;
+    GetSystemInfo(&info);
+    return info.dwNumberOfProcessors;
 }
 
 #ifdef ENABLE_MINIDUMPS

+ 2 - 0
Engine/Common/ProcessUtils.h

@@ -39,6 +39,8 @@ void getArguments(const char* cmdLine, std::vector<std::string>& arguments);
 bool getConsoleInput(std::string& line);
 //! Return the user's document directory which should have read/write access for writing logs, savegames etc.
 std::string getUserDocumentsDirectory();
+//! Return the number of logical processors
+unsigned getNumLogicalProcessors();
 
 #ifdef ENABLE_MINIDUMPS
 //! Write a minidump. Needs to be called from within a structured exception handler

+ 2 - 1
Engine/Engine/Engine.cpp

@@ -98,7 +98,8 @@ Engine::Engine(const std::string& logFileName, bool headless) :
         "ControlsUpdate", "ControlsPlayback", "MouseButtonDown", "MouseButtonUp", "MouseMove", "KeyDown", "KeyUp", "Char",
         "PeerConnected", "NetworkPacket", "PeerDisconnected", "PhysicsPreStep", "PhysicsPostStep", "PhysicsCollision",
         "EntityCollision", "WindowMessage", "WindowResized", "BeginFrame", "EndFrame", "SceneUpdate", "ScenePostUpdate",
-        "AsyncLoadProgress", "AsyncLoadFinished", "Focused", "Defocused", "Pressed", "Toggled", "ValueChanged", ""
+        "AsyncLoadProgress", "AsyncLoadFinished", "Focused", "Defocused", "Pressed", "Toggled", "SliderChanged", "ViewChanged",
+        "TextChanged", "TextFinished", ""
     };
     
     for (unsigned i = 0; inbuiltEvents[i].length(); ++i)

+ 38 - 20
Engine/Engine/RegisterUI.cpp

@@ -27,6 +27,7 @@
 #include "Cursor.h"
 #include "Engine.h"
 #include "Font.h"
+#include "LineEdit.h"
 #include "RegisterTemplates.h"
 #include "ScrollView.h"
 #include "Slider.h"
@@ -71,25 +72,6 @@ static void registerUIElement(asIScriptEngine* engine)
     engine->RegisterObjectMethod("Variant", "UIElement@+ getUIElement() const", asFUNCTION(getVariantPtr<UIElement>), asCALL_CDECL_OBJLAST);
 }
 
-static void registerText(asIScriptEngine* engine)
-{
-    registerUIElement<Text>(engine, "Text");
-    engine->RegisterObjectMethod("Text", "bool setFont(Font@+, int)", asMETHOD(Text, setFont), asCALL_THISCALL);
-    engine->RegisterObjectMethod("Text", "void setMaxWidth(int)", asMETHOD(Text, setMaxWidth), asCALL_THISCALL);
-    engine->RegisterObjectMethod("Text", "void setText(const string& in)", asMETHOD(Text, setText), asCALL_THISCALL);
-    engine->RegisterObjectMethod("Text", "void setTextAlignment(HorizontalAlignment)", asMETHOD(Text, setTextAlignment), asCALL_THISCALL);
-    engine->RegisterObjectMethod("Text", "void setTextSpacing(float)", asMETHOD(Text, setTextSpacing), asCALL_THISCALL);
-    engine->RegisterObjectMethod("Text", "Font@+ getFont() const", asMETHOD(Text, getFont), asCALL_THISCALL);
-    engine->RegisterObjectMethod("Text", "int getFontSize() const", asMETHOD(Text, getFontSize), asCALL_THISCALL);
-    engine->RegisterObjectMethod("Text", "int getMaxWidth() const", asMETHOD(Text, getMaxWidth), asCALL_THISCALL);
-    engine->RegisterObjectMethod("Text", "const string& getText() const", asMETHOD(Text, getText), asCALL_THISCALL);
-    engine->RegisterObjectMethod("Text", "HorizontalAlignment getTextAlignment() const", asMETHOD(Text, getTextAlignment), asCALL_THISCALL);
-    engine->RegisterObjectMethod("Text", "float getTextSpacing() const", asMETHOD(Text, getTextSpacing), asCALL_THISCALL);
-    engine->RegisterObjectMethod("Text", "uint getNumRows() const", asMETHOD(Text, getNumRows), asCALL_THISCALL);
-    engine->RegisterObjectMethod("Text", "int getRowHeight() const", asMETHOD(Text, getRowHeight), asCALL_THISCALL);
-    registerRefCasts<UIElement, Text>(engine, "UIElement", "Text");
-}
-
 static void registerBorderImage(asIScriptEngine* engine)
 {
     registerBorderImage<BorderImage>(engine, "BorderImage");
@@ -143,7 +125,7 @@ static void registerSlider(asIScriptEngine* engine)
     engine->RegisterObjectMethod("Slider", "UIElementOrientation getOrientation() const", asMETHOD(Slider, getOrientation), asCALL_THISCALL);
     engine->RegisterObjectMethod("Slider", "float getRange() const", asMETHOD(Slider, getRange), asCALL_THISCALL);
     engine->RegisterObjectMethod("Slider", "float getValue() const", asMETHOD(Slider, getValue), asCALL_THISCALL);
-    engine->RegisterObjectMethod("Slider", "BorderImage@+ getSlider() const", asMETHOD(Slider, getSlider), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Slider", "BorderImage@+ getSliderElement() const", asMETHOD(Slider, getSliderElement), asCALL_THISCALL);
     registerRefCasts<UIElement, Slider>(engine, "UIElement", "Slider");
 }
 
@@ -163,6 +145,41 @@ static void registerScrollView(asIScriptEngine* engine)
     registerRefCasts<UIElement, ScrollView>(engine, "UIElement", "ScrollView");
 }
 
+static void registerText(asIScriptEngine* engine)
+{
+    registerUIElement<Text>(engine, "Text");
+    engine->RegisterObjectMethod("Text", "bool setFont(Font@+, int)", asMETHOD(Text, setFont), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Text", "void setMaxWidth(int)", asMETHOD(Text, setMaxWidth), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Text", "void setText(const string& in)", asMETHOD(Text, setText), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Text", "void setTextAlignment(HorizontalAlignment)", asMETHOD(Text, setTextAlignment), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Text", "void setTextSpacing(float)", asMETHOD(Text, setTextSpacing), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Text", "Font@+ getFont() const", asMETHOD(Text, getFont), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Text", "int getFontSize() const", asMETHOD(Text, getFontSize), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Text", "int getMaxWidth() const", asMETHOD(Text, getMaxWidth), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Text", "const string& getText() const", asMETHOD(Text, getText), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Text", "HorizontalAlignment getTextAlignment() const", asMETHOD(Text, getTextAlignment), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Text", "float getTextSpacing() const", asMETHOD(Text, getTextSpacing), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Text", "uint getNumRows() const", asMETHOD(Text, getNumRows), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Text", "int getRowHeight() const", asMETHOD(Text, getRowHeight), asCALL_THISCALL);
+    registerRefCasts<UIElement, Text>(engine, "UIElement", "Text");
+}
+
+static void registerLineEdit(asIScriptEngine* engine)
+{
+    registerBorderImage<LineEdit>(engine, "LineEdit");
+    engine->RegisterObjectMethod("LineEdit", "void setText(const string& in)", asMETHOD(LineEdit, setText), asCALL_THISCALL);
+    engine->RegisterObjectMethod("LineEdit", "void setEchoCharacter(uint8)", asMETHOD(LineEdit, setEchoCharacter), asCALL_THISCALL);
+    engine->RegisterObjectMethod("LineEdit", "void setCursorBlinkRate(float)", asMETHOD(LineEdit, setCursorBlinkRate), asCALL_THISCALL);
+    engine->RegisterObjectMethod("LineEdit", "void setMaxLength(uint)", asMETHOD(LineEdit, setMaxLength), asCALL_THISCALL);
+    engine->RegisterObjectMethod("LineEdit", "const string& getText() const", asMETHOD(LineEdit, getText), asCALL_THISCALL);
+    engine->RegisterObjectMethod("LineEdit", "uint8 getEchoCharacter() const", asMETHOD(LineEdit, getEchoCharacter), asCALL_THISCALL);
+    engine->RegisterObjectMethod("LineEdit", "float getCursorBlinkRate() const", asMETHOD(LineEdit, getCursorBlinkRate), asCALL_THISCALL);
+    engine->RegisterObjectMethod("LineEdit", "uint getMaxLength() const", asMETHOD(LineEdit, getMaxLength), asCALL_THISCALL);
+    engine->RegisterObjectMethod("LineEdit", "Text@+ getTextElement() const", asMETHOD(LineEdit, getTextElement), asCALL_THISCALL);
+    engine->RegisterObjectMethod("LineEdit", "BorderImage@+ getCursorElement() const", asMETHOD(LineEdit, getCursorElement), asCALL_THISCALL);
+    registerRefCasts<UIElement, LineEdit>(engine, "UIElement", "LineEdit");
+}
+
 static void registerWindow(asIScriptEngine* engine)
 {
     registerBorderImage<Window>(engine, "Window");
@@ -281,6 +298,7 @@ void registerUILibrary(asIScriptEngine* engine)
     registerSlider(engine);
     registerScrollView(engine);
     registerText(engine);
+    registerLineEdit(engine);
     registerWindow(engine);
     registerUI(engine);
 }

+ 3 - 0
Engine/UI/BaseUIElementFactory.cpp

@@ -26,6 +26,7 @@
 #include "Button.h"
 #include "CheckBox.h"
 #include "Cursor.h"
+#include "LineEdit.h"
 #include "ScrollView.h"
 #include "Slider.h"
 #include "Text.h"
@@ -41,6 +42,8 @@ UIElement* BaseUIElementFactory::createElement(ShortStringHash type, const std::
         return new CheckBox(name);
     if (type == Cursor::getTypeStatic())
         return new Cursor(name);
+    if (type == LineEdit::getTypeStatic())
+        return new LineEdit(std::string(), name);
     if (type == ScrollView::getTypeStatic())
         return new ScrollView(name);
     if (type == Slider::getTypeStatic())

+ 0 - 3
Engine/UI/BorderImage.cpp

@@ -42,9 +42,6 @@ BorderImage::~BorderImage()
 
 void BorderImage::setStyle(const XMLElement& element, ResourceCache* cache)
 {
-    if (!cache)
-        EXCEPTION("Null resource cache for UI element");
-    
     UIElement::setStyle(element, cache);
     
     if (element.hasChildElement("texture"))

+ 0 - 5
Engine/UI/Button.cpp

@@ -24,8 +24,6 @@
 #include "Precompiled.h"
 #include "Button.h"
 #include "InputEvents.h"
-#include "ResourceCache.h"
-#include "Texture.h"
 #include "UIEvents.h"
 
 #include "DebugNew.h"
@@ -47,9 +45,6 @@ Button::~Button()
 
 void Button::setStyle(const XMLElement& element, ResourceCache* cache)
 {
-    if (!cache)
-        EXCEPTION("Null resource cache for UI element");
-    
     BorderImage::setStyle(element, cache);
     
     if (element.hasChildElement("inactiverect"))

+ 0 - 2
Engine/UI/CheckBox.cpp

@@ -24,8 +24,6 @@
 #include "Precompiled.h"
 #include "CheckBox.h"
 #include "InputEvents.h"
-#include "ResourceCache.h"
-#include "Texture.h"
 #include "UIEvents.h"
 
 #include "DebugNew.h"

+ 0 - 5
Engine/UI/Cursor.cpp

@@ -23,8 +23,6 @@
 
 #include "Precompiled.h"
 #include "Cursor.h"
-#include "ResourceCache.h"
-#include "Texture.h"
 
 #include "DebugNew.h"
 
@@ -42,9 +40,6 @@ Cursor::~Cursor()
 
 void Cursor::setStyle(const XMLElement& element, ResourceCache* cache)
 {
-    if (!cache)
-        EXCEPTION("Null resource cache for UI element");
-    
     BorderImage::setStyle(element, cache);
     
     if (element.hasChildElement("hotspot"))

+ 200 - 0
Engine/UI/LineEdit.cpp

@@ -0,0 +1,200 @@
+//
+// Urho3D Engine
+// Copyright (c) 2008-2011 Lasse Öörni
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#include "Precompiled.h"
+#include "LineEdit.h"
+#include "Text.h"
+#include "UIEvents.h"
+
+#include "DebugNew.h"
+
+LineEdit::LineEdit(const std::string& text, const std::string& name) :
+    BorderImage(name),
+    mEchoCharacter(0),
+    mCursorBlinkRate(1.0f),
+    mCursorBlinkTimer(0.0f),
+    mMaxLength(0),
+    mDefocus(false)
+{
+    mClipChildren = true;
+    mEnabled = true;
+    mFocusable = true;
+    mText = new Text();
+    mCursor = new BorderImage();
+    addChild(mText);
+    addChild(mCursor);
+    
+    // Show cursor on top of text, and set initial text
+    mCursor->setPriority(1);
+    setText(text);
+}
+
+LineEdit::~LineEdit()
+{
+}
+
+void LineEdit::setStyle(const XMLElement& element, ResourceCache* cache)
+{
+    BorderImage::setStyle(element, cache);
+    
+    if (element.hasChildElement("cursorblinkrate"))
+        setCursorBlinkRate(element.getChildElement("cursorblinkrate").getFloat("value"));
+    if (element.hasChildElement("echocharacter"))
+    {
+        std::string text = element.getChildElement("echocharacter").getString("value");
+        if (text.length())
+            setEchoCharacter(text[0]);
+    }
+    if (element.hasChildElement("maxlength"))
+        setMaxLength(element.getChildElement("maxlength").getInt("value"));
+    
+    XMLElement textElem = element.getChildElement("text");
+    if (textElem)
+    {
+        if (textElem.hasAttribute("value"))
+            setText(textElem.getString("value"));
+        mText->setStyle(textElem, cache);
+    }
+    XMLElement cursorElem = element.getChildElement("cursor");
+    if (cursorElem)
+        mCursor->setStyle(cursorElem, cache);
+}
+
+void LineEdit::update(float timeStep)
+{
+    if (mCursorBlinkRate > 0.0f)
+        mCursorBlinkTimer = fmodf(mCursorBlinkTimer + mCursorBlinkRate * timeStep, 1.0f);
+    else
+        mCursorBlinkTimer = 0.0f;
+    
+    bool cursorVisible = false;
+    
+    if (mFocus)
+    {
+        int textLength = 0;
+        const std::vector<int>& rowWidths = mText->getRowWidths();
+        if (rowWidths.size())
+            textLength = rowWidths[0];
+        
+        // This assumes that text alignment is top-left
+        mCursor->setPosition(mText->getPosition() + IntVector2(textLength, 0));
+        mCursor->setSize(mCursor->getWidth(), mText->getRowHeight());
+        cursorVisible = mCursorBlinkTimer < 0.5f;
+        
+        // Scroll if text is longer than what can be visible at once
+        int scrollThreshold = max(getWidth() - mClipBorder.mLeft - mClipBorder.mRight - mCursor->getWidth(), 0);
+        if (textLength > scrollThreshold)
+            setChildOffset(IntVector2(-(textLength - scrollThreshold), 0));
+        else
+            setChildOffset(IntVector2::sZero);
+    }
+    
+    mCursor->setVisible(cursorVisible);
+    
+    if (mDefocus)
+    {
+        setFocus(false);
+        mDefocus = false;
+    }
+}
+
+void LineEdit::onChar(unsigned char c)
+{
+    unsigned currentLength = mLine.length();
+    bool sendChangeEvent = false;
+    
+    if (c == '\b')
+    {
+        if (mLine.length())
+        {
+            mLine = mLine.substr(0, currentLength - 1);
+            sendChangeEvent = true;
+        }
+    }
+    else if (c == '\r')
+    {
+        using namespace TextFinished;
+        
+        VariantMap eventData;
+        eventData[P_ELEMENT] = (void*)this;
+        sendEvent(EVENT_TEXTFINISHED, eventData);
+        
+        mDefocus = true;
+    }
+    else if (c == 27)
+        mDefocus = true;
+    else if ((c >= 0x20) && ((!mMaxLength) || (currentLength < mMaxLength)))
+    {
+        mLine += (char)c;
+        sendChangeEvent = true;
+    }
+    
+    updateText();
+    
+    if (sendChangeEvent)
+    {
+        using namespace TextChanged;
+        
+        VariantMap eventData;
+        eventData[P_ELEMENT] = (void*)this;
+        eventData[P_TEXT] = mLine;
+        sendEvent(EVENT_TEXTFINISHED, eventData);
+    }
+}
+
+void LineEdit::setText(const std::string& text)
+{
+    mLine = text;
+    mText->setText(text);
+    updateText();
+}
+
+void LineEdit::setEchoCharacter(char c)
+{
+    mEchoCharacter = c;
+    updateText();
+}
+
+void LineEdit::setCursorBlinkRate(float rate)
+{
+    mCursorBlinkRate = max(rate, 0.0f);
+}
+
+void LineEdit::setMaxLength(unsigned length)
+{
+    mMaxLength = length;
+}
+
+void LineEdit::updateText()
+{
+    if (!mEchoCharacter)
+        mText->setText(mLine);
+    else
+    {
+        std::string echoText;
+        echoText.resize(mLine.length());
+        for (unsigned i = 0; i < mLine.length(); ++i)
+            echoText[i] = mEchoCharacter;
+        mText->setText(echoText);
+    }
+}

+ 93 - 0
Engine/UI/LineEdit.h

@@ -0,0 +1,93 @@
+//
+// Urho3D Engine
+// Copyright (c) 2008-2011 Lasse Öörni
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+//
+
+#ifndef UI_LINEEDIT_H
+#define UI_LINEEDIT_H
+
+#include "BorderImage.h"
+
+class Text;
+
+class LineEdit : public BorderImage
+{
+    DEFINE_TYPE(LineEdit);
+    
+public:
+    //! Construct with initial text and name
+    LineEdit(const std::string& text = std::string(), const std::string& name = std::string());
+    //! Destruct
+    virtual ~LineEdit();
+    
+    //! Set UI element style from XML data
+    virtual void setStyle(const XMLElement& element, ResourceCache* cache);
+    //! Perform UI element update
+    virtual void update(float timeStep);
+    
+    //! React to a character typed on keyboard
+    virtual void onChar(unsigned char c);
+    
+    //! Set text
+    void setText(const std::string& text);
+    //! Set echo character for password entry and such. 0 (default) shows the actual text
+    void setEchoCharacter(char c);
+    //! Set cursor blink rate. 0 disables blinking
+    void setCursorBlinkRate(float rate);
+    //! Set maximum text length. 0 for unlimited
+    void setMaxLength(unsigned length);
+    
+    //! Return text
+    const std::string& getText() const { return mLine; }
+    //! Return echo character
+    char getEchoCharacter() const { return mEchoCharacter; }
+    //! Return cursor blink rate
+    float getCursorBlinkRate() const { return mCursorBlinkRate; }
+    //! Return maximum text length
+    unsigned getMaxLength() const { return mMaxLength; }
+    //! Return text element
+    Text* getTextElement() const { return mText; }
+    //! Return cursor element
+    BorderImage* getCursorElement() const { return mCursor; }
+    
+protected:
+    //! Update displayed text
+    void updateText();
+    
+    //! Text line
+    std::string mLine;
+    //! Echo character
+    char mEchoCharacter;
+    //! Cursor blink rate
+    float mCursorBlinkRate;
+    //! Cursor blink timer
+    float mCursorBlinkTimer;
+    //! Maximum text length
+    unsigned mMaxLength;
+    //! Text element
+    SharedPtr<Text> mText;
+    //! Cursor element
+    SharedPtr<BorderImage> mCursor;
+    //! Defocus flag (defocus on next update)
+    bool mDefocus;
+};
+
+#endif // UI_LINEEDIT_H

+ 12 - 4
Engine/UI/ScrollView.cpp

@@ -22,7 +22,6 @@
 //
 
 #include "Precompiled.h"
-#include "ResourceCache.h"
 #include "ScrollView.h"
 #include "Slider.h"
 #include "UIEvents.h"
@@ -45,9 +44,6 @@ ScrollView::~ScrollView()
 
 void ScrollView::setStyle(const XMLElement& element, ResourceCache* cache)
 {
-    if (!cache)
-        EXCEPTION("Null resource cache for UI element");
-    
     BorderImage::setStyle(element, cache);
     
     if (element.hasChildElement("viewposition"))
@@ -125,6 +121,7 @@ void ScrollView::updateViewFromSliders()
     if ((!mHorizontalSlider) && (!mVerticalSlider))
         return;
     
+    IntVector2 oldPosition = mViewPosition;
     IntVector2 newPosition = mViewPosition;
     IntVector2 size = getSize();
     
@@ -134,6 +131,17 @@ void ScrollView::updateViewFromSliders()
         newPosition.mY = (int)(mVerticalSlider->getValue() * (float)size.mY);
     
     updateView(newPosition);
+    
+    if (mViewPosition != oldPosition)
+    {
+        using namespace ViewChanged;
+        
+        VariantMap eventData;
+        eventData[P_ELEMENT] = (void*)this;
+        eventData[P_X] = mViewPosition.mX;
+        eventData[P_Y] = mViewPosition.mY;
+        sendEvent(EVENT_VIEWCHANGED, eventData);
+    }
 }
 
 void ScrollView::updateSliders()

+ 2 - 7
Engine/UI/Slider.cpp

@@ -24,9 +24,7 @@
 #include "Precompiled.h"
 #include "InputEvents.h"
 #include "Log.h"
-#include "ResourceCache.h"
 #include "Slider.h"
-#include "Texture2D.h"
 #include "UIEvents.h"
 
 #include "DebugNew.h"
@@ -51,9 +49,6 @@ Slider::~Slider()
 
 void Slider::setStyle(const XMLElement& element, ResourceCache* cache)
 {
-    if (!cache)
-        EXCEPTION("Null resource cache for UI element");
-    
     BorderImage::setStyle(element, cache);
     
     if (element.hasChildElement("orientation"))
@@ -121,12 +116,12 @@ void Slider::onDragMove(const IntVector2& position, const IntVector2& screenPosi
     {
         mValue = newValue;
         
-        using namespace ValueChanged;
+        using namespace SliderChanged;
         
         VariantMap eventData;
         eventData[P_ELEMENT] = (void*)this;
         eventData[P_VALUE] = mValue;
-        sendEvent(EVENT_VALUECHANGED, eventData);
+        sendEvent(EVENT_SLIDERCHANGED, eventData);
     }
 }
 

+ 1 - 1
Engine/UI/Slider.h

@@ -64,7 +64,7 @@ public:
     //! Return slider current value
     float getValue() const { return mValue; }
     //! Return slider image element
-    BorderImage* getSlider() const { return mSlider; }
+    BorderImage* getSliderElement() const { return mSlider; }
     
 protected:
     //! Update slider image position & size

+ 7 - 5
Engine/UI/Text.cpp

@@ -26,8 +26,9 @@
 #include "Log.h"
 #include "Profiler.h"
 #include "ResourceCache.h"
+#include "StringUtils.h"
 #include "Text.h"
-#include "Texture.h"
+#include "Texture2D.h"
 
 #include "DebugNew.h"
 
@@ -47,9 +48,6 @@ Text::~Text()
 
 void Text::setStyle(const XMLElement& element, ResourceCache* cache)
 {
-    if (!cache)
-        EXCEPTION("Null resource cache for UI element");
-    
     UIElement::setStyle(element, cache);
     
     if (element.hasChildElement("font"))
@@ -60,7 +58,11 @@ void Text::setStyle(const XMLElement& element, ResourceCache* cache)
     if (element.hasChildElement("maxwidth"))
         setMaxWidth(element.getChildElement("maxwidth").getInt("value"));
     if (element.hasChildElement("text"))
-        setText(element.getChildElement("text").getString("value"));
+    {
+        std::string text = element.getChildElement("text").getString("value");
+        replaceInPlace(text, "\\n", "\n");
+        setText(text);
+    }
     if (element.hasChildElement("textspacing"))
         setTextSpacing(element.getChildElement("textspacing").getFloat("value"));
     if (element.hasChildElement("textalignment"))

+ 5 - 0
Engine/UI/UI.cpp

@@ -470,6 +470,11 @@ void UI::handleMouseButtonDown(StringHash eventType, VariantMap& eventData)
                 element->onDragStart(element->screenToElement(pos), pos, mMouseButtons);
             }
         }
+        else
+        {
+            // If clicked over no element, or a disabled element, lose focus
+            setFocusElement(0);
+        }
     }
 }
 

+ 1 - 1
Engine/UI/UIElement.cpp

@@ -223,7 +223,7 @@ float UIElement::getDerivedOpacity()
     return mDerivedOpacity;
 }
 
-void UIElement::onChar(unsigned key)
+void UIElement::onChar(unsigned char c)
 {
 }
 

+ 1 - 1
Engine/UI/UIElement.h

@@ -99,7 +99,7 @@ public:
     //! React to mouse drag end
     virtual void onDragEnd(const IntVector2& position, const IntVector2& screenPosition);
     //! React to a character typed on keyboard
-    virtual void onChar(unsigned key);
+    virtual void onChar(unsigned char c);
     
     //! Set name
     void setName(const std::string& name);

+ 22 - 1
Engine/UI/UIEvents.h

@@ -52,10 +52,31 @@ DEFINE_EVENT(EVENT_TOGGLED, Toggled)
 }
 
 //! UI slider value changed
-DEFINE_EVENT(EVENT_VALUECHANGED, ValueChanged)
+DEFINE_EVENT(EVENT_SLIDERCHANGED, SliderChanged)
 {
     EVENT_PARAM(P_ELEMENT, Element);            // UIElement pointer
     EVENT_PARAM(P_VALUE, Value);                // float
 }
 
+//! ScrollView position changed
+DEFINE_EVENT(EVENT_VIEWCHANGED, ViewChanged)
+{
+    EVENT_PARAM(P_ELEMENT, Element);            // UIElement pointer
+    EVENT_PARAM(P_X, X);                        // int
+    EVENT_PARAM(P_Y, Y);                        // int
+}
+
+//! Editable text changed
+DEFINE_EVENT(EVENT_TEXTCHANGED, TextChanged)
+{
+    EVENT_PARAM(P_ELEMENT, Element);            // UIElement pointer
+    EVENT_PARAM(P_TEXT, Text);                  // string
+}
+
+//! Text editing finished (enter pressed on a LineEdit)
+DEFINE_EVENT(EVENT_TEXTFINISHED, TextFinished)
+{
+    EVENT_PARAM(P_ELEMENT, Element);            // UIElement pointer
+}
+
 #endif // UI_UIEVENTS_H

+ 6 - 9
Engine/UI/Window.cpp

@@ -23,19 +23,19 @@
 
 #include "Precompiled.h"
 #include "InputEvents.h"
-#include "ResourceCache.h"
-#include "Texture.h"
 #include "Window.h"
 
 #include "DebugNew.h"
 
+static const int DEFAULT_RESIZE_BORDER = 4;
+
 Window::Window(const std::string& name) :
     BorderImage(name),
     mMovable(false),
     mResizable(false),
-    mMinSize(WINDOW_MIN_SIZE, WINDOW_MIN_SIZE),
+    mMinSize(0, 0),
     mMaxSize(M_MAX_INT, M_MAX_INT),
-    mResizeBorder(WINDOW_MIN_SIZE, WINDOW_MIN_SIZE, WINDOW_MIN_SIZE, WINDOW_MIN_SIZE),
+    mResizeBorder(DEFAULT_RESIZE_BORDER, DEFAULT_RESIZE_BORDER, DEFAULT_RESIZE_BORDER, DEFAULT_RESIZE_BORDER),
     mDragMode(DRAG_NONE)
 {
     mBringToFront = true;
@@ -51,9 +51,6 @@ Window::~Window()
 
 void Window::setStyle(const XMLElement& element, ResourceCache* cache)
 {
-    if (!cache)
-        EXCEPTION("Null resource cache for UI element");
-    
     BorderImage::setStyle(element, cache);
     
     if (element.hasChildElement("minsize"))
@@ -231,8 +228,8 @@ void Window::setResizeBorder(int left, int top, int right, int bottom)
 
 void Window::validateSize()
 {
-    mMinSize.mX = max(mMinSize.mX, WINDOW_MIN_SIZE);
-    mMinSize.mY = max(mMinSize.mY, WINDOW_MIN_SIZE);
+    mMinSize.mX = max(mMinSize.mX, 0);
+    mMinSize.mY = max(mMinSize.mY, 0);
     mMaxSize.mX = max(mMaxSize.mX, mMinSize.mX);
     mMaxSize.mY = max(mMaxSize.mY, mMinSize.mX);
     

+ 0 - 2
Engine/UI/Window.h

@@ -26,8 +26,6 @@
 
 #include "BorderImage.h"
 
-static const int WINDOW_MIN_SIZE = 4;
-
 //! Window movement and resizing modes
 enum WindowDragMode
 {

+ 150 - 146
Examples/Test/Application.cpp

@@ -514,174 +514,178 @@ void Application::handleUpdate(StringHash eventType, VariantMap& eventData)
         }
     }
     
-    Input* input = mEngine->getInput();
-    
-    float speedMultiplier = 1.0f;
-    if (input->getKeyDown(KEY_SHIFT))
-        speedMultiplier = 5.0f;
-    if (input->getKeyDown(KEY_CONTROL))
-        speedMultiplier = 0.1f;
-    
-    if (input->getKeyDown('W'))
-        mCameraEntity->getComponent<Camera>()->translateRelative(Vector3(0.0f,0.0f,10.0f) * timeStep * speedMultiplier);
-    if (input->getKeyDown('S'))
-        mCameraEntity->getComponent<Camera>()->translateRelative(Vector3(0.0f,0.0f,-10.0f) * timeStep * speedMultiplier);
-    if (input->getKeyDown('A'))
-        mCameraEntity->getComponent<Camera>()->translateRelative(Vector3(-10.0f,0.0f,0.0f) * timeStep * speedMultiplier);
-    if (input->getKeyDown('D'))
-        mCameraEntity->getComponent<Camera>()->translateRelative(Vector3(10.0f,0.0f,0.0f) * timeStep * speedMultiplier);
-    
-    if (input->getKeyPress('1'))
+    UI* ui = mEngine->getUI();
+    if (!ui->getFocusElement())
     {
-        Renderer* renderer = mEngine->getRenderer();
+        Input* input = mEngine->getInput();
         
-        int nextRenderMode = renderer->getRenderMode();
+        float speedMultiplier = 1.0f;
         if (input->getKeyDown(KEY_SHIFT))
+            speedMultiplier = 5.0f;
+        if (input->getKeyDown(KEY_CONTROL))
+            speedMultiplier = 0.1f;
+        
+        if (input->getKeyDown('W'))
+            mCameraEntity->getComponent<Camera>()->translateRelative(Vector3(0.0f,0.0f,10.0f) * timeStep * speedMultiplier);
+        if (input->getKeyDown('S'))
+            mCameraEntity->getComponent<Camera>()->translateRelative(Vector3(0.0f,0.0f,-10.0f) * timeStep * speedMultiplier);
+        if (input->getKeyDown('A'))
+            mCameraEntity->getComponent<Camera>()->translateRelative(Vector3(-10.0f,0.0f,0.0f) * timeStep * speedMultiplier);
+        if (input->getKeyDown('D'))
+            mCameraEntity->getComponent<Camera>()->translateRelative(Vector3(10.0f,0.0f,0.0f) * timeStep * speedMultiplier);
+        
+        if (input->getKeyPress('1'))
         {
-            --nextRenderMode;
-            if (nextRenderMode < 0)
-                nextRenderMode = 2;
+            Renderer* renderer = mEngine->getRenderer();
+            
+            int nextRenderMode = renderer->getRenderMode();
+            if (input->getKeyDown(KEY_SHIFT))
+            {
+                --nextRenderMode;
+                if (nextRenderMode < 0)
+                    nextRenderMode = 2;
+            }
+            else
+            {
+                ++nextRenderMode;
+                if (nextRenderMode > 2)
+                    nextRenderMode = 0;
+            }
+            
+            renderer->setMode((RenderMode)nextRenderMode, renderer->getWidth(), renderer->getHeight(), renderer->getFullscreen(),
+                renderer->getVsync(), renderer->getMultiSample());
         }
-        else
+        
+        if (input->getKeyPress('2'))
         {
-            ++nextRenderMode;
-            if (nextRenderMode > 2)
-                nextRenderMode = 0;
+            texturequality++;
+            if (texturequality > 2)
+                texturequality = 0;
+            
+            mEngine->getPipeline()->setTextureQuality(texturequality);
         }
         
-        renderer->setMode((RenderMode)nextRenderMode, renderer->getWidth(), renderer->getHeight(), renderer->getFullscreen(),
-            renderer->getVsync(), renderer->getMultiSample());
-    }
-    
-    if (input->getKeyPress('2'))
-    {
-        texturequality++;
-        if (texturequality > 2)
-            texturequality = 0;
+        if (input->getKeyPress('3'))
+        {
+            materialquality++;
+            if (materialquality > 2)
+                materialquality = 0;
+            mEngine->getPipeline()->setMaterialQuality(materialquality);
+        }
         
-        mEngine->getPipeline()->setTextureQuality(texturequality);
-    }
-    
-    if (input->getKeyPress('3'))
-    {
-        materialquality++;
-        if (materialquality > 2)
-            materialquality = 0;
-        mEngine->getPipeline()->setMaterialQuality(materialquality);
-    }
-    
-    if (input->getKeyPress('4'))
-    {
-        usespecular = !usespecular;
-        mEngine->getPipeline()->setSpecularLighting(usespecular);
-    }
-    
-    if (input->getKeyPress('5'))
-    {
-        drawshadows = !drawshadows;
-        mEngine->getPipeline()->setDrawShadows(drawshadows);
-    }
-    
-    if (input->getKeyPress('6'))
-    {
-        shadowmapsize *= 2;
-        if (shadowmapsize > 2048)
-            shadowmapsize = 512;
+        if (input->getKeyPress('4'))
+        {
+            usespecular = !usespecular;
+            mEngine->getPipeline()->setSpecularLighting(usespecular);
+        }
         
-        mEngine->getPipeline()->setShadowMapSize(shadowmapsize);
-    }
-    
-    if (input->getKeyPress('7'))
-    {
-        hiresshadowmap = !hiresshadowmap;
-        mEngine->getPipeline()->setShadowMapHiresDepth(hiresshadowmap);
-    }
-    
-    if (input->getKeyPress('8'))
-    {
-        useocclusion = !useocclusion;
-        mEngine->getPipeline()->setMaxOccluderTriangles(useocclusion ? 5000 : 0);
-    }
-    
-    if (input->getKeyPress('L'))
-    {
-        Light* cameraLight = mCameraEntity->getComponent<Light>();
-        attach = !attach;
-        if (attach)
+        if (input->getKeyPress('5'))
         {
-            cameraLight->setPosition(Vector3::sZero);
-            cameraLight->setRotation(Quaternion::sIdentity);
-            mCameraEntity->getComponent<Camera>()->addChild(cameraLight);
+            drawshadows = !drawshadows;
+            mEngine->getPipeline()->setDrawShadows(drawshadows);
         }
-        else
+        
+        if (input->getKeyPress('6'))
         {
-            // Detach child and set world transform to match what it was before detach
-            mCameraEntity->getComponent<Camera>()->removeChild(cameraLight, true);
+            shadowmapsize *= 2;
+            if (shadowmapsize > 2048)
+                shadowmapsize = 512;
+            
+            mEngine->getPipeline()->setShadowMapSize(shadowmapsize);
         }
-    }
-    
-    if (input->getKeyPress(' '))
-    {
-        drawdebug++;
-        if (drawdebug > 2) drawdebug = 0;
-        mEngine->setDebugDrawMode(drawdebug);
-    }
-    
-    if (input->getKeyPress('P'))
-    {
-        paused = !paused;
-    }
-    
-    if (input->getKeyPress('C'))
-    {
-        Camera* camera = mCameraEntity->getComponent<Camera>();
-        camera->setOrthographic(!camera->isOrthographic());
-    }
-    
-    if (input->getKeyPress('O'))
-    {
-        if (!mOcclusionDebugImage)
+        
+        if (input->getKeyPress('7'))
+        {
+            hiresshadowmap = !hiresshadowmap;
+            mEngine->getPipeline()->setShadowMapHiresDepth(hiresshadowmap);
+        }
+        
+        if (input->getKeyPress('8'))
         {
-            try
+            useocclusion = !useocclusion;
+            mEngine->getPipeline()->setMaxOccluderTriangles(useocclusion ? 5000 : 0);
+        }
+        
+        if (input->getKeyPress('L'))
+        {
+            Light* cameraLight = mCameraEntity->getComponent<Light>();
+            attach = !attach;
+            if (attach)
             {
-                Renderer* renderer = mEngine->getRenderer();
-                UIElement* uiRoot = mEngine->getUIRoot();
-                
-                mOcclusionDebugTexture = new Texture2D(renderer, TEXTURE_DYNAMIC);
-                mOcclusionDebugTexture->setNumLevels(1);
-                mOcclusionDebugTexture->setSize(256, 256, D3DFMT_R32F);
-                mOcclusionDebugImage = new BorderImage();
-                mOcclusionDebugImage->setSize(256, 256);
-                mOcclusionDebugImage->setTexture(mOcclusionDebugTexture);
-                mOcclusionDebugImage->setAlignment(HA_RIGHT, VA_BOTTOM);
-                uiRoot->addChild(mOcclusionDebugImage);
+                cameraLight->setPosition(Vector3::sZero);
+                cameraLight->setRotation(Quaternion::sIdentity);
+                mCameraEntity->getComponent<Camera>()->addChild(cameraLight);
             }
-            catch (...)
+            else
             {
+                // Detach child and set world transform to match what it was before detach
+                mCameraEntity->getComponent<Camera>()->removeChild(cameraLight, true);
             }
         }
-        else
+        
+        if (input->getKeyPress(' '))
         {
-            mOcclusionDebugImage->setVisible(!mOcclusionDebugImage->isVisible());
+            drawdebug++;
+            if (drawdebug > 2) drawdebug = 0;
+            mEngine->setDebugDrawMode(drawdebug);
         }
+        
+        if (input->getKeyPress('P'))
+        {
+            paused = !paused;
+        }
+        
+        if (input->getKeyPress('C'))
+        {
+            Camera* camera = mCameraEntity->getComponent<Camera>();
+            camera->setOrthographic(!camera->isOrthographic());
+        }
+        
+        if (input->getKeyPress('O'))
+        {
+            if (!mOcclusionDebugImage)
+            {
+                try
+                {
+                    Renderer* renderer = mEngine->getRenderer();
+                    UIElement* uiRoot = mEngine->getUIRoot();
+                    
+                    mOcclusionDebugTexture = new Texture2D(renderer, TEXTURE_DYNAMIC);
+                    mOcclusionDebugTexture->setNumLevels(1);
+                    mOcclusionDebugTexture->setSize(256, 256, D3DFMT_R32F);
+                    mOcclusionDebugImage = new BorderImage();
+                    mOcclusionDebugImage->setSize(256, 256);
+                    mOcclusionDebugImage->setTexture(mOcclusionDebugTexture);
+                    mOcclusionDebugImage->setAlignment(HA_RIGHT, VA_BOTTOM);
+                    uiRoot->addChild(mOcclusionDebugImage);
+                }
+                catch (...)
+                {
+                }
+            }
+            else
+            {
+                mOcclusionDebugImage->setVisible(!mOcclusionDebugImage->isVisible());
+            }
+        }
+        
+        if (input->getKeyPress('T'))
+            mEngine->getDebugHud()->toggle(DEBUGHUD_SHOW_PROFILER);
+        
+        if (input->getKeyPress('F'))
+        {
+            Pipeline* pipeline = mEngine->getPipeline();
+            EdgeFilterParameters params = pipeline->getEdgeFilter();
+            if (params.mMaxFilter > 0.0f)
+                params.mMaxFilter = 0.0f;
+            else
+                params.mMaxFilter = 1.0f;
+            pipeline->setEdgeFilter(params);
+        }
+        
+        if (input->getKeyPress(KEY_ESCAPE))
+            mEngine->exit();
     }
-    
-    if (input->getKeyPress('T'))
-        mEngine->getDebugHud()->toggle(DEBUGHUD_SHOW_PROFILER);
-    
-    if (input->getKeyPress('F'))
-    {
-        Pipeline* pipeline = mEngine->getPipeline();
-        EdgeFilterParameters params = pipeline->getEdgeFilter();
-        if (params.mMaxFilter > 0.0f)
-            params.mMaxFilter = 0.0f;
-        else
-            params.mMaxFilter = 1.0f;
-        pipeline->setEdgeFilter(params);
-    }
-    
-    if (input->getKeyPress(KEY_ESCAPE))
-        mEngine->exit();
 }
 
 void Application::handlePostRenderUpdate(StringHash eventType, VariantMap& eventData)
@@ -797,7 +801,7 @@ void Application::handleMouseButtonDown(StringHash eventType, VariantMap& eventD
     
     UI* ui = mEngine->getUI();
     
-    if ((button == MOUSEB_LEFT) && (!ui->getElementAt(ui->getCursorPosition())))
+    if ((button == MOUSEB_LEFT) && (!ui->getElementAt(ui->getCursorPosition())) && (!ui->getFocusElement()))
     {
         // Test creating a new physics object
         if (mCameraEntity)