5

I've been fighting with this problem for quite some time. I'm working on a file copy manager module, so far I have been able to make everything work perfectly with the exception of the cancel button. For some reason when I click the cancel button for a specific row, the button action targets several rows simultaneously.

File Copy Manager

After a couple of days of researching the issue I was able to make the object cancel the operation represented by the row successfully using:

-(IBAction)btnCancelOperationClick:(id)sender {
    NSInteger row = [_tableView rowForView:sender];
    if (row != -1) {
        FileCopyOperation *opr = [_fileCopyOperations objectAtIndex:row];
        [opr cancel];
        [_fileCopyOperations removeObjectAtIndex:row];
        [_tableView removeRowsAtIndexes:[NSIndexSet indexSetWithIndex:row] withAnimation:NSTableViewAnimationEffectFade];
    }
}

This works nicely, I'm able to cancel the operation and update my table accordingly. Everything else works as intended but there has to be something wrong with my code or bindings. I'm loading this cell from a nib, then I register this nib using:

[_tableView registerNib:[[NSNib alloc]initWithNibNamed:@"FileCopyCell" bundle:nil] forIdentifier:@"FileCopyCell"];

I made the QueueController the File's Owner and hooked everything like this:

Bindings

I would greatly appreciate if someone could point me in the right direction to get this thing working properly. Thanks in advance!

Edit to add more code sample.

-(NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row {
    FileCopyCell *cell = [tableView makeViewWithIdentifier:@"FileCopyCell" owner:self];
    FileCopyOperation *opr = [_fileCopyOperations objectAtIndex:row];

    [cell.fileName setStringValue:[NSString stringWithFormat:@"Copying \"%@\"",opr.fName]];
    [cell.progressBar setDoubleValue:((opr.bWritten.doubleValue / opr.fSize.doubleValue) * 100)];
    [cell.totalBytes setStringValue:[NSString stringWithFormat:@"of %@",[NSByteCountFormatter stringFromByteCount:opr.fSize.longLongValue countStyle:NSByteCountFormatterCountStyleFile]]];
    [cell.status setStringValue:[NSString stringWithFormat:@"%@",[NSByteCountFormatter stringFromByteCount:opr.bWritten.longLongValue countStyle:NSByteCountFormatterCountStyleFile]]];
    [cell.icon setImage:[[NSWorkspace sharedWorkspace]iconForFile:opr.srcURL.path]];
    [cell.cancelButton setTarget:self];
    return cell;
}

-(void)windowDidLoad {
    [super windowDidLoad];
    _fileCopyOperations = [NSMutableArray new];
    windowFrame = [self.window frame];
    rows = 0;

    [_tableView registerNib:[[NSNib alloc]initWithNibNamed:@"FileCopyCell" bundle:nil] forIdentifier:@"FileCopyCell"];

    if (!fileCopyManager) {
        fileCopyManager = [FileCopyManager sharedFileCopyManager];
        [fileCopyManager.fileCopyQueue addObserver:self forKeyPath:@"operationCount" options:NSKeyValueObservingOptionNew context:(void*)fileCopyManager];
    }

    [_scrollView setHasHorizontalScroller:NO];
    [_scrollView setHasVerticalScroller:NO];
}
larod
  • 435
  • 3
  • 11
  • 2
    I don't understand the nature of the problem. If I understand correctly, it's that "when I click the cancel button for a specific row, the button action targets several rows simultaneously". What does that mean exactly? Do multiple buttons highlight as you click? (Your screenshot sort of makes it look that way.) Is `-btnCancelOperationClick:` called multiple times? With what `sender`? Is the table definitely in the same order as `_fileCopyOperations` (no sort interposed between the array and the table contents)? – Ken Thomases Apr 16 '15 at 19:26
  • I am also confused. The code excerpt works without issues, or doesn't? For target-action, it looks fine. *With* the code, things are not working? – stevesliva Apr 16 '15 at 20:56
  • @KenThomases Yes, multiple buttons get highlighted as I click but -btnCancelOperationClick: is called only once and operates on the correct operation. Right now the problem is a cosmetic one, everything works properly but when i click on the cancel button of a specific row, the button is highlighted in the others. Yes, The table is in the same order as _fileOperations. – larod Apr 16 '15 at 21:14
  • 1
    @stevesliva Yes the code works but when I click on the cancel button it cancels the correct operation but the cancel button is highlighted on all the other rows as well. – larod Apr 16 '15 at 21:18
  • Does your table view delegate attempt to customize the view in `-tableView:viewForTableColumn:row:`, `-tableView:rowViewForRow:`, or `-tableView:didAddRowView:forRow:`? If so, show that code. Does `QueueController` implement `-awakeFromNib`? Show that code, too. (It will be called multiple times, which may make it do something wrong.) Likewise, show the code for any custom row view or cell view class that tries to configure things or touches the Cancel button. – Ken Thomases Apr 16 '15 at 22:54
  • @KenThomases I added the methods above, I didn't implement `-tableView:rowViewForRow:` or `tableView:didAddRowView:forRow:`. My QueueController class is a subclass of NSWindowController, I added my `-windowDidLoad` code above. – larod Apr 17 '15 at 03:10
  • I don't see anything wrong in the code. This is very strange. I'm not even sure how you would get this behavior if you wanted to. Are you manipulating the GUI (not necessarily the Cancel button) from background threads? If you're using bindings, that includes modifying properties that the GUI is bound to. – Ken Thomases Apr 17 '15 at 03:59
  • @KenThomases Is strange indeed, I'm not using bindings at all, I'm hooking everything up programatically. I'm thinking on trying to re-create the cell in the tableview nib instead of loading it from an external nib to see if I get a different result. I've been studying Apple's tableviewplayground but haven't found anything there to help me solve this problem. – larod Apr 17 '15 at 12:05

2 Answers2

5

The best approach is to subclass NSTableCellView and let it handle its own actions and represented object. For example, a cell representing a Foo instance could have two properties: foo and fooController. When the (nonatomic) foo setter is called, the cell can update its own UI to represent the passed Foo. When the Foo controller creates the table cell, it can instantiate a FooCell instance, set itself as fooController and assign the Foo instance and let the cell handle itself. The cancel button's target can be its own cell, and when the -cancel: action is called, it can tell its fooController what to do (since the Foo controller is responsible for updating the queue and the table view) and since it has a reference to its foo, it can pass that to the controller via some -cancelFoo:(Foo *)theFoo method without relying on the controller to look up its index (which may not be accurate if you're animating rows appearing and disappearing or the user is rapidly canceling a bunch in a row but their removal is delayed and updated asynchronously).

