When I am using h:selectOneRadio and supplying the list of values in a list as
the entire radio button section is exposed as a single unbroken list. I need to arrange it in 3 columns. I have tried giving
<h:panelGrid id="radioGrid" columns="3">
<h:selectOneRadio id="radio1" value="#{bean.var}">
<f:selectItems id="rval" value="#{bean.list}"/>
</h:selectOneRadio>
</h:panelGrid>
But there is no difference in the rendered section. Its not broken up into columns. What am I doing wrong?
I've adapted the code given by Damo, to work with h:selectOneRadio instead of h:selectManycheckbox. To get it working you will need to register it in your faces-config.xml, with:
<render-kit>
<renderer>
<component-family>javax.faces.SelectOne</component-family>
<renderer-type>javax.faces.Radio</renderer-type>
<renderer-class>test.components.SelectOneRadiobuttonListRenderer</renderer-class>
</renderer>
</render-kit>
To compile it, you will also need the JSF implementation (typically found in some sort of jsf-impl.jar in you app server).
The code outputs the radiobuttons in divs, instead of a table. You can then use CSS to style them however you would like. I would suggest giving a fixed width to the checkboxDiv and inner divs, and then having the inner divs display as inline blocks:
div.radioButtonDiv{
width: 300px;
}
div.radioButtonDiv div{
display: inline-block;
width: 100px;
}
Which should give the 3 columns you are looking for
The code:
package test.components;
import java.io.IOException;
import java.lang.reflect.Array;
import java.util.Collection;
import java.util.Iterator;
import javax.faces.component.NamingContainer;
import javax.faces.component.UIComponent;
import javax.faces.component.UISelectMany;
import javax.faces.component.UISelectOne;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.model.SelectItem;
import com.sun.faces.renderkit.RenderKitUtils;
import com.sun.faces.renderkit.html_basic.MenuRenderer;
import com.sun.faces.util.MessageUtils;
import com.sun.faces.util.Util;
/**
* This component ensures that h:selectOneRadio doesn't get rendered using
* tables. It is adapted from the code at:
* http://www.blog.locuslive.com/?p=15
*
* To register it for use, place the following in your faces config:
*
* <render-kit>
* <renderer>
* <component-family>javax.faces.SelectOne</component-family>
* <renderer-type>javax.faces.Radio</renderer-type>
* <renderer-class>test.components.SelectOneRadiobuttonListRenderer</renderer-class>
* </renderer>
* </render-kit>
*
* The original comment is below:
*
* ----------------------------------------------------------------------------- *
* This is a custom renderer for the h:selectManycheckbox
* It is intended to bypass the incredibly sucky table based layout used
* by the standard component.
*
* This layout uses an enclosing div with divs for each input.
* This gives a default layout similar to a vertical layout
* The layout can then be controlled by css
*
* This renderer assigns an class of "checkboxDiv" to the enclosing div
* The class and styleClass attributes are then applied to the internal
* divs that house the inputs
*
* The following attributes are ignored as they are no longer required when using CSS:
* - pageDirection
* - border
*
* Note that I am not supporting optionGroups at this stage. They would be relatively
* easy to implement with another enclosing div
*
* #author damianharvey
*
*/
public class SelectOneRadiobuttonListRenderer extends MenuRenderer {
public void encodeEnd(FacesContext context, UIComponent component)
throws IOException {
if (context == null) {
throw new NullPointerException(
MessageUtils.getExceptionMessageString(MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID,
"context"));
}
if (component == null) {
throw new NullPointerException(
MessageUtils.getExceptionMessageString(MessageUtils.NULL_PARAMETERS_ERROR_MESSAGE_ID,
"component"));
}
// suppress rendering if "rendered" property on the component is
// false.
if (!component.isRendered()) {
return;
}
ResponseWriter writer = context.getResponseWriter();
assert(writer != null);
writer.startElement("div", component);
if (shouldWriteIdAttribute(component)) {
writeIdAttributeIfNecessary(context, writer, component);
}
writer.writeAttribute("class", "radioButtonDiv", "class");
Iterator items = RenderKitUtils.getSelectItems(context, component).iterator();
SelectItem curItem = null;
int idx = -1;
while (items.hasNext()) {
curItem = (SelectItem) items.next();
idx++;
renderOption(context, component, curItem, idx);
}
writer.endElement("div");
}
protected void renderOption(FacesContext context, UIComponent component, SelectItem curItem, int itemNumber)
throws IOException {
ResponseWriter writer = context.getResponseWriter();
assert(writer != null);
// disable the check box if the attribute is set.
String labelClass = null;
boolean componentDisabled = Util.componentIsDisabled(component);
if (componentDisabled || curItem.isDisabled()) {
labelClass = (String) component.
getAttributes().get("disabledClass");
} else {
labelClass = (String) component.
getAttributes().get("enabledClass");
}
writer.startElement("div", component); //Added by DAMIAN
String styleClass = (String) component.getAttributes().get("styleClass");
String style = (String) component.getAttributes().get("style");
if (styleClass != null) {
writer.writeAttribute("class", styleClass, "class");
}
if (style != null) {
writer.writeAttribute("style", style, "style");
}
writer.startElement("input", component);
writer.writeAttribute("name", component.getClientId(context), "clientId");
String idString = component.getClientId(context) + NamingContainer.SEPARATOR_CHAR + Integer.toString(itemNumber);
writer.writeAttribute("id", idString, "id");
String valueString = getFormattedValue(context, component, curItem.getValue());
writer.writeAttribute("value", valueString, "value");
writer.writeAttribute("type", "radio", null);
Object submittedValues[] = getSubmittedSelectedValues(context, component);
boolean isSelected;
Class type = String.class;
Object valuesArray = null;
Object itemValue = null;
if (submittedValues != null) {
valuesArray = submittedValues;
itemValue = valueString;
} else {
valuesArray = getCurrentSelectedValues(context, component);
itemValue = curItem.getValue();
}
if (valuesArray != null) {
type = valuesArray.getClass().getComponentType();
}
// I don't know what this does, but it doens't compile. Commenting it
// out doesn't seem to hurt
// Map<String, Object> requestMap = context.getExternalContext().getRequestMap();
// requestMap.put(ConverterPropertyEditorBase.TARGET_COMPONENT_ATTRIBUTE_NAME,
// component);
Object newValue = context.getApplication().getExpressionFactory().
coerceToType(itemValue, type);
isSelected = isSelected(newValue, valuesArray);
if (isSelected) {
writer.writeAttribute(getSelectedTextString(), Boolean.TRUE, null);
}
// Don't render the disabled attribute twice if the 'parent'
// component is already marked disabled.
if (!Util.componentIsDisabled(component)) {
if (curItem.isDisabled()) {
writer.writeAttribute("disabled", true, "disabled");
}
}
// Apply HTML 4.x attributes specified on UISelectMany component to all
// items in the list except styleClass and style which are rendered as
// attributes of outer most table.
RenderKitUtils.renderPassThruAttributes(writer, component, new String[] { "border", "style" });
RenderKitUtils.renderXHTMLStyleBooleanAttributes(writer, component);
writer.endElement("input");
writer.startElement("label", component);
writer.writeAttribute("for", idString, "for");
// if enabledClass or disabledClass attributes are specified, apply
// it on the label.
if (labelClass != null) {
writer.writeAttribute("class", labelClass, "labelClass");
}
String itemLabel = curItem.getLabel();
if (itemLabel != null) {
writer.writeText(" ", component, null);
if (!curItem.isEscape()) {
// It seems the ResponseWriter API should
// have a writeText() with a boolean property
// to determine if it content written should
// be escaped or not.
writer.write(itemLabel);
}
else {
writer.writeText(itemLabel, component, "label");
}
}
writer.endElement("label");
writer.endElement("div"); //Added by Damian
}
// ------------------------------------------------- Package Private Methods
String getSelectedTextString() {
return "checked";
}
/** For some odd reason this is a private method in the MenuRenderer superclass
*
* #param context
* #param component
* #return
*/
private Object getCurrentSelectedValues(FacesContext context,
UIComponent component) {
if (component instanceof UISelectMany) {
UISelectMany select = (UISelectMany) component;
Object value = select.getValue();
if (value instanceof Collection) {
Collection<?> list = (Collection) value;
int size = list.size();
if (size > 0) {
// get the type of the first element - Should
// we assume that all elements of the List are
// the same type?
return list.toArray((Object[]) Array.newInstance(list.iterator().next().getClass(), size));
}
else {
return ((Collection) value).toArray();
}
}
else if (value != null && !value.getClass().isArray()) {
logger.warning("The UISelectMany value should be an array or a collection type, the actual type is " + value.getClass().getName());
}
return value;
}
UISelectOne select = (UISelectOne) component;
Object returnObject;
if (null != (returnObject = select.getValue())) {
Object ret = Array.newInstance(returnObject.getClass(), 1);
Array.set(ret, 0, returnObject);
return ret;
}
return null;
}
/** For some odd reason this is a private method in the MenuRenderer superclass
*
* #param context
* #param component
* #return
*/
private Object[] getSubmittedSelectedValues(FacesContext context, UIComponent component) {
if (component instanceof UISelectMany) {
UISelectMany select = (UISelectMany) component;
return (Object[]) select.getSubmittedValue();
}
UISelectOne select = (UISelectOne) component;
Object returnObject;
if (null != (returnObject = select.getSubmittedValue())) {
return new Object[] { returnObject };
}
return null;
}
/** For some odd reason this is a private method in the MenuRenderer superclass
*
* #param itemValue
* #param valueArray
* #return
*/
private boolean isSelected(Object itemValue, Object valueArray) {
if (null != valueArray) {
if (!valueArray.getClass().isArray()) {
logger.warning("valueArray is not an array, the actual type is " + valueArray.getClass());
return valueArray.equals(itemValue);
}
int len = Array.getLength(valueArray);
for (int i = 0; i < len; i++) {
Object value = Array.get(valueArray, i);
if (value == null) {
if (itemValue == null) {
return true;
}
}
else if (value.equals(itemValue)) {
return true;
}
}
}
return false;
}
}
The h:panelGrid contains only one child (a h:selectOneRadio), so it will only ever render one column. The h:selectOneRadio renders a HTML table too. Its renderer only offers two layouts (lineDirection and pageDirection).
You have a few options
use JavaScript to modify the table after page load
find a 3rd party control that implements the functionality you want
write your own selectOneRadio control
Tomahawk does the magic! Check it out!
http://wiki.apache.org/myfaces/Display_Radio_Buttons_In_Columns
Related
Problem
The setTableMenuButtonVisible of a TableView provides a mechanism to change the visibility of a table column. However that functionality leaves a lot to be desired:
The menu should remain open. I have e. g. 15 table columns and it's a pain to click menu open -> click column -> click menu open -> click next column -> ... It's a pain to change the visibility of multiple columns
There should be a select all / deselect all functionality
There should be a way to extend the menu with custom items
After you deselected all columns there's no way to reselect a column because the header is gone and with it the table menu
In other words: The current implementation of the table menu is rather useless.
Question
Does anyone know of a way about how to replace the existing tableview menu with a proper one? I've seen a solution with a ".show-hide-columns-button" style lookup and adding an event filter. However that was 2 years ago, maybe things changed.
Thank you very much!
This is how I'd like to have it, demonstrated via ContextMenu (i. e. right mouse button click on table):
public class TableViewSample extends Application {
private final TableView table = new TableView();
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
Scene scene = new Scene(new Group());
stage.setTitle("Table View Sample");
stage.setWidth(300);
stage.setHeight(500);
// create table columns
TableColumn firstNameCol = new TableColumn("First Name");
TableColumn lastNameCol = new TableColumn("Last Name");
TableColumn emailCol = new TableColumn("Email");
table.getColumns().addAll(firstNameCol, lastNameCol, emailCol);
// add context menu
CustomMenuItem cmi;
ContextMenu cm = new ContextMenu();
// select all item
Label selectAll = new Label( "Select all");
selectAll.addEventHandler( MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
for( Object obj: table.getColumns()) {
((TableColumn) obj).setVisible(true);
} }
});
cmi = new CustomMenuItem( selectAll);
cmi.setHideOnClick(false);
cm.getItems().add( cmi);
// deselect all item
Label deselectAll = new Label("Deselect all");
deselectAll.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
for (Object obj : table.getColumns()) {
((TableColumn) obj).setVisible(false);
}
}
});
cmi = new CustomMenuItem( deselectAll);
cmi.setHideOnClick(false);
cm.getItems().add( cmi);
// separator
cm.getItems().add( new SeparatorMenuItem());
// menu item for all columns
for( Object obj: table.getColumns()) {
TableColumn tableColumn = (TableColumn) obj;
CheckBox cb = new CheckBox( tableColumn.getText());
cb.selectedProperty().bindBidirectional( tableColumn.visibleProperty());
cmi = new CustomMenuItem( cb);
cmi.setHideOnClick(false);
cm.getItems().add( cmi);
}
// set context menu
table.setContextMenu(cm);
final VBox vbox = new VBox();
vbox.setSpacing(5);
vbox.setPadding(new Insets(10, 0, 0, 10));
vbox.getChildren().addAll(table);
((Group) scene.getRoot()).getChildren().addAll(vbox);
stage.setScene(scene);
stage.show();
}
}
Inspired by the solution of ControlsFX I solved the problem myself using reflection. If someone has a better idea and cleaner way without reflection, I'm all ears. I created a utils class in order to distinguish from the sample code.
import java.lang.reflect.Field;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.Label;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.input.MouseEvent;
import com.sun.javafx.scene.control.skin.TableHeaderRow;
import com.sun.javafx.scene.control.skin.TableViewSkin;
public class TableViewUtils {
/**
* Make table menu button visible and replace the context menu with a custom context menu via reflection.
* The preferred height is modified so that an empty header row remains visible. This is needed in case you remove all columns, so that the menu button won't disappear with the row header.
* IMPORTANT: Modification is only possible AFTER the table has been made visible, otherwise you'd get a NullPointerException
* #param tableView
*/
public static void addCustomTableMenu( TableView tableView) {
// enable table menu
tableView.setTableMenuButtonVisible(true);
// get the table header row
TableHeaderRow tableHeaderRow = getTableHeaderRow((TableViewSkin) tableView.getSkin());
// get context menu via reflection
ContextMenu contextMenu = getContextMenu(tableHeaderRow);
// setting the preferred height for the table header row
// if the preferred height isn't set, then the table header would disappear if there are no visible columns
// and with it the table menu button
// by setting the preferred height the header will always be visible
// note: this may need adjustments in case you have different heights in columns (eg when you use grouping)
double defaultHeight = tableHeaderRow.getHeight();
tableHeaderRow.setPrefHeight(defaultHeight);
// modify the table menu
contextMenu.getItems().clear();
addCustomMenuItems( contextMenu, tableView);
}
/**
* Create a menu with custom items. The important thing is that the menu remains open while you click on the menu items.
* #param cm
* #param table
*/
private static void addCustomMenuItems( ContextMenu cm, TableView table) {
// create new context menu
CustomMenuItem cmi;
// select all item
Label selectAll = new Label("Select all");
selectAll.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
for (Object obj : table.getColumns()) {
((TableColumn<?, ?>) obj).setVisible(true);
}
}
});
cmi = new CustomMenuItem(selectAll);
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
// deselect all item
Label deselectAll = new Label("Deselect all");
deselectAll.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
for (Object obj : table.getColumns()) {
((TableColumn<?, ?>) obj).setVisible(false);
}
}
});
cmi = new CustomMenuItem(deselectAll);
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
// separator
cm.getItems().add(new SeparatorMenuItem());
// menu item for each of the available columns
for (Object obj : table.getColumns()) {
TableColumn<?, ?> tableColumn = (TableColumn<?, ?>) obj;
CheckBox cb = new CheckBox(tableColumn.getText());
cb.selectedProperty().bindBidirectional(tableColumn.visibleProperty());
cmi = new CustomMenuItem(cb);
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
}
}
/**
* Find the TableHeaderRow of the TableViewSkin
*
* #param tableSkin
* #return
*/
private static TableHeaderRow getTableHeaderRow(TableViewSkin<?> tableSkin) {
// get all children of the skin
ObservableList<Node> children = tableSkin.getChildren();
// find the TableHeaderRow child
for (int i = 0; i < children.size(); i++) {
Node node = children.get(i);
if (node instanceof TableHeaderRow) {
return (TableHeaderRow) node;
}
}
return null;
}
/**
* Get the table menu, i. e. the ContextMenu of the given TableHeaderRow via
* reflection
*
* #param headerRow
* #return
*/
private static ContextMenu getContextMenu(TableHeaderRow headerRow) {
try {
// get columnPopupMenu field
Field privateContextMenuField = TableHeaderRow.class.getDeclaredField("columnPopupMenu");
// make field public
privateContextMenuField.setAccessible(true);
// get field
ContextMenu contextMenu = (ContextMenu) privateContextMenuField.get(headerRow);
return contextMenu;
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}
}
Example usage:
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class CustomTableMenuDemo extends Application {
private final ObservableList<Person> data =
FXCollections.observableArrayList(
new Person("Jacob", "Smith", "jacob.smith#example.com"),
new Person("Isabella", "Johnson", "isabella.johnson#example.com"),
new Person("Ethan", "Williams", "ethan.williams#example.com"),
new Person("Emma", "Jones", "emma.jones#example.com"),
new Person("Isabella", "Johnson", "isabella.johnson#example.com"),
new Person("Ethan", "Williams", "ethan.williams#example.com"),
new Person("Emma", "Jones", "emma.jones#example.com"),
new Person("Isabella", "Johnson", "isabella.johnson#example.com"),
new Person("Ethan", "Williams", "ethan.williams#example.com"),
new Person("Emma", "Jones", "emma.jones#example.com"),
new Person("Isabella", "Johnson", "isabella.johnson#example.com"),
new Person("Ethan", "Williams", "ethan.williams#example.com"),
new Person("Emma", "Jones", "emma.jones#example.com"),
new Person("Isabella", "Johnson", "isabella.johnson#example.com"),
new Person("Ethan", "Williams", "ethan.williams#example.com"),
new Person("Emma", "Jones", "emma.jones#example.com"),
new Person("Isabella", "Johnson", "isabella.johnson#example.com"),
new Person("Ethan", "Williams", "ethan.williams#example.com"),
new Person("Emma", "Jones", "emma.jones#example.com"),
new Person("Michael", "Brown", "michael.brown#example.com"));
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
stage.setTitle("Table Menu Demo");
stage.setWidth(500);
stage.setHeight(550);
// create table columns
TableColumn<Person, String> firstNameCol = new TableColumn<Person, String>("First Name");
firstNameCol.setMinWidth(100);
firstNameCol.setCellValueFactory(new PropertyValueFactory<Person, String>("firstName"));
TableColumn<Person, String> lastNameCol = new TableColumn<Person, String>("Last Name");
lastNameCol.setMinWidth(100);
lastNameCol.setCellValueFactory(new PropertyValueFactory<Person, String>("lastName"));
TableColumn<Person, String> emailCol = new TableColumn<Person, String>("Email");
emailCol.setMinWidth(180);
emailCol.setCellValueFactory(new PropertyValueFactory<Person, String>("email"));
TableView<Person> tableView = new TableView<>();
tableView.setPlaceholder(new Text("No content in table"));
tableView.setItems(data);
tableView.getColumns().addAll(firstNameCol, lastNameCol, emailCol);
final VBox vbox = new VBox();
vbox.setSpacing(5);
vbox.setPadding(new Insets(10, 10, 10, 10));
BorderPane borderPane = new BorderPane();
borderPane.setCenter( tableView);
vbox.getChildren().addAll( borderPane);
Scene scene = new Scene( vbox);
stage.setScene(scene);
stage.show();
// enable table menu button and add a custom menu to it
TableViewUtils.addCustomTableMenu(tableView);
}
public static class Person {
private final SimpleStringProperty firstName;
private final SimpleStringProperty lastName;
private final SimpleStringProperty email;
private Person(String fName, String lName, String email) {
this.firstName = new SimpleStringProperty(fName);
this.lastName = new SimpleStringProperty(lName);
this.email = new SimpleStringProperty(email);
}
public String getFirstName() {
return firstName.get();
}
public void setFirstName(String fName) {
firstName.set(fName);
}
public String getLastName() {
return lastName.get();
}
public void setLastName(String fName) {
lastName.set(fName);
}
public String getEmail() {
return email.get();
}
public void setEmail(String fName) {
email.set(fName);
}
}
}
Screenshots:
Custom table menu in action, the menu remains open while you click the buttons:
Custom table menu still available, even though no columns are visible:
Edit: And here's a version that instead of reflection uses some heuristic and replaces the internal mouse event handler (see the source of JavaFX's TableHeaderRow class if you want to know more):
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.geometry.Side;
import javafx.scene.Node;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.Label;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.input.MouseEvent;
import com.sun.javafx.scene.control.skin.TableHeaderRow;
import com.sun.javafx.scene.control.skin.TableViewSkin;
public class TableViewUtils {
/**
* Make table menu button visible and replace the context menu with a custom context menu via reflection.
* The preferred height is modified so that an empty header row remains visible. This is needed in case you remove all columns, so that the menu button won't disappear with the row header.
* IMPORTANT: Modification is only possible AFTER the table has been made visible, otherwise you'd get a NullPointerException
* #param tableView
*/
public static void addCustomTableMenu( TableView tableView) {
// enable table menu
tableView.setTableMenuButtonVisible(true);
// replace internal mouse listener with custom listener
setCustomContextMenu( tableView);
}
private static void setCustomContextMenu( TableView table) {
TableViewSkin<?> tableSkin = (TableViewSkin<?>) table.getSkin();
// get all children of the skin
ObservableList<Node> children = tableSkin.getChildren();
// find the TableHeaderRow child
for (int i = 0; i < children.size(); i++) {
Node node = children.get(i);
if (node instanceof TableHeaderRow) {
TableHeaderRow tableHeaderRow = (TableHeaderRow) node;
// setting the preferred height for the table header row
// if the preferred height isn't set, then the table header would disappear if there are no visible columns
// and with it the table menu button
// by setting the preferred height the header will always be visible
// note: this may need adjustments in case you have different heights in columns (eg when you use grouping)
double defaultHeight = tableHeaderRow.getHeight();
tableHeaderRow.setPrefHeight(defaultHeight);
for( Node child: tableHeaderRow.getChildren()) {
// child identified as cornerRegion in TableHeaderRow.java
if( child.getStyleClass().contains( "show-hide-columns-button")) {
// get the context menu
ContextMenu columnPopupMenu = createContextMenu( table);
// replace mouse listener
child.setOnMousePressed(me -> {
// show a popupMenu which lists all columns
columnPopupMenu.show(child, Side.BOTTOM, 0, 0);
me.consume();
});
}
}
}
}
}
/**
* Create a menu with custom items. The important thing is that the menu remains open while you click on the menu items.
* #param cm
* #param table
*/
private static ContextMenu createContextMenu( TableView table) {
ContextMenu cm = new ContextMenu();
// create new context menu
CustomMenuItem cmi;
// select all item
Label selectAll = new Label("Select all");
selectAll.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
for (Object obj : table.getColumns()) {
((TableColumn<?, ?>) obj).setVisible(true);
}
}
});
cmi = new CustomMenuItem(selectAll);
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
// deselect all item
Label deselectAll = new Label("Deselect all");
deselectAll.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
for (Object obj : table.getColumns()) {
((TableColumn<?, ?>) obj).setVisible(false);
}
}
});
cmi = new CustomMenuItem(deselectAll);
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
// separator
cm.getItems().add(new SeparatorMenuItem());
// menu item for each of the available columns
for (Object obj : table.getColumns()) {
TableColumn<?, ?> tableColumn = (TableColumn<?, ?>) obj;
CheckBox cb = new CheckBox(tableColumn.getText());
cb.selectedProperty().bindBidirectional(tableColumn.visibleProperty());
cmi = new CustomMenuItem(cb);
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
}
return cm;
}
}
Thank you, Roland for your solution.
That was great. I generalized your solution for a little bit to solve some problems:
avoid to have to assign the TableView with the new context menu after
the window shown (it could cause difficulty when showAndWait() should
be used. It solves the problem by registering the onShown event of
the containing Window.
corrects the mispositioning bug when clicking
the + button while the menu is already on. (Clicking the + while
the menu is visible will hide the menu.)
works using keyboard
possibility to add additional menu items
Usage:
contextMenuHelper = new TableViewContextMenuHelper(tableView);
// Adding additional menu options
MenuItem exportMenuItem = new MenuItem("Export...");
contextMenuHelper.getAdditionalMenuItems().add(exportMenuItem);
Maybe someone find it useful, here is my implementation:
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import com.sun.javafx.scene.control.skin.TableHeaderRow;
import com.sun.javafx.scene.control.skin.TableViewSkin;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.geometry.Side;
import javafx.scene.Node;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;
/**
* Helper class to replace default column selection popup for TableView.
*
* <p>
* The original idea credeted to Roland and was found on
* {#link http://stackoverflow.com/questions/27739833/adapt-tableview-menu-button}
* </p>
* <p>
* This improved version targets to solve several problems:
* <ul>
* <li>avoid to have to assign the TableView with the new context menu after the
* window shown (it could cause difficulty when showAndWait() should be used. It
* solves the problem by registering the onShown event of the containing Window.
* </li>
* <li>corrects the mispositioning bug when clicking the + button while the menu
* is already on.</li>
* <li>works using keyboard</li>
* <li>possibility to add additional menu items</li>
* </ul>
* </p>
* <p>
* Usage from your code:
*
* <pre>
* contextMenuHelper = new TableViewContextMenuHelper(this);
* // Adding additional menu items
* MenuItem exportMenuItem = new MenuItem("Export...");
* contextMenuHelper.getAdditionalMenuItems().add(exportMenuItem);
* </pre>
* </p>
*
* #author Roland
* #author bvissy
*
*/
public class TableViewContextMenuHelper {
private TableView<?> tableView;
private ContextMenu columnPopupMenu;
private boolean showAllColumnsOperators = true;
private List<MenuItem> additionalMenuItems = new ArrayList<>();
// Default key to show menu: Shortcut + Shift + Space
private Function<KeyEvent, Boolean> showMenuByKeyboardCheck =
ke -> ke.getCode().equals(KeyCode.SPACE) && ke.isShortcutDown() && ke.isShiftDown();
public TableViewContextMenuHelper(TableView<?> tableView) {
super();
this.tableView = tableView;
// Hooking at the event when the whole window is shown
// and then implementing the event handler assignment
tableView.sceneProperty().addListener(i -> {
tableView.getScene().windowProperty().addListener(i2 -> {
tableView.getScene().getWindow().setOnShown(i3 -> {
tableView.tableMenuButtonVisibleProperty().addListener((ob, o, n) -> {
if (n == true) {
registerListeners();
}
});
if (tableView.isTableMenuButtonVisible()) {
registerListeners();
}
});
});
});
}
/**
* Registers the listeners.
*/
private void registerListeners() {
final Node buttonNode = findButtonNode();
// Keyboard listener on the table
tableView.addEventHandler(KeyEvent.KEY_PRESSED, ke -> {
if (showMenuByKeyboardCheck.apply(ke)) {
showContextMenu();
ke.consume();
}
});
// replace mouse listener on "+" node
buttonNode.setOnMousePressed(me -> {
showContextMenu();
me.consume();
});
}
protected void showContextMenu() {
final Node buttonNode = findButtonNode();
setFixedHeader();
// When the menu is already shown clicking the + button hides it.
if (columnPopupMenu != null) {
columnPopupMenu.hide();
} else {
// Show the menu
final ContextMenu newColumnPopupMenu = createContextMenu();
newColumnPopupMenu.setOnHidden(ev -> {
columnPopupMenu = null;
});
columnPopupMenu = newColumnPopupMenu;
columnPopupMenu.show(buttonNode, Side.BOTTOM, 0, 0);
// Repositioning the menu to be aligned by its right side (keeping inside the table view)
columnPopupMenu.setX(
buttonNode.localToScreen(buttonNode.getBoundsInLocal()).getMaxX()
- columnPopupMenu.getWidth());
}
}
private void setFixedHeader() {
// setting the preferred height for the table header row
// if the preferred height isn't set, then the table header would disappear if there are no visible columns
// and with it the table menu button
// by setting the preferred height the header will always be visible
// note: this may need adjustments in case you have different heights in columns (eg when you use grouping)
Region tableHeaderRow = getTableHeaderRow();
double defaultHeight = tableHeaderRow.getHeight();
tableHeaderRow.setPrefHeight(defaultHeight);
}
private Node findButtonNode() {
TableHeaderRow tableHeaderRow = getTableHeaderRow();
if (tableHeaderRow == null) {
return null;
}
for (Node child : tableHeaderRow.getChildren()) {
// child identified as cornerRegion in TableHeaderRow.java
if (child.getStyleClass().contains("show-hide-columns-button")) {
return child;
}
}
return null;
}
private TableHeaderRow getTableHeaderRow() {
TableViewSkin<?> tableSkin = (TableViewSkin<?>) tableView.getSkin();
if (tableSkin == null) {
return null;
}
// get all children of the skin
ObservableList<Node> children = tableSkin.getChildren();
// find the TableHeaderRow child
for (int i = 0; i < children.size(); i++) {
Node node = children.get(i);
if (node instanceof TableHeaderRow) {
return (TableHeaderRow) node;
}
}
return null;
}
/**
* Create a menu with custom items. The important thing is that the menu
* remains open while you click on the menu items.
*
* #param cm
* #param table
*/
private ContextMenu createContextMenu() {
ContextMenu cm = new ContextMenu();
// create new context menu
CustomMenuItem cmi;
if (showAllColumnsOperators) {
// select all item
Label selectAll = new Label("Select all");
selectAll.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> doSelectAll(event));
cmi = new CustomMenuItem(selectAll);
cmi.setOnAction(e -> doSelectAll(e));
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
// deselect all item
Label deselectAll = new Label("Deselect all");
deselectAll.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> doDeselectAll(event));
cmi = new CustomMenuItem(deselectAll);
cmi.setOnAction(e -> doDeselectAll(e));
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
// separator
cm.getItems().add(new SeparatorMenuItem());
}
// menu item for each of the available columns
for (Object obj : tableView.getColumns()) {
TableColumn<?, ?> tableColumn = (TableColumn<?, ?>) obj;
CheckBox cb = new CheckBox(tableColumn.getText());
cb.selectedProperty().bindBidirectional(tableColumn.visibleProperty());
cmi = new CustomMenuItem(cb);
cmi.setOnAction(e -> {
cb.setSelected(!cb.isSelected());
e.consume();
});
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
}
if (!additionalMenuItems.isEmpty()) {
cm.getItems().add(new SeparatorMenuItem());
cm.getItems().addAll(additionalMenuItems);
}
return cm;
}
protected void doDeselectAll(Event e) {
for (Object obj : tableView.getColumns()) {
((TableColumn<?, ?>) obj).setVisible(false);
}
e.consume();
}
protected void doSelectAll(Event e) {
for (Object obj : tableView.getColumns()) {
((TableColumn<?, ?>) obj).setVisible(true);
}
e.consume();
}
public boolean isShowAllColumnsOperators() {
return showAllColumnsOperators;
}
/**
* Sets whether the Select all/Deselect all buttons are visible
*
* #param showAllColumnsOperators
*/
public void setShowAllColumnsOperators(boolean showAllColumnsOperators) {
this.showAllColumnsOperators = showAllColumnsOperators;
}
public List<MenuItem> getAdditionalMenuItems() {
return additionalMenuItems;
}
public Function<KeyEvent, Boolean> getShowMenuByKeyboardCheck() {
return showMenuByKeyboardCheck;
}
/**
* Overrides the keypress check to show the menu. Default is Shortcut +
* Shift + Space.
*
* <p>
* To disable keyboard shortcut use the <code>e -> false</code> function.
* </p>
*
* #param showMenuByKeyboardCheck
*/
public void setShowMenuByKeyboardCheck(Function<KeyEvent, Boolean> showMenuByKeyboardCheck) {
this.showMenuByKeyboardCheck = showMenuByKeyboardCheck;
}
}
Update
Concerning the fact that when you deselect all the columns, the header is still visible and so do the menu button. JDK 8u72
I tried to implement Balage1551's solution.
For my Application I had to change the listeners in TableViewContextMenuHelper(...).
Without this changes I received a NullPointerException every time i changed the actual Scene and returned afterwards to my Screen containing the tableview.
I hope someone else might find this helpful!
// Hooking at the event when the whole window is shown
// and then implementing the event handler assignment
/*tableView.sceneProperty().addListener(i -> {
tableView.getScene().windowProperty().addListener(i2 -> {
tableView.getScene().getWindow().setOnShown(i3 -> {
tableView.tableMenuButtonVisibleProperty().addListener((ob, o, n) -> {
if (n == true) {
registerListeners();
}
});
if (tableView.isTableMenuButtonVisible()) {
registerListeners();
}
});
});
});*/
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^OLD!^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
vvvvvvvvvvvvvvvvvvvvvvvvvvvvvNEW!vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
tableView.skinProperty().addListener((a, b, newSkin) -> {
tableView.tableMenuButtonVisibleProperty().addListener((ob, o, n) -> {
if (n == true) {
registerListeners();
}
});
if (tableView.isTableMenuButtonVisible()) {
registerListeners();
}
});
This adaptation allows to initialize the TableViewContextMenuHelper again when you open an other scene with:
javafx.stage.Stage.setScreen(...);
I have a table (actually a bunch of tables) where the columns aren't fixed. Every time the columns are changed the above solution was re-setting the list of columns. So if a column called "Collar Size" was hidden it would appear again when the table was refreshed with a new set of data.
This may be crude but I've added a Set to store the names of columns that were hidden last time and then re-hide them this time.
The gist is a Set:
private Set<String> turnedOff = new HashSet<>();
and then the management of adding and removing items from the set. I needed to add a listener on the table columns to hide new columns that matched a name previously hidden.
Other ideas on how to accomplish this will be appreciated.
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import com.sun.javafx.scene.control.skin.TableHeaderRow;
import com.sun.javafx.scene.control.skin.TableViewSkin;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.geometry.Side;
import javafx.scene.Node;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;
public class TableViewContextMenuHelper {
private Set<String> turnedOff = new HashSet<>();
private TableView<?> tableView;
private ContextMenu columnPopupMenu;
private boolean showAllColumnsOperators = true;
private List<MenuItem> additionalMenuItems = new ArrayList<>();
// Default key to show menu: Shortcut + Shift + Space
private Function<KeyEvent, Boolean> showMenuByKeyboardCheck =
ke -> ke.getCode().equals(KeyCode.SPACE) && ke.isShortcutDown() && ke.isShiftDown();
public TableViewContextMenuHelper(TableView<?> tableView) {
super();
this.tableView = tableView;
tableView.skinProperty().addListener((a, b, newSkin) -> {
tableView.tableMenuButtonVisibleProperty().addListener((ob, o, n) -> {
if (n == true) {
registerListeners();
}
});
if (tableView.isTableMenuButtonVisible()) {
registerListeners();
}
});
}
/**
* Registers the listeners.
*/
private void registerListeners() {
final Node buttonNode = findButtonNode();
// Keyboard listener on the table
tableView.addEventHandler(KeyEvent.KEY_PRESSED, ke -> {
if (showMenuByKeyboardCheck.apply(ke)) {
showContextMenu();
ke.consume();
}
});
// replace mouse listener on "+" node
buttonNode.setOnMousePressed(me -> {
showContextMenu();
me.consume();
});
tableView.getColumns().addListener(new ListChangeListener<TableColumn<?,?>>(){
#Override
public void onChanged(javafx.collections.ListChangeListener.Change<? extends TableColumn<?, ?>> c) {
while(c.next()){
if(c.getAddedSize()>0){
// hide "turned off" columns
for(TableColumn<?, ?> tc:c.getAddedSubList()){
if(turnedOff.contains(tc.getText())){
tc.setVisible(false);
}
}
}
}
}
});
}
protected void showContextMenu() {
final Node buttonNode = findButtonNode();
setFixedHeader();
// When the menu is already shown clicking the + button hides it.
if (columnPopupMenu != null) {
columnPopupMenu.hide();
} else {
// Show the menu
final ContextMenu newColumnPopupMenu = createContextMenu();
newColumnPopupMenu.setOnHidden(ev -> {
columnPopupMenu = null;
});
columnPopupMenu = newColumnPopupMenu;
columnPopupMenu.show(buttonNode, Side.BOTTOM, 0, 0);
// Repositioning the menu to be aligned by its right side (keeping inside the table view)
columnPopupMenu.setX(
buttonNode.localToScreen(buttonNode.getBoundsInLocal()).getMaxX()
- columnPopupMenu.getWidth());
}
}
private void setFixedHeader() {
// setting the preferred height for the table header row
// if the preferred height isn't set, then the table header would disappear if there are no visible columns
// and with it the table menu button
// by setting the preferred height the header will always be visible
// note: this may need adjustments in case you have different heights in columns (eg when you use grouping)
Region tableHeaderRow = getTableHeaderRow();
double defaultHeight = tableHeaderRow.getHeight();
tableHeaderRow.setPrefHeight(defaultHeight);
}
private Node findButtonNode() {
TableHeaderRow tableHeaderRow = getTableHeaderRow();
if (tableHeaderRow == null) {
return null;
}
for (Node child : tableHeaderRow.getChildren()) {
// child identified as cornerRegion in TableHeaderRow.java
if (child.getStyleClass().contains("show-hide-columns-button")) {
return child;
}
}
return null;
}
private TableHeaderRow getTableHeaderRow() {
TableViewSkin<?> tableSkin = (TableViewSkin<?>) tableView.getSkin();
if (tableSkin == null) {
return null;
}
// get all children of the skin
ObservableList<Node> children = tableSkin.getChildren();
// find the TableHeaderRow child
for (int i = 0; i < children.size(); i++) {
Node node = children.get(i);
if (node instanceof TableHeaderRow) {
return (TableHeaderRow) node;
}
}
return null;
}
/**
* Create a menu with custom items. The important thing is that the menu
* remains open while you click on the menu items.
*
* #param cm
* #param table
*/
private ContextMenu createContextMenu() {
ContextMenu cm = new ContextMenu();
// create new context menu
CustomMenuItem cmi;
if (showAllColumnsOperators) {
// select all item
Label selectAll = new Label("Select all");
selectAll.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> doSelectAll(event));
cmi = new CustomMenuItem(selectAll);
cmi.setOnAction(e -> doSelectAll(e));
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
// deselect all item
Label deselectAll = new Label("Deselect all");
deselectAll.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> doDeselectAll(event));
cmi = new CustomMenuItem(deselectAll);
cmi.setOnAction(e -> doDeselectAll(e));
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
// separator
cm.getItems().add(new SeparatorMenuItem());
}
// menu item for each of the available columns
for (Object obj : tableView.getColumns()) {
TableColumn<?, ?> tableColumn = (TableColumn<?, ?>) obj;
CheckBox cb = new CheckBox(tableColumn.getText());
cb.selectedProperty().bindBidirectional(tableColumn.visibleProperty());
cmi = new CustomMenuItem(cb);
if(turnedOff.contains(cb.getText())){
cb.setSelected(false);
}
cmi.setOnAction(e -> {
cb.setSelected(!cb.isSelected());
if(cb.isSelected()){
turnedOff.remove(cb.getText());
} else {
turnedOff.add(cb.getText());
}
e.consume();
});
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
}
if (!additionalMenuItems.isEmpty()) {
cm.getItems().add(new SeparatorMenuItem());
cm.getItems().addAll(additionalMenuItems);
}
return cm;
}
protected void doDeselectAll(Event e) {
for (TableColumn<?, ?> obj : tableView.getColumns()) {
turnedOff.add(obj.getText());
obj.setVisible(false);
}
e.consume();
}
protected void doSelectAll(Event e) {
for (TableColumn<?, ?> obj : tableView.getColumns()) {
turnedOff.remove(obj.getText());
obj.setVisible(true);
}
e.consume();
}
public boolean isShowAllColumnsOperators() {
return showAllColumnsOperators;
}
/**
* Sets whether the Select all/Deselect all buttons are visible
*
* #param showAllColumnsOperators
*/
public void setShowAllColumnsOperators(boolean showAllColumnsOperators) {
this.showAllColumnsOperators = showAllColumnsOperators;
}
public List<MenuItem> getAdditionalMenuItems() {
return additionalMenuItems;
}
public Function<KeyEvent, Boolean> getShowMenuByKeyboardCheck() {
return showMenuByKeyboardCheck;
}
/**
* Overrides the keypress check to show the menu. Default is Shortcut +
* Shift + Space.
*
* <p>
* To disable keyboard shortcut use the <code>e -> false</code> function.
* </p>
*
* #param showMenuByKeyboardCheck
*/
public void setShowMenuByKeyboardCheck(Function<KeyEvent, Boolean> showMenuByKeyboardCheck) {
this.showMenuByKeyboardCheck = showMenuByKeyboardCheck;
}
}
If you just want to listen to events from the Table Menu Button (and save/restore the state say to java.util.Preferences), then add a listener to the table's VisibleLeafColumns [the ObservableList from getColumns will not change with the selection].
I adjusted the code above to be more generic to work with both the TreeTableView and TableView.
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import javafx.beans.property.BooleanProperty;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.geometry.Side;
import javafx.scene.Node;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Control;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.TableColumnBase;
import javafx.scene.control.TableView;
import javafx.scene.control.TreeTableView;
import javafx.scene.control.skin.TableHeaderRow;
import javafx.scene.control.skin.TableViewSkinBase;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;
import org.jetbrains.annotations.NotNull;
/**
* Helper class to replace default column selection popup for TableView.
*
* <p>
* The original idea credeted to Roland and was found on https://stackoverflow.com/questions/27739833/adapt-tableview-menu-button
* </p>
* <p>
* This improved version targets to solve several problems:
* <ul>
* <li>avoid to have to assign the TableView with the new context menu after the
* window shown (it could cause difficulty when showAndWait() should be used. It
* solves the problem by registering the onShown event of the containing Window.
* </li>
* <li>corrects the mispositioning bug when clicking the + button while the menu
* is already on.</li>
* <li>works using keyboard</li>
* <li>possibility to add additional menu items</li>
* </ul>
* </p>
* <p>
* Usage from your code:
*
* <pre>
* contextMenuHelper = new TableViewContextMenuHelper(this);
* // Adding additional menu items
* MenuItem exportMenuItem = new MenuItem("Export...");
* contextMenuHelper.getAdditionalMenuItems().add(exportMenuItem);
* </pre>
* </p>
* <p>
* https://stackoverflow.com/questions/27739833/adapt-tableview-menu-button
*
* #author Roland
* #author bvissy
*/
public class TreeColumnMenuHelper {
private final Control tableView;
private final List<MenuItem> additionalMenuItems = new ArrayList<>();
private ContextMenu columnPopupMenu;
private boolean showAllColumnsOperators = true;
// Default key to show menu: Shortcut (CTRL on windows) + Shift + Space
private Function<KeyEvent, Boolean> showMenuByKeyboardCheck = ke ->
ke.getCode().equals(KeyCode.SPACE) && ke.isShortcutDown() && ke.isShiftDown();
public TreeColumnMenuHelper(TableView tableView) {
this((Control) tableView);
}
public TreeColumnMenuHelper(TreeTableView tableView) {
this((Control) tableView);
}
private TreeColumnMenuHelper(Control tableView) {
super();
this.tableView = tableView;
if (tableView.getSkin() != null) {
registerListeners();
return;
}
// listen to skin change - this should happen once the table is shown
tableView.skinProperty().addListener((a, b, newSkin) -> {
final BooleanProperty tableMenuButtonVisibleProperty = getTableMenuButtonVisibleProperty(tableView);
tableMenuButtonVisibleProperty.addListener((ob, o, n) -> {
if (n) {
registerListeners();
}
});
if (tableMenuButtonVisibleProperty.get()) {
registerListeners();
}
});
}
/**
*
* #return property that controls the menu button in the corner of the table
*/
private BooleanProperty getTableMenuButtonVisibleProperty(#NotNull Control tableView) {
if(tableView instanceof TableView tab) {
return tab.tableMenuButtonVisibleProperty();
}
if(tableView instanceof TreeTableView tree) {
return tree.tableMenuButtonVisibleProperty();
}
throw new IllegalArgumentException("Argument is no TableView or TreeTableView. Actual class: "+tableView.getClass().getName());
}
/**
* Get columns of the table or treetable
* #return list of columns
*/
private static List<? extends TableColumnBase> getColumns(Control table) {
if (table instanceof TableView tab) {
return tab.getColumns();
} else if (table instanceof TreeTableView tree) {
return tree.getColumns();
} else {
throw new IllegalArgumentException(
"Table argument is no TreeTableView or TableView. Actual class: " + table.getClass()
.getName());
}
}
/**
* Registers the listeners.
*/
private void registerListeners() {
final Node buttonNode = findButtonNode();
// Keyboard listener on the table
tableView.addEventHandler(KeyEvent.KEY_PRESSED, ke -> {
if (showMenuByKeyboardCheck.apply(ke)) {
showContextMenu();
ke.consume();
}
});
// replace mouse listener on "+" node
assert buttonNode != null;
buttonNode.setOnMousePressed(me -> {
showContextMenu();
me.consume();
});
}
protected void showContextMenu() {
final Node buttonNode = findButtonNode();
setFixedHeader();
// When the menu is already shown clicking the + button hides it.
if (columnPopupMenu != null) {
columnPopupMenu.hide();
} else {
// Show the menu
final ContextMenu newColumnPopupMenu = createContextMenu();
newColumnPopupMenu.setOnHidden(ev -> columnPopupMenu = null);
columnPopupMenu = newColumnPopupMenu;
columnPopupMenu.show(buttonNode, Side.BOTTOM, 0, 0);
// Repositioning the menu to be aligned by its right side (keeping inside the table view)
columnPopupMenu.setX(
buttonNode.localToScreen(buttonNode.getBoundsInLocal()).getMaxX() - columnPopupMenu
.getWidth());
}
}
private void setFixedHeader() {
// setting the preferred height for the table header row
// if the preferred height isn't set, then the table header would disappear if there are no visible columns
// and with it the table menu button
// by setting the preferred height the header will always be visible
// note: this may need adjustments in case you have different heights in columns (eg when you use grouping)
Region tableHeaderRow = getTableHeaderRow();
double defaultHeight = tableHeaderRow.getHeight();
tableHeaderRow.setPrefHeight(defaultHeight);
}
private Node findButtonNode() {
TableHeaderRow tableHeaderRow = getTableHeaderRow();
if (tableHeaderRow == null) {
return null;
}
for (Node child : tableHeaderRow.getChildren()) {
// child identified as cornerRegion in TableHeaderRow.java
if (child.getStyleClass().contains("show-hide-columns-button")) {
return child;
}
}
return null;
}
private TableHeaderRow getTableHeaderRow() {
TableViewSkinBase tableSkin = (TableViewSkinBase) tableView.getSkin();
if (tableSkin == null) {
return null;
}
// get all children of the skin
ObservableList<Node> children = tableSkin.getChildren();
// find the TableHeaderRow child
for (Node node : children) {
if (node instanceof TableHeaderRow header) {
return header;
}
}
return null;
}
/**
* Create a menu with custom items. The important thing is that the menu remains open while you
* click on the menu items.
*/
private ContextMenu createContextMenu() {
ContextMenu cm = new ContextMenu();
// create new context menu
CustomMenuItem cmi;
if (showAllColumnsOperators) {
// select all item
Label selectAll = new Label("Select all");
selectAll.addEventHandler(MouseEvent.MOUSE_CLICKED, this::doSelectAll);
cmi = new CustomMenuItem(selectAll);
cmi.setOnAction(this::doSelectAll);
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
// deselect all item
Label deselectAll = new Label("Deselect all");
deselectAll.addEventHandler(MouseEvent.MOUSE_CLICKED, this::doDeselectAll);
cmi = new CustomMenuItem(deselectAll);
cmi.setOnAction(this::doDeselectAll);
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
// separator
cm.getItems().add(new SeparatorMenuItem());
}
if (!additionalMenuItems.isEmpty()) {
cm.getItems().addAll(additionalMenuItems);
cm.getItems().add(new SeparatorMenuItem());
}
// menu item for each of the available columns
for (TableColumnBase col : getColumns(tableView)) {
CheckBox cb = new CheckBox(col.getText());
cb.selectedProperty().bindBidirectional(col.visibleProperty());
cmi = new CustomMenuItem(cb);
cmi.setOnAction(e -> {
cb.setSelected(!cb.isSelected());
e.consume();
});
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
}
return cm;
}
protected void setAllVisible(boolean visible) {
for (TableColumnBase col : getColumns(tableView)) {
col.setVisible(visible);
}
}
protected void doDeselectAll(Event e) {
setAllVisible(false);
e.consume();
}
protected void doSelectAll(Event e) {
setAllVisible(true);
e.consume();
}
public boolean isShowAllColumnsOperators() {
return showAllColumnsOperators;
}
/**
* Sets whether the Select all/Deselect all buttons are visible
*
* #param showAllColumnsOperators
*/
public void setShowAllColumnsOperators(boolean showAllColumnsOperators) {
this.showAllColumnsOperators = showAllColumnsOperators;
}
public List<MenuItem> getAdditionalMenuItems() {
return additionalMenuItems;
}
public Function<KeyEvent, Boolean> getShowMenuByKeyboardCheck() {
return showMenuByKeyboardCheck;
}
/**
* Overrides the keypress check to show the menu. Default is Shortcut + Shift + Space.
*
* <p>
* To disable keyboard shortcut use the <code>e -> false</code> function.
* </p>
*
* #param showMenuByKeyboardCheck
*/
public void setShowMenuByKeyboardCheck(Function<KeyEvent, Boolean> showMenuByKeyboardCheck) {
this.showMenuByKeyboardCheck = showMenuByKeyboardCheck;
}
}
I want to get selected object from <h:selectOneMenu>, but the problem is I couldn't find any generic converter for all type of entities.
My first question is, is there a generic converter for all type of entities? I don't want to write another converter again for each other entity. My second question is, is there a way to get selected object without any converter? I don't want to call the DB again and again.
I have a Car entity with id and name properties.
My first question is, is there a generic converter for all type of entities?
This does indeed not exist in standard JSF. The JSF utility library OmniFaces has such a converter in its assortiment, the omnifaces.SelectItemsConverter. All you need to do is to declare it as converter of an UISelectOne or UISelectMany component as follows:
<h:selectOneMenu ... converter="omnifaces.SelectItemsConverter">
See also the SelectItemsConverter showcase. This converter relies on the toString() of the object item. There's also another one, the omnifaces.SelectItemsIndexConverter, which relies instead on the index of the object item in the options list, see also the SelectItemsIndexConverter showcase.
There are currently no other JSF component/utility libraries offering the same.
Second question is, is there a way to get selected object without any converter?
No. Just use the OmniFaces one so that you don't need to create a custom converter which hits the DB. Or if you want to go overboard, create a custom renderer for <h:selectOneMenu> which renders the item index as option value and is able to set it as model value, but that's a lot of more work than a simple converter and you'd still need to do some additional work in order to get the desired object from the list based on the index — which just doesn't make any sense.
See also:
How to populate options of h:selectOneMenu from database?
Conversion Error setting value for 'null Converter'
Implement converters for entities with Java Generics
It seems like there should be a generic converter so that you can easily select the object from the drop down list without having to write a converter for every object type and without having to call the database (as most examples show). But there isn't that I know of, so I wrote my own converter to do this. Note that the converter expects the object to have a getId() method which returns a unique ID of some kind. If it doesn't it will fail. You can add logic to the getMethodName() if you need to determine the name of the method to use as a key programmatically. Note that we use Seam in our project. If you don't use Seam, the NO_SELECTION_VALUE parts can probably be removed as well as the three annotations on the class.
This code was inspired by: http://arjan-tijms.omnifaces.org/2011/12/automatic-to-object-conversion-in-jsf.html
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import javax.faces.component.UIComponent;
import javax.faces.component.UISelectItem;
import javax.faces.component.UISelectItems;
import javax.faces.context.FacesContext;
import javax.faces.convert.ConverterException;
import javax.faces.model.SelectItem;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.annotations.faces.Converter;
import org.jboss.seam.annotations.intercept.BypassInterceptors;
/**
* #author: Jason Wheeler
* #description Converter for lists (SelectOneMenu, SelectManyMenu, etc)
* #created: 09/05/2013
*/
#Name("listConverter")
#BypassInterceptors
#Converter
public class ListConverter implements javax.faces.convert.Converter {
private String NO_SELECTION_VALUE = "org.jboss.seam.ui.NoSelectionConverter.noSelectionValue";
#Override
public String getAsString(FacesContext facesContext, UIComponent component, Object obj) {
if (obj == null) {
return NO_SELECTION_VALUE;
} else {
try {
Method method = obj.getClass().getMethod(getMethodName(obj));
return String.valueOf(method.invoke(obj));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
public String getMethodName(Object obj) {
return "getId";
}
#Override
public Object getAsObject(FacesContext facesContext, UIComponent component, String val) throws ConverterException {
if (val == null) {
return null;
} else if (val.equals(NO_SELECTION_VALUE)) {
return null;
} else {
for (SelectItem item : getSelectItems(component)) {
if (val.equals(getAsString(facesContext, component, item.getValue()))) {
return item.getValue();
}
}
return null;
}
}
protected Collection<SelectItem> getSelectItems(UIComponent component) {
Collection<SelectItem> collection = new ArrayList<SelectItem>();
for (UIComponent child : component.getChildren()) {
if (child instanceof UISelectItem) {
UISelectItem ui = (UISelectItem) child;
SelectItem item = (SelectItem) ui.getValue();
collection.add(item);
} else if (child instanceof UISelectItems) {
UISelectItems ui = (UISelectItems) child;
Object value = ui.getValue();
collection.addAll((Collection<SelectItem>) value);
}
}
return collection;
}
}
I've just took #Bigwheels code, made some changes to work with JSF 2.0 and it fixed my problem:
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import javax.faces.component.UIComponent;
import javax.faces.component.UISelectItem;
import javax.faces.component.UISelectItems;
import javax.faces.context.FacesContext;
import javax.faces.convert.ConverterException;
import javax.faces.convert.FacesConverter;
import javax.faces.model.SelectItem;
import javax.persistence.Id;
#FacesConverter("selectItemConverter")
public class SelectItemConverter implements javax.faces.convert.Converter {
private String NO_SELECTION_VALUE = "SELECIONE";
#Override
public String getAsString(FacesContext facesContext, UIComponent component, Object obj) {
if (obj == null) {
return NO_SELECTION_VALUE;
} else {
try {
Method method = obj.getClass().getMethod(getIdMethodName(obj));
return String.valueOf(method.invoke(obj));
} catch (Exception e) {
throw new ConverterException(e);
}
}
}
public String getIdMethodName(Object obj) {
try {
Field[] fieldList = obj.getClass().getDeclaredFields();
Field id = null;
for (Field field : fieldList) {
if(field.isAnnotationPresent(Id.class)) {
id = field;
break;
}
}
return "get" + capitalize(id.getName());
} catch(Exception ex) {
throw new ConverterException(ex);
}
}
private String capitalize(final String line) {
return Character.toUpperCase(line.charAt(0)) + line.substring(1);
}
#Override
public Object getAsObject(FacesContext facesContext, UIComponent component, String val) throws ConverterException {
if (val == null) {
return null;
} else if (val.equals(NO_SELECTION_VALUE)) {
return null;
} else {
for (Object item : getSelectItems(component)) {
if (val.equals(getAsString(facesContext, component, item))) {
return item;
}
}
return null;
}
}
protected List getSelectItems(UIComponent component) {
List list = new ArrayList();
for (UIComponent child : component.getChildren()) {
if (child instanceof UISelectItem) {
UISelectItem ui = (UISelectItem) child;
SelectItem item = (SelectItem) ui.getValue();
list.add(item);
} else if (child instanceof UISelectItems) {
UISelectItems ui = (UISelectItems) child;
Object value = ui.getValue();
list.addAll((Collection<SelectItem>) value);
}
}
return list;
}
}
I am creating some forms and I need to create masks and validation for some fields.
Is it implemented in anyway in JavaFX?
My example of the mask.
Using:
<MaskField mask="+7(DDD)DDD-DDDD"/>
<MaskField mask="AA DDD AAA" placeholder="__ ### ___"/>
etc
Restricting input from Richard's fxexperience post:
TextField field = new TextField() {
#Override public void replaceText(int start, int end, String text) {
// If the replaced text would end up being invalid, then simply
// ignore this call!
if (!text.matches("[a-z]")) {
super.replaceText(start, end, text);
}
}
#Override public void replaceSelection(String text) {
if (!text.matches("[a-z]")) {
super.replaceSelection(text);
}
}
};
If you want to create your use a mask and create your own control, take a look at Richard's MoneyField, which also includes a sample project and source. Along the same lines there are controls to restict input to Integers, Doubles or formatted web colors (e.g. #rrggbb) in the fxexperience repository. All of these follow a common theme where they subclass Control, provide some properties to be get and set which define the public interface and then also define a private backing skin which handles rendering of the UI based on the values set through the public interface.
I had the same needs. I created this field, called it SpecialTextField, and pushed into GitHub. Example also there. Hope this help.
NOTE: this only works correctly with JRE 1.8.0_25 or lower. With JRE 1.8.0_48 or 0_51, the caret position is always set to 0 after each character input.
No, this is not implemented in standard JavaFX. You need to use some library or do it yourself.
This is my implementation of static mask for text fields. It works for date, phone and other types of static masks:
/**
* Adds a static mask to the specified text field.
* #param tf the text field.
* #param mask the mask to apply.
* Example of usage: addMask(txtDate, " / / ");
*/
public static void addMask(final TextField tf, final String mask) {
tf.setText(mask);
addTextLimiter(tf, mask.length());
tf.textProperty().addListener(new ChangeListener<String>() {
#Override
public void changed(final ObservableValue<? extends String> ov, final String oldValue, final String newValue) {
String value = stripMask(tf.getText(), mask);
tf.setText(merge(value, mask));
}
});
tf.setOnKeyPressed(new EventHandler<KeyEvent>() {
#Override
public void handle(final KeyEvent e) {
int caretPosition = tf.getCaretPosition();
if (caretPosition < mask.length()-1 && mask.charAt(caretPosition) != ' ' && e.getCode() != KeyCode.BACK_SPACE && e.getCode() != KeyCode.LEFT) {
tf.positionCaret(caretPosition + 1);
}
}
});
}
static String merge(final String value, final String mask) {
final StringBuilder sb = new StringBuilder(mask);
int k = 0;
for (int i = 0; i < mask.length(); i++) {
if (mask.charAt(i) == ' ' && k < value.length()) {
sb.setCharAt(i, value.charAt(k));
k++;
}
}
return sb.toString();
}
static String stripMask(String text, final String mask) {
final Set<String> maskChars = new HashSet<>();
for (int i = 0; i < mask.length(); i++) {
char c = mask.charAt(i);
if (c != ' ') {
maskChars.add(String.valueOf(c));
}
}
for (String c : maskChars) {
text = text.replace(c, "");
}
return text;
}
public static void addTextLimiter(final TextField tf, final int maxLength) {
tf.textProperty().addListener(new ChangeListener<String>() {
#Override
public void changed(final ObservableValue<? extends String> ov, final String oldValue, final String newValue) {
if (tf.getText().length() > maxLength) {
String s = tf.getText().substring(0, maxLength);
tf.setText(s);
}
}
});
}
See also:
JavaFX 2.2 TextField maxlength
Supported by current javafx-2 platform by default - No, but go through this link , it has many insights and sample code for Form validation in javaFX
public class NumberTextField extends TextField {
private int maxLenght;
public NumberTextField(int maxLenght) {
super();
this.maxLenght = maxLenght;
}
#Override
public void replaceText(int start, int end, String text) {
if (validate(text)) {
super.replaceText(start, end, text);
}
}
#Override
public void replaceSelection(String text) {
if (validate(text)) {
super.replaceSelection(text);
}
}
private boolean validate(String text) {
if (this.getText() != null) {
}
boolean status = ("".equals(text) || text.matches("[0-9]"));
if (this.getText() == null) {
return status;
} else {
return (status && this.getText().length() < maxLenght);
}
}
}
In some cases I would validate the text property:
myTextField
.textProperty()
.addListener(
(obs, oldVal, newVal) ->
{
if(!newVal.matches("\\d+"))
textField.setText(oldV);
});
Unlucky: textField.setText(oldV); will enter the same function again, testing unnecessarily if oldVal matches.
If the TextField becomes a value that doesn't matches before this listener is added to the TextField, enter a not matching new value will cause a loop!!!
To avoid this, it will be safer to write:
String acceptableValue = "0";
myTextField
.textProperty()
.addListener(
(obs, oldVal, newVal) ->
{
if(!newVal.matches("\\d+"))
textField.setText(oldVal.matches("\\d+") ? oldV : acceptableValue);
});
I wrote a class that extends the TextField and apply the mask.
package com.model;
import java.text.NumberFormat;
import java.util.Locale;
/**
* ATENTION
* DO NOT FORGUET TO IMPORT IN FXML
* <?import com.view.TextFieldMoney?>
*
* */
import javafx.scene.control.TextField;
public class TextFieldMoney extends TextField {
private int maxlength;
private String valor = "";
public TextFieldMoney() {
this.maxlength = 11;
}
public void setMaxlength(int maxlength) {
this.maxlength = maxlength;
}
#Override
public void replaceText(int start, int end, String text) {
// Delete or backspace user input.
if (getText() == null || getText().equalsIgnoreCase("")) {
valor = "";
}
if (text.equals("")) {
super.replaceText(start, end, text);
} else{
text = text.replaceAll("[^0-9]", "");
valor += text;
super.replaceText(start, end, text);
if (!valor.equalsIgnoreCase(""))
setText(formata(valor));
}
}
#Override
public void replaceSelection(String text) {
// Delete or backspace user input.
if (text.equals("")) {
super.replaceSelection(text);
} else if (getText().length() < maxlength) {
// Add characters, but don't exceed maxlength.
// text = MascaraFinanceira.show(text);
if (text.length() > maxlength - getText().length()) {
// text = MascaraFinanceira.show(text);
text = text.substring(0, maxlength - getText().length());
}
super.replaceSelection(text);
}
}
/*
*Return the number without money mask
**/
public String getCleanValue(){
String cleanString = getText().replaceAll("[^0-9]", "");
Double cleanNumber = new Double(cleanString);
return String.valueOf(cleanNumber/100);
}
private String formata(Double valor) {
Locale locale = new Locale("pt", "BR");
NumberFormat nf = NumberFormat.getInstance(locale);
nf.setMaximumFractionDigits(2);
nf.setMinimumFractionDigits(2);
return nf.format(valor);
}
public String formata(String valor) {
double v = new Double(valor);
return formata(v/100);
}
}
And in the FXML where is
<TextField fx:id="valorTextField" GridPane.columnIndex="2" GridPane.rowIndex="2" />
put
<TextFieldMoney fx:id="valorTextField" GridPane.columnIndex="2" GridPane.rowIndex="2" />
Hi I've this situation and I dont know why it's happening..
I have a selectonemenu like this
<ice:selectOneMenu id="ddlProfesion" value="#{FrmClientes.profesionSeleccionado}" style="width:230px">
<f:selectItems value="#{SessionBean1.listaProfesion}"/>
<f:converter converterId="DefaultSelectItemConverter" />
</ice:selectOneMenu>
the list of items
public List getListaProfesion() {
if (listaProfesion == null) {
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
session.beginTransaction();
listaProfesion = new ArrayList<SelectItem>();
List<Profesion> profesionList = session.getNamedQuery("Profesion.findAll").list();
for (Profesion c : profesionList) {
listaProfesion.add(new SelectItem(c, c.getNombre()));
}
return listaProfesion;
}
return listaProfesion;
}
now I have a datatable and when I click in a row a panelPopup open with the data of the object Profesion..
the code of the selectionListener in the rowSelector is this:
public void seleccionaTerceros(RowSelectorEvent event) {
Session session = HibernateUtil.getSessionFactory().getCurrentSession();
session.beginTransaction();
Query query = session.getNamedQuery("Clientes.findByTercero");
query.setParameter("tercero", "12332454");
// I send a parameter value for example
if (!query.list().isEmpty()) {
cliente = (Clientes) query.list().get(0);
profesionSeleccionado=cliente.getProfesionID();
} else {
cliente = null;
profesionSeleccionado=null;
}
setMostrarModal(true);
}
I set profesionSeleccionado to the Value of the objetc and doesnt work, I put this code in another location, like the constructor of the managed bean or in a button action.. and IT WORKS...
I have debbuged and see that the getter and the setter of the atribute are accesed twice, why, i dont know
please I need some guide, I'am new with this.. Thanks
pd: the code of the converter used to list objects in the selectonemenu is this
public class DefaultSelectItemConverter implements Converter {
/**
* Not explicitly documented.
*
* #see javax.faces.convert.Converter#getAsObject(javax.faces.context.FacesContext,
* javax.faces.component.UIComponent, java.lang.String)
*/
public Object getAsObject(FacesContext fcontext, UIComponent comp, String valueString) {
List<UIComponent> children = comp.getChildren();
for (UIComponent child : children) {
if (child instanceof UISelectItem) {
UISelectItem si = (UISelectItem) child;
if (si.getValue().toString().equals(valueString)) {
return si.getValue();
}
}
if (child instanceof UISelectItems) {
UISelectItems sis = (UISelectItems)child;
List<SelectItem> items = (List)sis.getValue();
for (SelectItem si : items) {
if (si.getValue().toString().equals(valueString)) {
return si.getValue();
}
}
}
}
throw new ConverterException("no conversion possible for string representation: " + valueString);
}
/**
* Not explicitly documented.
*
* #see javax.faces.convert.Converter#getAsString(javax.faces.context.FacesContext,
* javax.faces.component.UIComponent, java.lang.Object)
*/
public String getAsString(FacesContext fcontext, UIComponent comp, Object value) {
return value.toString();
}
}
I found the solution in this page
http://jira.icefaces.org/browse/ICE-2297
the problem was corrected putting immediate="false" on the RowSelector
:)
I'm trying to add a very simple file input to my webapp which I'm doing using JSF2.0 and RichFaces 3.3.3, the thing is I really dislike the richfaces fileInput component and I'm looking for something simpler (in terms of use and looks), so far I've found:
tomahawk's fileInput - but tomahawk only supports JSF1.2
trinidad fileInput - but trinidad for JSF2.0 is in alpha stage
primefaces - but of course they won't work with RichFaces 3.3.3 (only 4.0 which are in beta)
Are there any other options? I need something really simple like a normal html file input component.
You basically need to do two things:
Create a Filter which puts the multipart/form-data items in a custom map and replace the original request parameter map with it so that the normal request.getParameter() process keeps working.
Create a JSF 2.0 custom component which renders a input type="file" and which is aware of this custom map and can obtain the uploaded files from it.
#taher has already given a link where you could find insights and code snippets. The JSF 2.0 snippets should be reuseable. You yet have to modify the MultipartMap to use the good 'ol Apache Commons FileUpload API instead of the Servlet 3.0 API.
If I have time, I will by end of day rewrite it and post it here.
Update: I almost forgot you, I did a quick update to replace Servlet 3.0 API by Commons FileUpload API, it should work:
package net.balusc.http.multipart;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
public class MultipartMap extends HashMap<String, Object> {
// Constants ----------------------------------------------------------------------------------
private static final String ATTRIBUTE_NAME = "parts";
private static final String DEFAULT_ENCODING = "UTF-8";
private static final int DEFAULT_BUFFER_SIZE = 10240; // 10KB.
// Vars ---------------------------------------------------------------------------------------
private String encoding;
private String location;
// Constructors -------------------------------------------------------------------------------
/**
* Construct multipart map based on the given multipart request and file upload location. When
* the encoding is not specified in the given request, then it will default to <tt>UTF-8</tt>.
* #param multipartRequest The multipart request to construct the multipart map for.
* #param location The location to save uploaded files in.
* #throws ServletException If something fails at Servlet level.
* #throws IOException If something fails at I/O level.
*/
#SuppressWarnings("unchecked") // ServletFileUpload#parseRequest() isn't parameterized.
public MultipartMap(HttpServletRequest multipartRequest, String location)
throws ServletException, IOException
{
multipartRequest.setAttribute(ATTRIBUTE_NAME, this);
this.encoding = multipartRequest.getCharacterEncoding();
if (this.encoding == null) {
multipartRequest.setCharacterEncoding(this.encoding = DEFAULT_ENCODING);
}
this.location = location;
try {
List<FileItem> parts = new ServletFileUpload(new DiskFileItemFactory()).parseRequest(multipartRequest);
for (FileItem part : parts) {
if (part.isFormField()) {
processFormField(part);
} else if (!part.getName().isEmpty()) {
processFileField(part);
}
}
} catch (FileUploadException e) {
throw new ServletException("Parsing multipart/form-data request failed.", e);
}
}
// Actions ------------------------------------------------------------------------------------
#Override
public Object get(Object key) {
Object value = super.get(key);
if (value instanceof String[]) {
String[] values = (String[]) value;
return values.length == 1 ? values[0] : Arrays.asList(values);
} else {
return value; // Can be File or null.
}
}
/**
* #see ServletRequest#getParameter(String)
* #throws IllegalArgumentException If this field is actually a File field.
*/
public String getParameter(String name) {
Object value = super.get(name);
if (value instanceof File) {
throw new IllegalArgumentException("This is a File field. Use #getFile() instead.");
}
String[] values = (String[]) value;
return values != null ? values[0] : null;
}
/**
* #see ServletRequest#getParameterValues(String)
* #throws IllegalArgumentException If this field is actually a File field.
*/
public String[] getParameterValues(String name) {
Object value = super.get(name);
if (value instanceof File) {
throw new IllegalArgumentException("This is a File field. Use #getFile() instead.");
}
return (String[]) value;
}
/**
* #see ServletRequest#getParameterNames()
*/
public Enumeration<String> getParameterNames() {
return Collections.enumeration(keySet());
}
/**
* #see ServletRequest#getParameterMap()
*/
public Map<String, String[]> getParameterMap() {
Map<String, String[]> map = new HashMap<String, String[]>();
for (Entry<String, Object> entry : entrySet()) {
Object value = entry.getValue();
if (value instanceof String[]) {
map.put(entry.getKey(), (String[]) value);
} else {
map.put(entry.getKey(), new String[] { ((File) value).getName() });
}
}
return map;
}
/**
* Returns uploaded file associated with given request parameter name.
* #param name Request parameter name to return the associated uploaded file for.
* #return Uploaded file associated with given request parameter name.
* #throws IllegalArgumentException If this field is actually a Text field.
*/
public File getFile(String name) {
Object value = super.get(name);
if (value instanceof String[]) {
throw new IllegalArgumentException("This is a Text field. Use #getParameter() instead.");
}
return (File) value;
}
// Helpers ------------------------------------------------------------------------------------
/**
* Process given part as Text part.
*/
private void processFormField(FileItem part) {
String name = part.getFieldName();
String[] values = (String[]) super.get(name);
if (values == null) {
// Not in parameter map yet, so add as new value.
put(name, new String[] { part.getString() });
} else {
// Multiple field values, so add new value to existing array.
int length = values.length;
String[] newValues = new String[length + 1];
System.arraycopy(values, 0, newValues, 0, length);
newValues[length] = part.getString();
put(name, newValues);
}
}
/**
* Process given part as File part which is to be saved in temp dir with the given filename.
*/
private void processFileField(FileItem part) throws IOException {
// Get filename prefix (actual name) and suffix (extension).
String filename = FilenameUtils.getName(part.getName());
String prefix = filename;
String suffix = "";
if (filename.contains(".")) {
prefix = filename.substring(0, filename.lastIndexOf('.'));
suffix = filename.substring(filename.lastIndexOf('.'));
}
// Write uploaded file.
File file = File.createTempFile(prefix + "_", suffix, new File(location));
InputStream input = null;
OutputStream output = null;
try {
input = new BufferedInputStream(part.getInputStream(), DEFAULT_BUFFER_SIZE);
output = new BufferedOutputStream(new FileOutputStream(file), DEFAULT_BUFFER_SIZE);
IOUtils.copy(input, output);
} finally {
IOUtils.closeQuietly(output);
IOUtils.closeQuietly(input);
}
put(part.getFieldName(), file);
part.delete(); // Cleanup temporary storage.
}
}
You still need both the MultipartFilter and MultipartRequest classes as described in this article. You only need to remove the #WebFilter annotation and map the filter on an url-pattern of /* along with an <init-param> of location wherein you specify the absolute path where the uploaded files are to be stored. You can use the JSF 2.0 custom file upload component as described in this article unchanged.
Dear either you have to use rich:uploadFile or make custom component in JSF by following http://balusc.blogspot.com/2009/12/uploading-files-with-jsf-20-and-servlet.html
After I also tried tomahawk, I mentioned that it does not work with AJAX. So I decided to hack rich:fileUpload and perform the click on add button over a a4j:commandButton. Here's the code:
<a4j:form id="myForm">
<a4j:commandButton id="myButton" value="Upload" title="Upload" styleClass="myButtonClass"
onclick="document.getElementById('myForm:myFileUpload:file').click()/>
<rich:fileUpload id="myFileUpload" maxFilesQuantity="1" autoclear="true"
immediateUpload="true" styleClass="invisibleClass"
fileUploadListener="#{uploadBean.uploadListener}"/>
</a4j:form>
myForm:myFileUpload:file is the input-Element (type="file") for the Add-Button. invisibleClass should only contain display:none;. With style="display:none;" it won't work.
You can used rich faces 3.3.3 file upload.
Step 1 : fileUpload.xhtml
<rich:fileUpload id="fileupload" addControlLabel="Browse"
required="true"
fileUploadListener="#{testForm.listener}"
acceptedTypes="xml"
ontyperejected="alert('Only xml files are accepted');"
maxFilesQuantity="1" listHeight="57px" listWidth="100%"
disabled="#{testForm..disabled}" >
<a4j:support event="onclear"
action="#{testForm..clearUploadData}"
reRender="fileupload" />
</rich:fileUpload>
Step 2: FileUpload.java
public class FileUpload implements Serializable{
/**
*
*/
private static final long serialVersionUID = 1L;
private String Name;
private String mime;
private long length;
private byte [] file;
private String absolutePath;
public String getName() {
return Name;
}
/**
* #return the file
*/
public byte[] getFile() {
return file;
}
/**
* #param file the file to set
*/
public void setFile(byte[] file) {
this.file = file;
}
/**
* #param mime the mime to set
*/
public void setMime(String mime) {
this.mime = mime;
}
public void setName(String name) {
Name = name;
int extDot = name.lastIndexOf('.');
if(extDot > 0){
String extension = name.substring(extDot +1);
if("txt".equals(extension)){
mime="txt";
} else if("xml".equals(extension)){
mime="xml";
} else {
mime = "unknown file";
}
}
}
public long getLength() {
return length;
}
public void setLength(long length) {
this.length = length;
}
public String getMime(){
return mime;
}
/**
* #return the absolutePath
*/
public String getAbsolutePath() {
return absolutePath;
}
/**
* #param absolutePath the absolutePath to set
*/
public void setAbsolutePath(String absolutePath) {
this.absolutePath = absolutePath;
}
}
Step 3 :TestForm // calling listner
/**
*
* #param event
* #throws Exception
*/
public void listener(UploadEvent event) throws Exception{
UploadItem item = event.getUploadItem();
FileUpload file = new FileUpload();
file.setLength(item.getData().length);
file.setFile(item.getData());
file.setName(item.getFileName());
files.add(file);
}