G4P TextArea/Textfield Length Filter - ALMOST working

Hi Peter,

I started this as a new thread because the old one was really about filtering for data type.

Based on your helpful sketch, I’ve implemented the following to limit character entry to 3 in a textAREA:

public void textarea1_change1(GTextArea source, GEvent event) { //_CODE_:textarea1:904265:
  println("textarea1 - GTextArea >> GEvent." + event + " @ " + millis());
  String data = textarea1.getText();
  String shorterData = "";
  if (data.length() > 3) {
      try {
        shorterData = data.substring(0, data.length()-1);
        textarea1.setText(shorterData);
        textarea1.moveCaretTo(0, 3);
      }
      catch (Exception e){
        //e.printStackTrace(); 
      }
    }
} //_CODE_:textarea1:904265:

This ALMOST works as I wish. But the cursor goes back to between the 2nd and 3rd characters - I’d like it to just remain at the end. Increasing the moveCaret to 4 does not work.

Also, similar code with a text FIELD totally crashes:

public void txf1Change(GTextField source, GEvent event) {
  String data = txf1.getText();
  String shorterData = "";
  if (data != "") {
    Validate.Integer(source);
    //if (txf1.getText().length() > 3) txf1.setLocalColorScheme(GCScheme.SCHEME_10);
    if (data.length() > 3) {
      try {
        shorterData = data.substring(0, data.length()-1);
        txf1.setText(shorterData);
      }
      catch (Exception e){
        //e.printStackTrace(); 
      }
    }
  }
  println("text: '" + txf1.getText() + "'");
}

Here is the total code, 99.9% of it yours from previous topic. Oh I should mention I made a custom color scheme 10 instead of the RED_SCHEME so it’s more aggressive looking.

Thanks for any insights. Wonderful library!

Mike

import g4p_controls.*;

GTextField txf1, txf2, txf3, txf4;
GTextArea textarea1;

public void setup() {
  size(400, 400, JAVA2D);
  createGUI();
}

public void draw() {
  background(230, 230, 255);
}

public void txf1Change(GTextField source, GEvent event) {
  String data = txf1.getText();
  String shorterData = "";
  if (data != "") {
    Validate.Integer(source);
    //if (txf1.getText().length() > 3) txf1.setLocalColorScheme(GCScheme.SCHEME_10);
    if (data.length() > 3) {
      try {
        //txf1.appendText("\b");
        shorterData = data.substring(0, data.length()-1);
        //txf1.setText("");
        txf1.setText(shorterData);
      }
      catch (Exception e){
        //e.printStackTrace(); 
      }
    }
  }
  println("text: '" + txf1.getText() + "'");
}

public void txf2Change(GTextField source, GEvent event) {
  Validate.Integer(source, -50, 100);
}

public void txf3Change(GTextField source, GEvent event) {
  Validate.Float(source);
}

public void txf4Change(GTextField source, GEvent event) {
  Validate.Float(source, - Float.MAX_VALUE, -Float.MIN_VALUE);
}

public void textarea1_change1(GTextArea source, GEvent event) { //_CODE_:textarea1:904265:
  println("textarea1 - GTextArea >> GEvent." + event + " @ " + millis());
  String data = textarea1.getText();
  String shorterData = "";
  if (data.length() > 3) {
      try {
        shorterData = data.substring(0, data.length()-1);
        textarea1.setText(shorterData);
        textarea1.moveCaretTo(0, 3);
      }
      catch (Exception e){
        //e.printStackTrace(); 
      }
    }
} //_CODE_:textarea1:904265:

static class Validate {
  // Additional validation methods can be added to suit your needs 
  public static Integer Integer(GTextField tf) {
    tf.setLocalColorScheme(GCScheme.BLUE_SCHEME);
    Integer n = null;
    try {
      n = Integer.parseInt(tf.getText());
    }
    catch(NumberFormatException nfe) {
      tf.setLocalColorScheme(GCScheme.SCHEME_10);
    }
    return n;
  }

  public static Integer Integer(GTextField tf, int low, int high) {
    Integer n = Validate.Integer(tf);
    if (n != null && (n < low || n > high)) {
      tf.setLocalColorScheme(GCScheme.SCHEME_10);
    }
    return n;
  }

  public static Float Float(GTextField tf) {
    tf.setLocalColorScheme(GCScheme.BLUE_SCHEME);
    Float n = null;
    try {
      n = Float.parseFloat(tf.getText());
    }
    catch(NumberFormatException nfe) {
      tf.setLocalColorScheme(GCScheme.SCHEME_10);
    }
    return n;
  }