Nice and clean. Neat and organized containment / separation of responsibilities. The cell handles its own UI updating and actions and knows its own foo; the foo controller handles its foo collection, its table view, and the assignment of foos to foo cells.

Joshua Nozzi
  • 60,946
  • 14
  • 140
  • 135
  • Thanks for the suggestion, I will rewrite the app in this manner. Like you said, the code should be contained and separated to avoid these hiccups. – larod Apr 17 '15 at 14:01
  • I think the cancel button confusion is that you've set the controller to be File's Owner and it may be conflicting with the File's Owner class set in the cell's own xib itself. – Joshua Nozzi Apr 17 '15 at 14:30
  • Yes, that could be the problem, I find it hard to access the `[FileOperation cancel]` from the cellview nib. I have file operations running, I want to be able to cancel the file operation represented by the row. The user will hit the cancel button and that should send a message to the FileOperation and call the cancel method. I don't seem to find a way to access the controller if it's not through File's Owner. I'll try to make it work without accessing the QueueController through File's Owner. – larod Apr 17 '15 at 15:01
  • How can I setup a nstablecellview to handle its own actions and how can i set up it's represented object? Forward class declaration? Could you provide or point me to an example of how could one go about it? – larod Apr 17 '15 at 18:23
1

Thanks to Joshua Nozzi, following his recommendation, I moved the button action from the controller to the cell class. In the cell class I used the method below to access the represented object and send the [operation cancel] message.

-(IBAction)cancelOpr:(id)sender {
    NSButton *button = (NSButton*)sender;
    FileCopyOperation *opr = [(NSTableCellView*)[button superview]objectValue];
    [opr cancel];
    // This code calls the controller [removeObject:] method that takes care of cleaning everything out and updates the GUI accordingly.
    AppDelegate *ad = (AppDelegate*)[[NSApplication sharedApplication]delegate];
    QueueWindowController *qc = [ad getQueueController];
    [qc.fileCopyOperations performSelectorOnMainThread:@selector(removeObject:) withObject:opr waitUntilDone:YES];
}

You can get the full project here.

enter image description here

larod
  • 435
  • 3
  • 11