I want the context menu items of my natTable to change, depending on the selected row. How can I achieve this ?
UPDATE: this works so far, i have A and B showing on even/uneven lines. My next question is: does this mean that I have to call these methods for every menu item groups ?
myPopupMenuBuilder.withMenuItemProvider("A", new IMenuItemProvider() {
#Override
public void addMenuItem(NatTable paramNatTable, Menu paramMenu) {
MenuItem row = new MenuItem(paramMenu, SWT.PUSH);
row.setText("A");
}
}).withMenuItemProvider("B", new IMenuItemProvider() {
#Override
public void addMenuItem(NatTable paramNatTable, Menu paramMenu) {
MenuItem row = new MenuItem(paramMenu, SWT.PUSH);
row.setText("B");
}
}).withVisibleState("A", new IMenuItemState() {
#Override
public boolean isActive(NatEventData paramNatEventData) {
return paramNatEventData.getRowPosition() % 2 == 0;
}
}).withVisibleState("B", new IMenuItemState() {
#Override
public boolean isActive(NatEventData paramNatEventData) {
return paramNatEventData.getRowPosition() % 2 == 1;
}
});
It depends on how you implement the context menu. If you use the NatTable way of registering a context menu (which I typically suggest) you can use PopupMenuBuilder#withEnabledState() or PopupMenuBuilder#withVisibleState(). Inside the implementation of IMenuItemState you get the position where the click was performed, which then can be used to get the underlying data.
More information can be found in my blog post about that topic: NatTable context menus with Eclipse menus
If you want to use E4 menus, you will have to implement some additional logic to extract the necessary information from the click with regards to the NatTable.
Related
I'm using a Recycler View to show all the images from the galley or the external storage of a device in a Grid Layout Manager. And I'm using a Radio Button to show if the image is selected or not.
PROBLEM
Whenever I select or deselect a Radio Button from the visible Views in the Recycler View some other Views which are outside the Visible Screen got selected or deselected.
It is like I'm pressing on the same View of the Recycler View, but the images are different.
PROBLEM
well that's because of the recycler view concept of reusing the views instead of creating new views every time you scroll.
you see if you have 100 items you want to show in a recycler view and only 20 of them could appear to the user, recycler view creates only 20 view holder to represent the 20 items, whenever the user scroll recycler view will still have 20 view holder only but will just switch the data stored in this view holders rather than create new view holders.
now to handle selection of your items there's two ways to do this.
the naive way
hold selection in a boolean array inside the recycle view adapter.
whenever the user scrolls, the adapter calls onBindViewHolder to update the visible viewholder with the proper data.
so when onBindViewHolder gets called just set the radio button selection according the boolean array using the position sent in the method call
at the end of your usage to the recycler view you can create a getter method in the adapter to get the selection array list of boolean and pass the data based on it
public class PhotosGalleryAdapter extends RecyclerView.Adapter<PhotosGalleryViewHolder> {
ArrayList<Your_Data_ClassType> data;
ArrayList<Boolean> dataSelected ;
public PhotosGalleryAdapter(ArrayList<Your_Data_ClassType> data) {
this.data = data;
dataSelected = new ArrayList<>(data.size()) ;
}
...
#Override
public void onBindViewHolder(#NonNull PhotosGalleryViewHolder holder, int position) {
...
RadioButton radioButton = holder.getRadioButton()
radioButton.setChecked(dataSelected.get(position));
radioButton.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
#Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
dataSelected.set(holder.getAbsoluteAdapterPosition() , isChecked) ;
}
});
...
}
}
the other way is to use a selection tracker and it should be the correct way to handle selections in a recycler view.
the problem with this way is it needs a lot of editing to the code and creating new classes to include as parameters in the selection tracker, but in the end you'll find it worth the time you spent on it.
in order to start with this way you need to do the following :
firstly, decide what should be a key (String-Long-Parcelable) so the tracker should use to differentiate between your data , the safest way is either String or Parcelable as I once tried Long and ended up with lots and lots of problems (in your case I will assume it's the photo's uri which will be of type string)
secondly, you need to create two new classes, one that extends ItemDetailsLookup, and the other extends ItemKeyProvider, and should use the key as their generic type (the type that is put between <> )
your two classes should look like this (that you might copy them straight forward)
the one that extends ItemKeyProvider :
public class GalleryItemKeyProvider extends ItemKeyProvider<String>{
PhotosGalleryAdapter adapter ;
/**
* Creates a new provider with the given scope.
*
* #param scope Scope can't be changed at runtime.
*/
public GalleryItemKeyProvider(int scope,PhotosGalleryAdapter m_adapter) {
super(scope);
this.adapter = m_adapter;
}
#Nullable
#Override
public String getKey(int position) {
return adapter.getKey(position);
}
#Override
public int getPosition(#NonNull String key) {
return adapter.getPosition(key);
}
}
the one that extends ItemDetailsLookup :
public class GalleryDetailsLookup extends ItemDetailsLookup<String> {
private final RecyclerView recView ;
public GalleryDetailsLookup(RecyclerView m_recView){
this.recView = m_recView;
}
#Nullable
#Override
public ItemDetails<String> getItemDetails(#NonNull MotionEvent e) {
View view = recView.findChildViewUnder(e.getX(), e.getY());
if (view != null) {
RecyclerView.ViewHolder holder = recView.getChildViewHolder(view);
if (holder instanceof PhotosGalleryViewHolder) {
return ((PhotosGalleryViewHolder) holder).getItemDetails();
}
}
return null;
}
}
thirdly, you should include this new two methods in your adapter to be used by the above classes
public class PhotosGalleryAdapter extends RecyclerView.Adapter<PhotosGalleryViewHolder> {
...
public String getKey(int position) {
return data.get(position).getUri();
}
public int getPosition(String key) {
for (int i = 0; i < data.size(); i++) {
if (data.get(i).getUri() == key) return i;
}
return 0;
}
...
}
forthly (if there's an english word called forthly), you should initialize the tracker with all the above classes that were created before and he will handle the rest, the tracker takes as parameters
a unique selection tracker id (if that will be the only selection tracker you will use then name it anything)
the ItemKeyProvider that we created
the DetailsLookup that we created
a String-Long-Parcelable Storage to store the keys that were selected in (in our case it will be a String Storage)
a Selection predicate, it's responsible to handle the way of selection you want to do, you want it to be able to (select only one item-multiple selection with no limits- based on a weird algorithm like even only or odd only), in my case I will use a default multiple selection one but if you want to alter it with another selection algorithm you should create a new class that extends SelectionPredicates and implement your way of selection, you could also just check the other default ones might be what you're looking for.
anyway, that's how the initialization should look (you should put this code wherever you initialize your recycler view at whether it's in fragment or activity method):
private void initRecycleView() {
...
SelectionTracker<String> tracker = new SelectionTracker.Builder<>("PhotosGallerySelection",
Your_Recycler_View,
new GalleryItemKeyProvider(ItemKeyProvider.SCOPE_MAPPED, photosAdapter),
new GalleryDetailsLookup(Your_Recycler_View),
StorageStrategy.createStringStorage())
.withSelectionPredicate(SelectionPredicates.createSelectAnything())
.build();
...
}
I didn't find a way to let me initialize the adapter with data and then create the tracker inorder to make the viewholders know about their selection or not, so in this case I firstly created the tracker and then made the adapter know about it's data using a setter and notifyDataSetChanged
what I mean by that is after creating the tracker instantly set the tracker and data to the adapter, so the initRecycleView should look like this
private void initRecycleView() {
...
SelectionTracker<String> tracker = new SelectionTracker.Builder<>("PhotosGallerySelection",
Your_Recycler_View,
new GalleryItemKeyProvider(ItemKeyProvider.SCOPE_MAPPED, photosAdapter),
new GalleryDetailsLookup(Your_Recycler_View),
StorageStrategy.createStringStorage())
.withSelectionPredicate(SelectionPredicates.createSelectAnything())
.build();
photosAdapter.setTracker(tracker);
photosAdapter.setData(data);
photosAdapter.notifyDataSetChanged();
...
}
Last but no least, you should handle how the view holders should know if they were selected or not, so you should let the adapter know about the tracker and its data by creating a setter method in it, that's how the adapter should look like in the end :
public class PhotosGalleryAdapter extends RecyclerView.Adapter<PhotosGalleryViewHolder> {
ArrayList<Your_Data_Class> data;
private SelectionTracker<String> tracker;
public PhotosGalleryAdapter() {
data = new ArrayList<>();
}
public ArrayList<Your_Data_Class> getData() {
return data;
}
public void setData(ArrayList<Your_Data_Class> m_data) {
this.data = m_data;
}
#Override
public ScheduleViewHolder onCreateViewHolder(#NonNull ViewGroup parent, int viewType) {
...
}
#Override
public void onBindViewHolder(#NonNull PhotosGalleryViewHolder holder, int position) {
...
boolean isSelected = tracker.isSelected(data.get(i).getUri());
RadioButton radioButton = holder.getRadioButton;
radioButton.setChecked(isSelected);
}
#Override
public int getItemCount() {
return data.size();
}
public String getKey(int position) {
return data.get(position).getUri();
}
public int getPosition(String key) {
for (int i = 0; i < data.size(); i++) {
if (data.get(i).getUri() == key) return i;
}
return 0;
}
public void setTracker(SelectionTracker<String> m_tracker) {
this.tracker = m_tracker;
}
}
(as you may notice if you initialized the adapter with its data through the constructor, when he asks the tracker if there were an item selected or not, it will result in a NullPointerException as at the moment of initializing the adapter you still didn't initialize the tracker)
that way you could keep track of your selection the way google suggests in their documentation (which I honestly don't know why the made it very complicate like that).
if you want to know all the selected item in the end of your application/fragment use, you should call tracker.getSelection() which will return a Selection List for you to iterate on
There's a tiny problem/feature with the tracker that it won't start selecting the first item until you use a long press on it, that happens only in the first item you select, if you do want this feature (start selecting mode by long press) then leave it as it is
incase you don't want it you can make the tracker select a ghost key (any unique string key that means nothing to your data) at the beginning which should later enable the selection mode with a simple click on any photo
tracker.select("");
this also the way to make a default/old selection at the beginning, you could make a for loop and call tracker.select(Key) if you do want the tracker to start with few items being selected
N.B : incase you use the Ghost Key method you should watchout that the selection array that will get returned when you call tracker.getSelection() will also contain this Ghost Key.
at the end if you do have the curiosity of reading about selection tracker in the documentation follow this link
or maybe if you know how to read kotlin follow this two links
implementing-selection-in-recyclerview
a guide to recyclerview selection
I was stuck in the selection problem for days before I figure how to do all that so I hope you find your way through it.
Omar Shawky has covered the solutions.
With my answer I will stress on the reason why someone may face this sort of an issues with recycler views and how to avoid this common issue in the future (avoiding pitfalls).
Reason:
This issue happens because RecyclerView recycles views. So a RecyclerView item's view once inflated can get reused to show another off screen (to be scrolled to) item. This helps reduces re-inflation of views which otherwise can be taxing.
So if the radio button of an item's view is selected, and the same view gets reused to show some other item, then that new item can also have a selected radio button.
Solution:
The simplest solution for such issues is to have an if else logic in your ViewHolder to provide logic for both selected and de-selected cases. We also do not rely on information from radio button itself for initial setup (we do not use radioButton.isSelected() at the time of setup)
e.g code to write inside your ViewHolder class:
private boolean isRadioButtonChecked = false; // ViewHolder class level variable. Default value is unchecked
// Now while binding in your ViewHolder class:
// Setup Radio button (assuming there is just one radio button for a recyclerView item).
// Handle both selected and de-selected cases like below (code can be simplified but elaborating for understanding):
if (isRadioButtonChecked) {
radioButton.setChecked(true);
} else {
radioButton.setChecked(false);
}
radioButton.setOnCheckedChangeListener(
(radioButton, isChecked) -> isRadioButtonChecked = isChecked);
Do not do any of the following while setting up:
private boolean isRadioButtonChecked = false; // class variable
//while binding do not only handle select case. We should handle both cases.
if (isRadioButtonChecked) { // --> Pitfall
radioButton.setChecked(true);
}
radioButton.setOnCheckedChangeListener((radioButton, isChecked) -> isRadioButtonChecked = isChecked);
OR
// During initial setup do not use radio button itself to get information.
if (radioButton.isChecked()) { // --> Pitfall
radioButton.setChecked();
}
I am working on an editor plugin for Eclipse that handles my own script language. In the editor, I have a hover that shows short information about element under the mouse cursor.
Now, I am trying to create a toolbar on the bottom of the hover and place a button there that will open a more detailed description online.
I have written my code based on answer to that question. The button is visible and it works when it is clicked.
However, it disappears a short time after I move my mouse over the hover. Why is this happening and how can I prevent that?
Here is the relevant part of my code:
#Override
public IInformationControlCreator getHoverControlCreator() {
return new IInformationControlCreator() {
#Override
public IInformationControl createInformationControl(final Shell parent) {
ToolBarManager tbm = new ToolBarManager(SWT.FLAT);
DefaultInformationControl defaultInformationControl = new DefaultInformationControl(parent, tbm);
Action action = new Action() {
#Override
public void run() {
MessageDialog.openInformation(parent, "omg", "It works.");
}
};
action.setText("123 test 321");
Bundle bundle = FrameworkUtil.getBundle(this.getClass());
URL url = FileLocator.find(bundle, new Path("icons/test.gif"), null);
action.setImageDescriptor(ImageDescriptor.createFromURL(url));
tbm.add(action);
tbm.update(true);
return defaultInformationControl;
}
};
}
When hover is created with DefaultInformationControl(parent, tbm) then toolbar is visible. However when you move mouse over the hover, then it gains focus. Then method getInformationPresenterControlCreator() from DefaultInformationControl is called.
It looks like (from source code):
public IInformationControlCreator getInformationPresenterControlCreator() {
return new IInformationControlCreator() {
/*
* #see org.eclipse.jface.text.IInformationControlCreator#createInformationControl(org.eclipse.swt.widgets.Shell)
*/
public IInformationControl createInformationControl(Shell parent) {
return new DefaultInformationControl(parent,
(ToolBarManager) null, fPresenter);
}
};
}
Look at return line. It nulls your Toolbar manager. That is the reason is gone.
Quick solution might be to create a new class which extends DefaultInformationControl and then in overrides
#Override
public IInformationControlCreator getInformationPresenterControlCreator() {
return new YourOwnInformationControlCreator();
}
This way you can pass correct ToolbarManager.
I have a ListView that I am working to add a ContextMenu to. I have the ContextMenu working find but have another issue.
My setCellFactory code, used to setup the context menus:
lvAppetites.setCellFactory(lv -> {
ListCell<Appetite> cell = new ListCell<>();
ContextMenu contextMenu = new ContextMenu();
MenuItem editAppetiteMenu = new MenuItem();
editAppetiteMenu.textProperty().bind(Bindings.format("Edit ..."));
editAppetiteMenu.setOnAction(event -> {
// Code to load the editor window
editAppetite(cell.getItem());
});
contextMenu.getItems().add(editAppetiteMenu);
MenuItem deleteAppetiteMenu = new MenuItem();
deleteAppetiteMenu.textProperty().bind(Bindings.format("Delete ..."));
deleteAppetiteMenu.setOnAction(event -> {
// Code to delete the appetite
});
contextMenu.getItems().add(deleteAppetiteMenu);
contextMenu.getItems().add(new SeparatorMenuItem());
MenuItem addAppetiteMenu = new MenuItem();
addAppetiteMenu.textProperty().bind(Bindings.format("Add New ..."));
addAppetiteMenu.setOnAction(event -> {
// Code to delete the appetite
});
contextMenu.getItems().add(addAppetiteMenu);
cell.textProperty().bind(cell.itemProperty().asString());
// If nothing selected, remove the context menu
cell.emptyProperty().addListener((obs, wasEmpty, isNowEmpty) -> {
if (isNowEmpty) {
cell.setContextMenu(null);
} else {
cell.setContextMenu(contextMenu);
}
});
return cell;
});
My ListView is searchable through a TextField with a listener; the listener filters the items in the ListView as the user types.
The problem now, is that as the list is filtered, any empty cells now display null.
From reading another question, I'm fairly confident that the ListView is still displaying a graphic for the removed cells. I know how to handle that within the ListView by overriding the updateItem method, but how would I handle this from within my setCellFactory method instead?
Is that even possible or will I need to refactor my entire ListView?
Thank you, as always, for all your help!
The problem arises from the line
cell.textProperty().bind(cell.itemProperty().asString());
When the cell is empty, the item will be null, so the binding will (I believe) evaluate to the string "null".
Try something that tests for the cell being empty or the item being null, e.g.
cell.textProperty().bind(Bindings
.when(cell.emptyProperty())
.then("")
.otherwise(cell.itemProperty().asString()));
or (thanks to #fabian for refining this version)
cell.textProperty().bind(Bindings.createStringBinding(
() -> Objects.toString(cell.getItem(), ""),
cell.itemProperty()));
I load a listview with a list (a).
In response to a toggle change, I switch the listview content to another list (b).
If list (a) is longer than list (b), then the excess items remain displayed in the listview even though it is no longer selectable.
For instance, buyList = {'100532','100533'} and saleList = {'100000'}. If I start with radioBuy selected, the list will show '100532' and '100533'. Switching the toggle from radioBuy to radioSale will cause the list to display '100000' and '100533'. '100533' is not selectable, but it remains visible.
The observable lists themselves have been shown to be the correct size and contain the correct items. It's just that the ListView display shows some old data.
Things I've tried:
1. ticketListView.getItems().clear() and then rebuilding a new list before the list switch.
2. Using ticketListView.getItems().removeAll(0, ticketListView.getItems().size())
3. Using a loop to remove each item individually.
4. Using ticketListView.refresh() in combination with all the above.
My toggle ChangeListener:
typeGroup.selectedToggleProperty().addListener(new ChangeListener<Toggle>() {
public void changed(ObservableValue<? extends Toggle> observable, Toggle oldValue, Toggle newValue) {
if (typeGroup.getSelectedToggle().equals(radioBuy))
ticketListView.setItems(buyList);
if (typeGroup.getSelectedToggle().equals(radioTransfer))
ticketListView.setItems(transList);
if (typeGroup.getSelectedToggle().equals(radioSale))
ticketListView.setItems(saleList);
}
});
How do I get my listview to blank itself before it switches content?
Sorry for wasting everyone's time. It turns out I was indeed using a custom cell factory:
ticketListView.setCellFactory(new Callback<ListView<Header>,
ListCell<Header>>() {
public ListCell<Header> call(ListView<Header> headerListView) {
return new ListCell<Header>() {
#Override
protected void updateItem(Header item, boolean empty) {
super.updateItem(item, empty);
if (item != null) {
setText(item.getTicket());
}
}
};
}
}
);
Removing this code and doing as I mentioned earlier, overriding toString, solved the problem. Obviously I need to study cell factories a little more...
if (item != null) {
setText(item.getTicket());
} else {
setText(null);
}
You should add this code to clear empty cells.
This is my problem:
https://youtu.be/k-N5uthYhYw
and this is my onBindViewHolder() method.
// Replace the contents of a view (invoked by the layout manager)
#Override
public void onBindViewHolder(final ViewHolder holder, final int position) {
// - get element from your dataset at this position
// - replace the contents of the view with that element
holder.specName.setText(specList.get(position).getSpecName());
// Assign a tag number to later identify what radio-button
holder.specRadioBtn.setTag(new Integer(position));
/* Event listenr for longClick - we prob. won't use it, but it's here just in case */
holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
#Override
public boolean onLongClick(View v) {
Toast.makeText(context, "Long press", Toast.LENGTH_SHORT).show();
return false;
}
});
/* Little hack to select its Radio Button when a specific row is tapped */
holder.itemView.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
// Turn rowSelectedFlag to true since the user selected this row
rowSelectedFlag = true;
// When the user taps on a row select that row's radio button
holder.specRadioBtn.setChecked(true);
// I'm not sure why, but locally the interface needs to be started by pointing it
// to where it should drive the data (to send the params)
tempInterface = new AdminUserSpecialty();
// Call the interface to send the data (row spec-name and id) back to AdminUserSpecialty
tempInterface.activateSpecSelect(specList.get(position).getSpecName().toString(),
specList.get(position).getSpecId().toString(), rowSelectedFlag);
int clickedPos = ((Integer) holder.specRadioBtn.getTag());
// Check if the radio button is already selected
if (holder.specRadioBtn.isChecked()) {
if (lastCheckedBtn != null) {
// Don't deselect if user taps on the same row several times
if (lastCheckedBtn == holder.specRadioBtn) {
// do nothing
}
// Otherwise do deselect the previously selected radio button
else {
lastCheckedBtn.setChecked(false);
}
}
lastCheckedBtn = holder.specRadioBtn;
lastCheckedPos = clickedPos;
}
// If radio is not checked set the lastCheckedBtn to null (reset counter)
else {
lastCheckedBtn = null;
}
}
});
/* ----------------------------------------------------------------------*/
}
I can't seem to preserve my radio-button selection on RecyclerView scroll. On scroll the selection becomes erratic and random. I understand that one of RecyclerView's features is to recycle rows as they leave the screen, but what do I need to do to keep my selection? Thanks much.
I know that this was answered already but if some of you are still looking for an easier answer and your application does not rely on the RecyclerView view recycling feature much (for example if you have a fixed size list of items...) you can always set your recycler view cache view size. That way it would not recycler your views hence it would not recycler the views and you will avoid copy selected values to another views...
yourRecyclerView..setItemViewCacheSize(yourItemList.size());
Save the checked / unchecked status of the radio button (you should use checkbox instead if you want to allow the user to select multiple items) to your model (i.e. your items in the list should have a field for this) when the onClick event happens. When you bind the ViewHolder, make sure you set checkbox's value to whatever you saved in your model.
It's happen because of the Recycling mechanism
(PS: its the same for the ListView or RecyclerView).
To fix that:
1) Add a booelan variable to your model to save the state of the RadioButton
2) Update your RadioButton state in onBindViewHolder() method from this boolean in the model.
3) Add setOnCheckedChangeListener() to your RadioButton to listen to his state (checked/unchecked) and to update the boolean in your model when the state changes.