  public static Float Float(GTextField tf, float low, float high) {
    Float n = Validate.Float(tf);
    if (n != null && (n < low || n > high)) {
      tf.setLocalColorScheme(GCScheme.SCHEME_10);
    }
    return n;
  }
  
  //public void Length (GTextField tf) {
  //  if (tf.getText().length() > 3) tf.setLocalColorScheme(GCScheme.SCHEME_10);
  //}
}//end class validate

public void createGUI() {
  G4P.messagesEnabled(false);
  G4P.setGlobalColorScheme(GCScheme.BLUE_SCHEME);
  G4P.setMouseOverEnabled(false);
  G4P.setInputFont("Arial", G4P.PLAIN, 16);
  surface.setTitle("Validating number input");
  txf1 = new GTextField(this, 10, 20, 380, 30, G4P.SCROLLBARS_NONE);
  txf1.setPromptText("Enter an integer");
  txf1.setOpaque(true);
  txf1.addEventHandler(this, "txf1Change");
  txf2 = new GTextField(this, 10, 60, 380, 30, G4P.SCROLLBARS_NONE);
  txf2.setPromptText("Enter integer -50 to 100 incl");
  txf2.setOpaque(true);
  txf2.addEventHandler(this, "txf2Change");
  txf3 = new GTextField(this, 10, 100, 380, 30, G4P.SCROLLBARS_NONE);
  txf3.setPromptText("Enter any decimal number");
  txf3.setOpaque(true);
  txf3.addEventHandler(this, "txf3Change");
  txf4 = new GTextField(this, 10, 140, 380, 30, G4P.SCROLLBARS_NONE);
  txf4.setPromptText("Enter any negative number");
  txf4.setOpaque(true);
  txf4.addEventHandler(this, "txf4Change");
  
  textarea1 = new GTextArea(this, 11, 313, 372, 80, G4P.SCROLLBARS_NONE);
  textarea1.setOpaque(true);
  textarea1.addEventHandler(this, "textarea1_change1");
}

Unfortunately that won’t work you cannot change the displayed text while in text edit mode. That means you can’t call setText(...) inside the event handler. Attempting this can produce non recoverable exceptions.

2 Likes

Hi Peter,

Thanks for explaining.

Is there a way to move the flashing cursor (caret?) inside the text field during editing? I noticed with the method I used in the code above, with the textarea, everything works fine except after entering too many characters, the cursor is in the penultimate position (one behind the last character). And then it doesn’t move if you type additional things.

Any way to move that cursor/caret to the end, aside from clicking the mouse inside the control? I have been poring over your excellent online docs, but I can’t seem to figure it out…

Thanks for your support!

Mike

It was intended that the caret could only be moved using keyboard and mouse events captured by G4P and never by the user in their own code.

G4P contains many caret move methods but they all have protected access. It seems that you have found the one method with public access. This is my mistake and it will be changed to protected for the next version. Sorry about that but it was never intended to be executed from user code.

So in answer to your question “it is not possible to move the caret position in your own code”.

2 Likes

Fair enough. I really appreciate your support.

Wonderful library.

Thank you and all the best,

Mike

Just a quick explanation.

The move caret methods were never meant to have public access because they can cause unstable behaviour if used inappropriately.

In all my libraries I have attempted to

  1. anticipate and provide the functionality that the majority of users might want
  2. provide a stable public API with predictable behaviour
  3. encapsulate core functionality so it can’t be used inappropriately

To satisfy (3) I could use the private access modifier but this would be very restrictive so I encapsulate the core functionality with protected access. This allows more experienced programmers to add their own functionality by inheriting from core library classes.

In G4P some attributes have incorrect access modifiers which affect (3) but these will also be fixed in the next release.

2 Likes

Number filters -

Just noticed that th GTextField there are two setNumeric methods which I had forgottrm about and you might experiment with :smile:

2 Likes

Thank you for explaining. I know your reasoning is well thought out. I’d expect nothing less from you :slight_smile:

I did attempt to experiment with the setNumeric but was unsuccessful. At least how I tried it there was no effect. But as you know by now I’m a novice.

Would there be any way to test for capital letters only? Using ascii key codes or something?

Thank you again Peter.

By the way as a followup, I’ve gotten a lot of what you shared implemented and working reliably. I’m very pleased. Thank you for the incredible support.

I extended your validation a little bit, with the below, which works very well. Although, I’m breaking the rule and using getText() - but no issues so far.

The validation for the capital letters was adapted from here: Validate Text in Java and Other Languages

