Nifty – how is the selected text background color set?

Just tinkering around with textfields a bit, and was wondering if I was missing something in how coloring selected text works. I can set the selected foreground color in the standard ways:



Builder:

[java]

new StyleBuilder() {{

id( “textStyle#text” );

font( “Interface/fonts/Consolas14.fnt” );

selectionColor( “#f22f” );



}};

[/java]



Runtime (via bind callback):

[java]

TextField tf = element.findNiftyControl( “TextField”, TextField.class );

TextRenderer tr = tf.getElement().findElementByName( “#text” ).getRenderer( TextRenderer.class );

tr.setTextSelectionColor( new Color( “#f22f” ) );

[/java]



Both of these can set the selected color - what the font is painted in, but I haven’t found a way to set the background behind the selected characters. Is there a way?



Thanks…

Do you mean specifically when text is selected in a text control?



… or do you just mean the background colour behind the entire text?

I mean the selected text background color only, not the overall background color for the textfield control.

Unless this has changed recently, you can’t programmatically.



Although I would imagine you can check the control’s definition and change it there. I haven’t changed that yet since I’m not entirely sure what palette I’ll definitively use. But, my guess is it should be there (control definition) to change.

1 Like

Ah, fair enough. Just wanted to make sure I wasn’t missing something silly. Might try a custom effect once my nifty skill catches up with my notes.:slight_smile:



Thanks again.

I implemented this feature in my gui and I think it is really hard (impossible?) to implement it as custom effect.

Anyway I don’t think a custom effect makes sense.

Also the version of the TextFieldControl I know needs some improvements because it fails sometimes.

Unfortunately the TextFieldControl does not allow to implement a subclass that makes sense.

I just implemented my own control based on the TextFieldControl class setting a panel behind the selected text.

Interesting, you probably just saved me some headache. :?



If you don’t mind me asking - what were you doing in your gui that led you to build OS-like text selection? I’m slowly working toward a multi-line editor control, is why I ask.



I just implemented my own control based on the TextFieldControl class setting an panel behind the selected text.


So a little absolute-positioned panel behind the selected area? Nice.

I’m afraid my solution is no help as it does not support multi line text selection; the TextFieldControl does not support it either ?

So, how did you implement a multi line textfield ?

I remember this thread

http://hub.jmonkeyengine.org/groups/gui/forum/topic/editable-textarea/





No problem sharing my humble code:



[java]package customControl;



import java.util.Properties;



import de.lessvoid.nifty.ClipboardAWT;

import de.lessvoid.nifty.Nifty;

import de.lessvoid.nifty.builder.PanelBuilder;

import de.lessvoid.nifty.controls.AbstractController;

import de.lessvoid.nifty.controls.FocusHandler;

import de.lessvoid.nifty.controls.TextField;

import de.lessvoid.nifty.controls.TextFieldChangedEvent;

import de.lessvoid.nifty.controls.textfield.TextFieldLogic;

import de.lessvoid.nifty.controls.textfield.TextFieldView;

import de.lessvoid.nifty.effects.EffectEventId;

import de.lessvoid.nifty.elements.Element;

import de.lessvoid.nifty.elements.render.TextRenderer;

import de.lessvoid.nifty.elements.tools.FontHelper;

import de.lessvoid.nifty.input.NiftyInputEvent;

import de.lessvoid.nifty.screen.Screen;

import de.lessvoid.nifty.tools.SizeValue;

import de.lessvoid.xml.xpp3.Attributes;



/**

  • A TextFieldControl.

    */

    public class CustomControl extends AbstractController implements TextField, TextFieldView {

    private static final int CURSOR_Y = 0;

    private Nifty nifty;

    private Screen screen;

    private Element textElement;

    private Element fieldElement;

    private Element cursorElement;

    private Element selectionBG;

    private TextFieldLogic textField;

    private int firstVisibleCharacterIndex;

    private int lastVisibleCharacterIndex;

    private int fieldWidth;

    private int fromClickCursorPos;

    private int toClickCursorPos;

    private FocusHandler focusHandler;

    private Character passwordChar;



    @Override

    public void bind(final Nifty nifty, final Screen screen, final Element element, final Properties parameter, final Attributes controlDefinitionAttributes) {

    super.bind(element);



    this.nifty = nifty;

    this.screen = screen;

    this.fromClickCursorPos = -1;

    this.toClickCursorPos = -1;



    this.textField = new TextFieldLogic(parameter.getProperty("text", ""), new ClipboardAWT(), this);

    this.textField.toFirstPosition();



    this.textElement = getElement().findElementByName("#text");

    this.fieldElement = getElement().findElementByName("#field");

    this.cursorElement = getElement().findElementByName("#cursor");



    passwordChar = null;

    if (parameter.containsKey("passwordChar")) {

    passwordChar = parameter.get("passwordChar").toString().charAt(0);

    }

    if (parameter.containsKey("maxLength")) {

    setMaxLength(new Integer(parameter.getProperty("maxLength")));

    }

    }



    @Override

    public void init(final Properties parameter, final Attributes controlDefinitionAttributes) {

    this.focusHandler = screen.getFocusHandler();



    this.textField.initWithText(textElement.getRenderer(TextRenderer.class).getOriginalText());

    this.fieldWidth = this.fieldElement.getWidth() - this.cursorElement.getWidth();



    TextRenderer textRenderer = textElement.getRenderer(TextRenderer.class);

    this.firstVisibleCharacterIndex = 0;

    this.lastVisibleCharacterIndex = FontHelper.getVisibleCharactersFromStart(textRenderer.getFont(), this.textField.getText(), fieldWidth, 1.0f);



    updateCursor();

    super.init(parameter, controlDefinitionAttributes);



    final String backgroundSelectionColor = parameter.getProperty("backgroundSelectionColor", "#fff8");

    selectionBG = new PanelBuilder() {{

    backgroundColor(backgroundSelectionColor);

    x("0px");

    y("3px");

    width("0px");

    height("75%");

    }}.build(nifty, nifty.getCurrentScreen(), getElement().findElementByName("#selection_panel"));

    }



    @Override

    public void onStartScreen() {

    // stub method

    }



    @Override

    public void layoutCallback() {

    this.fieldWidth = this.fieldElement.getWidth() - this.cursorElement.getWidth();

    }



    public void onClick(final int mouseX, final int mouseY) {

    String visibleString = textField.getText().substring(firstVisibleCharacterIndex, lastVisibleCharacterIndex);

    int indexFromPixel = getCursorPosFromMouse(mouseX, visibleString);

    if (indexFromPixel != -1)

    fromClickCursorPos = firstVisibleCharacterIndex + indexFromPixel;



    textField.resetSelection();

    textField.setCursorPosition(fromClickCursorPos);

    updateCursor();

    }



    public void onClickMouseMove(final int mouseX, final int mouseY) {

    String visibleString = textField.getText().substring(firstVisibleCharacterIndex, lastVisibleCharacterIndex);

    int indexFromPixel = getCursorPosFromMouse(mouseX, visibleString);

    if (indexFromPixel != -1)

    toClickCursorPos = firstVisibleCharacterIndex + indexFromPixel;



    textField.setCursorPosition(fromClickCursorPos);

    textField.startSelecting();

    textField.setCursorPosition(toClickCursorPos);

    if (mouseX >= fieldElement.getWidth())

    textField.cursorRight();

    if (mouseX <= fieldElement.getX())

    textField.cursorLeft();

    textField.endSelecting();



    updateCursor();

    }



    private int getCursorPosFromMouse(final int mouseX, final String visibleString) {

    TextRenderer textRenderer = textElement.getRenderer(TextRenderer.class);

    return FontHelper.getCharacterIndexFromPixelPosition(textRenderer.getFont(), visibleString, (mouseX - fieldElement.getX()), 1.0f);

    }



    public boolean inputEvent(final NiftyInputEvent inputEvent) {

    if (inputEvent == NiftyInputEvent.MoveCursorLeft) {

    textField.cursorLeft();

    updateCursor();

    return true;

    } else if (inputEvent == NiftyInputEvent.MoveCursorRight) {

    textField.cursorRight();

    updateCursor();

    return true;

    } else if (inputEvent == NiftyInputEvent.Delete) {

    textField.delete();

    updateCursor();

    return true;

    } else if (inputEvent == NiftyInputEvent.Backspace) {

    textField.backspace();

    updateCursor();

    return true;

    } else if (inputEvent == NiftyInputEvent.MoveCursorToLastPosition) {

    textField.toLastPosition();

    updateCursor();

    return true;

    } else if (inputEvent == NiftyInputEvent.MoveCursorToFirstPosition) {

    textField.toFirstPosition();

    updateCursor();

    return true;

    } else if (inputEvent == NiftyInputEvent.SelectionStart) {

    textField.startSelecting();

    updateCursor();

    return true;

    } else if (inputEvent == NiftyInputEvent.SelectionEnd) {

    textField.endSelecting();

    updateCursor();

    return true;

    } else if (inputEvent == NiftyInputEvent.Cut) {

    textField.cut(passwordChar);

    updateCursor();

    return true;

    } else if (inputEvent == NiftyInputEvent.Copy) {

    textField.copy(passwordChar);

    updateCursor();

    return true;

    } else if (inputEvent == NiftyInputEvent.Paste) {

    textField.put();

    updateCursor();

    return true;

    } else if (inputEvent == NiftyInputEvent.SelectAll) {

    if (textField.getText().length() > 0) {

    textField.setCursorPosition(0);

    textField.startSelecting();

    textField.setCursorPosition(textField.getText().length());

    textField.endSelecting();

    updateCursor();

    return true;

    }

    } else if (inputEvent == NiftyInputEvent.Character) {

    textField.insert(inputEvent.getCharacter());

    updateCursor();

    return true;

    } else if (inputEvent == NiftyInputEvent.NextInputElement) {

    if (focusHandler != null) {

    focusHandler.getNext(fieldElement).setFocus();

    updateCursor();

    return true;

    }

    } else if (inputEvent == NiftyInputEvent.PrevInputElement) {

    textField.endSelecting();

    if (focusHandler != null) {

    focusHandler.getPrev(fieldElement).setFocus();

    updateCursor();

    return true;

    }

    }



    updateCursor();

    return false;

    }



    private void updateCursor() {

    TextRenderer textRenderer = textElement.getRenderer(TextRenderer.class);

    String text = textField.getText();

    checkBounds(text, textRenderer);

    calcLastVisibleIndex(textRenderer);



    // update text

    if (isPassword(passwordChar)) {

    int numChar = text.length();

    char[] chars = new char[numChar];

    for (int i = 0; i < numChar; ++i) {

    chars = passwordChar;

    }

    text = new String(chars);

    }

    textRenderer.setText(text);

    // textRenderer.setSelection(textField.getSelectionStart(), textField.getSelectionEnd());



    // calculate cursor position

    int cursorPos = textField.getCursorPosition();



    // outside, move window to fit cursorPos inside [first,last]

    calcFirstVisibleIndex(cursorPos);

    calcLastVisibleIndex(textRenderer);



    String substring2 = text.substring(0, firstVisibleCharacterIndex);

    int d = textRenderer.getFont().getWidth(substring2);

    textRenderer.setXoffsetHack(-d);



    String substring = text.substring(0, cursorPos);

    int textWidth = textRenderer.getFont().getWidth(substring);

    int cursorPixelPos = textWidth - d;



    if (selectionBG != null) {

    selectionBG.setConstraintWidth(new SizeValue("0px"));



    if (textField.hasSelection()) {

    int startpx = textRenderer.getFont().getWidth(text.substring(0, textField.getSelectionStart())) - d;

    int endpx = textRenderer.getFont().getWidth(text.substring(0, textField.getSelectionEnd())) - d;



    selectionBG.setConstraintWidth(new SizeValue((endpx - startpx) + "px"));

    selectionBG.setConstraintX(new SizeValue(startpx + "px"));

    }

    }

    cursorElement.setConstraintX(new SizeValue(cursorPixelPos + "px"));

    cursorElement.setConstraintY(new SizeValue((getElement().getHeight() - cursorElement.getHeight()) / 2 + CURSOR_Y + "px"));

    cursorElement.startEffect(EffectEventId.onActive, null);

    if (getElement() != null) {

    getElement().layoutElements();

    }

    }



    private boolean isPassword(final Character currentPasswordChar) {

    return currentPasswordChar != null;

    }



    private void calcFirstVisibleIndex(final int cursorPos) {

    if (cursorPos > lastVisibleCharacterIndex) {

    int cursorPosDelta = cursorPos - lastVisibleCharacterIndex;

    firstVisibleCharacterIndex += cursorPosDelta;

    } else if (cursorPos < firstVisibleCharacterIndex) {

    int cursorPosDelta = firstVisibleCharacterIndex - cursorPos;

    firstVisibleCharacterIndex -= cursorPosDelta;

    }

    }



    private void checkBounds(final String text, final TextRenderer textRenderer) {

    int textLen = text.length();

    if (firstVisibleCharacterIndex > textLen) {

    // re position so that we show at much possible text

    lastVisibleCharacterIndex = textLen;

    firstVisibleCharacterIndex = FontHelper.getVisibleCharactersFromEnd(textRenderer.getFont(), text, fieldWidth, 1.0f);

    }

    }



    private void calcLastVisibleIndex(final TextRenderer textRenderer) {

    String currentText = this.textField.getText();

    if (firstVisibleCharacterIndex < currentText.length()) {

    String textToCheck = currentText.substring(firstVisibleCharacterIndex);

    int lengthFitting = FontHelper.getVisibleCharactersFromStart(textRenderer.getFont(), textToCheck, fieldWidth, 1.0f);

    lastVisibleCharacterIndex = lengthFitting + firstVisibleCharacterIndex;

    } else {

    lastVisibleCharacterIndex = firstVisibleCharacterIndex;

    }

    }



    @Override

    public void onFocus(final boolean getFocus) {

    if (cursorElement != null) {

    super.onFocus(getFocus);

    if (getFocus) {

    cursorElement.startEffect(EffectEventId.onCustom);

    } else {

    cursorElement.stopEffect(EffectEventId.onCustom);

    textField.resetSelection();

    }

    updateCursor();

    }

    }



    @Override

    public String getText() {

    return textField.getText();

    }



    @Override

    public void setText(final String newText) {

    textField.initWithText(nifty.specialValuesReplace(newText));

    updateCursor();

    }



    @Override

    public void setMaxLength(final int maxLength) {

    textField.setMaxLength(maxLength);

    updateCursor();

    }



    @Override

    public void setCursorPosition(final int position) {

    textField.setCursorPosition(position);

    updateCursor();

    }



    @Override

    public void textChangeEvent(final String newText) {

    nifty.publishEvent(getElement().getId(), new TextFieldChangedEvent(this, newText));

    }



    @Override

    public void enablePasswordChar(final char passwordChar) {

    this.passwordChar = passwordChar;

    updateCursor();

    }



    @Override

    public void disablePasswordChar() {

    this.passwordChar = null;

    updateCursor();

    }



    @Override

    public boolean isPasswordCharEnabled() {

    return passwordChar != null;

    }



    }[/java]



    Actually there are 5 main issues that differ from the TextFieldControl class:


  1. I introduced a new panel ‘selectionBG’ displaying the selected background.
  2. I introduced a kind of scrolling selection selecting non-visible text by adding some lines in the onClickMouseMove(…)-method.
  3. I commented out the textRenderer.setSelection(…)-line in updateCursor() because it causes a text displacement.

    So the color of the selected text is not considered any more.
  4. Intuitively I limited the layouting in the updateCursor()-method to the textfield element due to performance reasons.
  5. We want the selection to be removed when the textfield losses the focus.





    In the end one have to define a custom control to use the control above:

    [xml]<controlDefinition style="custom-textfield" name="custom-textfield" controller="customControl.CustomControl" inputMapping="de.lessvoid.nifty.controls.textfield.TextFieldInputMapping" passwordChar="$passwordChar" maxLength="$maxLength">

    <panel style="#panel" focusable="true">

    <interact onClick="onClick()" onClickMouseMove="onClickMouseMove()" />

    <panel id="#field" style="#field" visibleToMouse="true">

    <panel id="#selection_panel" childLayout="absolute" />

    <text id="#text" style="#text" text="$text" />

    </panel>

    <panel style="#cursor-panel">

    <image id="#cursor" style="#cursor"/>

    </panel>

    </panel>

    </controlDefinition>[/xml]



    In a xml-file the color of the selected background is set as follows:

    [xml]<control name="custom-textfield" width="100%" height="100%" maxLength="150" backgroundSelectionColor="#052a" />[/xml]





    I did not test whether this control works with the standard textfield style.

    I guess one have to set the childLayout attribute of the #field panel to overlay.

Cool what you did there. :slight_smile: I remember seeing that textarea post, yeah.



So, how did you implement a multi line textfield ?

I'm still thinking through how to, haven't built it yet. Current approach I want to try is repositioning a TextField over one line within a ListBox of text (like the Console control ListBox does with text). There will be some tricks to map the underlying stream of text to lines in the list as changes happen - but probably doable. Your idea of panels for selection might work too - just use multiple panels (optional partial-line opening selection, full line/lines center selection, optional partial-line ending selection). Will see.