// // ServerTableController.m // p4scout // // Created by Work on 8/30/08. // Copyright 2008 Perforce Software, Inc. All rights reserved. // #import "ServerTableController.h" #import "ServerDetailViewController.h" #import "P4ConnectionEditor.h" #import "P4Server.h" #import "ServerTableCell.h" #import "p4scoutAppDelegate.h" #import "UIToolbarAdditions.h" #import "P4DefaultsKeys.h" #define CellActivityViewTag 1 // Uncomment this to update the suggested label dynamically when entering // server information in the form. //#define SHOW_SUGGESTED_LABEL static const int ActivityButtonBarItemPosition = 2; enum eTableSections { SavedServers = 0 , FoundServers , NumberOfSections }; @interface ServerTableController () -(void)initMemberVariables; -(UITableViewCell*)visibleCellForServer:(P4Server*)server; -(void)configureCell:(UITableViewCell *)cell withServer:(P4Server *)server; -(void)updateRefreshButton; -(int)reachableServerCount; -(void)addActiveServer:(P4Server*)server; -(void)removeActiveServer:(P4Server*)server; -(void)stopObservingServer:(P4Server*)server; -(void)startObservingServer:(P4Server*)server; @end @implementation ServerTableController @synthesize servers=_servers, tableView=_tableView, browsing; @synthesize refreshButton; -(id)initWithNibName:(NSString *)nibName bundle:(NSBundle *)nibBundle { // Override initWithStyle: if you create the controller programmatically and want to perform customization that is not appropriate for viewDidLoad. if (!(self = [super initWithNibName:nibName bundle:nibBundle])) return self; [self initMemberVariables]; return self; } - (id)initWithCoder:(NSCoder *)decoder { // Override initWithStyle: if you create the controller programmatically and want to perform customization that is not appropriate for viewDidLoad. if (!(self = [super initWithCoder:decoder])) return self; [self initMemberVariables]; return self; } -(void)initMemberVariables { _servers = [[NSMutableArray arrayWithCapacity:1] retain]; foundServers = [[NSMutableArray arrayWithCapacity:1] retain]; serverBrowser = [[NSNetServiceBrowser alloc] init]; serverBrowser.delegate = self; [serverBrowser scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; } - (void)dealloc { for ( P4Server * server in _servers ) { [self stopObservingServer:server]; } for ( P4Server * server in foundServers ) { [self stopObservingServer:server]; } [_servers release]; [foundServers release]; [_tableView release]; [serverBrowser removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; [serverBrowser release]; [super dealloc]; } // Implement viewDidLoad to do additional setup after loading the view. - (void)viewDidLoad { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidFinishLaunching:) name:UIApplicationDidFinishLaunchingNotification object:nil]; NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; [defaults addObserver:self forKeyPath:kServerDiscoveryPreferenceKey options:NSKeyValueObservingOptionNew context:nil]; [defaults addObserver:self forKeyPath:kDefaultUserPreferenceKey options:NSKeyValueObservingOptionNew context:nil]; [super viewDidLoad]; } - (void)viewDidUnload { [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidFinishLaunchingNotification object:nil]; NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; [defaults removeObserver:self forKeyPath:kServerDiscoveryPreferenceKey]; [defaults removeObserver:self forKeyPath:kDefaultUserPreferenceKey]; } -(void)applicationDidFinishLaunching:(NSNotification *)notification { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; self.browsing = [defaults boolForKey:kServerDiscoveryPreferenceKey]; } #pragma mark NSNetService methods -(void)netService:(NSNetService *)netService didNotResolve:(NSDictionary *)errorDict { for (P4Server * server in foundServers) { if (![server.label isEqualToString:netService.name]) continue; [self stopObservingServer:server]; [foundServers removeObject:server]; [_tableView reloadData]; break; } [netService stop]; [netService removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; [netService release]; } -(void)netServiceDidResolveAddress:(NSNetService *)netService { for (P4Server * server in foundServers) { if (![server.label isEqualToString:netService.name]) continue; server.p4port = [NSString stringWithFormat:@"%@:%d", netService.hostName, netService.port]; [server refresh:self]; break; } [netService stop]; [netService removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; [netService release]; } -(void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser didFindService:(NSNetService *)netService moreComing:(BOOL)moreServicesComing { P4Server * server = [[[P4Server alloc] initAsDiscovered:YES] autorelease]; server.label = netService.name; // we'll set the p4port value after we've resolved the address to avoid further // lookups by the client API // server.p4port = netService.name; [netService scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode]; netService.delegate = self; [netService retain]; [netService resolveWithTimeout:60]; [self startObservingServer:server]; [foundServers addObject:server]; if (!moreServicesComing ) { [_tableView reloadData]; [self updateRefreshButton]; } } -(void)netServiceBrowser:(NSNetServiceBrowser *)netServiceBrowser didRemoveService:(NSNetService *)netService moreComing:(BOOL)moreServicesComing { for (P4Server * server in foundServers) { if ([server.label isEqualToString:netService.name]) { [self stopObservingServer:server]; [foundServers removeObject:server]; break; } } if ( !moreServicesComing ) { [_tableView reloadData]; [self updateRefreshButton]; } } -(NSArray*)serverListForSection:(int)section { switch (section) { case SavedServers: return _servers; break; case FoundServers: return foundServers; break; default: NSAssert(false, @"Should never hit default condition!"); return 0; break; } } -(P4Server*)serverForIndexPath:(NSIndexPath*)indexPath { NSArray * serverList = [self serverListForSection:indexPath.section]; P4Server * s = [serverList objectAtIndex:indexPath.row]; return s; } - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { if ( foundServers.count ) return NumberOfSections; return 1; } -(void)startObservingServer:(P4Server*)server { [server addObserver:self forKeyPath:InfoPropertyKey options:NSKeyValueObservingOptionNew context:NULL]; [server addObserver:self forKeyPath:ConnectionStatePropertyKey options:NSKeyValueObservingOptionNew context:NULL]; [server addObserver:self forKeyPath:ActivePropertyKey options:NSKeyValueObservingOptionNew context:NULL]; [server addObserver:self forKeyPath:ReachablePropertyKey options:NSKeyValueObservingOptionNew context:NULL]; [server addObserver:self forKeyPath:LabelPropertyKey options:NSKeyValueObservingOptionNew context:NULL]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(serverErrorReceived:) name:P4ServerErrorReceivedNotification object:server]; } -(void)stopObservingServer:(P4Server*)s { [s removeObserver:self forKeyPath:ConnectionStatePropertyKey]; [s removeObserver:self forKeyPath:InfoPropertyKey]; [s removeObserver:self forKeyPath:ActivePropertyKey]; [s removeObserver:self forKeyPath:ReachablePropertyKey]; [s removeObserver:self forKeyPath:LabelPropertyKey]; [[NSNotificationCenter defaultCenter] removeObserver:self name:P4ServerErrorReceivedNotification object:s]; } -(void)serverErrorReceived:(NSNotification*)errorNotification { UITableViewCell *cell = [self visibleCellForServer:[errorNotification object]]; if ( cell ) [self configureCell:cell withServer:[errorNotification object]]; } -(void)removeServer:(P4Server *)s { if ( s == nil ) return; if ( [_servers containsObject:s] || [foundServers containsObject:s] ) { [self stopObservingServer:s]; if ( s.active ) [self removeActiveServer:s]; if ( [_servers containsObject:s] ) { NSIndexPath * indexPath = [NSIndexPath indexPathForRow:[_servers indexOfObject:s] inSection:SavedServers]; [_servers removeObject:s]; [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationRight]; } else if ( [foundServers containsObject:s] ) { NSIndexPath * indexPath = [NSIndexPath indexPathForRow:[foundServers indexOfObject:s] inSection:FoundServers]; [foundServers removeObject:s]; if ( [foundServers count] == 0 ) { [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:FoundServers] withRowAnimation:UITableViewRowAnimationNone]; } else { [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationNone]; } } [self updateRefreshButton]; } } -(void)addServer:(P4Server *)s { if ( s == nil ) return; if ( [_servers containsObject:s] ) return; if ( s.active ) [self addActiveServer:s]; [self startObservingServer:s]; [_servers addObject:s]; [self.tableView reloadData]; [self updateRefreshButton]; } -(void)setServers:(NSArray *)s { if ( s == _servers ) return; for ( P4Server * server in _servers ) { [self stopObservingServer:server]; if ( server.active ) [self removeActiveServer:server]; } [_servers release]; _servers = nil; if (!s) return; _servers = [[NSMutableArray arrayWithArray:s] retain]; for ( P4Server * server in _servers ) { [self startObservingServer:server]; if ( server.active ) [self addActiveServer:server]; } [self.tableView reloadData]; [self updateRefreshButton]; } -(UITableViewCell*)visibleCellForServer:(P4Server*)server { NSArray *visibleCells = self.tableView.visibleCells; NSArray *indexPaths = self.tableView.indexPathsForVisibleRows; if ([visibleCells count] == 0) return nil; if ([indexPaths count] == 0) return nil; int i = 0; for (UITableViewCell *cell in visibleCells) { NSIndexPath * indexPath = [indexPaths objectAtIndex:i++]; P4Server * searchedServer = [self serverForIndexPath:indexPath]; if ( searchedServer == server) { return cell; } } return nil; } -(NSString *)nameForServer:(P4Server *)server { NSString * name = server.label; #ifdef SHOW_SUGGESTED_LABEL if ( name == nil && server.connectionState == Online ) name = [server.info objectForKey:@"serverName"]; if (name == nil && server.connectionState == Online) name = [server.info objectForKey:@"serverAddress"]; #endif if (name == nil) name = server.p4port; if (name == nil) name = @"perforce:1666"; return name; } -(void)configureCell:(UITableViewCell *)c withServer:(P4Server *)s { ServerTableCell * cell = (ServerTableCell*)c; UIActivityIndicatorView * activityView = cell.activityView; UIImageView * imageView = cell.imageView; UILabel * serverLabel = cell.serverLabel; serverLabel.text = [self nameForServer:s]; if ( activityView ) { if ( s.active ) [activityView startAnimating]; else [activityView stopAnimating]; } switch ( s.connectionState ) { case Connecting: case Online: imageView.image = [UIImage imageNamed:@"StatusOnline.png"]; break; case Offline: imageView.image = [UIImage imageNamed:@"StatusOffline.png"]; break; case Idle: break; default: NSAssert(NO, @"Unrecognized connectionState"); break; } if (s.hasAlert ) imageView.image = [UIImage imageNamed:@"StatusAlert.png"]; if (!s.reachable ) { imageView.image = [UIImage imageNamed:@"StatusUnreachable.png"]; } } -(void)addActiveServer:(P4Server*)server { numberOfActiveServers++; if ( numberOfActiveServers == 1 ) { [toolbar insertItem:activityIndicatorViewItem inItemsAtIndex:ActivityButtonBarItemPosition animated:NO]; [activityIndicatorView startAnimating]; statusLabel.textAlignment = UITextAlignmentLeft; statusLabel.text = @"Updating..."; } } -(void)removeActiveServer:(P4Server*)server { numberOfActiveServers--; if ( numberOfActiveServers == 0 ) { [activityIndicatorView stopAnimating]; [toolbar removeItemFromItemsAtIndex:ActivityButtonBarItemPosition animated:NO]; statusLabel.textAlignment = UITextAlignmentCenter; NSDateFormatter * dateFormatter = [[[NSDateFormatter alloc] init] autorelease]; [dateFormatter setDateStyle:NSDateFormatterShortStyle]; [dateFormatter setTimeStyle:NSDateFormatterShortStyle]; statusLabel.text = [NSString stringWithFormat:@"Last update: %@", [dateFormatter stringFromDate:[NSDate date]]]; } } -(void)setBrowsing:(BOOL)b { if ( browsing == b ) return; browsing = b; if ( browsing ) { [serverBrowser searchForServicesOfType:@"_p4._tcp" inDomain:@"local"]; } else { [serverBrowser stop]; NSArray * array = [NSArray arrayWithArray:foundServers]; for ( P4Server * s in array ) [self removeServer:s]; [_tableView reloadData]; } } -(void)observeValueForKeyPath:(NSString *)key ofObject:(id)object change:(NSDictionary *)dict context:(void *)context { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; if ( object == defaults ) { if ( [key isEqualToString:kServerDiscoveryPreferenceKey] ) { self.browsing = [defaults boolForKey:kServerDiscoveryPreferenceKey]; } else if ( [key isEqualToString:kDefaultUserPreferenceKey] ) { [self refresh:self]; } } if ( [key isEqualToString:ActivePropertyKey] ) { BOOL isActive = [[dict objectForKey:NSKeyValueChangeNewKey] boolValue]; if ( isActive ) [self addActiveServer:object]; else [self removeActiveServer:object]; } if ( [key isEqualToString:ReachablePropertyKey] ) { [self updateRefreshButton]; } UITableViewCell *cell = [self visibleCellForServer:object]; if ( cell ) [self configureCell:cell withServer:object]; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [[self serverListForSection:section] count]; } - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { if ( foundServers.count == 0 ) return nil; switch (section) { case SavedServers: return @"Saved"; break; case FoundServers: return @"Discovered"; break; default: NSAssert( false, @"Should never hit default condition" ); return nil; break; }; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { NSAssert ( indexPath.section < NumberOfSections, @"More sections than defined" ); static NSString *CellIdentifier = @"ServerTableCell"; ServerTableCell *cell = (ServerTableCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { NSArray * a = [[NSBundle mainBundle] loadNibNamed:@"ServerTableViewCell" owner:self options:nil]; cell = (ServerTableCell *)[a lastObject]; } if (cell == nil) { CGRect frame = CGRectMake(0, 0, 300, 44); cell = [[[UITableViewCell alloc] initWithFrame:frame reuseIdentifier:CellIdentifier] autorelease]; cell.imageView.image = [UIImage imageNamed:@"StatusUnreachable.png"]; cell.showsReorderControl = true; UIActivityIndicatorView * av = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; av.tag = CellActivityViewTag; av.hidesWhenStopped; [cell.contentView addSubview:av]; av.hidden = true; [av release]; } // Configure the cell P4Server * s = [self serverForIndexPath:indexPath]; [self configureCell:cell withServer:s]; return cell; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [tableView deselectRowAtIndexPath:indexPath animated:YES]; if (self.editing) return; ServerDetailViewController * detail = [[ServerDetailViewController alloc] initWithNibName:@"ServerDetail" bundle:nil]; detail.server = [self serverForIndexPath:indexPath]; [self.navigationController pushViewController:detail animated:YES]; [detail release]; } - (BOOL)tableView:(UITableView*)tableView canEditRowAtIndexPath:(NSIndexPath*)indexPath { return ( indexPath.section == SavedServers ); } - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if ( editingStyle != UITableViewCellEditingStyleDelete ) return; if ( indexPath.section != SavedServers ) return; [self removeServer:[_servers objectAtIndex:indexPath.row]]; } /* - (void)viewWillAppear:(BOOL)animated { [super viewWillAppear:animated]; } */ /* - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; } */ /* - (void)viewWillDisappear:(BOOL)animated { } */ /* - (void)viewDidDisappear:(BOOL)animated { } */ /* - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; } */ #pragma mark - #pragma mark actions -(IBAction)cancel:(id)sender { [self.navigationController dismissModalViewControllerAnimated:YES]; [self setEditing:NO animated:YES]; } - (IBAction)save:(id)sender { [self.navigationController dismissModalViewControllerAnimated:YES]; if (connectionEditorController.isNewServer) { [connectionEditorController.server refresh:self]; [self addServer:connectionEditorController.server]; } // Dismiss the modal view to return to the main list [self setEditing:NO animated:YES]; } -(int)reachableServerCount { int serverCount = 0; for ( P4Server * s in _servers ) if (s.reachable) serverCount++; for ( P4Server * s in foundServers ) if (s.reachable) serverCount++; return serverCount; } -(void)updateRefreshButton { self.refreshButton.enabled = ([self reachableServerCount] > 0); } -(IBAction)refresh:(id)sender { for ( P4Server * s in _servers ) [s refresh:self]; for ( P4Server * s in foundServers ) [s refresh:self]; } -(IBAction)add:(id)sender { connectionEditorController.server = [[[P4Server alloc] init] autorelease]; connectionEditorController.title = @"New Server"; connectionEditorController.isNewServer = true; [self.navigationController presentModalViewController:serverAddDialogController animated:YES]; } @end