static class Validate {
  // Additional validation methods can be added to suit your needs 
  public static Integer Integer(int faderNum, GTextField tf) {
    
    setColorScheme(faderNum, tf);
    
    Integer n = null;
    try {
      n = Integer.parseInt(tf.getText());
    }
    catch(NumberFormatException nfe) {
      tf.setLocalColorScheme(GCScheme.SCHEME_10);
    }
    return n;
  }

  public static Integer Integer(int faderNum, GTextField tf, int low, int high) {
    Integer n = Validate.Integer(faderNum, tf);
    if (n != null && (n < low || n > high)) {
      tf.setLocalColorScheme(GCScheme.SCHEME_10);
    }
    return n;
  }

  public static Float Float(GTextField tf) {
    tf.setLocalColorScheme(GCScheme.BLUE_SCHEME);
    Float n = null;
    try {
      n = Float.parseFloat(tf.getText());
    }
    catch(NumberFormatException nfe) {
      tf.setLocalColorScheme(GCScheme.SCHEME_10);
    }
    return n;
  }

  public static Float Float(GTextField tf, float low, float high) {
    Float n = Validate.Float(tf);
    if (n != null && (n < low || n > high)) {
      tf.setLocalColorScheme(GCScheme.SCHEME_10);
    }
    return n;
  }
  
   public static String Caps(int faderNum, GTextField tf) {
     
     setColorScheme(faderNum, tf);
     
     String str = tf.getText();
     
     // ReGex to check if a string contains uppercase letters only
     String regex = "^([A-Z])+$";

    // Compile the ReGex
    Pattern p = Pattern.compile(regex);

    // If the string is empty then print No
    if (str == null) {
      System.out.println("empty");
      return str;
    }

    // Find match between given string & regular expression
    Matcher m = p.matcher(str);

    // what to do if matched
    if (m.matches() && str.length() < 4) {
      System.out.println("Yes");
      setColorScheme(faderNum, tf);
    }
    else {
      System.out.println("No");
      tf.setLocalColorScheme(GCScheme.SCHEME_10);
    }
      
    return str;
  }//end caps
  
  //public void Length (GTextField tf) {
  //  if (tf.getText().length() > 3) tf.setLocalColorScheme(GCScheme.SCHEME_10);
  //}
  
  public static int setColorScheme(int faderNum, GTextField tf) {
    
    switch (faderNum) {      
      case 0:
        tf.setLocalColorScheme(GCScheme.SCHEME_14);
        break;        
      case 1:
        tf.setLocalColorScheme(GCScheme.SCHEME_13);
        break;        
      case 2:
        tf.setLocalColorScheme(GCScheme.SCHEME_12);
        break;        
      case 3:
        tf.setLocalColorScheme(GCScheme.SCHEME_11);
        break;            
    }  
    
    return 0;
    
  }
}//end class validate

Also used this for one of the texfields:

public void tfCC0_change1(GTextField source, GEvent event) { //_CODE_:tfCC0:800899:
  println("textfield1 - GTextField >> GEvent." + event + " @ " + millis());
  String data = source.getText();
  if (!data.equals("")) Validate.Integer(0, source, 0, 127);
}

You can use getText() anywhere because it is not changing the text in the field.

When I get a chance I will show you a more flexible / extensible way to validate and change text in a textfield. The method is based on what I use in canvasGUI a GUI library I created for p5.js but will need some mods to translate it to Java.

2 Likes

Truly generous of you. Many thanks Peter :smiling_face_with_three_hearts:

Sorry for the delay but have been busy.

The sketch below shows one way to validate text in a textfield. Hopefully the comments will show how it might be customised to suit your needs but you may want further discussion or explanation of my approach, if so just say :smile:

import java.util.*;
import java.util.regex.*;
import g4p_controls.*;

GTextField txf1, txf2, txf3, txf4, txf5;

public void setup() {
  size(400, 250);
  createGUI();
}

public void draw() {
  background(230, 230, 255);
}

/*
 * The next 5 methods are the event handlers for the text fields.
 * In each case the first statement requests the validation to
 * be formed.
 * The next statement identifies the colour scheme to be used based
 * on the validity test and whether an 'empty' text field is
 * acceptable.
 * The last statement calls the updateTextField method to process
 * the validation result.
 */
public void txf1Events(GTextField source, GEvent event) {
  VR vr = Validate.Integer(source.getText());
  int colScheme = !vr.valid && vr.text.trim().length() > 0
    ? GCScheme.RED_SCHEME : GCScheme.BLUE_SCHEME;
  updateTextField(source, vr, colScheme, event);
}

