[Solved] Editbox's validation string

For help with general CEGUI usage:
- Questions about the usage of CEGUI and its features, if not explained in the documentation.
- Problems with the CMAKE configuration or problems occuring during the build process/compilation.
- Errors or unexpected behaviour.

Moderators: CEGUI MVP, CEGUI Team

IrmatDen
Not too shy to talk
Not too shy to talk
Posts: 26
Joined: Sat Feb 26, 2011 08:04

[Solved] Editbox's validation string

Postby IrmatDen » Sat Feb 26, 2011 08:18

Hello all,

I've met a problem regarding user input validation while he's still typing in some text. If the regex used to validate the input doesn't get a complete match, the input seems to be refused. As a test case, we can use the Demo 6 sample, and modify the line 208 (with the "<<<<" as comment in the code) :

Code: Select all

    Editbox* ebox = static_cast<Editbox*>(winMgr.createWindow("TaharezLook/Editbox", "Demo6/ControlPanel/ColumnPanel/NewColIDBox"));
    st->addChild(ebox);
    ebox->setPosition(UVector2(cegui_reldim(0.02f), cegui_reldim( 0.32f)));
    ebox->setSize(UVector2(cegui_reldim(0.2f), cegui_reldim( 0.2f)));
    ebox->setValidationString("\\d*");   // <<<<<<<<
    ebox->setText("Test -- ");

This validator is totally valid and usable since an empty box is a valid match, as well as any numeric characters count.

But if one modifies the regex as follow:

Code: Select all

ebox->setValidationString("\\.\\d+");

Now, the user can't use this editbox at all since:
* <empty> is not a valid input (the user can't even delete the last character or the whole content)
* typing a '.', while a partial match, isn't validated since it isn't suffixed by 1 or more numeric items, so the user can't start to make a valid input

I've searched a lot in the doc, in the forums, and the code, but I really don't know what I could get wrong or what initialization may be missing.
So, before patching a non-issue, is there something obvious I may have missed please? :)

Thanks in advance!

PS: tried & seen in precompiled 0.7.5 Windows SDK, and latest mercurial changeset.
Last edited by IrmatDen on Tue Mar 01, 2011 10:08, edited 1 time in total.

User avatar
Kulik
CEGUI Team
Posts: 1382
Joined: Mon Jul 26, 2010 18:47
Location: Czech Republic
Contact:

Re: Editbox's validation string

Postby Kulik » Sat Feb 26, 2011 13:27

The role of the validation string is to validate all input at the time it is inputted. If you want to validate the input at the time the input is actually changed (and user exits the editbox), subscribe to the EventTextAccepted event.

You have to make sure user can write the text you want by crafting "specialised" regexes for this.

IrmatDen
Not too shy to talk
Not too shy to talk
Posts: 26
Joined: Sat Feb 26, 2011 08:04

Re: Editbox's validation string

Postby IrmatDen » Sat Feb 26, 2011 20:10

Ok, too much Qt I guess :D I was looking to get the same behavior as their validator model, which allows partial match during input.

What I want to do is an IP validator, so the user shouldn't be able to type "900" because the last "0" would never be accepted while he's typing; I don't like the idea of waiting for him to leave the editbox before reporting an error. There's currently no way to do that, right?

User avatar
CrazyEddie
CEGUI Project Lead
Posts: 6760
Joined: Wed Jan 12, 2005 12:06
Location: England
Contact:

Re: Editbox's validation string

Postby CrazyEddie » Mon Feb 28, 2011 09:55

Basically, I think you're asking for what I will call 'soft validation' - where the app is informed that the string does not match against the regex, but input is allowed to continue anyway, as opposed to what I will call the 'hard validation' that we have right now that actively prevents the string from ever becoming something that does not match the regex.

As the system stands right now, this is not possible, so in these cases listening to some other event (either Editbox::EventTextAccepted or maybe Window::EventTextChanged) and doing your own validation is the way to go (yeah, I accept that this is a PITA ;)).

For the future, I think a change can be made to the Editbox::EventInvalidEntryAttempted which allows the application to essentially override the 'hard validation' and allow the character to be inserted into the editbox anyway - since this is a behavioural change, it can't be made for the stable branch, so would be an 0.8.0 thing.

CE.

IrmatDen
Not too shy to talk
Not too shy to talk
Posts: 26
Joined: Sat Feb 26, 2011 08:04

Re: Editbox's validation string

Postby IrmatDen » Mon Feb 28, 2011 11:35

CrazyEddie wrote:Basically, I think you're asking for what I will call 'soft validation' - where the app is informed that the string does not match against the regex, but input is allowed to continue anyway, as opposed to what I will call the 'hard validation' that we have right now that actively prevents the string from ever becoming something that does not match the regex.

Yes, exactly.

CrazyEddie wrote:As the system stands right now, this is not possible, so in these cases listening to some other event (either Editbox::EventTextAccepted or maybe Window::EventTextChanged) and doing your own validation is the way to go (yeah, I accept that this is a PITA ;)).

I realize it's not a killer-most-needed-ultra-feature in a GUI system; just a nice cherry :)

CrazyEddie wrote:For the future, I think a change can be made to the Editbox::EventInvalidEntryAttempted which allows the application to essentially override the 'hard validation' and allow the character to be inserted into the editbox anyway - since this is a behavioural change, it can't be made for the stable branch, so would be an 0.8.0 thing.

I was thinking of making a patch (basically, a 2nd matchRegex(...) method in RegexMatcher); should I post it here in case it might be a little time saver for you?

IrmatDen
Not too shy to talk
Not too shy to talk
Posts: 26
Joined: Sat Feb 26, 2011 08:04

Re: Editbox's validation string

Postby IrmatDen » Mon Feb 28, 2011 22:05

Here's what I got to work; I can't be sure that it covers every use of the regex matcher as I've only checked Editbox so far...

Changes to RegexMatcher:
* added a MatchState to RegexMatcher (3 states, as in Qt),
* added another match method (virtual MatchState matchRegexSoft(const String& str) const = 0;),
* kept the existing hard validation method, but with a default implementation (might be useful to override for regex matching optimizations, as *seems* to be the case with pcre).

Changes to PCRERegexMatcher:
* using above changes :)
* regex compiled & executed with ANCHORED option set

Changes to Editbox:
* isStringValid() now takes a second argument (bool check_hard_match), which defaults to true,
* modified isStringValid() calls in onCharacter(), handleBackspace() and handleDelete() to specify a soft match.

Hg shelve file content (no other modifications than this one were done to the codebase):

Code: Select all

diff --git a/cegui/include/CEGUIPCRERegexMatcher.h b/cegui/include/CEGUIPCRERegexMatcher.h
--- a/cegui/include/CEGUIPCRERegexMatcher.h
+++ b/cegui/include/CEGUIPCRERegexMatcher.h
@@ -45,9 +45,10 @@
     ~PCRERegexMatcher();
 
     // implement required interface
-    void setRegexString(const String& regex);
-    const String& getRegexString() const;
-    bool matchRegex(const String& str) const;
+    void         setRegexString(const String& regex);
+    const String&   getRegexString() const;
+    MatchState      matchRegexSoft(const String& str) const;
+    bool         matchRegex(const String& str) const;
 
 private:
     //! free the compiled PCRE regex, if any.
diff --git a/cegui/include/CEGUIRegexMatcher.h b/cegui/include/CEGUIRegexMatcher.h
--- a/cegui/include/CEGUIRegexMatcher.h
+++ b/cegui/include/CEGUIRegexMatcher.h
@@ -40,4 +40,15 @@
     public AllocatedObject<RegexMatcher>
 {
 public:
+    //! enumerates possible match modes
+    enum MatchState
+    {
+        //! Input string clearly can't be a match, even in the future
+        MS_Invalid,
+        //! Input string is valid, but incomplete
+        MS_Intermediate,
+        //! Input string is valid as a final result
+        MS_Acceptable
+    };
+
     //! Destructor.
@@ -43,11 +54,21 @@
     //! Destructor.
-    virtual ~RegexMatcher() {}
-    //! Set the regex string that will be matched against.
-    virtual void setRegexString(const String& regex) = 0;
-    //! Return reference to current regex string set.
-    virtual const String& getRegexString() const = 0;
-    //! Return whether a given string matches the set regex.
-    virtual bool matchRegex(const String& str) const = 0;
+    virtual               ~RegexMatcher() {}
+   
+   //! Set the regex string that will be matched against.
+    virtual void         setRegexString(const String& regex) = 0;
+   
+   //! Return reference to current regex string set.
+    virtual const String&   getRegexString() const = 0;
+   
+   //! Return compliancy of the input string with the validator.
+    virtual MatchState      matchRegexSoft(const String& str) const = 0;
+   
+   //! Return whether a given string matches the set regex.
+   // This method is still present to allow full checks to use eventual regex engine optimization when a partial match is not desired.
+    virtual bool         matchRegex(const String& str) const
+   {
+      return matchRegexSoft(str) == MS_Acceptable;
+   }
 };
 
 } // End of  CEGUI namespace section
