I'm using GWT 2.1.0
I have a CellTable populated with Columns that use different Cells to edit different types of values (e.g. date, string, etc). I want the user to be able to click in a cell, type a value, and hit enter to go directly to editing the next cell down, or tab to go directly to editing the next cell over.
I've been looking through the Cell and CellTable interfaces but can't find anything that looks relevant. How can I achieve this effect?
I had a similar requirement and I could not find a out-of-the-box solution. I ended up subclassing TextInputCell and add tabIndex support myself.
Here's some bits and pieces of the subclass (hopefully it will compile, too lazy to check). Unfortunately I cannot post the entire subclass, since it has lot may other things which are not related to the current question. This solution takes care of tabbing to the next cell, but for enter support, you may need to override onBrowserEvent.
public class EditTextInputCell extends TextInputCell
{
int startTabIndex;
interface TabbedTemplate extends SafeHtmlTemplates
{
#Template( "<input type=\"text\" value=\"{0}\" tabindex=\"{1}\" class=\"{2}\" title=\"{3}\"></input>" )
SafeHtml input( String value, String tabindex, String styleClass, String title );
}
private static TabbedTemplate template;
public EditTextInputCell( int startTabIndex )
{
this.startTabIndex = startTabIndex;
}
#Override
public boolean isEditing( Context context, Element parent, String value )
{
return true;
}
#Override
public void render( Context context, String value, SafeHtmlBuilder sb )
{
// Get the view data.
Object key = context.getKey( );
ValidationData viewData = getViewData( key );
if ( viewData != null && value.equals( viewData.getCurrentValue( ) ) )
{
clearViewData( key );
viewData = null;
}
String strToDisp = ( viewData != null && viewData.getCurrentValue( ) != null ) ? viewData.getCurrentValue( ) : value;
String tabIndex = "" + startTabIndex + context.getIndex( ) + context.getColumn( );
boolean invalid = ( viewData == null ) ? false : viewData.isInvalid( );
String styleClass = "cellTableCell-valid";
String errorMessage = "";
if ( invalid )
{
styleClass = "cellTableCell-invalid";
errorMessage = viewData.getMessage( );
}
if ( strToDisp != null )
{
SafeHtml html = SimpleSafeHtmlRenderer.getInstance( ).render( strToDisp );
// Note: template will not treat SafeHtml specially
sb.append( getTemplate( ).input( html.asString( ), tabIndex, styleClass, errorMessage ) );
}
else
{
sb.appendHtmlConstant( "<input type=\"text\" tabindex=\"" + tabIndex + "\" class=\"" + styleClass + "\" title=\"" + errorMessage + "\"></input>" );
}
}
private TabbedTemplate getTemplate( )
{
if ( template == null )
{
template = GWT.create( TabbedTemplate.class );
}
return template;
}}
Related
I am trying to change the value of a TextField for display only. Ie - when users attempt to enter phone number, they only enter the digits and when they leave the field, it displays formatted without changing the data in the field.
Let's say I have a TextField for a phone number and it should allow digits only, maximum of 10 characters:
2085551212
I can handle that with a TextFormatter using a UnaryOperator.
UnaryOperator<TextFormatter.Change> filter = new UnaryOperator<TextFormatter.Change>() {
#Override
public TextFormatter.Change apply(TextFormatter.Change change) {
int maxlength = 14;
if(change.getControlText().indexOf('(') == -1) {
maxlength = 10;
}
System.out.println(change);
if (change.getControlText().length() + change.getText().length() >= maxlength) {
int maxPos = maxlength - change.getControlText().length();
change.setText(change.getText().substring(0, maxPos));
}
String text = change.getText();
for (int i = 0; i < text.length(); i++)
if (!Character.isDigit(text.charAt(i)))
return null;
return change;
}
};
However I would like to format the value to be when it's 10 characters long (likely unformatted when != 10):
(208) 555-1212
When I use a TextFormatter to format it, it changes the value of the string to (208) 555-1212. We store only the digits in the database 2085551212.
I attempted this with a StringConverter. But I couldn't make it work. In the toString() method I strip out the formatting, however when I do that my TextField doesn't display.
StringConverter<String> formatter = new StringConverter<String>() {
#Override
public String fromString(String string) {
System.out.println("fromString(): before = " + string);
if (string.length() == 14) {
System.out.println("fromString(): after = " + string);
return string;
} else if (string.length() == 10 && string.indexOf('-') == -1) {
String result = String.format("(%s) %s-%s", string.substring(0, 3), string.substring(3, 6),
string.substring(6, 10));
System.out.println("fromString(): after = " + result);
return result;
} else {
return null;
}
}
#Override
public String toString(String object) {
System.out.println("toString(): before = " + object);
if(object == null) {
return "";
}
Pattern p = Pattern.compile("[\\p{Punct}\\p{Blank}]", Pattern.UNICODE_CHARACTER_CLASS);
Matcher m = p.matcher(object);
object = m.replaceAll("");
System.out.println("toString(): after = " + object);
return object;
}
};
I bound to a TextField like this which I assumed would work:
txtPhone.setTextFormatter(new TextFormatter<String>(formatter, null, filter));
t2.textProperty().bindBidirectional(foo.fooPropertyProperty(), formatter); //I was just testing to see the results in another textfield to see if it would work.
So I am at a loss. I essentially want to allow only digits and then when the user leaves the field present the value in a formatted way - without actually changing the string value that goes to the database.
You are confusing the purpose of toString() and fromString() methods with each other in your converter. toString() converts the value property of your text editor to the displayed text, not the other way around. Try switching the code in these methods and it should work.
The reason why your text field does not display anything after loosing focus is because fromString() method is called and returns null (from the else clause). This commits null to the value property of your editor. The change in value property updates the displayed text (textProperty) by calling toString(null) which changes the text property of your editor to an empty string.
EDIT
Below is my test code that is a follow-up to the discussion in the comments. I reused a large amount of your original code. I created an FXML JavaFX project and defined TextField and Label in FXML file. The TextField accepts user's input and formats it. The Label displays value of the text formatter (only digits) that should go to the database. The value is accessible by calling formatter.valueProperty().get(). I hope it helps.
import java.net.URL;
import java.util.ResourceBundle;
import java.util.function.UnaryOperator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.util.StringConverter;
public class FXMLDocumentController implements Initializable {
// label displays phone number containing only digits (for database)
#FXML private Label label;
/* field displays formatted text (XXX)-XXX-XXXX after user types
10 digits and presses Enter or if the field looses focus */
#FXML private TextField field;
#Override
public void initialize(URL url, ResourceBundle rb) {
UnaryOperator<TextFormatter.Change> filter = new UnaryOperator<TextFormatter.Change>() {
#Override
public TextFormatter.Change apply(TextFormatter.Change change) {
if (!change.isContentChange()) {
/* nothing is added or deleted but change must be returned
* as it contains selection info and caret position
*/
return change;
}
int maxlength = 14;
if (change.getControlText().indexOf('(') == -1) {
maxlength = 10;
}
if (change.getControlNewText().length() > maxlength
|| change.getText().matches("\\D+")) {
// invalid input. Cancel the change
return null;
}
return change;
}
};
StringConverter<String> converter = new StringConverter<String>() {
// updates displayed text from commited value
#Override
public String toString(String commitedText) {
if (commitedText == null) {
// don't change displayed text
return field.getText();
}
if (commitedText.length() == 10 && !commitedText.matches("\\D+")) {
return String.format("(%s) %s-%s", commitedText.substring(0, 3), commitedText.substring(3, 6),
commitedText.substring(6, 10));
} else {
/* Commited text can be either null or 10 digits.
* Nothing else is allowed by fromString() method unless changed directly
*/
throw new IllegalStateException(
"Unexpected or incomplete phone number value: " + commitedText);
}
}
// commits displayed text to value
#Override
public String fromString(String displayedText) {
// remove formatting characters
Pattern p = Pattern.compile("[\\p{Punct}\\p{Blank}]", Pattern.UNICODE_CHARACTER_CLASS);
Matcher m = p.matcher(displayedText);
displayedText = m.replaceAll("");
if (displayedText.length() != 10) {
// user is not done typing the number. Don't commit
return null;
}
return displayedText;
}
};
TextFormatter<String> formatter = new TextFormatter<String>(converter, "1234567890", filter);
field.setTextFormatter(formatter);
label.textProperty().bind(formatter.valueProperty());
}
}
I am using DataGrid in a GWT. One of the column is TextIntputCell. I want to place one tooltip on the same cell to show the updated value at each FieldUpdter Event.
Following is the code.
TextInputCell commentCell= new TextInputCell();{
#Override
public void render(com.google.gwt.cell.client.Cell.Context context, String value, SafeHtmlBuilder sb) {
String imagePath = GWT.getModuleBaseURL() + "/images/about.png";
sb.appendHtmlConstant(" ");
sb.appendHtmlConstant("<img src = '" + imagePath + "'height = '20px' width = '20px' title='"+value+"'/>");
}
};
commentColumn = new Column<RegisteredClasses, String>(commentCell) {
#Override
public String getValue(RegisteredClasses object) {
return "";
}
};
FieldUpdater<RegisteredClasses, String> commentUpdater = new FieldUpdater<RegisteredClasses, String>() {
#Override
public void update(int index, RegisteredClasses object, String value) {
if(value != null ){
object.setCommentsByContractor(value);
}
}
};
commentColumn.setFieldUpdater(commentUpdater);
table.addColumn(commentColumn, localizableResource.onAcceptanceComment());
table.setColumnWidth(commentColumn, "280px");
This is not working as the render event is called once only. Is there any way to call it explicitly.
I have a GWT view from which I grab the value of a dropdown and store it in a DB. The dropdown has the values "one" "two" "three". When I go back to the same view and I have "Two" stored in the DB then I want "Two" to be the selected item. However the only way I can get this to work at the moment is by iterating through each item in the listbox to find the one which matches and then set this as the selected one. Is there a better way to achieve this? I don't want to have to save the selected index.
I recommend you to extend ListBox and implement TakesValue interface. And in this class maintain a list variable which holds all the items in the ListBox. setValue and getValue should looks like the following code snippet -
private List<String> listItems = new ArrayList<String>();
public class MyListBox extends ListBox implements TakesValue<String>
{
public void setValue( String value )
{
if ( listItems.size() > 0 )
{
int valueIndex = 0;
if ( listItems.contains( value ) )
{
valueIndex = listItems.indexOf( value );
this.value = value;
}
setItemSelected( valueIndex, true );
}
}
public String getValue()
{
int selectedIndex = super.getSelectedIndex();
String value = null;
if ( selectedIndex >= 0 )
{
value = super.getValue( selectedIndex );
if ( "null".equals( value ) )
{
value = null;
}
}
return value;
}
public void setOptions(List<String> options)
{
listItems.clear();
listItems.addAll( items );
for ( String item : listItems )
{
addItem( item, item );
}
}
}
Now its just a matter of doing listBox.setValue( value ) method call from the view java file. Prior to this options must be set.
itemBox.addKeyUpHandler( new KeyUpHandler()
{
public void onKeyUp( KeyUpEvent event )
{
String currentValue = itemBox.getValue().trim();
// handle backspace
if( event.getNativeKeyCode() == KeyCodes.KEY_BACKSPACE )
{
if( "".equals( currentValue ) )
{
doTheJob( );
}
}
}
} );
Expect behavior:
when the textbox is empty, then i hit delete, will run doTheJob();
Current behavior:
when there is one character, i hit delete, it will trigger doTheJob();
In other word, is there any way i can get textbox content before i hit delete key?
I tried to used a var to hold the last value, but it needs to register another listener and the impl is not so effective.
Thanks for your input.
////////////////////Edit //////////////////////
use KeyDownHandler did solve the above problem, however lead to another problem:
i use itemBox.setValue( "" ); to clear the textbox but it will always have a comma there.
itemBox.addKeyDownHandler( new KeyDownHandler()
{
public void onKeyDown( KeyDownEvent event )
{
// handle backspace
if( event.getNativeKeyCode() == KeyCodes.KEY_BACKSPACE )
{
String currentValue = itemBox.getValue().trim();
if( "".equals( currentValue ) )
{
doTheJob();
}
}
// handle comma
else if( event.getNativeKeyCode() == 188 )
{
doOtherJob();
//clear TextBox for new input
itemBox.setValue( "" );
itemBox.setFocus( setFocus );
}
}
} );
after
itemBox.setFocus( setFocus );
prevent event bubbling with
event.preventDefault();
event.stopPropagation();
So the event will be canceled before comma is added to the textbox content.
I have a cell table with an async data provider. If I update the data via the data provider the table renders the new data correctly but the selection model still holds onto and returns old objects.
Any ideas how to refresh the selection model?
I think you should make your SelectionModel work with different instance of the same "logical" object using the appropriate ProvidesKey. For instance, you could use ProvidesKey that calls getId on the object, so that two objects with the same such ID would be considered equal; so even if the SelectionModel holds onto the old object, it can still answer "yes, it's selected" when you give it the new object.
FYI, this is exactly what the EntityProxyKeyProvider does (using the stableId of the proxy). And the SimpleKeyProvider, used by default when you don't specify one, uses the object itself as its key.
I came across the same issue. Currently I have this as single selection model.
SelectedRow = store it when you select it.
Then when data is reloaded you can clear it by
celltable.getSelectionModel().setSelected(SelectedRow, false);
I guess it is too late for you but hope it helps someone else.
Here is my manual method for refreshing the SelectionModel. This allows you to use the selectedSet() when needed and it will actually contain the current data, rather than the old data - including the removal of deleted rows and updated fields!
I have included bits & pieces of a class extending DataGrid. This should have all the logic at least to solve your problems.
When a row is selected, call saveSelectionKeys().
When the grid data is altered call refeshSelectedSet().
If you know the key type, you can replace the isSameKey() method with something easier to deal with. This class uses generics, so this method attempts to figure out the object conversion itself.
.
public abstract class AsyncDataGrid<T> extends DataGrid<T> {
...
private MultiSelectionModel<T> selectionModel_;
private ListDataProvider<T> dataProvider_;
private List<T> dataList_;
private Set<Object> priorSelectionKeySet_;
private boolean canCompareKeys_;
...
public AsyncDataGrid( final ProvidesKey<T> keyProvider ){
super( keyProvider );
...
dataProvider_ = new ListDataProvider<T>();
dataList_ = dataProvider_.getList();
canCompareKeys_ = true;
...
}
private void saveSelectionKeys(){
priorSelectionKeySet_ = new HashSet<Object>();
Set<T> selectedSet = selectionModel_.getSelectedSet();
for( Iterator<T> it = selectedSet.iterator(); it.hasNext(); ) {
priorSelectionKeySet_.add( super.getValueKey( it.next() ) );
}
}
private void refeshSelectedSet(){
selectionModel_.clear();
if( priorSelectionKeySet_ != null ){
if( !canCompareKeys_ ) return;
for( Iterator<Object> keyIt = priorSelectionKeySet_.iterator(); keyIt.hasNext(); ) {
Object priorKey = keyIt.next();
for( Iterator<T> it = dataList_.iterator(); it.hasNext(); ) {
T row = it.next();
Object rowKey = super.getValueKey( row );
if( isSameKey( rowKey, priorKey ) ) selectionModel_.setSelected( row, true );
}
}
}
}
private boolean isSameRowKey( final T row1, final T row2 ) {
if( (row1 == null) || (row2 == null) ) return false;
Object key1 = super.getValueKey( row1 );
Object key2 = super.getValueKey( row2 );
return isSameKey( key1, key2 );
}
private boolean isSameKey( final Object key1, final Object key2 ){
if( (key1 == null) || (key1 == null) ) return false;
if( key1 instanceof Integer ){
return ( ((Integer) key1) - ((Integer) key2) == 0 );
}
else if( key1 instanceof Long ){
return ( ((Long) key1) - ((Long) key2) == 0 );
}
else if( key1 instanceof String ){
return ( ((String) key1).equals( ((String) key2) ) );
}
canCompareKeys_ = false;
return false;
}
}
I fixed my particular issue by using the following code to return the visible selection. It uses the selection model to determine what is selected and combines this with what is visible. The objects themselves are returned from the CellTable data which is always upto date if the data has ever been changed via an async provider (the selection model data maybe stale but the keys will be correct)
public Set<T> getVisibleSelection() {
/*
* 1) the selection model contains selection that can span multiple pages -
* we want to return just the visible selection
* 2) return the object from the cellTable and NOT the selection - the
* selection may have old, stale, objects if the data has been updated
* since the selection was made
*/
Set<Object> selectedSet = getKeys(selectionModel.getSelectedSet());
List<T> visibleSet = cellTable.getVisibleItems();
Set<T> visibleSelectionSet = new HashSet<T>();
for (T visible : visibleSet) {
if (selectedSet.contains(KEY_PROVIDER.getKey(visible))) {
visibleSelectionSet.add(visible);
}
}
return visibleSelectionSet;
}
public static Set<Object> getKeys(Collection<T> objects) {
Set<Object> ids = new HashSet<Object>();
for (T object : objects) {
ids.add(KEY_PROVIDER.getKey(object));
}
return ids;
}