public void txf2Events(GTextField source, GEvent event) {
  VR vr = Validate.Integer(source.getText(), -50, 100);
  int colScheme = !vr.valid && vr.text.trim().length() > 0
    ? GCScheme.RED_SCHEME : GCScheme.BLUE_SCHEME;
  updateTextField(source, vr, colScheme, event);
}

public void txf3Events(GTextField source, GEvent event) {
  VR vr = Validate.Float(source.getText());
  int colScheme = !vr.valid && vr.text.trim().length() > 0
    ? GCScheme.RED_SCHEME : GCScheme.BLUE_SCHEME;
  updateTextField(source, vr, colScheme, event);
}

public void txf4Events(GTextField source, GEvent event) {
  VR vr = Validate.Float(source.getText(), -Float.MAX_VALUE, -Float.MIN_VALUE);
  int colScheme = !vr.valid && vr.text.trim().length() > 0
    ? GCScheme.RED_SCHEME : GCScheme.BLUE_SCHEME;
  updateTextField(source, vr, colScheme, event);
}

public void txf5Events(GTextField source, GEvent event) {
  VR vr = Validate.FullName(source.getText());
  int colScheme = !vr.valid && vr.text.trim().length() > 0
    ? GCScheme.RED_SCHEME : GCScheme.BLUE_SCHEME;
  updateTextField(source, vr, colScheme, event);
}

/*
   * Default method for handling validation results. Effectively it changes the
 * text field colour scheme depending on whether the data is valid or not.
 * To avoid validation as user types remove the CHANGED case section.
 * The user can create additional update methods to suit their needs.
 */
public void updateTextField(GTextField source, VR vr, int colScheme, GEvent event) {
  switch(event) {
  case LOST_FOCUS:
    // If validation has calculated an acceptable value display it
    if (!vr.valid && vr.value != null) {
      // It is safe to call setText(...) here because the text field
      // is not longer in edit mode.
      source.setText(vr.value.toString());
      colScheme = GCScheme.BLUE_SCHEME; // it is now valid
    }
    source.setLocalColorScheme(colScheme);
    break;
  case GETS_FOCUS:
    // Assumes that the data is valid when the text field gets focus
    source.setLocalColorScheme(GCScheme.BLUE_SCHEME);
    break;
  case CHANGED:
    // Change the colour scheme as the data input is changed
    source.setLocalColorScheme(colScheme);
    break;
  default:
    break;
  }
}

/*
 * Validation result object.
 *
 * The attribute 'valid' stores the result of the validation.
 * The attribute 'value' can be of any type e.g. String, Float, Date or any user
 * defined class. The user determines what this attribute is used for e.g. default
 * value to use with invalid data.
 */
public static class VR {
  public String text;        // original text to be validated
  public boolean valid = false;   // result of validation
  public Object value = null;    // calculated during validation

  public VR(String text) {
    this.text = text;
  }

  // Useful for testing processes
  public String toString() {
    StringBuilder s = new StringBuilder("'")
      .append(text).append("'    ").append(valid ? "VALID    " : "INVALID    ")
      .append(value == null ? "----" : value.toString()).append("'");
    return s.toString();
  }
}

/*
 * This static class contains all methods used to validate text
 * of type String. These methods are not aware, or need to be aware,
 * of where the text comes from.
 *
 * It returns a VR (validation result object) but does not change
 * the state of the text field. Any changes of state should be done
 * in the event handler (separation of responsibilities)
 *
 * Note that the numeric validation methods can provide a valid
 * value in the VR object.
 * the text is invalid and the cthis can be used or not
 */
public static class Validate {
  // Additional validation methods can be added to suit user needs
  public static VR Integer(String text) {
    VR vr = new VR(text);
    Integer n = null;
    try {
      n = Integer.parseInt(text);
      vr.valid = true;
    }
    catch(NumberFormatException nfe) {
      // Optionally user can supply a valid value that will be
      // used when the input is invalid. Remove this line to
      // leave invalid data in place
      n = 0;
    }
    vr.value = n;
    return vr;
  }

  public static VR Integer(String text, int low, int high) {
    VR vr = new VR(text);
    Integer n = null;
    try {
      n = Integer.parseInt(text);
      vr.valid = (n >= low && n <= high);
    }
    catch(NumberFormatException nfe) {
      // Optionally user can supply a valid value that will be
      // used when the input is invalid.
      // Remove this line to leave invalid data in place
      n = low;
    }
    vr.value = n;
    return vr;
  }