diff --git a/cegui/include/elements/CEGUIEditbox.h b/cegui/include/elements/CEGUIEditbox.h
--- a/cegui/include/elements/CEGUIEditbox.h
+++ b/cegui/include/elements/CEGUIEditbox.h
@@ -431,7 +431,7 @@
         return true if the given string matches the validation regular
         expression.
     */
-    bool isStringValid(const String& str) const;
+    bool isStringValid(const String& str, bool check_hard_match = true) const;
 
     //! Processing for backspace key
     void handleBackspace(void);
diff --git a/cegui/src/CEGUIPCRERegexMatcher.cpp b/cegui/src/CEGUIPCRERegexMatcher.cpp
--- a/cegui/src/CEGUIPCRERegexMatcher.cpp
+++ b/cegui/src/CEGUIPCRERegexMatcher.cpp
@@ -52,7 +52,7 @@
     // try to compile this new regex string
     const char* prce_error;
     int pcre_erroff;
-    d_regex = pcre_compile(regex.c_str(), PCRE_UTF8,
+    d_regex = pcre_compile(regex.c_str(), PCRE_UTF8 | PCRE_ANCHORED,
                            &prce_error, &pcre_erroff, 0);
 
     // handle failure
@@ -72,6 +72,46 @@
 }
 
 //----------------------------------------------------------------------------//
+RegexMatcher::MatchState PCRERegexMatcher::matchRegexSoft(const String& str) const
+{
+    // if the regex is not valid, then an exception is thrown
+    if (!d_regex)
+        CEGUI_THROW(InvalidRequestException("PCRERegexMatcher::matchRegex: "
+            "Attempt to use invalid RegEx '" + d_string + "'."));
+
+    int match[3];
+    const char* utf8_str = str.c_str();
+    const int len = static_cast<int>(strlen(utf8_str));
+
+   // nothing to check if the string is empty
+   if (len == 0)
+      return MS_Intermediate;
+
+    const int result = pcre_exec(d_regex, 0, utf8_str, len, 0, PCRE_PARTIAL_SOFT | PCRE_ANCHORED, match, 3);
+
+   MatchState matchState = MS_Invalid;
+
+   if (match[1] - match[0] == len)
+   {
+      if (result >= 0)
+         matchState = MS_Acceptable;
+      else if (result == PCRE_ERROR_PARTIAL)
+         matchState = MS_Intermediate;
+   }
+
+    // a match must be for the entire string
+    if ((result < 0) && (result != PCRE_ERROR_PARTIAL) &&
+      (result != PCRE_ERROR_NOMATCH) && (result != PCRE_ERROR_NULL))
+   {
+        CEGUI_THROW(InvalidRequestException("PCRERegexMatcher::matchRegex: "
+            "An internal error occurred while attempting to match the RegEx '" +
+            d_string + "'."));
+   }
+   
+   return matchState;
+}
+
+//----------------------------------------------------------------------------//
 bool PCRERegexMatcher::matchRegex(const String& str) const
 {
     // if the regex is not valid, then an exception is thrown
diff --git a/cegui/src/elements/CEGUIEditbox.cpp b/cegui/src/elements/CEGUIEditbox.cpp
--- a/cegui/src/elements/CEGUIEditbox.cpp
+++ b/cegui/src/elements/CEGUIEditbox.cpp
@@ -326,5 +326,5 @@
 }
 
 //----------------------------------------------------------------------------//
