// // MainWindowController.m // Perforce // // Created by Adam Czubernat on 09.05.2013. // Copyright (c) 2013 Perforce Software, Inc. All rights reserved. // #import "MainWindowController.h" #import "DebugWindowController.h" #import "LoginPanelController.h" #import "WorkspacePanelController.h" #import "SubmitPanelController.h" #import "SyncPanelController.h" #import "LoadingPanelController.h" #import "SidebarViewController.h" #import "BrowserViewController.h" #import "ColumnViewController.h" #import "IconViewController.h" #import "P4Workspace.h" #import "P4Reachability.h" #define PANE_DEFAULT_WIDTH 200.0f #define PANE_MIN_WIDTH 150.0f #define PATH_DEFAULT_HEIGHT 40.0f @interface MainWindowController () { NSString *launchURL; NSMutableArray *redoStack; NSMutableArray *undoStack; SidebarViewController *sidebarController; BrowserViewController *browserController; LoginPanelController *loginPanel; DebugWindowController *debugController; P4ItemAction *pendingAction; LoadingPanelController *pendingActionPanel; // Subviews __weak IBOutlet PSView *_topBar; __weak IBOutlet PSSplitView *_mainSplitView; __weak IBOutlet NSView *_leftPane; __weak IBOutlet PSSplitView *_rightSplitView; __weak IBOutlet NSView *_rightTopPane; __weak IBOutlet NSView *_rightBottonPane; __weak IBOutlet PSView *_pathSeparator; __weak IBOutlet NSPathControl *_pathControl; __weak IBOutlet NSView *_networkWarning; // Top Bar buttons __weak IBOutlet NSButton *_backButton; __weak IBOutlet NSButton *_nextButton; __weak IBOutlet NSButton *_addButton; __weak IBOutlet NSButton *_createDirectoryButton; __weak IBOutlet NSButton *_checkInAllButton; __weak IBOutlet NSButton *_viewColumnButton; __weak IBOutlet NSButton *_viewIconButton; __weak IBOutlet NSButton *_syncButton; __weak IBOutlet PSActivityIndicator *_syncButtonIndicator; __weak IBOutlet NSSearchField *_searchField; // Search __weak NSView *_searchBar; } - (void)changePath:(NSString *)path; - (void)next; - (void)back; - (void)clear; - (void)updateNavigationButtons; - (void)showNetworkWarning:(BOOL)show animated:(BOOL)animated; - (void)setBrowserController:(BrowserViewController *)browser; - (void)syncWorkspace; - (void)submitFiles:(NSArray *)paths; - (void)addFiles; - (void)createDirectory; - (void)syncStartedNotification:(NSNotification *)notification; - (void)syncFinishedNotification:(NSNotification *)notification; - (void)submitFinishedNotification:(NSNotification *)notification; - (void)sessionExpiredNotification:(NSNotification *)notification; - (void)networkReachabilityNotification:(NSNotification *)notification; - (void)pathControlDoubleClick:(id)sender; - (IBAction)backButtonPressed:(id)sender; - (IBAction)nextButtonPressed:(id)sender; - (IBAction)addButtonPressed:(id)sender; - (IBAction)createDirectoryButtonPressed:(id)sender; - (IBAction)checkInAllButtonPressed:(id)sender; - (IBAction)viewColumnButtonPressed:(id)sender; - (IBAction)viewIconButtonPressed:(id)sender; - (IBAction)syncButtonPressed:(id)sender; - (IBAction)searchAction:(id)sender; @end @implementation MainWindowController - (id)init { return self = [super initWithWindowNibName:NSStringFromClass([self class])]; } - (void)awakeFromNib { [super awakeFromNib]; [self.window setFrameUsingName:@"MainWindow"]; [self.window setFrameAutosaveName:@"MainWindow"]; sidebarController = [[SidebarViewController alloc] init]; sidebarController.delegate = self; [_mainSplitView setDelegate:self]; [_mainSplitView replaceSubview:_leftPane with:sidebarController.view]; [_mainSplitView adjustSubviews]; [_mainSplitView setPosition:PANE_DEFAULT_WIDTH ofDividerAtIndex:0]; // Custom appearance of vertical divider [_mainSplitView setDividerWidth:0.0f]; [_mainSplitView setHidesDivider:YES]; [_pathSeparator setImage:[NSImage imageNamed:@"SeparatorVerticalDark.png"]]; [_pathSeparator setContentMode:PSViewContentModeCenter]; [_rightSplitView setDelegate:self]; [_rightSplitView setPosition:(_rightSplitView.bounds.size.height - 1.0f - PATH_DEFAULT_HEIGHT) ofDividerAtIndex:0]; [_rightSplitView adjustSubviews]; [_pathControl setFocusRingType:NSFocusRingTypeNone]; [_pathControl setPathComponentCells:@[]]; [_pathControl setDoubleAction:@selector(pathControlDoubleClick:)]; [_pathControl setTarget:self]; [_pathControl setDelegate:self]; // Config _topBar.backgroundColor = [NSUserDefaults colorForKey:kColorBackgroundTopBar]; _pathControl.backgroundColor = [NSUserDefaults colorForKey:kColorBackgroundBottomBar]; [(NSBox *)_rightTopPane setFillColor:[NSUserDefaults colorForKey:kColorBackgroundColumnContainer]]; [self showNetworkWarning:![P4Reachability isConnected] animated:NO]; } - (void)windowDidLoad { [super windowDidLoad]; [P4Reachability startNotifier]; } - (void)showWindow:(id)sender { [super showWindow:sender]; if ([[P4Workspace sharedInstance] isLoggedIn]) return; // Show login screen loginPanel = [LoginPanelController loginPanel]; loginPanel.delegate = self; loginPanel.allowsAutomaticLogin = YES; loginPanel.allowsSSOLogin = NO; [loginPanel presentWithWindow:self.window]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(syncStartedNotification:) name:P4SyncStartedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(syncFinishedNotification:) name:P4SyncFinishedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(submitFinishedNotification:) name:P4SubmitFinishedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(sessionExpiredNotification:) name:P4SessionExpiredNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkReachabilityNotification:) name:P4ReachabilityNotification object:nil]; } #pragma mark - Public - (void)loadURL:(NSString *)url { if (![url hasPrefix:@"p4://"]) return; // Check if logged in if (![[P4Workspace sharedInstance] isLoggedIn] || ![[P4Workspace sharedInstance] workspace]) { // Store url until user logs in launchURL = url; return; } launchURL = nil; url = [url substringFromIndex:3]; url = [url stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding]; // Prepare local path NSString *root = [[P4Workspace sharedInstance] root]; NSString *relativePath = [url stringByRemovingPrefix:@"//"]; NSString *path = [root stringByAppendingString:relativePath]; // Check if local path exists BOOL dir = NO; BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&dir]; if (exists && dir == [url hasSuffix:@"/"]) { // Load local workspace url [browserController loadPath:path]; } else { // Load depot url [browserController loadPath:url]; } } #pragma mark - Private - (void)changePath:(NSString *)path { [sidebarController setWorkingPath:path]; if (!path || [path isEqualToString:undoStack.lastObject]) return; [undoStack addObject:path]; redoStack = [NSMutableArray array]; [self updateNavigationButtons]; } - (void)next { [undoStack addObject:redoStack.lastObject]; [redoStack removeLastObject]; [self updateNavigationButtons]; [browserController loadPath:undoStack.lastObject]; } - (void)back { [redoStack addObject:undoStack.lastObject]; [undoStack removeLastObject]; [self updateNavigationButtons]; [browserController loadPath:undoStack.lastObject]; } - (void)clear { undoStack = [NSMutableArray array]; redoStack = [NSMutableArray array]; [self updateNavigationButtons]; } - (void)updateNavigationButtons { [_backButton setEnabled:undoStack.count > 1]; [_nextButton setEnabled:redoStack.count]; } - (void)showNetworkWarning:(BOOL)show animated:(BOOL)animated { if (show == [_networkWarning isHidden]) { CGRect frame = _pathControl.frame; frame.size.width = _networkWarning.frame.origin.x; if (!show) frame.size.width += _networkWarning.frame.size.width; if (animated) { [_networkWarning setAlphaValue:!show]; [_networkWarning setHidden:NO]; [NSView animateWithDuration:0.25f animations:^{ [[_networkWarning animator] setAlphaValue:show]; [[_pathControl animator] setFrame:frame]; } completion:^{ [_networkWarning setHidden:!show]; }]; } else { [_pathControl setFrame:frame]; [_networkWarning setHidden:!show]; } } } - (void)setBrowserController:(BrowserViewController *)browser { if (browserController == browser) return; BOOL column = [browser isKindOfClass:[ColumnViewController class]]; [_viewColumnButton setEnabled:!column]; [_viewIconButton setEnabled:column]; [_rightSplitView replaceSubview:_rightTopPane with:browser.view]; _rightTopPane = browser.view; [browser setRootItem:browserController.rootItem]; [browser setWorkingItem:browserController.workingItem]; [browser setSelectedItems:browserController.selectedItems]; [browser setDelegate:self]; // Swap next responders [browser setNextResponder:browser.view.nextResponder]; [browser.view setNextResponder:browser]; [self.window makeFirstResponder:browser.view]; browserController = browser; PSLogStore(@"BrowserController", @"%@", browser); // Swap search bars if (_searchBar) { NSView *view = browserController.searchBar; CGFloat height = view.frame.size.height; [_rightSplitView replaceSubview:_searchBar with:view]; _searchBar = view; CGRect frame = view.frame; frame.size.height = height; view.frame = frame; } } - (void)syncWorkspace { SyncPanelController *syncPanel = [[SyncPanelController alloc] init]; [syncPanel presentWithWindow:self.window]; } - (void)submitFiles:(NSArray *)paths { SubmitPanelController *submitPanel = [[SubmitPanelController alloc] init]; [submitPanel presentWithWindow:self.window]; [submitPanel selectPaths:paths]; } - (void)addFiles { NSOpenPanel *openPanel = [NSOpenPanel openPanel]; [openPanel setCanChooseFiles:YES]; [openPanel setCanChooseDirectories:YES]; [openPanel setAllowsMultipleSelection:YES]; [openPanel setCanCreateDirectories:YES]; [openPanel setPrompt:@"Select"]; NSURL *directory = [NSURL fileURLWithPath:NSHomeDirectory()]; [openPanel setDirectoryURL:directory]; [openPanel beginSheetModalForWindow:self.window completionHandler:^(NSInteger result) { if (result != NSOKButton) return; NSArray *selectedPaths = [[openPanel URLs] valueForKey:@"path"]; [[browserController workingItem] performAction:@selector(addFiles:) object:selectedPaths delegate:self]; }]; } - (void)createDirectory { [[browserController workingItem] performAction:@selector(createDirectory) object:nil delegate:self]; } #pragma mark - SplitView delegate - (CGFloat)splitView:(NSSplitView *)splitView constrainMaxCoordinate:(CGFloat)proposedMaximumPosition ofSubviewAt:(NSInteger)dividerIndex { if (splitView == _mainSplitView) return proposedMaximumPosition * 0.5f; // Half of window size return proposedMaximumPosition; } - (CGFloat)splitView:(NSSplitView *)splitView constrainMinCoordinate:(CGFloat)proposedMinimumPosition ofSubviewAt:(NSInteger)dividerIndex { if (splitView == _mainSplitView) return PANE_MIN_WIDTH; return proposedMinimumPosition; } - (BOOL)splitView:(NSSplitView *)splitView canCollapseSubview:(NSView *)subview { if (splitView == _mainSplitView) return subview == sidebarController.view; // Collapsable side pane return NO; } - (BOOL)splitView:(NSSplitView *)splitView shouldAdjustSizeOfSubview:(NSView *)view { if (view == sidebarController.view) return (sidebarController.view.bounds.size.width > // Don't resize side pane / unless it's larger than half of window size _mainSplitView.bounds.size.width * 0.5f); if (view == _rightBottonPane) return NO; if (view == _searchBar) return NO; return YES; } - (NSRect)splitView:(NSSplitView *)splitView effectiveRect:(NSRect)proposedEffectiveRect forDrawnRect:(NSRect)drawnRect ofDividerAtIndex:(NSInteger)dividerIndex { if (splitView == _rightSplitView) return CGRectZero; return proposedEffectiveRect; } #pragma mark - LoginPanel Delegate - (void)loginPanelDidLogin { WorkspacePanelController *workspacePanel = [[WorkspacePanelController alloc] init]; workspacePanel.delegate = self; [workspacePanel presentWithWindow:self.window]; } - (void)loginPanelDidCancel { [loginPanel dismiss]; loginPanel = nil; } - (void)loginPanelDidLoginWithWorkspace:(NSString *)workspace { [self workspacePanelDidSelectWorkspace:workspace]; } - (void)loginPanelDidRelogin { [loginPanel dismiss]; loginPanel = nil; [browserController reload]; } #pragma mark - WorkspacePanel Delegate - (void)workspacePanelDidSelectWorkspace:(NSString *)workspace { [loginPanel dismiss]; loginPanel = nil; [self clear]; // Load workspace defaults [P4WorkspaceDefaults setWorkspace:workspace]; Class browserClass = [[P4WorkspaceDefaults sharedInstance] iconView] ? [IconViewController class] : [ColumnViewController class]; if (![browserController isKindOfClass:browserClass]) [self setBrowserController:[[browserClass alloc] init]]; [sidebarController reload]; [browserController setRootItem:nil]; if (launchURL) [self loadURL:launchURL]; else [browserController loadPath:[P4Workspace sharedInstance].root]; #ifdef DEBUG [[P4Workspace sharedInstance] setAutosyncInterval:5*60]; #else [[P4Workspace sharedInstance] setAutosyncInterval:2*60]; #endif } - (void)workspacePanelDidCreateWorkspace:(NSString *)workspace { [self workspacePanelDidSelectWorkspace:workspace]; } - (void)workspacePanelDidCancel { } #pragma mark - Sidebar Delegate - (void)sidebarDidSelectPath:(NSString *)path { [browserController loadPath:path]; } #pragma mark - BrowserView Delegate - (void)browserView:(BrowserViewController *)browser didChangeWorkingItem:(P4Item *)item { BOOL editable = item.isEditable; [_addButton setEnabled:editable]; [_createDirectoryButton setEnabled:editable]; [self changePath:item.path]; } - (void)browserView:(BrowserViewController *)browser didChangeSelectedItem:(P4Item *)item { NSColor *color = [NSUserDefaults colorForKey:kColorBackgroundBottomBar]; NSPathComponentCell *(^createCellBlock)(NSString *, NSString *) = ^NSPathComponentCell *(NSString *name, NSString *path) { PSPathComponentCell *pathCell = [[PSPathComponentCell alloc] init]; pathCell.font = [NSFont fontWithName:@"Helvetica-Bold" size:12]; pathCell.textColor = [NSColor darkGrayColor]; [pathCell setBackgroundColor:color]; [pathCell setImage:[NSImage imageNamed:@"IconFolder"]]; [pathCell setTitle:name]; [pathCell setRepresentedObject:path]; return pathCell; }; NSString *rootName = nil; NSString *rootPath = nil; NSString *relative = item.path; if ([item.status isEqualToString:@"delete"] || [item.status isEqualToString:@"move/delete"]) relative = item.remotePath; if ([relative hasPrefix:@"//"]) { rootPath = @"//"; rootName = @"All Files"; } else if ([relative hasPrefix:@"/"]) { rootPath = [P4Workspace sharedInstance].root; rootName = @"Workspace"; } else { rootPath = browserController.rootItem.path; rootName = browserController.rootItem.name; relative = nil; } relative = [relative stringByRemovingPrefix:rootPath]; relative = [relative stringByRemovingSuffix:@"/"]; NSMutableArray *cells = [NSMutableArray array]; if (rootName) [cells addObject:createCellBlock(rootName, rootPath)]; NSString *path = rootPath; for (NSString *component in [relative pathComponents]) { path = [path stringByAppendingFormat:@"%@/", component]; [cells addObject:createCellBlock(component, path)]; } PSPathComponentCell *lastCell = [cells lastObject]; if (item && !item.isDirectory) lastCell.image = item.icon; [lastCell setLastItem:YES]; [lastCell setRepresentedObject:nil]; [_pathControl setPathComponentCells:cells]; } - (void)browserView:(id)browser didChangeSearchQuery:(NSString *)query { if (query) { if (!_searchBar.superview) { _searchBar = browserController.searchBar; [_rightSplitView insertFirstSubview:_searchBar animated:YES]; } // Update undo stack if ([[undoStack lastObject] hasPrefix:@"search://"]) { [undoStack removeLastObject]; NSString *path = browserController.workingItem.path ?: @"search://"; // Dirty way to ensure non-nil [undoStack addObject:path]; } } else { _searchField.stringValue = @""; if (_searchBar.superview) { [_rightSplitView removeFirstSubviewAnimated:YES]; _searchBar = nil; } // Remove history stack entries if ([[undoStack lastObject] hasPrefix:@"search://"]) [undoStack removeLastObject]; if ([[redoStack lastObject] hasPrefix:@"search://"]) [redoStack removeLastObject]; [self updateNavigationButtons]; } } #pragma mark - P4ItemAction delegate - (void)action:(P4ItemAction *)action loadingMessage:(NSString *)message { if (pendingActionPanel) return; pendingAction = action; pendingActionPanel = [[LoadingPanelController alloc] initWithStyle:NSProgressIndicatorSpinningStyle]; pendingActionPanel.title = message; [pendingActionPanel presentWithWindow:self.window]; } - (void)action:(P4ItemAction *)action didFinish:(NSArray *)response error:(NSError *)error { if (pendingAction == action) { [pendingActionPanel dismiss]; pendingActionPanel = nil; pendingAction = nil; } if (error) { // Don't show alert for session expiration if (error.code == P4ErrorSessionExpired) return; NSAlert *alert = [NSAlert alertWithError:error]; [alert setMessageText:@"\nAction failed"]; [alert setInformativeText:error.localizedDescription]; [alert setIcon:[[NSImage alloc] init]]; [alert beginSheetModalForWindow:self.window modalDelegate:nil didEndSelector:nil contextInfo:NULL]; return; } if (action.selector == @selector(createDirectory)) [browserController editItemName:[action.item cachedItemForPath:response.lastObject]]; else if (action.selector == @selector(checkIn)) [self submitFiles:@[ action.item.remotePath ]]; else if (action.selector == @selector(checkInItems:)) [self submitFiles:[action.items valueForKey:@"remotePath"]]; else if (action.selector == @selector(checkInAll)) [self submitFiles:response]; else if (action.selector == @selector(addFiles:)) [self submitFiles:[response valueForKey:@"depotFile"]]; else if (action.selector == @selector(showVersions)) [browserController showVersions:action.item]; } - (BOOL)action:(id)action prompt:(NSString *)message { return [[NSAlert alertWithMessageText:@"Confirm" defaultButton:@"Yes" alternateButton:@"No" otherButton:nil informativeTextWithFormat:@"%@", message] runModal] == NSAlertDefaultReturn; } #pragma mark - P4Workspace notifications - (void)syncStartedNotification:(NSNotification *)notification { [_checkInAllButton setEnabled:NO]; [_syncButtonIndicator setHidden:NO]; [_syncButtonIndicator setAlphaValue:0.0f]; [NSView animateWithDuration:0.25f animations:^{ [_syncButton.animator setAlphaValue:0.0f]; [_syncButtonIndicator.animator setAlphaValue:1.0f]; } completion:^{ [_syncButton setHidden:YES]; }]; } - (void)syncFinishedNotification:(NSNotification *)notification { [_checkInAllButton setEnabled:YES]; [_syncButton setHidden:NO]; [_syncButton setAlphaValue:0.0f]; [NSView animateWithDuration:0.25f animations:^{ [_syncButton.animator setAlphaValue:1.0f]; [_syncButtonIndicator.animator setAlphaValue:0.0f]; } completion:^{ [_syncButton setAlphaValue:1.0f]; [_syncButton setHidden:NO]; // Make sure it's not hidden [_syncButtonIndicator setHidden:YES]; }]; [browserController reload]; } - (void)submitFinishedNotification:(NSNotification *)notification { [browserController reload]; } #pragma mark - Network notifications - (void)sessionExpiredNotification:(NSNotification *)notification { if (loginPanel) return; loginPanel = [LoginPanelController reloginPanel]; loginPanel.delegate = self; [loginPanel presentExclusiveWithWindow:self.window]; } - (void)networkReachabilityNotification:(NSNotification *)notification { [self showNetworkWarning:![P4Reachability isConnected] animated:YES]; } #pragma mark - PathControl - (NSDragOperation)pathControl:(NSPathControl *)pathControl validateDrop:(id )info { return NSDragOperationNone; } - (void)pathControlDoubleClick:(id)sender { NSPathComponentCell *cell = [_pathControl clickedPathComponentCell]; NSString *path = cell.representedObject; if (path) [browserController loadPath:path]; } #pragma mark - NSMenu delegate - (void)menuWillOpen:(NSMenu *)menu { if (menu == fileMenu) { BOOL connected = [[P4Workspace sharedInstance] isLoggedIn]; [[menu itemAtIndex:0] setEnabled:!connected]; [[menu itemAtIndex:1] setEnabled:connected]; [[menu itemAtIndex:2] setEnabled:connected]; } else if (menu == actionsMenu) { NSArray *selectedItems = [browserController selectedItems]; if (selectedItems.count > 1) [menu setItems:selectedItems delegate:self]; else if (selectedItems.count) [menu setItem:selectedItems.lastObject delegate:self]; else { [menu removeAllItems]; [[menu addItemWithTitle:@"No selection" action:nil keyEquivalent:@""] setEnabled:NO]; } } else if (menu == viewMenu) { BOOL column = [browserController isKindOfClass:[ColumnViewController class]]; [[menu itemAtIndex:0] setEnabled:!column]; [[menu itemAtIndex:1] setEnabled:column]; [[menu itemAtIndex:0] setState:column]; [[menu itemAtIndex:1] setState:!column]; } } #pragma mark - Menu Actions - (IBAction)fileMenuConnect:(id)sender { if (loginPanel) return; loginPanel = [LoginPanelController loginPanel]; loginPanel.delegate = self; [loginPanel presentWithWindow:self.window]; } - (IBAction)fileMenuConnectionDetails:(id)sender { if (loginPanel) return; loginPanel = [LoginPanelController connectionPanel]; loginPanel.delegate = self; [loginPanel presentWithWindow:self.window]; } - (IBAction)fileMenuDisconnect:(id)sender { LoadingPanelController *loading = [[LoadingPanelController alloc] initWithStyle:NSProgressIndicatorSpinningStyle]; loading.title = @"Logging out..."; [loading presentWithWindow:self.window]; [[P4Workspace sharedInstance] logout:^(P4Operation *operation, NSArray *response) { [P4WorkspaceDefaults setWorkspace:nil]; [sidebarController reload]; [browserController setRootItem:nil]; [loading dismiss]; [self fileMenuConnect:nil]; }]; } - (IBAction)viewMenuColumnView:(id)sender { [[P4WorkspaceDefaults sharedInstance] setIconView:NO]; [self setBrowserController:[[ColumnViewController alloc] init]]; } - (IBAction)viewMenuIconView:(id)sender { [[P4WorkspaceDefaults sharedInstance] setIconView:YES]; [self setBrowserController:[[IconViewController alloc] init]]; } - (IBAction)debugMenuRunCommand:(id)sender { debugController = [[DebugWindowController alloc] init]; [debugController showWindow:self]; } - (IBAction)debugMenuSaveLog:(id)sender { PSDebugLogSave(); } #pragma mark - Toolbar actions - (IBAction)backButtonPressed:(id)sender { [self back]; } - (IBAction)nextButtonPressed:(id)sender { [self next]; } - (IBAction)syncButtonPressed:(id)sender { [self syncWorkspace]; } - (IBAction)addButtonPressed:(id)sender { [self addFiles]; } - (IBAction)checkInAllButtonPressed:(id)sender { [self submitFiles:nil]; } - (IBAction)viewColumnButtonPressed:(id)sender { [self viewMenuColumnView:sender]; } - (IBAction)viewIconButtonPressed:(id)sender { [self viewMenuIconView:sender]; } - (IBAction)createDirectoryButtonPressed:(id)sender { [self createDirectory]; } #pragma mark - Search action - (IBAction)searchAction:(id)sender { [browserController search:[_searchField stringValue]]; } @end