  public static VR Float(String text) {
    VR vr = new VR(text);
    Float n = null;
    try {
      n = Float.parseFloat(text);
      vr.valid = true;
    }
    catch(NumberFormatException nfe) {
      // Optionally user can supply a valid value that will be
      // used when the input is invalid. Remove this line to
      // leave invalid data in place
      n = 0f;
    }
    vr.value = n;
    return vr;
  }

  public static VR Float(String text, float low, float high) {
    VR vr = new VR(text);
    Float n = null;
    try {
      n = Float.parseFloat(text);
      vr.valid = (n >= low && n <= high);
    }
    catch(NumberFormatException nfe) {
      // Optionally user can supply a valid value that will be
      // used when the input is invalid. Remove this line to
      // leave invalid data in place
      n = low;
    }
    vr.value = n;
    return vr;
  }

  public static VR FullName(String text) {
    VR vr = new VR(text);
    // Contains one or more letters (a-z, A-Z)?
    boolean match = Pattern.matches("(\\s*[a-zA-Z]+)+\\s*", vr.text);
    if (match) {
      // we have some letters that could be a name so capitalize it
      vr.value = Validate.capitalizeText(vr.text, "mc", "mac");
      vr.valid = text.equals(vr.value);
    } else {
      // no letters so no possible name
      vr.value = null;
      vr.valid = false;
    }
    return vr;
  }

  /*
     * This method will capitalize every word in the text. If additional prefixes
   * are provided then for every word starting with the prefix the character
   * after the prefix it will be capitalized for instance -
   * capitalizeText("are mcdondald's burgers awful?", "mc", "mac") results in
   * "Are McDondald's Burgers Awful?"
   * @param text the text to be validated
   * @param prefixes optional list of word prefixes
   * @return validation result object
   */
  public static String capitalizeText(String text, String... prefixes) {
    List<String> patterns = new ArrayList<String>();
    patterns.add("\\b\\w");
    for (String pfx : prefixes)
      if (pfx.trim().length() > 0) patterns.add("\\b" + pfx.trim() + "\\S");
    for (String pattern : patterns) {
      StringBuffer s = new StringBuffer();
      Pattern p = Pattern.compile(pattern, Pattern.CASE_INSENSITIVE);
      Matcher m = p.matcher(text);
      while (m.find()) {
        String mg = m.group();
        int mgl = mg.length();
        mg = mgl == 1 ? mg.toUpperCase() :
          mg.substring(0, mgl-1) + mg.substring(mgl-1).toUpperCase();
        m.appendReplacement(s, mg);
      }
      m.appendTail(s);
      text = s.toString();
      s = new StringBuffer();
    }
    return text;
  }
}

public void createGUI() {
  G4P.messagesEnabled(false);
  G4P.setGlobalColorScheme(GCScheme.BLUE_SCHEME);
  G4P.setMouseOverEnabled(false);
  G4P.setInputFont("Arial", G4P.PLAIN, 16);
  surface.setTitle("Validating number input");
  txf1 = new GTextField(this, 10, 20, 380, 30, G4P.SCROLLBARS_NONE);
  txf1.setPromptText("Enter an integer");
  txf1.setOpaque(true);
  txf1.addEventHandler(this, "txf1Events");
  txf2 = new GTextField(this, 10, 60, 380, 30, G4P.SCROLLBARS_NONE);
  txf2.setPromptText("Enter integer -50 to 100 incl");
  txf2.setOpaque(true);
  txf2.addEventHandler(this, "txf2Events");
  txf3 = new GTextField(this, 10, 100, 380, 30, G4P.SCROLLBARS_NONE);
  txf3.setPromptText("Enter any decimal number");
  txf3.setOpaque(true);
  txf3.addEventHandler(this, "txf3Events");
  txf4 = new GTextField(this, 10, 140, 380, 30, G4P.SCROLLBARS_NONE);
  txf4.setPromptText("Enter any negative number");
  txf4.setOpaque(true);
  txf4.addEventHandler(this, "txf4Events");
  txf5 = new GTextField(this, 10, 180, 380, 30, G4P.SCROLLBARS_NONE);
  txf5.setPromptText("Enter full name");
  txf5.setOpaque(true);
  txf5.addEventHandler(this, "txf5Events");
}
2 Likes

Peter - thank you so much! I bet that took you ages!!! What a gift of your time and talents.

I really, really appreciate it. I will work with your new methods here and I’ll post back progress. I’m so grateful you support your creation so fully and with such compassion for fellow programmers.

Beautiful work. Thank you so much for all your time!!!

Mike