I just developed a JavaFX applications this twenty different pages. Each page has a table and I wanted to place a context menu on each table.
Basically its always the same code for placing the context menu to the table but I am hoping that method references can help here a little bit.
This is the actual code snippet:
resultTable.setRowFactory(new Callback<TableView<InterfaceModel>, TableRow<InterfaceModel>>() {
#Override
public TableRow<InterfaceModel> call(TableView<InterfaceModel> tableView) {
final TableRow<InterfaceModel> row = new TableRow<InterfaceModel>();
final ContextMenu rowMenu = new ContextMenu();
MenuItem editItem = new MenuItem("EDIT");
editItem.setOnAction(event -> {
// action if edit was selected
});
And I want something like that:
ContextMenuHelper helper = new ContextMenuHelper(resultTable);
helper.addItem("Edit", [referenceToAMethod]);
helper.addItem("Item 2", [referenceToADifferentMethod]);
What I mean is that this helper creates the context menu. All this helper needs is the label for the entry and a method to call after selection of this entry.
Is that possible with the method-refereces from java 8?
Thanks,
Hauke
If you just want to define a method for creating a MenuItem, then it's easy enough: you just need to decide on the functional interface you will need for the parameter that takes the method reference (or lambda, etc). E.g. if the method signature takes no parameters and has void return type, you could use Runnable:
public MenuItem createItem(String text, Runnable handler) {
MenuItem item = new MenuItem(text);
item.setOnAction(e -> handler.run());
}
You probably want the menu item event handler to have access to the table item in the row, in which case it would need a reference to the row:
public <T> MenuItem createItem(String text, TableRow<T> row, Consumer<T> handler) {
MenuItem item = new MenuItem(text);
item.setOnAction(e -> handler.accept(row.getItem()));
}
Then you can do
TableView<InterfaceModel> table = new TableView<>();
ContextMenuHelper helper = new ContextMenuHelper();
table.setRowFactory(t -> {
TableRow<InterfaceModel> row = new TableRow<>();
ContextMenu menu = new ContextMenu();
row.setContextMenu(menu);
menu.getItems().addItem(helper.createItem("Edit", row, this::edit));
// etc...
});
with
private void edit(InterfaceModel model) {
// ...
}
What you didn't actually ask, but I'm sort of guessing you really want, is for the "helper" class to actually set the row factory and create all the menus, etc. This is a bit harder to structure, because you need to entirely build the context menu inside the row factory, so you need to know all the menu items before you can actually set the row factory. For this, you probably want to consider a builder pattern:
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.util.Callback;
public class TableRowContextMenuBuilder<T> {
private final List<MenuItemConfig<T>> items ;
private boolean built ;
public TableRowContextMenuBuilder() {
this.items = new ArrayList<>();
}
public static <T> TableRowContextMenuBuilder<T> create(Class<T> type) {
return new TableRowContextMenuBuilder<>();
}
public TableRowContextMenuBuilder<T> addItem(String text, Consumer<T> handler) {
if (built) {
throw new IllegalStateException("Row factory is already built: cannot add new items");
}
items.add(new MenuItemConfig<T>(text, handler));
return this ;
}
public TableRowContextMenuBuilder<T> addItem(String text, Runnable handler) {
return addItem(text, t -> handler.run());
}
public Callback<TableView<T>, TableRow<T>> build() {
if (built) {
throw new IllegalStateException("Cannot build row factory more than once");
}
built = true ;
return t -> {
TableRow<T> row = new TableRow<>();
ContextMenu menu = new ContextMenu();
row.setContextMenu(menu);
items.stream()
.map(config -> config.asMenuItem(row))
.forEach(menu.getItems()::add);
return row ;
};
}
public void buildForTable(TableView<T> table) {
table.setRowFactory(build());
}
private static class MenuItemConfig<T> {
private final String text ;
private final Consumer<T> handler ;
MenuItemConfig(String text, Consumer<T> handler) {
this.text = text;
this.handler = handler;
}
MenuItem asMenuItem(TableRow<T> row) {
MenuItem item = new MenuItem(text);
item.setOnAction(e -> handler.accept(row.getItem()));
return item ;
}
}
}
And now you can do
TableView<InterfaceModel> table = new TableView<>();
TableViewContextMenuBuilder.create(InterfaceModel.class)
.menuBuilder.addItem("Edit", this::edit);
.menuBuilder.addItem("Item 2", this::handleOtherItem);
// ...
.buildForTable(table);
with the appropriate methods defined:
private void edit(InterfaceModel model) { /* ... */}
private void handleOtherItem(InterfaceModel model) { /* ... */}
Related
I have a ChoiceBox where I can select the language for my program. When I select another language, the label gets translated as desired (because it is recomputed using ChoiceBoxSkin#getDisplayText and my StringConverter takes the language into account), but the elements in the popup list stay the same.
Now, I could do something like
public void updateStrings() {
var converter = getConverter();
setConverter(null);
setConverter(converter);
var selected = valueProperty().getValue();
valueProperty().setValue(null);
valueProperty().setValue(selected);
}
in my ChoiceBox-subclass. This will re-populate the popup list with the correctly translated texts. Setting the value again is necessary beacause ChoiceBoxSkin#updatePopupItems (which is triggered when changing the converter) also resets the toggleGroup. That means that the selected item would no longer be marked as selected in the popup list.
Despite being kind of ugly, this actually works for my current use case. However, it breaks if any listener of the valueProperty does something problematic on either setting it to null or selecting the desired item a second time.
Am I missing a cleaner or just all-around better way to achieve this?
Another approach might be to use a custom ChoiceBoxSkin. Extending that, I'd have access to ChoiceBoxSkin#getChoiceBoxPopup (although that is commented with "Test only purpose") and could actually bind the text properties of the RadioMenuItems to the corresponding translated StringProperty. But that breaks as soon as ChoiceBoxSkin#updatePopupItems is triggered from anywhere else...
A MRP should be:
import javafx.scene.control.ChoiceBox;
import javafx.util.StringConverter;
public class LabelChangeChoiceBox extends ChoiceBox<String> {
private boolean duringUpdate = false;
public LabelChangeChoiceBox() {
getItems().addAll("A", "B", "C");
setConverter(new StringConverter<>() {
#Override
public String toString(String item) {
return item + " selected:" + valueProperty().getValue();
}
#Override
public String fromString(String unused) {
throw new UnsupportedOperationException();
}
});
valueProperty().addListener((observable, oldValue, newValue) -> {
if(duringUpdate) {
return;
}
duringUpdate = true;
updateStrings();
duringUpdate = false;
});
}
public void updateStrings() {
var converter = getConverter();
setConverter(null);
setConverter(converter);
var selected = valueProperty().getValue();
valueProperty().setValue(null);
valueProperty().setValue(selected);
}
}
And an Application-class like
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
import ui.LabelChangeChoiceBox;
public class Launcher extends Application {
#Override
public void start(Stage stage) {
Scene scene = new Scene(new LabelChangeChoiceBox());
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch();
}
}
This works but needs the duringUpdate variable and can break if there is another change listener.
I’m not sure if this meets your needs, as your description of the problem is unclear in a few places.
Here’s a ChoiceBox which updates its converter using its own chosen language, and also retains its value when that change occurs:
import java.util.Locale;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.geometry.Insets;
import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.control.ChoiceBox;
import javafx.scene.layout.BorderPane;
import javafx.util.StringConverter;
public class FXLocaleSelector
extends Application {
#Override
public void start(Stage stage) {
ChoiceBox<Locale> choiceBox = new ChoiceBox<>();
choiceBox.getItems().addAll(
Locale.ENGLISH,
Locale.FRENCH,
Locale.GERMAN,
Locale.ITALIAN,
Locale.CHINESE,
Locale.JAPANESE,
Locale.KOREAN
);
choiceBox.converterProperty().bind(
Bindings.createObjectBinding(
() -> createConverter(choiceBox.getValue()),
choiceBox.valueProperty()));
BorderPane pane = new BorderPane(choiceBox);
pane.setPadding(new Insets(40));
stage.setScene(new Scene(pane));
stage.setTitle("Locale Selector");
stage.show();
}
private StringConverter<Locale> createConverter(Locale locale) {
Locale conversionLocale =
(locale != null ? locale : Locale.getDefault());
return new StringConverter<Locale>() {
#Override
public String toString(Locale value) {
if (value != null) {
return value.getDisplayName(conversionLocale);
} else {
return "";
}
}
#Override
public Locale fromString(String s) {
return null;
}
};
}
public static void main(String[] args) {
launch(FXLocaleSelector.class, args);
}
}
Not entirely certain whether or not I understand your requirement correctly, my assumptions:
there's a ChoiceBox which contains the "language" for your ui, including the itself: lets say it contains the items Locale.ENGLISH and Locale.GERMAN, the visual representation of its items should be "English", "German" if its value is Locale.ENGLISH and "Englisch", "Deutsch" if its value is Locale.GERMAN
the visual representation is done by a StringConverter configurable with the value
If so, the solution is in separating out concerns - actually, it's not: the problem described (and hacked!) in the question is JDK-8088507: setting the converter doesn't update the selection of the menu items in the drop down. One hack is as bad or good as another, my personal preferenced would go for a custom skin which
adds a change listener to the converter property
reflectively calls updateSelection
Something like:
public static class MyChoiceBoxSkin<T> extends ChoiceBoxSkin<T> {
public MyChoiceBoxSkin(ChoiceBox<T> control) {
super(control);
registerChangeListener(control.converterProperty(), e -> {
// my local reflection helper, use your own
FXUtils.invokeMethod(ChoiceBoxSkin.class, this, "updateSelection");
});
}
}
Note: the hacks - this nor the OP's solution - do not solve the missing offset of the popup on first opening (initially or after selecting an item in the popup).
Not a solution to the question, just one way to have a value-dependent converter ;)
have a StringConverter with a fixed value (for simplicity) for conversion
have a converter controller having that a property with that value and a second property with a converter configured with the value: make sure the converter is replaced on change of the value
bind the controller's value to the box' value and the box' converter to the controller's converter
In (very raw) code:
public static class LanguageConverter<T> extends StringConverter<T> {
private T currentLanguage;
public LanguageConverter(T language) {
currentLanguage = language;
}
#Override
public String toString(T object) {
Object value = currentLanguage;
return "" + object + (value != null ? value : "");
}
#Override
public T fromString(String string) {
return null;
}
}
public static class LanguageController<T> {
private ObjectProperty<StringConverter<T>> currentConverter = new SimpleObjectProperty<>();
private ObjectProperty<T> currentValue = new SimpleObjectProperty<>() {
#Override
protected void invalidated() {
currentConverter.set(new LanguageConverter<>(get()));
}
};
}
Usage:
ChoiceBox<String> box = new ChoiceBox<>();
box.getItems().addAll("A", "B", "C");
box.getSelectionModel().selectFirst();
LanguageController<String> controller = new LanguageController<>();
controller.currentValue.bind(box.valueProperty());
box.converterProperty().bind(controller.currentConverter);
This is a complicated problem, and unfortunately a small SSCCE would not be possible. Therefore I have a long SCCE that demonstrates the problem. The simplified sample program uses a simplified data source - Time Zones. To use it, select a Time Zone in the table, then change the filter with the buttons at the top. Notice the text at the bottom changing to show the application selection. The undesired behavior is that when shifting to a filter that does not include the selected value, the selected value in the model is cleared. Surprisingly, when the selection is filtered out, the value is updated to being not set; but when filtered back in, the application selection is returned.
The Swing-based application's design is a Model-ViewModel-View, the Model is supposed to be the authoritative source for the application's data, including what the current selection is. The Model can have multiple ViewModel-Views displaying the data in the Model. This current selection may be reflected in multiple views. The user should be able to change the selection from any View. The selection may or may not be visible in all Views if it doesn't apply to some Views (a real-world example might be a View that shows vehicle maintenance may not show trips being taken by the vehicle).
The sample program has a JLabel as a simplified View-only of the application's selection, which displays at the bottom of the app the selection in the model.
The other more relevant View is a JTable that shows one Time Zone (as a String) per row. It has a custom ListSelectionModel as the ViewModel that forwards change requests to the application Model, and listens to changes from the application Model and applies them to the selection by calling methods on super. This works as expected, at least until filtering is applied.
The process of filtering is done mostly within the JTable and its inner classes, such as JTable$SortManager. It seems to remember and clear the selection, perform the sort and filter, and then restore the selection or nothing if the selected value is not in the newly filtered set.
Unfortunately, in the ListSelectionModel, these clearing and selecting operations are changing the underlying selection in the application Model. In my actual application, the selection loads a lot more information to display about the selection, and is a relatively expensive operation, so spurious changes to this value should be avoided.
So the question is this: Is there a way to prevent the application Model's selection from being changed when changing the table filter? I imagine the solution would fall under one of these categories:
There may be some way of detecting within the ListSelectionModel when the filter/sort is in progress, and not update the application model while that is happening
There may be something that can be overridden somewhere to change the undesired behavior
Here is the sample code:
import java.awt.BorderLayout;
import java.util.ArrayList;
import java.util.List;
import java.util.TimeZone;
import javax.swing.Box;
import javax.swing.DefaultListSelectionModel;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import javax.swing.RowFilter;
import javax.swing.SwingUtilities;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableModel;
import javax.swing.table.TableRowSorter;
public class TableProblem
extends JFrame
{
public static final class ApplicationModel
{
String[] data = TimeZone.getAvailableIDs();
public String[] getData() { return data; }
private String modelSelection;
public String getModelSelection() { return modelSelection; }
public void setModelSelection(String value) { modelSelection = value; fireModelSelectionChange(); }
private void fireModelSelectionChange()
{ selectionListeners.forEach(l -> l.modelSelectionChanged(modelSelection, findModelIndex(modelSelection))); }
private int findModelIndex(String value)
{
if (value != null)
for (int i = 0; i < data.length; i++)
if (value.equals(data[i]))
return i;
return -1;
}
private List<ApplicationModelSelectionListener> selectionListeners = new ArrayList<>();
public void addSelectionListener(ApplicationModelSelectionListener l) { selectionListeners.add(l); }
}
public interface ApplicationModelSelectionListener
{
void modelSelectionChanged(String selection, int selectedModelIndex);
}
/** This class acts as the selection ViewModel. The actual model is the
* passed-in ApplicationModel.
*/
public final class TimeZoneListSelectionModel
extends DefaultListSelectionModel
implements ApplicationModelSelectionListener
{
private final ApplicationModel appMdl;
private static final long serialVersionUID = 1L;
private TimeZoneListSelectionModel(ApplicationModel appMdl)
{
setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
this.appMdl = appMdl;
appMdl.addSelectionListener(this);
}
// Requests to ListSelectionModel to modify the selection are passed
// to the appMdl
#Override
public void clearSelection()
{
appMdl.setModelSelection(null);
}
#Override
public void setSelectionInterval(int index0, int index1)
{
int modelIdx = tbl.convertRowIndexToModel(index0);
String value = appMdl.getData()[modelIdx];
appMdl.setModelSelection(value);
}
#Override
public void addSelectionInterval(int index0, int index1)
{
int modelIdx = tbl.convertRowIndexToModel(index0);
String value = appMdl.getData()[modelIdx];
appMdl.setModelSelection(value);
}
// Notification from the app model about selection change gets
// percolated back to the user interface
#Override
public void modelSelectionChanged(String selection, int selectedModelIndex)
{
if (selectedModelIndex == -1)
{
super.clearSelection();
return;
}
int viewIndex = tbl.convertRowIndexToView(selectedModelIndex);
if (viewIndex == -1)
super.clearSelection();
else
super.setSelectionInterval(viewIndex, viewIndex);
}
}
public static final class TimeZoneTableModel
extends AbstractTableModel
{
private static final long serialVersionUID = 1L;
private final String[] data;
public TimeZoneTableModel(String[] data)
{
this.data = data;
}
#Override public int getRowCount() { return data.length; }
#Override public int getColumnCount() { return 1; }
#Override
public Object getValueAt(int rowIndex, int columnIndex)
{
if (columnIndex == 0)
return data[rowIndex];
throw new IllegalArgumentException("columnIndex="+columnIndex+" should be < 1");
}
#Override public String getColumnName(int column)
{ return "Time Zone"; }
}
private static final class StringRowFilter
extends RowFilter<TableModel, Integer>
{
private String prefix;
public void setPrefix(String value) { prefix = value; rowSorter.sort(); }
private final TableRowSorter<TableModel> rowSorter;
public StringRowFilter(TableRowSorter<TableModel> rowSorter)
{
this.rowSorter = rowSorter;
}
#Override
public boolean include(
Entry<? extends TableModel, ? extends Integer> entry)
{
if (prefix == null)
return true;
String lowerCase = entry.getStringValue(0).toLowerCase();
return lowerCase.startsWith(prefix);
}
}
private static final long serialVersionUID = 1L;
public static void main(String[] args)
{
ApplicationModel appMdl = new ApplicationModel();
SwingUtilities.invokeLater(() -> new TableProblem(appMdl).setVisible(true));
}
private final JTable tbl;
public TableProblem(ApplicationModel appMdl)
{
super("View-ModelView-Model Test");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
TimeZoneTableModel mdl = new TimeZoneTableModel(appMdl.getData());
tbl = new JTable(mdl);
tbl.setAutoCreateRowSorter(true);
TimeZoneListSelectionModel tzListSelectionModel = new TimeZoneListSelectionModel(appMdl);
tbl.setSelectionModel(tzListSelectionModel);
#SuppressWarnings("unchecked")
TableRowSorter<TableModel> rowSorter = (TableRowSorter<TableModel>)tbl.getRowSorter();
StringRowFilter filter = new StringRowFilter(rowSorter);
rowSorter.setRowFilter(filter);
Box filterButtons = createFilterButtons(filter);
Box vbox = Box.createVerticalBox();
vbox.add(filterButtons);
vbox.add(new JScrollPane(tbl));
JLabel mdlSelect = new JLabel("App Model selection: ");
appMdl.addSelectionListener((selection, selectedModelIndex) ->
mdlSelect.setText("App Model selection: " + selection + " (" +
selectedModelIndex + ")"));
vbox.add(mdlSelect);
add(vbox, BorderLayout.CENTER);
pack();
}
private static Box createFilterButtons(StringRowFilter filter)
{
Box filterButtons = Box.createHorizontalBox();
filterButtons.add(new JLabel("Filter: "));
for (String filterStr : "All,Africa,America,Antarctica,Asia,Australia,Canada,Europe,Pacific,Us".split(","))
addFilterButton(filter, filterButtons, filterStr);
return filterButtons;
}
private static void addFilterButton(StringRowFilter filter,
Box filterButtons, String buttonName)
{
String filterPrefix = "All".equals(buttonName) ? null : buttonName.toLowerCase();
JButton asiaButton = new JButton(buttonName);
asiaButton.addActionListener(ae -> filter.setPrefix(filterPrefix));
filterButtons.add(asiaButton);
}
}
A disappointing and unsatisfying solution to this problem is upon requesting a change of the filter value, before telling the JTable's RowSorter, clear out the currently selected item in the model.
This is a change in the behavior, where the selection gets cleared, but prevents spurious clearing and resetting of the value when clicking through the filter values.
The change to the example code would involve passing the selection model to the action of the filter button. The three methods that would change are below.
public TableProblem(ApplicationModel appMdl)
{
super("View-ModelView-Model Test");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
TimeZoneTableModel mdl = new TimeZoneTableModel(appMdl.getData());
tbl = new JTable(mdl);
tbl.setAutoCreateRowSorter(true);
TimeZoneListSelectionModel tzListSelectionModel = new TimeZoneListSelectionModel(appMdl);
tbl.setSelectionModel(tzListSelectionModel);
#SuppressWarnings("unchecked")
TableRowSorter<TableModel> rowSorter = (TableRowSorter<TableModel>)tbl.getRowSorter();
StringRowFilter filter = new StringRowFilter(rowSorter);
rowSorter.setRowFilter(filter);
Box filterButtons = createFilterButtons(filter, tzListSelectionModel);
Box vbox = Box.createVerticalBox();
vbox.add(filterButtons);
vbox.add(new JScrollPane(tbl));
JLabel mdlSelect = new JLabel("App Model selection: ");
appMdl.addSelectionListener((selection, selectedModelIndex) ->
mdlSelect.setText("App Model selection: " + selection + " (" +
selectedModelIndex + ")"));
vbox.add(mdlSelect);
add(vbox, BorderLayout.CENTER);
pack();
}
private static Box createFilterButtons(StringRowFilter filter,
TimeZoneListSelectionModel tzListSelectionModel)
{
Box filterButtons = Box.createHorizontalBox();
filterButtons.add(new JLabel("Filter: "));
for (String filterStr : "All,Africa,America,Antarctica,Asia,Australia,Canada,Europe,Pacific,Us".split(","))
addFilterButton(filter, filterButtons, filterStr, tzListSelectionModel);
return filterButtons;
}
private static void addFilterButton(StringRowFilter filter,
Box filterButtons, String buttonName,
TableProblem.TimeZoneListSelectionModel tzListSelectionModel)
{
String filterPrefix = "All".equals(buttonName) ? null : buttonName.toLowerCase();
JButton asiaButton = new JButton(buttonName);
asiaButton.addActionListener(ae -> {
tzListSelectionModel.clearSelection();
filter.setPrefix(filterPrefix);
});
filterButtons.add(asiaButton);
}
Notice I will not be marking this Answer as the solution because it is more of a workaround, and an actual solution to the problem is preferred.
Im trying to upgrade from PrimeFaces 3.5 to PrimeFaces 4.0, the line of code that its fine on version 3.5 is this one:
private MenuModel modelPrincipal;
private Menubar menuBar;
menuBar.getChildren().addAll(modelPrincipal.getContents());
But when I upgrade to version 4.0 I have to change it as follows:
private org.primefaces.model.menu.DefaultMenuModel modelPrincipal;
private Menubar menuBar;
menuBar.getChildren().addAll((Collection<? extends UIComponent>) modelPrincipal.getElements());
and it throws the Exception of title, do you guys know a workaround for it? I couldn't find anything on the documentation of the migration https://github.com/primefaces/primefaces/wiki/Migration-Guide
I also tried:
menuBar.getElements().addAll(modelPrincipal.getElements());
But gives me same Exception
Any help is appreciated
EDIT (Minimal (this is as minimal as I could make the code to make the error appear) Reproducible Example):
This is PrimeFaces 4.0:
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import javax.faces.component.UIComponent;
import org.primefaces.component.menubar.Menubar;
import org.primefaces.model.menu.DefaultMenuItem;
import org.primefaces.model.menu.DefaultSubMenu;
public class MenuExampleMB {
private org.primefaces.model.menu.DefaultMenuModel modelPrincipal;
private Menubar menuBar;
private HashMap<String, SubMenuItemObject> menuUrlCodigos;
public static void main(String[] args) {
MenuExampleMB menuExampleMB = new MenuExampleMB();
// MenuList
List<MenuObject> menuList = new ArrayList<>();
// SubMenuItemList
List<SubMenuItemObject> subMenuList = new ArrayList<>();
// Add 1 item to the SubMenuItemList
subMenuList.add(new SubMenuItemObject("1", "SubMenu", "an url"));
// MenuObject
MenuObject menu1 = new MenuObject("Menu 1");
// Set the SubMenu list for this MenuObject
menu1.setlSubmenus(subMenuList);
menuList.add(menu1);
// Call method that brings the Exception
menuExampleMB.loadMenu(menuList);
}
public void loadMenu(List<MenuObject> lMenus) {
menuUrlCodigos = new HashMap<String, SubMenuItemObject>();
modelPrincipal = new org.primefaces.model.menu.DefaultMenuModel();
menuBar = new Menubar();
// Go through the list of MenuObject and create each SubMenu and add them to
// DefaultMenuModel
for (MenuObject menu : lMenus) {
DefaultSubMenu subMenu = new DefaultSubMenu();
subMenu = agregarSubmenu(menu.getlSubmenus());
subMenu.setLabel(menu.getTitulo());
modelPrincipal.addElement(subMenu);
}
// This is the one that brings the exception.
menuBar.getChildren().addAll((Collection<? extends UIComponent>) modelPrincipal.getElements());
}
private DefaultSubMenu agregarSubmenu(List<SubMenuItemObject> lSubMenuUsuario) {
DefaultMenuItem menuItem = null;
DefaultSubMenu subMenuPadre = new DefaultSubMenu();
for (SubMenuItemObject subMenuItem : lSubMenuUsuario) {
// Ask if this submenu has sons
if (subMenuItem.getlSubmenus().size() > 0) {
DefaultSubMenu subMenuHijo = new DefaultSubMenu();
subMenuHijo.setLabel(subMenuItem.getTitulo());
/*
* Invoke this method with recursivity to get all the sons of this menu
*
*/
subMenuHijo.getElements().addAll(agregarSubmenu(subMenuItem.getlSubmenus()).getElements());
// Add sons to the father submenu.
subMenuPadre.getElements().add(subMenuHijo);
} else {
// This submenu doesn't have sons so its created as an unique son of the
// father submenu.
menuItem = agregarItem(subMenuItem);
subMenuPadre.getElements().add(menuItem);
subMenuPadre.setLabel(subMenuItem.getTitulo());
}
}
return subMenuPadre;
}
private DefaultMenuItem agregarItem(SubMenuItemObject pSubMenuItem) {
DefaultMenuItem menuItem = new DefaultMenuItem();
menuItem.setValue(pSubMenuItem.getTitulo());
menuItem.setUrl(pSubMenuItem.getUrl());
menuUrlCodigos.put(pSubMenuItem.getUrl(), pSubMenuItem);
return menuItem;
}
}
class MenuObject {
private String titulo;
private List<SubMenuItemObject> lSubmenus = new ArrayList<SubMenuItemObject>();
public MenuObject(String pTitulo) {
titulo = pTitulo;
}
public String getTitulo() {
return titulo;
}
public void setTitulo(String titulo) {
this.titulo = titulo;
}
public List<SubMenuItemObject> getlSubmenus() {
return lSubmenus;
}
public void setlSubmenus(List<SubMenuItemObject> lSubmenus) {
this.lSubmenus = lSubmenus;
}
}
class SubMenuItemObject {
private String codigo;
private String titulo;
private String url;
private List<String[]> lJerarquia;
private List<String> lTabs;
private List<SubMenuItemObject> lSubmenus = new ArrayList<SubMenuItemObject>();
public SubMenuItemObject(String pCodigo, String pTitulo, String pUrl) {
codigo = pCodigo;
titulo = pTitulo;
url = pUrl;
lJerarquia = new ArrayList<String[]>();
}
public SubMenuItemObject() {
}
public String getCodigo() {
return codigo;
}
public void setCodigo(String codigo) {
this.codigo = codigo;
}
public String getTitulo() {
return titulo;
}
public void setTitulo(String titulo) {
this.titulo = titulo;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public List<SubMenuItemObject> getlSubmenus() {
return lSubmenus;
}
public void setlSubmenus(List<SubMenuItemObject> lSubmenus) {
this.lSubmenus = lSubmenus;
}
public List<String[]> getlJerarquia() {
return lJerarquia;
}
public void setlJerarquia(List<String[]> lJerarquia) {
this.lJerarquia.clear();
this.lJerarquia.addAll(lJerarquia);
}
public List<String> getlTabs() {
return lTabs;
}
public void setlTabs(List<String> lTabs) {
this.lTabs = lTabs;
}
}
The classcast in
menuBar.getChildren().addAll((Collection<? extends UIComponent>) modelPrincipal.getElements());
is sort of logical since the org.primefaces.model.menu.DefaultMenuItem elements in the model are not in any way a UIComponent (you can see this by following the source up to the base class/interface). Even if you'd not have explicitly casted this it would have failed with the same error but on another line in another class.
Doing the
menuBar.getElements().addAll(modelPrincipal.getElements());
Fails for the same reason. If you'd inspected the code in the getElements() method in the menuBar, you'd have seen
public List getElements() {
MenuModel model = getModel();
if (model != null) {
return model.getElements();
}
else {
return getChildren();
}
}
You could have even debugged it and seen that the model was null and then the getChildren() would be returned, effectively ending up in the same calls as in your first attempt. But...
You'd also see the getModel() in there, maybe being a hint in where to look for the solution. The menuBar has a setModel(...) to where you can set the model you programmatically created. So the solution for this is
menuBar.setModel(modelPrincipal);
IF you create the menuBar programmatically.
But most often it is added in the xhtml via
<p:menu model="#{menuBean.model}" />
Some additional hints:
A search for "primefaces menu model 4.0 3.5" resulted in hits that would have given you help/hints (at least G00gle showed then to me)
typing 'menumodel' in the PrimeFaces 7 documentation would have given a hint to the dynamic menu part in the generic menu component, having a full complete example. https://primefaces.github.io/primefaces/7_0/#/components/menu
Having an IDE with code completion in java code or xhtml and check the api's would have shown the setModel method or the model attribute.
But I agree, a little more explanation in the migration document would have helped, but at the time (when at least I migrated from 3.5 to 4) this was mentioned in the forums etc... so it was 'actual' at the time
I am still new to Vaadin so, please bear with it.
We are currently migrating from Vaadin framework 8.0 to 8.3.2. One of the reasons of doing is that there's a requirement of using tree for the menu. Since 8.0 doesn't have tree, the workaround for generating a menu is by instantiating an inner Button class with the help of an Enum class in a loop (for user permission control):
public final class ValoMenuItemButton extends Button {
private static final String STYLE_SELECTED = "selected";
private final DashboardViewType view;
public ValoMenuItemButton(final DashboardViewType view) {
this.view = view;
setPrimaryStyleName("valo-menu-item");
setIcon(view.getIcon());
setCaption(view.getViewName().substring(0, 1).toUpperCase()
+ view.getViewName().substring(1));
DashboardEventBus.register(this);
addClickListener(new ClickListener() {
#Override
public void buttonClick(final ClickEvent event) {
UI.getCurrent().getNavigator()
.navigateTo(view.getViewName());
}
});
}
#Subscribe
public void postViewChange(final PostViewChangeEvent event) {
removeStyleName(STYLE_SELECTED);
if (event.getView() == view) {
addStyleName(STYLE_SELECTED);
}
}
}
The enum class structure is built in this manner:
AUDIT("Receipt Validation", RcptValidation.class, FontAwesome.BAR_CHART_O, false),
AUDIT1("Matching - Receipt not in SYCARDA", RcptNotInSycarda.class, FontAwesome.BAR_CHART_O, false),
AUDIT2("Matching - Receipt not in POS", RcptNotInPos.class, FontAwesome.BAR_CHART_O, false),
AUDIT3("Missing Sequence", MissSequence.class, FontAwesome.BAR_CHART_O, false),
AUDIT4("*Debug Purposes", LineAmtVsTotal.class, FontAwesome.BAR_CHART_O, false);
private DashboardViewType(final String viewName,
final Class<? extends View> viewClass, final Resource icon,
final boolean stateful) {
this.viewName = viewName;
this.viewClass = viewClass;
this.icon = icon;
this.stateful = stateful;
}
So far, I've not found any examples that are written around the v8 framework, while most of the sample code that I've seen are based on v7 framework.
I've attempted to write something like this, but the tree sub menus do not come out as it is (I've left out the expand and collapse event as that can be handled later).
My attempted code on the tree is this:
TreeData <String> treeData = new TreeData();
treeData.addRootItems("Dashboard","Sales","Sales Pattern","Top SKUs","Audit");
// The loop starts here (for DashboardViewType view: DashboardViewType.values)
if(enabled){
if(StringUtils.startsWith(view.getViewName(), "SALES")){
if (StringUtils.contains(view.getViewName(),"SALES_PATTERN")){
treeData.addItem( "Sales Pattern", view.getViewName());
}else{ treeData.addItem( "Sales", view.getViewName());
}
}else if (StringUtils.startsWith(view.getViewName(), "TOP_SKUS")){
treeData.addItem( "Top SKUs", view.getViewName());
}else if (StringUtils.startsWith(view.getViewName(), "AUDIT")){
treeData.addItem( "Audit", view.getViewName());
}else if (StringUtils.startsWith(view.getViewName(), "DASHBOARD")){
treeData.addItem( "Dashboard", view.getViewName());
}
DashboardEventBus.register(view);
}
// loop ends here
Tree<String> tree = new Tree<>("Sycarda Dashboard");
tree.setDataProvider(new TreeDataProvider<>(treeData));
tree.setItemIconGenerator(item -> { return FontAwesome.BAR_CHART_O; });
tree.expand("Sales","Sales Pattern","Top SKUs","Audit");
tree.addSelectionListener(e -> new Button.ClickListener() {
#Override public void buttonClick(Button.ClickEvent event) {
DashboardEventBus.register(event);
UI.getCurrent().getNavigator().navigateTo(event.getClass().getName());
}
});
This was posted originally at the Vaadin forum, but since there were no answers to that, I am putting it here. I'd appreciate if there's any input or another approach for this problem.
Thanks in advance.
In Vaadin 8 you can simply define the "get children" method when adding the data. In your case the enum class should provide some method like "getSubItems", which you could then set as the value provider. The following example shows it in a similar way, where "rootItems" is simply the same as your top level enum instances and MenuItem the same as your enumeration.
static {
rootItems = Arrays.asList(...);
}
#PostConstruct
private void init() {
Tree<MenuItem> tree = new Tree<>();
tree.setItems(rootItems, MenuItem::getSubItems);
}
private class MenuItem {
private String name;
private Resource icon;
private Collection<MenuItem> subItems;
public Collection<MenuItem> getSubItems() {
return subItems;
}
// ... other getter and constructor omitted;
}
Someone has shown an example and it is similar to what Stefan mentioned. In context with my requirement, the steps involved include:
Create a wrapper class that includes:
private DashboardViewType view;
private Resource icon;
private boolean stateful;
private Class<? extends View> viewClass;
private String viewName;
//Create the get / set methods for those attributes above
//Constructor for the wrapper class is below.
public TreeMenuItem(DashboardViewType view){
this.view = view;
}
For the Enum class additional main menu items are added. Default main class can be used since you can't put a null.
public enum DashboardViewType {
SALES("Sales",DashboardView.class,FontAwesome.HOME,false),
SALES_PATTERN("Sales Pattern",DashboardView.class,FontAwesome.HOME,false),
TOP_SKUs("Top SKUs",DashboardView.class,FontAwesome.HOME,false),
AUDIT("Audit",DashboardView.class,FontAwesome.HOME,false)
}
The tree is built in this manner:
private Component buildTree(){
Tree<TreeMenuItem> tree = new Tree<>();
TreeData<TreeMenuItem> treeData = new TreeData<>();
//This is for items that have no child.
TreeMenuItem dashboardItem = new TreeMenuItem(DashboardViewType.DASHBOARD);
dashboardItem.setIcon(VaadinIcons.HOME_O);
dashboardItem.setStateful(DashboardViewType.DASHBOARD.isStateful());
dashboardItem.setViewName(DashboardViewType.DASHBOARD.getViewName());
treeData.addItem(null, dashboardItem);
for (DashboardViewType type : DashboardViewType.values()) {
TreeMenuItem menuItem = new TreeMenuItem(type);
menuItem.setIcon(VaadinIcons.HOME_O);
menuItem.setViewName(type.getViewName());
menuItem.setStateful(false);
treeData.addItem(null, menuItem);
getSubMenuItems(type).forEach(subView -> {
TreeMenuItem subItem = new TreeMenuItem(subView);
subItem.setViewName(subView.getViewName().substring(0, 1).toUpperCase()
+ subView.getViewName().substring(1));
subItem.setIcon(subView.getIcon());
subItem.setStateful(subView.isStateful());
subItem.setView(subView);
subItem.setViewClass(subView.getViewClass());
treeData.addItem(menuItem, subItem);
});
}
}
tree.setDataProvider(new TreeDataProvider<>(treeData));
tree.setItemIconGenerator(TreeMenuItem::getIcon);
tree.setItemCaptionGenerator(TreeMenuItem::getViewName);
tree.addItemClickListener((Tree.ItemClick<TreeMenuItem> event) -> {
DashboardEventBus.register(event.getItem().getView()); UI.getCurrent().getNavigator().navigateTo(event.getItem().getViewName());
});
}
The logic to create subviews:
private List getSubMenuItems(DashboardViewType type) {
List<DashboardViewType> dashboardList;
switch(type){
case TOP_SKUs:
dashboardList = new LinkedList<>(Arrays.asList(DashboardViewType.TOP_SKUs1,
DashboardViewType.TOP_SKUs2,
DashboardViewType.TOP_SKUs3,
DashboardViewType.TOP_SKUs4));
filterByUserLevel(dashboardList,subACL4);
return dashboardList;
case AUDIT:
dashboardList = new LinkedList<>(Arrays.asList(DashboardViewType.AUDIT1,
DashboardViewType.AUDIT2,
DashboardViewType.AUDIT3,
DashboardViewType.AUDIT4,
DashboardViewType.AUDIT5));
filterByUserLevel(dashboardList,subACL5);
return dashboardList;
case DASHBOARD:
break;
default:
break;
}
return Collections.emptyList();
}
Add additional cases if required so. After that, the function controls remove the elements that are not part of the user level:
private List<DashboardType> filterByUserLevel(List<DashboardType>list, String u){
if(list.size() == subACL.length()){
for(int i=0; i<list.size(); i++){
if(StringUtils.substring(subACL, i, i+1).equalsIgnoreCase("0")){
list.remove(i);
}
}
Collections.sort(list);
return list;
//this removes unwanted sub-menu items according current user level.
}
}
so i have a table view with 3 columns and one of them is a column of comboboxes, the way i create the column of combobox is as so
Source = new TableColumn<>("Configure Interface as..");
Source.setCellValueFactory(i -> {
final StringProperty value = i.getValue().optionProperty();
// binding to constant value
return Bindings.createObjectBinding(() -> value);
});
Source.setCellFactory(col -> {
TableCell<TableViewTest, StringProperty> c = new TableCell<>();
ComboBox<String> comboBox = new ComboBox<>(options);
c.itemProperty().addListener((observable, oldValue, newValue) -> {
if (oldValue != null) {
comboBox.valueProperty().unbindBidirectional(oldValue);
}
if (newValue != null) {
comboBox.valueProperty().bindBidirectional(newValue);
}
});
c.graphicProperty().bind(Bindings.when(c.emptyProperty()).then((Node) null).otherwise(comboBox));
return c;
});
the column gets its values from the getter method optionProperty() which resides within my TableViewTest class.
So the problem i'm having is I have another combobox (comboBoxA) that is above my tableview table in my gui, and when ever i change the value of comboBoxA i want to change the values of the comboboxes with the column.
I can do this by calling the following code within the method that is listening for the selection change of comboboxA
Source.setCellValueFactory(i -> {
final StringProperty value = i.getValue().optionTwoProperty();
// binding to constant value
return Bindings.createObjectBinding(() -> value);
});
but the values don't change unless is start scrolling down to near the bottom of the table. is there a way to force the comboboxes to change to the new values within the getter method optionTwoProperty() without me having to scroll down?.
EDIT
Okay so the line
final StringProperty value = i.getValue().optionTwoProperty();
doesnt actaully get called until i start scrolling down.
So, with help from fabian, I think I understand that you want the combo box above the table to change the property in your model class that is represented in the cells in the table column.
One way to do this is to make the type of the combo box function that maps the model class to a property, and populate it with functions mapping to each of the properties you want.
Then you can represent the cell value factory for the table column with a binding that observes all the possible properties that could be represented, along with the selected value in the combo box, and returns the value computed by applying the function from the combo box to the model instance (and retrieving its wrapped value).
For the cell factory for the column, you can observe the selected value in the cell's combo box. When it changes, use the selected item in the combo box above the table to figure out which property to update.
Here's a SSCCE:
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.ListCell;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class TableWithSetAllComboBox extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Item> table = new TableView<>();
TableColumn<Item, String> itemCol = new TableColumn<>("Item");
itemCol.setCellValueFactory(cellData -> Bindings.createStringBinding(() -> cellData.getValue().getName()));
table.getColumns().add(itemCol);
TableColumn<Item, String> choiceCol = new TableColumn<>("Choice");
ComboBox<Function<Item, StringProperty>> option = new ComboBox<>();
option.getItems().add(Item::choiceProperty);
option.getItems().add(Item::choice2Property);
option.setCellFactory(lv -> createListCell());
option.setButtonCell(createListCell());
option.getSelectionModel().select(0);
ObservableList<String> choices = FXCollections.observableArrayList("First choice", "Second choice", "Third choice");
choiceCol.setCellFactory(col -> {
TableCell<Item, String> cell = new TableCell<>();
ComboBox<String> combo = new ComboBox<>(choices);
cell.graphicProperty().bind(Bindings.when(cell.emptyProperty()).then((Node)null).otherwise(combo));
combo.valueProperty().addListener((obs, oldValue, newValue) -> {
if (! cell.isEmpty() && newValue != null) {
Item item = table.getItems().get(cell.getIndex()) ;
StringProperty property = option.getValue().apply(item);
property.set(newValue);
}
});
cell.itemProperty().addListener((obs, oldItem, newItem) -> combo.setValue(newItem));
return cell ;
});
choiceCol.setPrefWidth(150);
table.getColumns().add(choiceCol);
choiceCol.setCellValueFactory(cellData -> Bindings.createStringBinding(
() -> option.getValue().apply(cellData.getValue()).get(),
cellData.getValue().choiceProperty(),
cellData.getValue().choice2Property(),
option.valueProperty()));
choiceCol.setGraphic(option);
choiceCol.setPrefWidth(200);
for (int i = 1; i <= 30 ; i++) table.getItems().add(new Item("Item "+i ,choices.get(0)));
Button debug = new Button("Debug");
debug.setOnAction(e -> table.getItems().stream().
map(item -> String.format("%s (%s, %s)", item.getName(), item.getChoice(), item.getChoice2())).
forEach(System.out::println));
BorderPane root = new BorderPane(table);
BorderPane.setMargin(debug, new Insets(5));
root.setBottom(debug);
primaryStage.setScene(new Scene(root, 600, 600));
primaryStage.show();
}
private ListCell<Function<Item, StringProperty>> createListCell() {
return new ListCell<Function<Item, StringProperty>>() {
#Override
public void updateItem(Function<Item, StringProperty> item, boolean empty) {
super.updateItem(item, empty);
setText(empty ? null : item.apply(new Item("", "")).getName());
}
};
}
public static class Item {
private final String name ;
private final StringProperty choice ;
private final StringProperty choice2 ;
public Item(String name, String choice) {
this.choice = new SimpleStringProperty(this, "Choice", choice);
this.choice2 = new SimpleStringProperty(this, "Choice 2", "Second choice");
this.name = name ;
}
public final StringProperty choiceProperty() {
return this.choice;
}
public final java.lang.String getChoice() {
return this.choiceProperty().get();
}
public final void setChoice(final java.lang.String choice) {
this.choiceProperty().set(choice);
}
public String getName() {
return name;
}
public final StringProperty choice2Property() {
return this.choice2;
}
public final java.lang.String getChoice2() {
return this.choice2Property().get();
}
public final void setChoice2(final java.lang.String choice2) {
this.choice2Property().set(choice2);
}
}
public static void main(String[] args) {
launch(args);
}
}
The issue is the TableView not listening to modifications of the cellValueFactory property of the elements of it's columns. Therefore the TableView doesn't know it should redraw it's cells. In JavaFX 8u60 the refresh() method was added for this purpose (for some reason I can't find it in the online javadoc though), which allows you to change the code of your method changing the cellValueFactory like this:
Source.setCellValueFactory(i -> {
final StringProperty value = i.getValue().optionTwoProperty();
// binding to constant value
return Bindings.createObjectBinding(() -> value);
});
tableview.refresh();
In older versions you have to use the workaround of setting the column value to trigger a change in the list:
List<TableColumn<TableViewTest, ?>> columns = tableview.getColumns();
columns.set(columns.indexOf(Source), Source);
But this workaround could cease to work in future versions, since the list is not actually modified with this operation and triggering a list change event is not required by the contract of ObservableList (but replacing the TableColumn with a new instance (and copying the properties) should always work).
Hard to say given the code snippets. Maybe you're not on the javaFX thread when doing the update? In that case use Platform.runLater(...), or share some minimal amout of code to reproduce the problem.