-bool Editbox::isStringValid(const String& str) const
+bool Editbox::isStringValid(const String& str, bool check_hard_match) const
 {
@@ -330,5 +330,11 @@
 {
-    return d_validator ? d_validator->matchRegex(str) : true;
+   if (!d_validator)
+      return true;
+
+   if (check_hard_match)
+      return d_validator->matchRegex(str);
+
+   return d_validator->matchRegexSoft(str) != RegexMatcher::MS_Invalid;
 }
 
 //----------------------------------------------------------------------------//
@@ -476,7 +482,7 @@
         {
             tmp.insert(getSelectionStartIndex(), 1, e.codepoint);
 
-            if (isStringValid(tmp))
+            if (isStringValid(tmp, false))
             {
                 // erase selection using mode that does not modify getText()
                 // (we just want to update state)
@@ -592,7 +598,7 @@
         {
             tmp.erase(getSelectionStartIndex(), getSelectionLength());
 
-            if (isStringValid(tmp))
+            if (isStringValid(tmp, false))
             {
                 // erase selection using mode that does not modify getText()
                 // (we just want to update state)
@@ -613,7 +619,7 @@
         {
             tmp.erase(d_caretPos - 1, 1);
 
-            if (isStringValid(tmp))
+            if (isStringValid(tmp, false))
             {
                 setCaretIndex(d_caretPos - 1);
 
@@ -644,7 +650,7 @@
         {
             tmp.erase(getSelectionStartIndex(), getSelectionLength());
 
-            if (isStringValid(tmp))
+            if (isStringValid(tmp, false))
             {
                 // erase selection using mode that does not modify getText()
                 // (we just want to update state)
@@ -665,7 +671,7 @@
         {
             tmp.erase(d_caretPos, 1);
 
-            if (isStringValid(tmp))
+            if (isStringValid(tmp, false))
             {
                 // set text to the newly modified string
                 setText(tmp);


PS: probably not worth a new post, but just in case it's not something wrong with my setup:
* changeset 2490 (9db16178c3a5)
* cmake-generated Visual C++2010 solution
* manually runned make.bat

To compile the lua module I had to make those tweaks:
* in make.bat: removed the src in out path

Code: Select all

tolua++cegui -o ../lua_CEGUI.cpp -L exceptions.lua CEGUI.pkg

* in WidgetLookFeel.pkg: added the 2nd parameter to the ctor:

Code: Select all

WidgetLookFeel(utf8string name, utf8string inherited);

User avatar
CrazyEddie
CEGUI Project Lead
Posts: 6760
Joined: Wed Jan 12, 2005 12:06
Location: England
Contact:

Re: Editbox's validation string

Postby CrazyEddie » Tue Mar 01, 2011 07:56

Thanks for the patch, I'll create and link it to a ticket on mantis, this way I'll not forget about it in a week or so :) Currently the regex matching is only used in CEGUI::Editbox, so if it's tested and working there, you are done IMO.

I can confirm the points in your P.S: The batch file has been out of date forever (since I do not use it), though I will get that fixed. The missing parameter on the WidgetLookFeel binding is due to a recent addition which I did not get around to adding (alternatively, I forgot about it!) - so thanks for the reminder.

It's good to see someone braving the storm of the default branch over v0-7 stable branch :D

CE.

IrmatDen
Not too shy to talk
Not too shy to talk
Posts: 26
Joined: Sat Feb 26, 2011 08:04

Re: Editbox's validation string

Postby IrmatDen » Tue Mar 01, 2011 10:08

You're welcome, I'm glad (should I admit proud too? :D) I've been able to help!

And thanks a bunch for this framework, it's such a time-saver, I'm really impressed so far; got my (admittedly simple) menu in no time :)

IrmatDen
Not too shy to talk
Not too shy to talk
Posts: 26
Joined: Sat Feb 26, 2011 08:04

Re: [Solved] Editbox's validation string

Postby IrmatDen » Tue Mar 01, 2011 10:55

Ahem, sorry for this update, but I've made a false assumption that the text would go through an hard validation process when the user validate his input or the control loses focus. I'll post an updated shelve soon. :oops:

IrmatDen
Not too shy to talk
Not too shy to talk
Posts: 26
Joined: Sat Feb 26, 2011 08:04

Re: [Solved] Editbox's validation string

Postby IrmatDen » Tue Mar 01, 2011 11:49

New shelve file, with hard validation done when editing has ended or editbox lost focus:

Code: Select all

diff --git a/cegui/include/CEGUIPCRERegexMatcher.h b/cegui/include/CEGUIPCRERegexMatcher.h
--- a/cegui/include/CEGUIPCRERegexMatcher.h
+++ b/cegui/include/CEGUIPCRERegexMatcher.h
@@ -45,10 +45,11 @@
     ~PCRERegexMatcher();
 
     // implement required interface
-    void setRegexString(const String& regex);
-    const String& getRegexString() const;
-    bool matchRegex(const String& str) const;
+    void         setRegexString(const String& regex);
+    const String&   getRegexString() const;
+    MatchState      matchRegexSoft(const String& str) const;
+    bool         matchRegex(const String& str) const;
 
 private:
     //! free the compiled PCRE regex, if any.
     void release();
@@ -51,7 +52,9 @@
 
 private:
     //! free the compiled PCRE regex, if any.
     void release();
+   //! implementation of the actual regex matching.
+   MatchState doMatch(const String& str, bool partial_match_allowed) const;
 
     //! Copy of the regex string assigned.
     String d_string;
diff --git a/cegui/include/CEGUIRegexMatcher.h b/cegui/include/CEGUIRegexMatcher.h
--- a/cegui/include/CEGUIRegexMatcher.h
+++ b/cegui/include/CEGUIRegexMatcher.h
@@ -40,4 +40,15 @@
     public AllocatedObject<RegexMatcher>
 {
 public:
+    //! enumerates possible match modes
+    enum MatchState
+    {
+        //! Input string clearly can't be a match, even in the future
+        MS_Invalid,
+        //! Input string is valid, but incomplete
+        MS_Intermediate,
+        //! Input string is valid as a final result
+        MS_Acceptable
+    };
+
     //! Destructor.
@@ -43,11 +54,21 @@
     //! Destructor.
-    virtual ~RegexMatcher() {}
-    //! Set the regex string that will be matched against.
-    virtual void setRegexString(const String& regex) = 0;
-    //! Return reference to current regex string set.
-    virtual const String& getRegexString() const = 0;
-    //! Return whether a given string matches the set regex.
-    virtual bool matchRegex(const String& str) const = 0;
+    virtual               ~RegexMatcher() {}
+   
+   //! Set the regex string that will be matched against.
+    virtual void         setRegexString(const String& regex) = 0;
+   
+   //! Return reference to current regex string set.
+    virtual const String&   getRegexString() const = 0;
+   
+   //! Return compliancy of the input string with the validator.
+    virtual MatchState      matchRegexSoft(const String& str) const = 0;
+   
+   //! Return whether a given string matches the set regex.
+   // This method is still present to allow full checks to use eventual regex engine optimization when a partial match is not desired.
+    virtual bool         matchRegex(const String& str) const
+   {
+      return matchRegexSoft(str) == MS_Acceptable;
+   }
 };
 
 } // End of  CEGUI namespace section
diff --git a/cegui/include/elements/CEGUIEditbox.h b/cegui/include/elements/CEGUIEditbox.h
--- a/cegui/include/elements/CEGUIEditbox.h
+++ b/cegui/include/elements/CEGUIEditbox.h
@@ -431,7 +431,7 @@
         return true if the given string matches the validation regular
         expression.
     */
-    bool isStringValid(const String& str) const;
+    bool isStringValid(const String& str, bool check_hard_match = true) const;
 
     //! Processing for backspace key
     void handleBackspace(void);
@@ -566,6 +566,7 @@
     void onCharacter(KeyEventArgs& e);
     void onKeyDown(KeyEventArgs& e);
     void onTextChanged(WindowEventArgs& e);
+   void onDeactivated(ActivationEventArgs &e);
 
     //! True if the editbox is in read-only mode
     bool d_readOnly;
diff --git a/cegui/src/CEGUIPCRERegexMatcher.cpp b/cegui/src/CEGUIPCRERegexMatcher.cpp
--- a/cegui/src/CEGUIPCRERegexMatcher.cpp
+++ b/cegui/src/CEGUIPCRERegexMatcher.cpp
@@ -52,7 +52,7 @@
     // try to compile this new regex string
     const char* prce_error;
     int pcre_erroff;
-    d_regex = pcre_compile(regex.c_str(), PCRE_UTF8,
+    d_regex = pcre_compile(regex.c_str(), PCRE_UTF8 | PCRE_ANCHORED,
                            &prce_error, &pcre_erroff, 0);
 
     // handle failure
@@ -72,5 +72,11 @@
 }
 
 //----------------------------------------------------------------------------//
+RegexMatcher::MatchState PCRERegexMatcher::matchRegexSoft(const String& str) const
+{
+   return doMatch(str, true);
+}
+
+//----------------------------------------------------------------------------//
 bool PCRERegexMatcher::matchRegex(const String& str) const
 {
@@ -75,26 +81,6 @@
 bool PCRERegexMatcher::matchRegex(const String& str) const
 {
-    // if the regex is not valid, then an exception is thrown
-    if (!d_regex)
-        CEGUI_THROW(InvalidRequestException("PCRERegexMatcher::matchRegex: "
-            "Attempt to use invalid RegEx '" + d_string + "'."));
-
-    int match[3];
-    const char* utf8_str = str.c_str();
-    const int len = static_cast<int>(strlen(utf8_str));
-    const int result = pcre_exec(d_regex, 0, utf8_str, len, 0, 0, match, 3);
-
-    // a match must be for the entire string
-    if (result >= 0)
-        return (match[1] - match[0] == len);
-    // no match found or if test string or regex is 0
-    else if ((result == PCRE_ERROR_NOMATCH) || (result == PCRE_ERROR_NULL))
-        return false;
-    // anything else is an error
-    else
-        CEGUI_THROW(InvalidRequestException("PCRERegexMatcher::matchRegex: "
-            "An internal error occurred while attempting to match the RegEx '" +
-            d_string + "'."));
+   return doMatch(str, false) == MS_Acceptable;
 }
 
 //----------------------------------------------------------------------------//
@@ -107,4 +93,45 @@
     }
 }
 
+//----------------------------------------------------------------------------//
+RegexMatcher::MatchState PCRERegexMatcher::doMatch(const String& str, bool partial_match_allowed) const
+{
+    // if the regex is not valid, then an exception is thrown
+    if (!d_regex)
+        CEGUI_THROW(InvalidRequestException("PCRERegexMatcher::matchRegex: "
+            "Attempt to use invalid RegEx '" + d_string + "'."));
+
+    int match[3];
+    const char* utf8_str = str.c_str();
+    const int len = static_cast<int>(strlen(utf8_str));
+
+   // nothing to check if the string is empty
+   if (len == 0)
+      return MS_Intermediate;
+
+   const int pcre_options = (partial_match_allowed ? (PCRE_PARTIAL_SOFT | PCRE_ANCHORED) : PCRE_ANCHORED);
+    const int result = pcre_exec(d_regex, 0, utf8_str, len, 0, pcre_options, match, 3);
+
+   MatchState matchState = MS_Invalid;
+
+   if (match[1] - match[0] == len)
+   {
+      if (result >= 0)
+         matchState = MS_Acceptable;
+      else if (result == PCRE_ERROR_PARTIAL) // this return code will never appear if PCRE_PARTIAL_SOFT is *not* set, no need for double checking.
+         matchState = MS_Intermediate;
+   }
+
+    // a match must be for the entire string
+    if ((result < 0) && (result != PCRE_ERROR_PARTIAL) &&
+      (result != PCRE_ERROR_NOMATCH) && (result != PCRE_ERROR_NULL))
+   {
+        CEGUI_THROW(InvalidRequestException("PCRERegexMatcher::matchRegex: "
+            "An internal error occurred while attempting to match the RegEx '" +
+            d_string + "'."));
+   }
+   
+   return matchState;
+}
+
 } // End of  CEGUI namespace section
diff --git a/cegui/src/elements/CEGUIEditbox.cpp b/cegui/src/elements/CEGUIEditbox.cpp
--- a/cegui/src/elements/CEGUIEditbox.cpp
+++ b/cegui/src/elements/CEGUIEditbox.cpp
@@ -326,5 +326,5 @@
 }
 
 //----------------------------------------------------------------------------//
-bool Editbox::isStringValid(const String& str) const
+bool Editbox::isStringValid(const String& str, bool check_hard_match) const
 {
@@ -330,5 +330,11 @@
 {
-    return d_validator ? d_validator->matchRegex(str) : true;
+   if (!d_validator)
+      return true;
+
+   if (check_hard_match)
+      return d_validator->matchRegex(str);
+
+   return d_validator->matchRegexSoft(str) != RegexMatcher::MS_Invalid;
 }
 
 //----------------------------------------------------------------------------//
@@ -476,7 +482,7 @@
         {
             tmp.insert(getSelectionStartIndex(), 1, e.codepoint);
 
-            if (isStringValid(tmp))
+            if (isStringValid(tmp, false))
             {
                 // erase selection using mode that does not modify getText()
                 // (we just want to update state)
@@ -582,6 +588,21 @@
 }
 
 //----------------------------------------------------------------------------//
+void Editbox::onDeactivated(ActivationEventArgs &e)
+{
+   if (!isStringValid(getText()))
+   {
+      // Trigger invalid modification attempted event.
+      WindowEventArgs args(this);
+      onInvalidEntryAttempted(args);
+   }
+
+   Window::onDeactivated(e);
+
+   ++e.handled;
+}
+
+//----------------------------------------------------------------------------//
 void Editbox::handleBackspace(void)
 {
     if (!isReadOnly())
@@ -592,7 +613,7 @@
         {
             tmp.erase(getSelectionStartIndex(), getSelectionLength());
 
-            if (isStringValid(tmp))
+            if (isStringValid(tmp, false))
             {
                 // erase selection using mode that does not modify getText()
                 // (we just want to update state)
@@ -613,7 +634,7 @@
         {
             tmp.erase(d_caretPos - 1, 1);
 
-            if (isStringValid(tmp))
+            if (isStringValid(tmp, false))
             {
                 setCaretIndex(d_caretPos - 1);
 
@@ -644,7 +665,7 @@
         {
             tmp.erase(getSelectionStartIndex(), getSelectionLength());
 
-            if (isStringValid(tmp))
+            if (isStringValid(tmp, false))
             {
                 // erase selection using mode that does not modify getText()
                 // (we just want to update state)
@@ -665,7 +686,7 @@
         {
             tmp.erase(d_caretPos, 1);
 
-            if (isStringValid(tmp))
+            if (isStringValid(tmp, false))
             {
                 // set text to the newly modified string
                 setText(tmp);
@@ -826,7 +847,16 @@
 //----------------------------------------------------------------------------//
 void Editbox::onTextAcceptedEvent(WindowEventArgs& e)
 {
-    fireEvent(EventTextAccepted, e, EventNamespace);
+   if (isStringValid(getText()))
+   {
+      fireEvent(EventTextAccepted, e, EventNamespace);
+   }
+   else
+   {
+      // Trigger invalid modification attempted event.
+      WindowEventArgs args(this);
+      onInvalidEntryAttempted(args);
+   }
 }
 
 //----------------------------------------------------------------------------//


Complete changes sum-up:

Changes to RegexMatcher:
* added a MatchState to RegexMatcher (3 states, as in Qt),
* added another match method (virtual MatchState matchRegexSoft(const String& str) const = 0;),
* kept the existing hard validation method, but with a default implementation (might be useful to override for regex matching optimizations, as *seems* to be the case with pcre).

Changes to PCRERegexMatcher:
* using above changes :)
* regex compiled & executed with ANCHORED option set

Changes to Editbox:
* isStringValid() now takes a second argument (bool check_hard_match), which defaults to true,
* modified isStringValid() calls in onCharacter(), handleBackspace() and handleDelete() to specify a soft match.
* added onDeactivated() override
* modified onTextAcceptedEvent() override

(please, triple check the two events override, I'm really new to CEGUI, so I'm not sure to have gotten events handling right)

I hope I haven't left anything incomplete this time, sorry again :oops:

User avatar
CrazyEddie
CEGUI Project Lead
Posts: 6760
Joined: Wed Jan 12, 2005 12:06
Location: England
Contact:

Re: [Solved] Editbox's validation string

Postby CrazyEddie » Wed Mar 02, 2011 07:51

Thanks for the updated patch. All things being well, I should get around to going through this over the coming weekend - so if you do make any more updates to it, so long as they occur before then, there will be no harm done! :lol:

CE.

IrmatDen
Not too shy to talk
Not too shy to talk
Posts: 26
Joined: Sat Feb 26, 2011 08:04

Re: [Solved] Editbox's validation string

Postby IrmatDen » Fri Mar 04, 2011 06:02

Since it's not yet the week-end, new update! (and I think last one :D)

I've added an event (EventValidEntry) which fires as soon as the text becomes a possible match. It allows for quicker feedback, like enabling/disabling a button without the user having to press Enter.
Sample scenario: for an IP validation, the user wants to enter "192.168.0.10", so here is a part of the event flow:
- Input "1" => text is "1" => fires EventInvalidEntryAttempted
- Input "9" => text is "19" => fires EventInvalidEntryAttempted
- ...
- Input "." => text is "192.168.0." => fires EventInvalidEntryAttempted
- Input "1" => text is "192.168.0.1" => fires EventValidEntry
- Input "0" => text is "192.168.0.10" => fires EventValidEntry
- Input "a" => text is "192.168.0.10" => No longer fires EventInvalidEntryAttempted, this is now a no-op!

To implement that, I've had to add a last match state variable to Editbox. This var is flagged as mutable to keep the isTextValid() method const. I don't know if you're going to pull your hairs out on this one; some people just hate that a const method is not perfectly const; please, let me know your position on this so I can tweak that accordingly if needed :)

Complete changes sum-up:

Changes to RegexMatcher:
* added a MatchState to RegexMatcher (3 states, as in Qt),
* added another match method (virtual MatchState matchRegexSoft(const String& str) const = 0;),
* kept the existing hard validation method, but with a default implementation (might be useful to override for regex matching optimizations, as *seems* to be the case with pcre).

Changes to PCRERegexMatcher:
* using above changes :)
* regex compiled & executed with ANCHORED option set

Changes to Editbox:
* isStringValid() now takes a second argument (bool check_hard_match), which defaults to true,
* modified isStringValid() calls in onCharacter(), handleBackspace() and handleDelete() to specify a soft match.
* added onDeactivated() override
* modified onTextAcceptedEvent() override
* added EventValidEntry event, fired as soon as the input is an acceptable & total match (and keep being fired upon each modification as long as the text is valid)
* remembers last match state in a mutable flag

New Hg shelve content:

Code: Select all

diff --git a/cegui/include/CEGUIPCRERegexMatcher.h b/cegui/include/CEGUIPCRERegexMatcher.h
--- a/cegui/include/CEGUIPCRERegexMatcher.h
+++ b/cegui/include/CEGUIPCRERegexMatcher.h
@@ -45,10 +45,11 @@
     ~PCRERegexMatcher();
 
     // implement required interface
-    void setRegexString(const String& regex);
-    const String& getRegexString() const;
-    bool matchRegex(const String& str) const;
+    void         setRegexString(const String& regex);
+    const String&   getRegexString() const;
+    MatchState      matchRegexSoft(const String& str) const;
+    bool         matchRegex(const String& str) const;
 
 private:
     //! free the compiled PCRE regex, if any.
     void release();
@@ -51,7 +52,9 @@
 
 private:
     //! free the compiled PCRE regex, if any.
     void release();
+   //! implementation of the actual regex matching.
+   MatchState doMatch(const String& str, bool partial_match_allowed) const;
 
     //! Copy of the regex string assigned.
     String d_string;
diff --git a/cegui/include/CEGUIRegexMatcher.h b/cegui/include/CEGUIRegexMatcher.h
--- a/cegui/include/CEGUIRegexMatcher.h
+++ b/cegui/include/CEGUIRegexMatcher.h
@@ -40,4 +40,15 @@
     public AllocatedObject<RegexMatcher>
 {
 public:
+    //! enumerates possible match modes
+    enum MatchState
+    {
+        //! Input string clearly can't be a match, even in the future
+        MS_Invalid,
+        //! Input string is valid, but incomplete
+        MS_Intermediate,
+        //! Input string is valid as a final result
+        MS_Acceptable
+    };
+
     //! Destructor.
@@ -43,11 +54,21 @@
     //! Destructor.
-    virtual ~RegexMatcher() {}
-    //! Set the regex string that will be matched against.
-    virtual void setRegexString(const String& regex) = 0;
-    //! Return reference to current regex string set.
-    virtual const String& getRegexString() const = 0;
-    //! Return whether a given string matches the set regex.
-    virtual bool matchRegex(const String& str) const = 0;
+    virtual               ~RegexMatcher() {}
+   
+   //! Set the regex string that will be matched against.
+    virtual void         setRegexString(const String& regex) = 0;
+   
+   //! Return reference to current regex string set.
+    virtual const String&   getRegexString() const = 0;
+   
+   //! Return compliancy of the input string with the validator.
+    virtual MatchState      matchRegexSoft(const String& str) const = 0;
+   
+   //! Return whether a given string matches the set regex.
+   // This method is still present to allow full checks to use eventual regex engine optimization when a partial match is not desired.
+    virtual bool         matchRegex(const String& str) const
+   {
+      return matchRegexSoft(str) == MS_Acceptable;
+   }
 };
 
 } // End of  CEGUI namespace section
diff --git a/cegui/include/elements/CEGUIEditbox.h b/cegui/include/elements/CEGUIEditbox.h
--- a/cegui/include/elements/CEGUIEditbox.h
+++ b/cegui/include/elements/CEGUIEditbox.h
@@ -33,6 +33,7 @@
 #include "../CEGUIBase.h"
 #include "../CEGUIWindow.h"
 #include "CEGUIEditboxProperties.h"
+#include "CEGUIRegexMatcher.h"
 
 #if defined(_MSC_VER)
 #   pragma warning(push)
@@ -111,6 +112,12 @@
      * WindowEventArgs::window set to the Editbox whose text has become invalid.
      */
     static const String EventTextInvalidated;
+    /** Event fired when the current text has become a possible match as regards
+     * to the validation string.
+     * Handlers are passed a const WindowEventArgs reference with
+     * WindowEventArgs::window set to the Editbox whose text has become invalid.
+     */
+    static const String EventValidEntry;
     /** Event fired when the user attempts to chage the text in a way that would
      * make it invalid as regards to the validation string.
      * Handlers are passed a const WindowEventArgs reference with
@@ -431,7 +438,7 @@
         return true if the given string matches the validation regular
         expression.
     */
-    bool isStringValid(const String& str) const;
+    bool isStringValid(const String& str, bool check_hard_match = true) const;
 
     //! Processing for backspace key
     void handleBackspace(void);
@@ -518,10 +525,12 @@
         Handler called when something has caused the current text to now fail
         validation.
 
-        This can be caused by changing the validation string or setting a
-        maximum length that causes the current text to be truncated.
+        This can be caused by changing the validation string, setting a
+        maximum length that causes the current text to be truncated or simply
+      while the user modifies the text in a way that doesn't make the input
+      fully valid.
     */
     virtual void onTextInvalidatedEvent(WindowEventArgs& e);
 
     /*!
     \brief
@@ -523,8 +532,15 @@
     */
     virtual void onTextInvalidatedEvent(WindowEventArgs& e);
 
     /*!
     \brief
+        Handler called when something has caused the current text to now succeed
+        validation.
+    */
+    virtual void onValidEntryEvent(WindowEventArgs& e);
+
+    /*!
+    \brief
         Handler called when the user attempted to make a change to the edit box
         that would have caused it to fail validation.
     */
@@ -566,6 +582,7 @@
     void onCharacter(KeyEventArgs& e);
     void onKeyDown(KeyEventArgs& e);
     void onTextChanged(WindowEventArgs& e);
+   void onDeactivated(ActivationEventArgs &e);
 
     //! True if the editbox is in read-only mode
     bool d_readOnly;
@@ -589,6 +606,8 @@
     bool d_dragging;
     //! Selection index for drag selection anchor point.
     size_t d_dragAnchorIdx;
+   //! Remembers how the current text matches the validation string.
+   mutable RegexMatcher::MatchState d_lastMatchState;
 
 private:
     static EditboxProperties::ReadOnly         d_readOnlyProperty;
diff --git a/cegui/src/CEGUIPCRERegexMatcher.cpp b/cegui/src/CEGUIPCRERegexMatcher.cpp
--- a/cegui/src/CEGUIPCRERegexMatcher.cpp
+++ b/cegui/src/CEGUIPCRERegexMatcher.cpp
@@ -52,7 +52,7 @@
     // try to compile this new regex string
     const char* prce_error;
     int pcre_erroff;
-    d_regex = pcre_compile(regex.c_str(), PCRE_UTF8,
+    d_regex = pcre_compile(regex.c_str(), PCRE_UTF8 | PCRE_ANCHORED,
                            &prce_error, &pcre_erroff, 0);
 
     // handle failure
@@ -72,5 +72,11 @@
 }
 
 //----------------------------------------------------------------------------//
+RegexMatcher::MatchState PCRERegexMatcher::matchRegexSoft(const String& str) const
+{
+   return doMatch(str, true);
+}
+
+//----------------------------------------------------------------------------//
 bool PCRERegexMatcher::matchRegex(const String& str) const
 {
@@ -75,26 +81,6 @@
 bool PCRERegexMatcher::matchRegex(const String& str) const
 {
-    // if the regex is not valid, then an exception is thrown
-    if (!d_regex)
-        CEGUI_THROW(InvalidRequestException("PCRERegexMatcher::matchRegex: "
-            "Attempt to use invalid RegEx '" + d_string + "'."));
-
-    int match[3];
-    const char* utf8_str = str.c_str();
-    const int len = static_cast<int>(strlen(utf8_str));
-    const int result = pcre_exec(d_regex, 0, utf8_str, len, 0, 0, match, 3);
-
-    // a match must be for the entire string
-    if (result >= 0)
-        return (match[1] - match[0] == len);
-    // no match found or if test string or regex is 0
-    else if ((result == PCRE_ERROR_NOMATCH) || (result == PCRE_ERROR_NULL))
-        return false;
-    // anything else is an error
-    else
-        CEGUI_THROW(InvalidRequestException("PCRERegexMatcher::matchRegex: "
-            "An internal error occurred while attempting to match the RegEx '" +
-            d_string + "'."));
+   return doMatch(str, false) == MS_Acceptable;
 }
 
 //----------------------------------------------------------------------------//
@@ -107,4 +93,45 @@
     }
 }
 
+//----------------------------------------------------------------------------//
+RegexMatcher::MatchState PCRERegexMatcher::doMatch(const String& str, bool partial_match_allowed) const
+{
+    // if the regex is not valid, then an exception is thrown
+    if (!d_regex)
+        CEGUI_THROW(InvalidRequestException("PCRERegexMatcher::matchRegex: "
+            "Attempt to use invalid RegEx '" + d_string + "'."));
+
+    int match[3];
+    const char* utf8_str = str.c_str();
+    const int len = static_cast<int>(strlen(utf8_str));
+
+   // nothing to check if the string is empty
+   if (len == 0)
+      return MS_Intermediate;
+
+   const int pcre_options = (partial_match_allowed ? (PCRE_PARTIAL_SOFT | PCRE_ANCHORED) : PCRE_ANCHORED);
+    const int result = pcre_exec(d_regex, 0, utf8_str, len, 0, pcre_options, match, 3);
+
+   MatchState matchState = MS_Invalid;
+
+   if (match[1] - match[0] == len)
+   {
+      if (result >= 0)
+         matchState = MS_Acceptable;
+      else if (result == PCRE_ERROR_PARTIAL) // this return code will never appear if PCRE_PARTIAL_SOFT is *not* set, no need for double checking.
+         matchState = MS_Intermediate;
+   }
+
+    // a match must be for the entire string
+    if ((result < 0) && (result != PCRE_ERROR_PARTIAL) &&
+      (result != PCRE_ERROR_NOMATCH) && (result != PCRE_ERROR_NULL))
+   {
+        CEGUI_THROW(InvalidRequestException("PCRERegexMatcher::matchRegex: "
+            "An internal error occurred while attempting to match the RegEx '" +
+            d_string + "'."));
+   }
+   
+   return matchState;
+}
+
 } // End of  CEGUI namespace section
diff --git a/cegui/src/elements/CEGUIEditbox.cpp b/cegui/src/elements/CEGUIEditbox.cpp
--- a/cegui/src/elements/CEGUIEditbox.cpp
+++ b/cegui/src/elements/CEGUIEditbox.cpp
@@ -71,6 +71,7 @@
 const String Editbox::EventValidationStringChanged( "ValidationStringChanged" );
 const String Editbox::EventMaximumTextLengthChanged( "MaximumTextLengthChanged" );
 const String Editbox::EventTextInvalidated("TextInvalidated");
+const String Editbox::EventValidEntry("ValidEntry");
 const String Editbox::EventInvalidEntryAttempted( "InvalidEntryAttempted" );
 const String Editbox::EventCaretMoved( "CaretMoved" );
 const String Editbox::EventTextSelectionChanged( "TextSelectionChanged" );
@@ -326,5 +327,5 @@
 }
 
 //----------------------------------------------------------------------------//
-bool Editbox::isStringValid(const String& str) const
+bool Editbox::isStringValid(const String& str, bool check_hard_match) const
 {
@@ -330,5 +331,16 @@
 {
-    return d_validator ? d_validator->matchRegex(str) : true;
+   if (!d_validator)
+   {
+      d_lastMatchState = RegexMatcher::MS_Acceptable;
+      return true;
+   }
+   
+   d_lastMatchState = d_validator->matchRegexSoft(str);
+
+   if (check_hard_match)
+      return d_lastMatchState == RegexMatcher::MS_Acceptable;
+
+   return d_lastMatchState != RegexMatcher::MS_Invalid;
 }
 
 //----------------------------------------------------------------------------//
@@ -463,6 +475,8 @@
     // fire event.
     fireEvent(EventCharacterKey, e, Window::EventNamespace);
 
+   RegexMatcher::MatchState oldMatchState = d_lastMatchState;
+
     // only need to take notice if we have focus
     if (e.handled == 0 && hasInputFocus() && !isReadOnly() &&
         getFont()->isCodepointAvailable(e.codepoint))
@@ -476,7 +490,7 @@
         {
             tmp.insert(getSelectionStartIndex(), 1, e.codepoint);
 
-            if (isStringValid(tmp))
+            if (isStringValid(tmp, false))
             {
                 // erase selection using mode that does not modify getText()
                 // (we just want to update state)
@@ -491,6 +505,18 @@
 
                 // char was accepted into the Editbox - mark event as handled.
                 ++e.handled;
+
+            // Check if the string is a possible full match, and react accordingly.
+            if (d_lastMatchState == RegexMatcher::MS_Acceptable)
+            {
+               WindowEventArgs args(this);
+               onValidEntryEvent(args);
+            }
+            else // otherwise, just signify to event listeners that text isn't yet valid.
+            {
+               WindowEventArgs args(this);
+               onInvalidEntryAttempted(args);
+            }
             }
             else
             {
@@ -494,9 +520,8 @@
             }
             else
             {
-                // Trigger invalid modification attempted event.
-                WindowEventArgs args(this);
-                onInvalidEntryAttempted(args);
+            // *Adding* an invalid character doesn't make the entry invalidated since it is not actually added to the editbox's text.
+            d_lastMatchState = oldMatchState;
             }
 
         }
@@ -582,6 +607,21 @@
 }
 
 //----------------------------------------------------------------------------//
+void Editbox::onDeactivated(ActivationEventArgs &e)
+{
+   if (!isStringValid(getText()))
+   {
+      // Trigger invalid modification attempted event.
+      WindowEventArgs args(this);
+      onInvalidEntryAttempted(args);
+   }
+
+   Window::onDeactivated(e);
+
+   ++e.handled;
+}
+
+//----------------------------------------------------------------------------//
 void Editbox::handleBackspace(void)
 {
     if (!isReadOnly())
@@ -592,7 +632,7 @@
         {
             tmp.erase(getSelectionStartIndex(), getSelectionLength());
 
-            if (isStringValid(tmp))
+            if (isStringValid(tmp, false))
             {
                 // erase selection using mode that does not modify getText()
                 // (we just want to update state)
@@ -600,6 +640,18 @@
 
                 // set text to the newly modified string
                 setText(tmp);
+
+            // Check if the string is a possible full match, and react accordingly.
+            if (d_lastMatchState == RegexMatcher::MS_Acceptable)
+            {
+               WindowEventArgs args(this);
+               onValidEntryEvent(args);
+            }
+            else // otherwise, just signify to event listeners that text isn't yet valid.
+            {
+               WindowEventArgs args(this);
+               onInvalidEntryAttempted(args);
+            }
             }
             else
             {
@@ -613,9 +665,9 @@
         {
             tmp.erase(d_caretPos - 1, 1);
 
-            if (isStringValid(tmp))
+            if (isStringValid(tmp, false))
             {
                 setCaretIndex(d_caretPos - 1);
 
                 // set text to the newly modified string
                 setText(tmp);
@@ -617,8 +669,20 @@
             {
                 setCaretIndex(d_caretPos - 1);
 
                 // set text to the newly modified string
                 setText(tmp);
+
+            // Check if the string is a possible full match, and react accordingly.
+            if (d_lastMatchState == RegexMatcher::MS_Acceptable)
+            {
+               WindowEventArgs args(this);
+               onValidEntryEvent(args);
+            }
+            else // otherwise, just signify to event listeners that text isn't yet valid.
+            {
+               WindowEventArgs args(this);
+               onInvalidEntryAttempted(args);
+            }
             }
             else
             {
@@ -644,7 +708,7 @@
         {
             tmp.erase(getSelectionStartIndex(), getSelectionLength());
 
-            if (isStringValid(tmp))
+            if (isStringValid(tmp, false))
             {
                 // erase selection using mode that does not modify getText()
                 // (we just want to update state)
@@ -652,6 +716,18 @@
 
                 // set text to the newly modified string
                 setText(tmp);
+
+            // Check if the string is a possible full match, and react accordingly.
+            if (d_lastMatchState == RegexMatcher::MS_Acceptable)
+            {
+               WindowEventArgs args(this);
+               onValidEntryEvent(args);
+            }
+            else // otherwise, just signify to event listeners that text isn't yet valid.
+            {
+               WindowEventArgs args(this);
+               onInvalidEntryAttempted(args);
+            }
             }
             else
             {
@@ -665,7 +741,7 @@
         {
             tmp.erase(d_caretPos, 1);
 
-            if (isStringValid(tmp))
+            if (isStringValid(tmp, false))
             {
                 // set text to the newly modified string
                 setText(tmp);
@@ -669,6 +745,18 @@
             {
                 // set text to the newly modified string
                 setText(tmp);
+
+            // Check if the string is a possible full match, and react accordingly.
+            if (d_lastMatchState == RegexMatcher::MS_Acceptable)
+            {
+               WindowEventArgs args(this);
+               onValidEntryEvent(args);
+            }
+            else // otherwise, just signify to event listeners that text isn't yet valid.
+            {
+               WindowEventArgs args(this);
+               onInvalidEntryAttempted(args);
+            }
             }
             else
             {
@@ -798,6 +886,12 @@
 }
 
 //----------------------------------------------------------------------------//
+void Editbox::onValidEntryEvent(WindowEventArgs& e)
+{
+    fireEvent(EventValidEntry, e, EventNamespace);
+}
+
+//----------------------------------------------------------------------------//
 void Editbox::onInvalidEntryAttempted(WindowEventArgs& e)
 {
     fireEvent(EventInvalidEntryAttempted , e, EventNamespace);
@@ -826,7 +920,16 @@
 //----------------------------------------------------------------------------//
 void Editbox::onTextAcceptedEvent(WindowEventArgs& e)
 {
-    fireEvent(EventTextAccepted, e, EventNamespace);
+   if (isStringValid(getText()))
+   {
+      fireEvent(EventTextAccepted, e, EventNamespace);
+   }
+   else
+   {
+      // Trigger invalid modification attempted event.
+      WindowEventArgs args(this);
+      onInvalidEntryAttempted(args);
+   }
 }
 
 //----------------------------------------------------------------------------//

User avatar
CrazyEddie
CEGUI Project Lead
Posts: 6760
Joined: Wed Jan 12, 2005 12:06
Location: England
Contact:

Re: [Solved] Editbox's validation string

Postby CrazyEddie » Fri Mar 04, 2011 08:51

Cool, thanks :) The mutable field is fine with me, we use this technique a lot already where it's appropriate.

CE.

IrmatDen
Not too shy to talk
Not too shy to talk
Posts: 26
Joined: Sat Feb 26, 2011 08:04

Re: [Solved] Editbox's validation string

Postby IrmatDen » Sat Mar 12, 2011 17:25

Guess what? Week-end update! :D
Now, the text set programmatically through Window::setText() will fire EventValidEntry or EventInvalidEntryAttempted, according to a hard match.

Complete changes sum-up:

Changes to RegexMatcher:
* added a MatchState to RegexMatcher (3 states, as in Qt),
* added another match method (virtual MatchState matchRegexSoft(const String& str) const = 0;),
* kept the existing hard validation method, but with a default implementation (might be useful to override for regex matching optimizations, as *seems* to be the case with pcre).

Changes to PCRERegexMatcher:
* using above changes :)
* regex compiled & executed with ANCHORED option set

Changes to Editbox:
* isStringValid() now takes a second argument (bool check_hard_match), which defaults to true,
* modified isStringValid() calls in onCharacter(), handleBackspace() and handleDelete() to specify a soft match.
* added onDeactivated() override
* modified onTextAcceptedEvent() override
* added EventValidEntry event, fired as soon as the input is an acceptable & total match (and keep being fired upon each modification as long as the text is valid)
* remembers last match state in a mutable flag
* tweaked EditBox::onTextChanged() to fire EventValidEntry or EventInvalidEntryAttempted according to a hard match.

Hg shelve content:

Code: Select all

diff --git a/cegui/include/CEGUIPCRERegexMatcher.h b/cegui/include/CEGUIPCRERegexMatcher.h
--- a/cegui/include/CEGUIPCRERegexMatcher.h
+++ b/cegui/include/CEGUIPCRERegexMatcher.h
@@ -45,10 +45,11 @@
     ~PCRERegexMatcher();
 
     // implement required interface
-    void setRegexString(const String& regex);
-    const String& getRegexString() const;
-    bool matchRegex(const String& str) const;
+    void         setRegexString(const String& regex);
+    const String&   getRegexString() const;
+    MatchState      matchRegexSoft(const String& str) const;
+    bool         matchRegex(const String& str) const;
 
 private:
     //! free the compiled PCRE regex, if any.
     void release();
@@ -51,7 +52,9 @@
 
 private:
     //! free the compiled PCRE regex, if any.
     void release();
+   //! implementation of the actual regex matching.
+   MatchState doMatch(const String& str, bool partial_match_allowed) const;
 
     //! Copy of the regex string assigned.
     String d_string;
diff --git a/cegui/include/CEGUIRegexMatcher.h b/cegui/include/CEGUIRegexMatcher.h
--- a/cegui/include/CEGUIRegexMatcher.h
+++ b/cegui/include/CEGUIRegexMatcher.h
@@ -40,4 +40,15 @@
     public AllocatedObject<RegexMatcher>
 {
 public:
+    //! enumerates possible match modes
+    enum MatchState
+    {
+        //! Input string clearly can't be a match, even in the future
+        MS_Invalid,
+        //! Input string is valid, but incomplete
+        MS_Intermediate,
+        //! Input string is valid as a final result
+        MS_Acceptable
+    };
+
     //! Destructor.
@@ -43,11 +54,21 @@
     //! Destructor.
-    virtual ~RegexMatcher() {}
-    //! Set the regex string that will be matched against.
-    virtual void setRegexString(const String& regex) = 0;
-    //! Return reference to current regex string set.
-    virtual const String& getRegexString() const = 0;
-    //! Return whether a given string matches the set regex.
-    virtual bool matchRegex(const String& str) const = 0;
+    virtual               ~RegexMatcher() {}
+   
+   //! Set the regex string that will be matched against.
+    virtual void         setRegexString(const String& regex) = 0;
+   
+   //! Return reference to current regex string set.
+    virtual const String&   getRegexString() const = 0;
+   
+   //! Return compliancy of the input string with the validator.
+    virtual MatchState      matchRegexSoft(const String& str) const = 0;
+   
+   //! Return whether a given string matches the set regex.
+   // This method is still present to allow full checks to use eventual regex engine optimization when a partial match is not desired.
+    virtual bool         matchRegex(const String& str) const
+   {
+      return matchRegexSoft(str) == MS_Acceptable;
+   }
 };
 
 } // End of  CEGUI namespace section
diff --git a/cegui/include/elements/CEGUIEditbox.h b/cegui/include/elements/CEGUIEditbox.h
--- a/cegui/include/elements/CEGUIEditbox.h
+++ b/cegui/include/elements/CEGUIEditbox.h
@@ -33,6 +33,7 @@
 #include "../CEGUIBase.h"
 #include "../CEGUIWindow.h"
 #include "CEGUIEditboxProperties.h"
+#include "CEGUIRegexMatcher.h"
 
 #if defined(_MSC_VER)
 #   pragma warning(push)
@@ -111,6 +112,12 @@
      * WindowEventArgs::window set to the Editbox whose text has become invalid.
      */
     static const String EventTextInvalidated;
+    /** Event fired when the current text has become a possible match as regards
+     * to the validation string.
+     * Handlers are passed a const WindowEventArgs reference with
+     * WindowEventArgs::window set to the Editbox whose text has become invalid.
+     */
+    static const String EventValidEntry;
     /** Event fired when the user attempts to chage the text in a way that would
      * make it invalid as regards to the validation string.
      * Handlers are passed a const WindowEventArgs reference with
@@ -431,7 +438,7 @@
         return true if the given string matches the validation regular
         expression.
     */
-    bool isStringValid(const String& str) const;
+    bool isStringValid(const String& str, bool check_hard_match = true) const;
 
     //! Processing for backspace key
     void handleBackspace(void);
@@ -518,10 +525,12 @@
         Handler called when something has caused the current text to now fail
         validation.
 
-        This can be caused by changing the validation string or setting a
-        maximum length that causes the current text to be truncated.
+        This can be caused by changing the validation string, setting a
+        maximum length that causes the current text to be truncated or simply
+      while the user modifies the text in a way that doesn't make the input
+      fully valid.
     */
     virtual void onTextInvalidatedEvent(WindowEventArgs& e);
 
     /*!
     \brief
@@ -523,8 +532,15 @@
     */
     virtual void onTextInvalidatedEvent(WindowEventArgs& e);
 
     /*!
     \brief
+        Handler called when something has caused the current text to now succeed
+        validation.
+    */
+    virtual void onValidEntryEvent(WindowEventArgs& e);
+
+    /*!
+    \brief
         Handler called when the user attempted to make a change to the edit box
         that would have caused it to fail validation.
     */
@@ -566,6 +582,7 @@
     void onCharacter(KeyEventArgs& e);
     void onKeyDown(KeyEventArgs& e);
     void onTextChanged(WindowEventArgs& e);
+   void onDeactivated(ActivationEventArgs &e);
 
     //! True if the editbox is in read-only mode
     bool d_readOnly;
@@ -589,6 +606,8 @@
     bool d_dragging;
     //! Selection index for drag selection anchor point.
     size_t d_dragAnchorIdx;
+   //! Remembers how the current text matches the validation string.
+   mutable RegexMatcher::MatchState d_lastMatchState;
 
 private:
     static EditboxProperties::ReadOnly         d_readOnlyProperty;
diff --git a/cegui/src/CEGUIPCRERegexMatcher.cpp b/cegui/src/CEGUIPCRERegexMatcher.cpp
--- a/cegui/src/CEGUIPCRERegexMatcher.cpp
+++ b/cegui/src/CEGUIPCRERegexMatcher.cpp
@@ -52,7 +52,7 @@
     // try to compile this new regex string
     const char* prce_error;
     int pcre_erroff;
-    d_regex = pcre_compile(regex.c_str(), PCRE_UTF8,
+    d_regex = pcre_compile(regex.c_str(), PCRE_UTF8 | PCRE_ANCHORED,
                            &prce_error, &pcre_erroff, 0);
 
     // handle failure
@@ -72,5 +72,11 @@
 }
 
 //----------------------------------------------------------------------------//
+RegexMatcher::MatchState PCRERegexMatcher::matchRegexSoft(const String& str) const
+{
+   return doMatch(str, true);
+}
+
+//----------------------------------------------------------------------------//
 bool PCRERegexMatcher::matchRegex(const String& str) const
 {
@@ -75,26 +81,6 @@
 bool PCRERegexMatcher::matchRegex(const String& str) const
 {
-    // if the regex is not valid, then an exception is thrown
-    if (!d_regex)
-        CEGUI_THROW(InvalidRequestException("PCRERegexMatcher::matchRegex: "
-            "Attempt to use invalid RegEx '" + d_string + "'."));
-
-    int match[3];
-    const char* utf8_str = str.c_str();
-    const int len = static_cast<int>(strlen(utf8_str));
-    const int result = pcre_exec(d_regex, 0, utf8_str, len, 0, 0, match, 3);
-
-    // a match must be for the entire string
-    if (result >= 0)
-        return (match[1] - match[0] == len);
-    // no match found or if test string or regex is 0
-    else if ((result == PCRE_ERROR_NOMATCH) || (result == PCRE_ERROR_NULL))
-        return false;
-    // anything else is an error
-    else
-        CEGUI_THROW(InvalidRequestException("PCRERegexMatcher::matchRegex: "
-            "An internal error occurred while attempting to match the RegEx '" +
-            d_string + "'."));
+   return doMatch(str, false) == MS_Acceptable;
 }
 
 //----------------------------------------------------------------------------//
@@ -107,4 +93,45 @@
     }
 }
 
+//----------------------------------------------------------------------------//
+RegexMatcher::MatchState PCRERegexMatcher::doMatch(const String& str, bool partial_match_allowed) const
+{
+    // if the regex is not valid, then an exception is thrown
+    if (!d_regex)
+        CEGUI_THROW(InvalidRequestException("PCRERegexMatcher::matchRegex: "
+            "Attempt to use invalid RegEx '" + d_string + "'."));
+
+    int match[3];
+    const char* utf8_str = str.c_str();
+    const int len = static_cast<int>(strlen(utf8_str));
+
+   // nothing to check if the string is empty
+   if (len == 0)
+      return MS_Intermediate;
+
+   const int pcre_options = (partial_match_allowed ? (PCRE_PARTIAL_SOFT | PCRE_ANCHORED) : PCRE_ANCHORED);
+    const int result = pcre_exec(d_regex, 0, utf8_str, len, 0, pcre_options, match, 3);
+
+   MatchState matchState = MS_Invalid;
+
+   if (match[1] - match[0] == len)
+   {
+      if (result >= 0)
+         matchState = MS_Acceptable;
+      else if (result == PCRE_ERROR_PARTIAL) // this return code will never appear if PCRE_PARTIAL_SOFT is *not* set, no need for double checking.
+         matchState = MS_Intermediate;
+   }
+
+    // a match must be for the entire string
+    if ((result < 0) && (result != PCRE_ERROR_PARTIAL) &&
+      (result != PCRE_ERROR_NOMATCH) && (result != PCRE_ERROR_NULL))
+   {
+        CEGUI_THROW(InvalidRequestException("PCRERegexMatcher::matchRegex: "
+            "An internal error occurred while attempting to match the RegEx '" +
+            d_string + "'."));
+   }
+   
+   return matchState;
+}
+
 } // End of  CEGUI namespace section
diff --git a/cegui/src/elements/CEGUIEditbox.cpp b/cegui/src/elements/CEGUIEditbox.cpp
--- a/cegui/src/elements/CEGUIEditbox.cpp
+++ b/cegui/src/elements/CEGUIEditbox.cpp
@@ -71,6 +71,7 @@
 const String Editbox::EventValidationStringChanged( "ValidationStringChanged" );
 const String Editbox::EventMaximumTextLengthChanged( "MaximumTextLengthChanged" );
 const String Editbox::EventTextInvalidated("TextInvalidated");
+const String Editbox::EventValidEntry("ValidEntry");
 const String Editbox::EventInvalidEntryAttempted( "InvalidEntryAttempted" );
 const String Editbox::EventCaretMoved( "CaretMoved" );
 const String Editbox::EventTextSelectionChanged( "TextSelectionChanged" );
@@ -276,4 +277,8 @@
         {
             String newText = getText();
             newText.resize(d_maxTextLen);
+
+         // Ensure the valid/invalid entry event isn't fire here.
+         const bool eventsMuted = isMuted();
+         setMutedState(true);
             setText(newText);
@@ -279,4 +284,6 @@
             setText(newText);
+         setMutedState(false);
+
             onTextChanged(args);
 
             // see if new text is valid
@@ -314,4 +321,8 @@
         {
             String newText = getText();
             newText.erase(getSelectionStartIndex(), getSelectionLength());
+
+         // Ensure the valid/invalid entry event isn't fire here.
+         const bool eventsMuted = isMuted();
+         setMutedState(true);
             setText(newText);
@@ -317,4 +328,5 @@
             setText(newText);
+         setMutedState(false);
 
             // trigger notification that text has changed.
             WindowEventArgs args(this);
@@ -326,5 +338,5 @@
 }
 
 //----------------------------------------------------------------------------//
-bool Editbox::isStringValid(const String& str) const
+bool Editbox::isStringValid(const String& str, bool check_hard_match) const
 {
@@ -330,5 +342,16 @@
 {
-    return d_validator ? d_validator->matchRegex(str) : true;
+   if (!d_validator)
+   {
+      d_lastMatchState = RegexMatcher::MS_Acceptable;
+      return true;
+   }
+   
+   d_lastMatchState = d_validator->matchRegexSoft(str);
+
+   if (check_hard_match)
+      return d_lastMatchState == RegexMatcher::MS_Acceptable;
+
+   return d_lastMatchState != RegexMatcher::MS_Invalid;
 }
 
 //----------------------------------------------------------------------------//
@@ -463,6 +486,8 @@
     // fire event.
     fireEvent(EventCharacterKey, e, Window::EventNamespace);
 
+   RegexMatcher::MatchState oldMatchState = d_lastMatchState;
+
     // only need to take notice if we have focus
     if (e.handled == 0 && hasInputFocus() && !isReadOnly() &&
         getFont()->isCodepointAvailable(e.codepoint))
@@ -476,7 +501,7 @@
         {
             tmp.insert(getSelectionStartIndex(), 1, e.codepoint);
 
-            if (isStringValid(tmp))
+            if (isStringValid(tmp, false))
             {
                 // erase selection using mode that does not modify getText()
                 // (we just want to update state)
@@ -486,8 +511,12 @@
                 // handlers!)
                 d_caretPos++;
 
-                // set text to the newly modified string
-                setText(tmp);
+                // set text to the newly modified string while
+            // ensuring the valid/invalid entry event isn't fire here.
+            const bool eventsMuted = isMuted();
+            setMutedState(true);
+            setText(tmp);
+            setMutedState(false);
 
                 // char was accepted into the Editbox - mark event as handled.
                 ++e.handled;
@@ -491,6 +520,18 @@
 
                 // char was accepted into the Editbox - mark event as handled.
                 ++e.handled;
+
+            // Check if the string is a possible full match, and react accordingly.
+            if (d_lastMatchState == RegexMatcher::MS_Acceptable)
+            {
+               WindowEventArgs args(this);
+               onValidEntryEvent(args);
+            }
+            else // otherwise, just signify to event listeners that text isn't yet valid.
+            {
+               WindowEventArgs args(this);
+               onInvalidEntryAttempted(args);
+            }
             }
             else
             {
@@ -494,9 +535,8 @@
             }
             else
             {
-                // Trigger invalid modification attempted event.
-                WindowEventArgs args(this);
-                onInvalidEntryAttempted(args);
+            // *Adding* an invalid character doesn't make the entry invalidated since it is not actually added to the editbox's text.
+            d_lastMatchState = oldMatchState;
             }
 
         }
@@ -582,6 +622,21 @@
 }
 
 //----------------------------------------------------------------------------//
+void Editbox::onDeactivated(ActivationEventArgs &e)
+{
+   if (!isStringValid(getText()))
+   {
+      // Trigger invalid modification attempted event.
+      WindowEventArgs args(this);
+      onInvalidEntryAttempted(args);
+   }
+
+   Window::onDeactivated(e);
+
+   ++e.handled;
+}
+
+//----------------------------------------------------------------------------//
 void Editbox::handleBackspace(void)
 {
     if (!isReadOnly())
@@ -592,9 +647,9 @@
         {
             tmp.erase(getSelectionStartIndex(), getSelectionLength());
 
-            if (isStringValid(tmp))
+            if (isStringValid(tmp, false))
             {
                 // erase selection using mode that does not modify getText()
                 // (we just want to update state)
                 eraseSelectedText(false);
 
@@ -596,10 +651,26 @@
             {
                 // erase selection using mode that does not modify getText()
                 // (we just want to update state)
                 eraseSelectedText(false);
 
-                // set text to the newly modified string
-                setText(tmp);
+                // set text to the newly modified string while
+            // ensure the valid/invalid entry event isn't fire here.
+            const bool eventsMuted = isMuted();
+            setMutedState(true);
+            setText(tmp);
+            setMutedState(false);
+
+            // Check if the string is a possible full match, and react accordingly.
+            if (d_lastMatchState == RegexMatcher::MS_Acceptable)
+            {
+               WindowEventArgs args(this);
+               onValidEntryEvent(args);
+            }
+            else // otherwise, just signify to event listeners that text isn't yet valid.
+            {
+               WindowEventArgs args(this);
+               onInvalidEntryAttempted(args);
+            }
             }
             else
             {
@@ -613,7 +684,7 @@
         {
             tmp.erase(d_caretPos - 1, 1);
 
-            if (isStringValid(tmp))
+            if (isStringValid(tmp, false))
             {
                 setCaretIndex(d_caretPos - 1);
 
@@ -617,8 +688,24 @@
             {
                 setCaretIndex(d_caretPos - 1);
 
-                // set text to the newly modified string
-                setText(tmp);
+                // set text to the newly modified string while
+            // ensuring the valid/invalid entry event isn't fire here.
+            const bool eventsMuted = isMuted();
+            setMutedState(true);
+            setText(tmp);
+            setMutedState(false);
+
+            // Check if the string is a possible full match, and react accordingly.
+            if (d_lastMatchState == RegexMatcher::MS_Acceptable)
+            {
+               WindowEventArgs args(this);
+               onValidEntryEvent(args);
+            }
+            else // otherwise, just signify to event listeners that text isn't yet valid.
+            {
+               WindowEventArgs args(this);
+               onInvalidEntryAttempted(args);
+            }
             }
             else
             {
@@ -644,9 +731,9 @@
         {
             tmp.erase(getSelectionStartIndex(), getSelectionLength());
 
-            if (isStringValid(tmp))
+            if (isStringValid(tmp, false))
             {
                 // erase selection using mode that does not modify getText()
                 // (we just want to update state)
                 eraseSelectedText(false);
 
@@ -648,10 +735,26 @@
             {
                 // erase selection using mode that does not modify getText()
                 // (we just want to update state)
                 eraseSelectedText(false);
 
-                // set text to the newly modified string
-                setText(tmp);
+                // set text to the newly modified string while
+            // ensuring the valid/invalid entry event isn't fire here.
+            const bool eventsMuted = isMuted();
+            setMutedState(true);
+            setText(tmp);
+            setMutedState(false);
+
+            // Check if the string is a possible full match, and react accordingly.
+            if (d_lastMatchState == RegexMatcher::MS_Acceptable)
+            {
+               WindowEventArgs args(this);
+               onValidEntryEvent(args);
+            }
+            else // otherwise, just signify to event listeners that text isn't yet valid.
+            {
+               WindowEventArgs args(this);
+               onInvalidEntryAttempted(args);
+            }
             }
             else
             {
@@ -665,5 +768,5 @@
         {
             tmp.erase(d_caretPos, 1);
 
-            if (isStringValid(tmp))
+            if (isStringValid(tmp, false))
             {
@@ -669,6 +772,22 @@
             {
-                // set text to the newly modified string
-                setText(tmp);
+                // set text to the newly modified string while
+            // ensuring the valid/invalid entry event isn't fire here.
+            const bool eventsMuted = isMuted();
+            setMutedState(true);
+            setText(tmp);
+            setMutedState(false);
+
+            // Check if the string is a possible full match, and react accordingly.
+            if (d_lastMatchState == RegexMatcher::MS_Acceptable)
+            {
+               WindowEventArgs args(this);
+               onValidEntryEvent(args);
+            }
+            else // otherwise, just signify to event listeners that text isn't yet valid.
+            {
+               WindowEventArgs args(this);
+               onInvalidEntryAttempted(args);
+            }
             }
             else
             {
@@ -798,6 +917,12 @@
 }
 
 //----------------------------------------------------------------------------//
+void Editbox::onValidEntryEvent(WindowEventArgs& e)
+{
+    fireEvent(EventValidEntry, e, EventNamespace);
+}
+
+//----------------------------------------------------------------------------//
 void Editbox::onInvalidEntryAttempted(WindowEventArgs& e)
 {
     fireEvent(EventInvalidEntryAttempted , e, EventNamespace);
@@ -826,7 +951,16 @@
 //----------------------------------------------------------------------------//
 void Editbox::onTextAcceptedEvent(WindowEventArgs& e)
 {
-    fireEvent(EventTextAccepted, e, EventNamespace);
+   if (isStringValid(getText()))
+   {
+      fireEvent(EventTextAccepted, e, EventNamespace);
+   }
+   else
+   {
+      // Trigger invalid modification attempted event.
+      WindowEventArgs args(this);
+      onInvalidEntryAttempted(args);
+   }
 }
 
 //----------------------------------------------------------------------------//
@@ -842,6 +976,18 @@
     if (d_caretPos > getText().length())
         setCaretIndex(getText().length());
 
+   // allows validation events to be fired
+   if (isStringValid(getText(), true))
+   {
+      WindowEventArgs args(this);
+      onValidEntryEvent(args);
+   }
+   else
+   {
+      WindowEventArgs args(this);
+      onInvalidEntryAttempted(args);
+   }
+
     ++e.handled;
 }
 

User avatar
CrazyEddie
CEGUI Project Lead
Posts: 6760
Joined: Wed Jan 12, 2005 12:06
Location: England
Contact:

Re: [Solved] Editbox's validation string

Postby CrazyEddie » Mon Mar 14, 2011 09:38

Thanks for the new update, I apologise for not having gotten around to processing this yet, this is mainly due to some health related issues. I will try to get this in during this week.

CE


Return to “Help”

Who is online

Users browsing this forum: No registered users and 24 guests