Using CheckBoxes to manage selection in an NSTableView - checkbox

I have the classic setup of an NSTableView with the columns bound to various keyPaths of the arrangedObjects of an NSArrayController, and the NSArrayController bound to an array of dictionaries.
With this setup, selecting one or multiple rows in the tableview works automatically. The table view and array controller work together and it's easy to query the NSArrayController to get the list of selected objects.
One of my table columns contains NSButtonCells, the checkbox kind. Is there a way to use Cocoa Bindings to bind the checkbox in each row to that row's selection state ? I know that I could add another value to the NSDictionary representing each row, but that would duplicate the selection information that is already available in NSArrayController.
If it is necessary to do that, would also appreciate a quick sketch of your implementation.
Thanks

So, the answer to this is not for the faint of heart. The reason is you are trying to get NSTableView to do something it doesn't naturally want to do.
Start out by using cocoa's native NSTableView multiple selection behavior:
- clicking on a row selects it and deselects other rows
- holding control and clicking on a column toggles the selection state of that row only
Now, add a column of checkboxes. For this row, the rules are different:
- clicking on a checkbox toggles the selection state of that row only
This would be easy if we could capture clicks to the checkboxes and process them ourselves. Now we can, but the problem is that after we process them, they still get forwarded on to the NSTableView, altering the selection in the usual way. [Note: there may be some way to avoid this forwarding - if you know of one, please let me know]
So here's how you can (finally) accomplished this:
- add a "selectedInView" field to each object in the underlying object array. Add an observer to the associated NSArrayController for the keyPath: "selectedObjects". When the selection changes, set the selectedInView field accordingly for each object. Something like this:
if([keyPath isEqualToString:#"selectedObjects"]) {
// For table view checkbox's: keep "selectedInView" of song dictionaries up to date
[_arrayController.arrangedObjects enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
BOOL sel = [_arrayController.selectedObjects containsObject:obj];
if([[obj objectForKey:#"selectedInView"] boolValue] != sel)[obj setValue:[NSNumber numberWithBool:sel] forKey:#"selectedInView"];
}];
Now comes the tricky part: the only time the checkboxes malfunction are when there is already a selection present. Here are the types of cases:
Setup: Row's 1,2,3 are selected. Checkbox clicked on row 4.
Result: Checkbox on row four is selected. Row four is selected. Row's 1,2,3 are deselected (because that's what NSTableView does naturally)
To solve this, whenever a checkbox is clicked you need to create a temporary array to remember the current selection, plus or minus the checkbox that just got clicked:
- (void)tableView:(NSTableView *)tableView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row {
if([tableColumn.identifier isEqualToString:#"CheckBox"]) {
NSMutableDictionary *song = [_arrayController.arrangedObjects objectAtIndex:row];
if(!_tempSelectedSongs && _arrayController.selectedObjects) _tempSelectedSongs = [[NSMutableArray arrayWithArray:_arrayController.selectedObjects] retain];
if(_tempSelectedSongs) {
if([_tempSelectedSongs containsObject:song]) {
[_tempSelectedSongs removeObject:song];
} else if(![_tempSelectedSongs containsObject:song]) {
[_tempSelectedSongs addObject:song];
}
}
}
}
Now after the tableview has done it's selection processing, we want to set the selection to what it should be. There is a promising looking function that allows you to modify the tableview selection BEFORE it actually does the selecting. You can modify it like so:
- (NSIndexSet *)tableView:(NSTableView *)tableView selectionIndexesForProposedSelection:(NSIndexSet *)proposedSelectionIndexes {
NSMutableIndexSet *newSet = [NSMutableIndexSet indexSet];
if(_tempSelectedSongs) {
NSMutableIndexSet *indexSet = [NSMutableIndexSet indexSet];
[_tempSelectedSongs enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSUInteger index = [_arrayController.arrangedObjects indexOfObject:obj];
if(index != NSNotFound) [indexSet addIndex:index];
}];
proposedSelectionIndexes = indexSet;
[_tempSelectedSongs release]; _tempSelectedSongs = nil; [_tempSelectedSongsTimer invalidate]; [_tempSelectedSongsTimer release]; _tempSelectedSongsTimer = nil;
}
[proposedSelectionIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) {
NSProgressIndicator *progress = ((BDDiscreteProgressCell *)[[_arrayController.arrangedObjects objectAtIndex:idx] objectForKey:#"BDCell"]).progress;
if(!progress)
[newSet addIndex:idx];
}];
return newSet;
}
This works great, however there is a problem with the order in which the NSTableView delegate functions are called. Obviously we need the first function - where we setup the temporary array - to be called BEFORE the second function - where we use the information.
For whatever reason, it turns out that when you DE-select a checkbox, this is how things work,
but when you SELECT a checkbox, the opposite occurs. So for this case, you can add some more code to your above keyPath observer:
if([keyPath isEqualToString:#"selectedObjects"]) {
if(_tempSelectedSongs) {
_arrayController.selectedObjects = _tempSelectedSongs;
[_tempSelectedSongs release]; _tempSelectedSongs = nil;
}
// For table view checkbox's: keep "selectedInView" of song dictionaries up to date
[_arrayController.arrangedObjects enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
BOOL sel = [_arrayController.selectedObjects containsObject:obj];
if([[obj objectForKey:#"selectedInView"] boolValue] != sel)[obj setValue:[NSNumber numberWithBool:sel] forKey:#"selectedInView"];
}];
}
Edit: turns out there is an additional case: if a single row is selected and it's checkbox is "unclicked," this does not automatically trigger a selectedObjects notification, so you must run a function on a timer to implement the new selection.

Related

How to not duplicate same item in a list dart?

I have created a listView and button and when I click the button it adds an item to listView.
The problem is I don't want actually to repeat the same item in the list.
I've tried the .contains method but it didn't work.
I want a good solution please,
There are different ways to achieve this:
1) Iterate the list and check if every element doesn't have the
properties you consider equal:
items = [Item(id: 1), Item(id: 2)];
newItem = Item(id: 2);
if (items.every((item) => item.id != newItem.id)) {
items.add(newItem);
}
2) Use contains() and override == operator (and override hashCode too)
in the object class with the properties you consider equal.
items = [Item(id: 1), Item(id: 2)];
newItem = Item(id: 2);
if (!items.contains(newItem)) {
items.add(newItem);
}
// inside Item class
#override
bool operator ==(other) {
return this.id == other.id;
}
#override
int get hashCode => id.hashCode;
3) Instead of List use Set, where each element can occur only once. Its default implementation is LinkedHashSet that keeps track of the order.
Instead of List, Use Set.
void main() {
Set<String> currencies = {'EUR', 'USD', 'JPY'};
currencies.add('EUR');
currencies.add('USD');
currencies.add('INR');
print(currencies);
}
output: {EUR, USD, JPY, INR} // unique items only
Reference: Set<E> class
Check if the List already contains the element before you add it:
https://api.flutter.dev/flutter/dart-core/List-class.html
if(!List.contains(element) { add }
The contains method checks for equality, not for reference, so it must work as long as you compare a similar element. If your code isn't working, please provide it to us. Thanks.
If your list contains custom objects you may need to override the equality operator in the custom class.
You could also use a Set instead of a List.

getting values of all ticked items in checkboxes

Let us suppose that I have three checkboxes and each checkbox has certain items to be checked say Item 1 ,Item 2,Item 3 which has value 'a' ,'b','c'.So if we click on item1,item2 and mark them ,we get an array with values a and b .Now if we click item3,we would get c .Again if we go to another checkbox now and click on another item in another checkbox,we would get another array which holds the values for the items we marked in second checkbox .
I need to store all marked items in an array and display it .The problem is to get all the ticked items for all the different checkboxes.
will the following perhaps set you in the right direction?
var checkboxValues = [];
var checkboxes = document.querySelectorAll('input[type="checkbox"]');
for (var i = 0; i < checkboxes.length; i++) {
if (checkboxes[i].checked) {
checkboxValues.push(checkboxes[i].value);
}
}
console.log('checkboxValues', checkboxValues);

Preserve selection in angular ui-grid while updating data

http://plnkr.co/edit/r9hMZk?p=preview
I have a ui-grid where I have enabled multi selection. I want to be able to update the data whilst preserving the selection. If I just update the data then the selection is not preserved.
$scope.gridOpts.data = data2;
However I have defined a rowIdentity function, so that the id column uniquely identifies a row.
"rowIdentity" : function(row) {
return row.id;
}
Now if I select rows with id=Bob and id=Lorraine, then update the data, the rows are still selected. However the other fields in those rows are not updated.
How can I both preserve the selection and update all the data?
I think you need to keep track of you IDs yourself. So, you should remove the rowIdentifier, and instead add this piece at the beginning of your swapData function.
$scope.selIds = [];
for (var selRow of $scope.gridApi.selection.getSelectedRows()) {
$scope.selIds.push(selRow.id);
}
In addition to that, add an event handler on rowsRendered to re-select the previously selected rows
gridApi.core.on.rowsRendered($scope,function() {
for (var selId of $scope.selIds) {
for (var row of $scope.gridOpts.data) {
if (selId == row.id) {
$scope.gridApi.selection.selectRow(row);
}
}
}
});
You can put this in your registerApi callback.

Programmatically change grid column order

I want to sort the columns in my grid, just like the rows. I have made a simple sort function that is called from an actioncolumn handler:
sortColumns:function(record) { // The record after which's values the columns are ordered
var columns = this.columns;
Ext.Array.sort(columns,function(col1,col2) {
if(record.get(col1.dataIndex) > record.get(col2.dataIndex)) return 1;
if(record.get(col1.dataIndex) < record.get(col2.dataIndex)) return -1;
if(col1.dataIndex > col2.dataIndex) return 1;
if(col1.dataIndex < col2.dataIndex) return 1;
throw new Error("Comparing column with itself shouldn't happen.");
});
this.setColumns(columns);
});
The setColumns line now throws the error
Cannot add destroyed item 'gridcolumn-1595' to Container 'headercontainer-1598'
which is because the "old" columns are destroyed first, and then the "new" columns, which are the same and thus destroyed, are applied.
I only want to change the order, but I didn't find any function to do it. Do you know how to do it?
Drag-drop ordering of the columns works, so it is doable; but I don't find the source code where sencha did implement that drag-drop thingy. Do you know where to look for that code?
Reconfigure method needs two arguments
grid.reconfigure(store, columns)
Here is the fiddle that changes the columns programatically https://fiddle.sencha.com/#fiddle/17bk
I have found that columns are items of the grid's headerCt, so the following works well, and unlike the other answers, it does not create new column components, keeping the column state and everything:
var headerCt = normalGrid.headerCt,
columns = headerCt.items.getRange();
Ext.Array.sort(columns,function(col1,col2) {
if(record.get(col1.dataIndex) < record.get(col2.dataIndex)) return -1;
if(record.get(col1.dataIndex) > record.get(col2.dataIndex)) return 1;
if(col1.dataIndex < col2.dataIndex) return -1;
if(col1.dataIndex > col2.dataIndex) return 1;
return 0;
});
headerCt.suspendLayouts();
for(var i=0;i<columns.length;i++)
{
headerCt.moveAfter(columns[i],(columns[i-1] || null));
}
headerCt.resumeLayouts(true);
There is a reconfigure method which can be used to achieve reordering, e.g:
grid.reconfigure(columns);
Check the this.
I couldn't manage to do it without storing columns in a custom field and using reconfigure, maybe someone can suggest something better (reconfigure doesn't work well with just regular columns field it seems):
Ext.define('MyGrid', {
extend: 'Ext.grid.Panel',
//just renamed "columns"
myColumnConfigs: [
//all your column configs
]
});
//to rearrange inside controller, also need to call it on grid render
var grid = this.getView();
var columns = grid.myColumnConfigs;
//...do your sorting on columns array
grid.reconfigure(columns);

How to count number of rows in sencha gridview?

I have a Gridview on my page and I'm using buffered store. Is there a way to get the visible number of row count. Thank you
Here is a sample code that you can try: (I hope you'll get some idea from this)
// The below condition has to be checked for each record
// record: record instance
var me = this; // grid scope
Ext.Array.each(me.columns, function (item) { // iterate through each column in the grid
if (item.hidden || !item.dataIndex) { // you can avoid hidden columns and one's that re not bound to the store
return;
}
var cellVal;
try {
cellVal = Ext.fly( me.view.getCell(record, item)).select('cell selector class').elements[0].innerHTML;
} catch (e) {
// handle if you want
}
if (!Ext.isEmpty(cellVal)) {
// this record has been rendered
}
}, this);
This will get you all the records that are rendered. Since you are using a bufferedRenderer, this will also return the records that are rendered but not in the view, you can check and put an offset for the buffer.
Note: I've a similar logic in working in ExtJs 5 but haven't tested in touch.

Resources