#import #import "game/sdl/ios/chatview.h" #import "game/sdl/ios/chatcell.h" #import "game/sdl/ios/iphone.h" @implementation ChatView #define TEXTFIELDWIDTH_LANDSCAPE self.bounds.size.width - 81 // #define TEXTFIELDWIDTH_PORTRAIT 238 #define TEXTFIELDHEIGHT_LANDSCAPE 24 // #define TEXTFIELDHEIGHT_PORTRAIT 29 #define TEXTFIELDFONTSIZE_LANDSCAPE 16 // #define TEXTFIELDFONTSIZE_PORTRAIT 20 #define TITLELABELWIDTH_LANDSCAPE 100 #define CHAT_HISTORY 100 #define CHAT_QUEUE 100 #define DEVICE_OS [[[UIDevice currentDevice] systemVersion] intValue] - (id)init { if (!(self == [super init])) { return nil; } tableView_ = nil; toolbar_ = nil; textField_ = nil; chatQueue_ = [[NSMutableArray alloc] initWithCapacity:CHAT_QUEUE]; suspended_ = NO; chatEntries_ = [[NSMutableArray alloc] initWithCapacity:CHAT_HISTORY]; entryFont_ = [UIFont systemFontOfSize:12]; CGSize size10Spaces = [@" " sizeWithFont:entryFont_]; width10Spaces_ = size10Spaces.width; chatc_ = NULL; title_ = @"Chat"; titleLabel_ = nil; toolbar2_ = nil; clear_ = YES; [self loadView]; return self; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (void)registerNotifications { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardShown:) name:UIKeyboardDidShowNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardHidden:) name:UIKeyboardDidHideNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillChangeFrame:) name:UIKeyboardWillChangeFrameNotification object:nil]; } - (void)loadView { // Create parent view that subviews go into CGRect frame = CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height); [self setFrame:frame]; self.autoresizesSubviews = YES; self.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; // Create toolbar, add to parent toolbar_ = [[UIToolbar alloc] initWithFrame:CGRectZero]; toolbar_.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleWidth; toolbar_.autoresizesSubviews = YES; toolbar_.backgroundColor = [UIColor darkGrayColor]; [self addSubview: toolbar_]; // Create text field - will be added to toolbar in layout because // it is the only way the size custom toolbar views textField_ = [[UITextField alloc] initWithFrame:CGRectZero]; textField_.borderStyle = UITextBorderStyleRoundedRect; textField_.textColor = [UIColor blackColor]; textField_.placeholder = @""; textField_.autocorrectionType = UITextAutocorrectionTypeDefault; textField_.keyboardType = UIKeyboardTypeDefault; textField_.returnKeyType = UIReturnKeySend; textField_.clearButtonMode = UITextFieldViewModeWhileEditing; textField_.delegate = self; // toolbar2 is the navigation header toolbar2_ = [[UIToolbar alloc] initWithFrame:CGRectZero]; toolbar2_.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleWidth; toolbar2_.autoresizesSubviews = YES; toolbar2_.backgroundColor = [UIColor darkGrayColor]; [self addSubview: toolbar2_]; // titleLabel that will be added to toolbar2 later titleLabel_ = [[UILabel alloc] initWithFrame:CGRectZero]; titleLabel_.font = [UIFont boldSystemFontOfSize:18]; titleLabel_.textAlignment = NSTextAlignmentCenter; titleLabel_.textColor = [UIColor whiteColor]; titleLabel_.backgroundColor = [UIColor clearColor]; //titleLabel_.shadowColor = [UIColor darkGrayColor]; titleLabel_.opaque = NO; titleLabel_.text = title_; // Layout views. Repositions and resizes appropriately. [self layoutViews]; [self registerNotifications]; } - (void)reCreateTableView { [tableView_ removeFromSuperview]; tableView_ = nil; // Create tableview and add to parent tableView_ = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; tableView_.delegate = self; tableView_.dataSource = self; tableView_.autoresizesSubviews = YES; tableView_.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth; tableView_.separatorStyle = UITableViewCellSeparatorStyleNone; [self layoutTableView]; [self addSubview: tableView_]; } - (void)layoutTableView { CGRect rcToolbar = toolbar_.frame; CGRect rcToolbar2 = toolbar2_.frame; CGRect rcTableView = tableView_.frame; rcTableView.origin.y = rcToolbar2.size.height; rcTableView.size.height = rcToolbar.origin.y - rcTableView.origin.y; rcTableView.size.width = rcToolbar.size.width; tableView_.frame = rcTableView; } - (void)layoutViews { // Resize the textField appropriately. Note the toolbar is a different // height in portrait vs. landscape. Also note, custom view button // bar items can only be resized by re-added them to the toolbar. CGRect rcTextField = textField_.frame; rcTextField.size.height = TEXTFIELDHEIGHT_LANDSCAPE; rcTextField.size.width = TEXTFIELDWIDTH_LANDSCAPE; textField_.font = [UIFont systemFontOfSize: TEXTFIELDFONTSIZE_LANDSCAPE]; textField_.frame = rcTextField; UIBarButtonItem *textFieldButton = [[UIBarButtonItem alloc] initWithCustomView:textField_]; UIBarButtonItem *sendButton = [[UIBarButtonItem alloc] initWithTitle:@"Send" style:UIBarButtonItemStyleBordered target:self action:@selector(onSend)]; NSArray *array = [NSArray arrayWithObjects: textFieldButton, sendButton, (char *)NULL]; [toolbar_ setItems:nil]; [toolbar_ setItems:array animated:NO]; [toolbar_ sizeToFit]; // The toolbar is taller when in portrait mode, so other things // need to be layed out. int cyToolbar = rcTextField.size.height + 8; CGRect rcToolbar = toolbar_.frame; CGRect rcParent = self.bounds; rcToolbar.size.height = cyToolbar; rcToolbar.origin.y = rcParent.size.height - rcToolbar.size.height; toolbar_.frame = rcToolbar; // Size the label width [titleLabel_ sizeToFit]; CGRect frame = titleLabel_.frame; frame.size.width = TITLELABELWIDTH_LANDSCAPE; titleLabel_.frame = frame; // Toolbar2 goes at the top UIBarButtonItem *playersButton = [[UIBarButtonItem alloc] initWithTitle:@"Players" style:UIBarButtonItemStyleDone target:self action:@selector(onPlayers)]; UIBarButtonItem *doneButton = [[UIBarButtonItem alloc] initWithTitle:@"Done" style:UIBarButtonItemStyleDone target:self action:@selector(onDone)]; keyboardButton_ = [[UIBarButtonItem alloc] initWithTitle:@"Keyboard" style:UIBarButtonItemStyleDone target:self action:@selector(onKeyboard)]; [keyboardButton_ setEnabled:NO]; UIBarButtonItem *titleItem = [[UIBarButtonItem alloc] initWithCustomView:titleLabel_]; UIBarButtonItem *sizingItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:self action:nil]; array = [NSArray arrayWithObjects: doneButton, sizingItem, titleItem, sizingItem, keyboardButton_, playersButton, (char *)NULL]; [toolbar2_ setItems:nil]; [toolbar2_ setItems:array animated:NO]; [toolbar2_ sizeToFit]; // Toolbar2 goes at the top CGRect rcToolbar2 = toolbar2_.frame; rcToolbar2.origin.y = 0; rcToolbar2.size.height = cyToolbar; toolbar2_.frame = rcToolbar2; // TableView //[self layoutTableView]; } - (void)onSend { if (chatc_ == NULL) { return; } // For some currently unknown reason, sometimes there are leading zeros // in this UITextField text. Remove them, and remove whitespace. NSString *text = textField_.text; int count = text.length; int index = 0; for (; index < count; index++) { unichar ch = [text characterAtIndex:index]; if (ch != 0) { break; } } text = [[text substringFromIndex:index] stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceCharacterSet]]; if (text.length != 0) { NSData *data = [text dataUsingEncoding:[NSString defaultCStringEncoding] allowLossyConversion:YES]; std::string s((const char *)[data bytes], [data length]); wi::ChatParams *params = new wi::ChatParams(s); chatc_->thread().Post(kidmOnSend, chatc_, params); } textField_.text = nil; } - (BOOL)textFieldShouldReturn:(UITextField *)textField { [self onSend]; return NO; } - (void)scrollToBottom { [tableView_ beginUpdates]; // Scroll to the bottom int index = [chatEntries_ count] - 1; if (index >= 0) { NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0]; [tableView_ scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:NO]; } [tableView_ endUpdates]; } - (void)onPlayers { if (chatc_ != NULL) { chatc_->thread().Post(kidmOnPlayers, chatc_); } } - (void)show { BOOL created = NO; if (clear_ || tableView_ == nil) { // Put queued up chat into the model suspended_ = NO; [self updateChatModel]; // Create the table view. It will automagically reload the model [self reCreateTableView]; clear_ = NO; created = YES; } [IPhone presentView:self]; [tableView_ setSeparatorStyle:UITableViewCellSeparatorStyleNone]; [textField_ becomeFirstResponder]; // Only scroll to the bottom if the table view existed already. // Otherwise it will crash, because data model loading is // asynchronous. if (created == NO) { [self scrollToBottom]; } } - (void)onDone { [textField_ resignFirstResponder]; [self removeFromSuperview]; if (chatc_ != NULL) { chatc_->thread().Post(kidmOnDismissed, chatc_); } } - (void)onKeyboard { [keyboardButton_ setEnabled:NO]; if (textField_.isFirstResponder) { [textField_ resignFirstResponder]; } else { [textField_ becomeFirstResponder]; } } - (void)keyboardShown:(NSNotification *)notification { if (!self.superview) { return; } [self scrollToBottom]; [keyboardButton_ setEnabled:YES]; } - (void)keyboardHidden:(NSNotification *)notification { if (!self.superview) { return; } [self scrollToBottom]; [keyboardButton_ setEnabled:YES]; } - (void)keyboardWillChangeFrame:(NSNotification *)notification { NSDictionary *info = [notification userInfo]; CGRect keyboardFrameEnd = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]; // Move the toolbar CGRect rcToolbar = [toolbar_ frame]; if (DEVICE_OS <= 7) { // Don't ask... you dont even want to know about this calculation... // The keyboardFrameEnd rect is based on portrait, but self.frame and its subviews' frames // are landscape. So calculating there toolbar_ should be based on keyboardFrameEnd is a pain. // Lets hope we don't ever need to modify/maintain this <= iOS 7 code. if ([[UIApplication sharedApplication] statusBarOrientation] == UIDeviceOrientationLandscapeRight) { // Strangely enough, this seems to fire when my device is physically in landscapeLeft mode rcToolbar.origin.y = keyboardFrameEnd.origin.x - toolbar_.frame.size.height; } if ([[UIApplication sharedApplication] statusBarOrientation] == UIDeviceOrientationLandscapeLeft) { // Strangely enough, this seems to fire when my device is physically in landscapeRight mode rcToolbar.origin.y = self.frame.size.height - keyboardFrameEnd.origin.x - keyboardFrameEnd.size.width - toolbar_.frame.size.height; } } else { // DEVICE_OS <= 8 rcToolbar.origin.y = keyboardFrameEnd.origin.y - rcToolbar.size.height; } [toolbar_ setFrame:rcToolbar]; [self bringSubviewToFront:toolbar_]; // Resize the table view [self layoutTableView]; } - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [chatEntries_ count]; } - (void)initializeChatCell:(int)rowIndex cell:(ChatCell *)cell { CGSize size; size.width = self.bounds.size.width; size.height = [self getRowHeight:rowIndex forWidth:size.width]; NSMutableDictionary *entry = (NSMutableDictionary *) [chatEntries_ objectAtIndex:rowIndex]; if (entry == nil) { [cell setup:@"error" user:@"error" size:size]; return; } NSString *user = [entry objectForKey:@"user"]; NSString *chat = [entry objectForKey:@"chat"]; [cell setup:chat user:user size:size]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath: (NSIndexPath *)indexPath { ChatCell *cell = (ChatCell *)[tableView_ dequeueReusableCellWithIdentifier:@"chatcell"]; if (cell == nil) { cell = [[ChatCell alloc] initWithFrame:CGRectZero reuseIdentifier:@"chatcell"]; } [self initializeChatCell:indexPath.row cell:cell]; return cell; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { NSMutableDictionary *entry = (NSMutableDictionary *) [chatEntries_ objectAtIndex:indexPath.row]; if (entry == nil) { return 0; } return [self getRowHeight:indexPath.row forWidth:self.bounds.size.width]; } - (NSArray *)updateChatModel { NSMutableArray *indexPaths = [NSMutableArray arrayWithCapacity:10]; while ([chatQueue_ count] != 0) { NSDictionary *dict = (NSDictionary *)[chatQueue_ objectAtIndex:0]; NSString *chat = [dict objectForKey:@"chat"]; NSString *user = [dict objectForKey:@"player"]; if ([user length] == 0) { // System message. NSMutableDictionary *entry = [NSMutableDictionary dictionaryWithObjectsAndKeys: @"", @"user", chat, @"chat", (char *)NULL]; [chatEntries_ addObject:entry]; } else { // Regular chat message. Add ':' to the end of user NSString *newUser = [NSString stringWithFormat:@"%@:", user]; // Insert spaces into chat so that user's name can draw into that // space with a different UILabel. Hack-o-rama. CGSize sizeUser = [newUser sizeWithFont:entryFont_]; CGFloat ratio = sizeUser.width / width10Spaces_; int countSpaces = ceil(ratio * 10.0) + 2; char szSpaces[256]; if (countSpaces > sizeof(szSpaces)) { countSpaces = sizeof(szSpaces); } memset(szSpaces, ' ', sizeof(szSpaces)); szSpaces[countSpaces - 1] = 0; NSString *newChat = [NSString stringWithFormat:@"%s%@", szSpaces, chat]; NSMutableDictionary *entry = [NSMutableDictionary dictionaryWithObjectsAndKeys: newUser, @"user", newChat, @"chat", (char *)NULL]; [chatEntries_ addObject:entry]; } [chatQueue_ removeObjectAtIndex:0]; NSIndexPath *indexPath = [NSIndexPath indexPathForRow: [chatEntries_ count] - 1 inSection:0]; [indexPaths addObject:indexPath]; } return indexPaths; } - (void)updateChatRows { if (suspended_ == YES) { return; } NSArray *indexPaths = [self updateChatModel]; if ([indexPaths count] != 0) { [tableView_ beginUpdates]; [tableView_ insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationBottom]; [tableView_ endUpdates]; } } - (void)suspendUpdates:(BOOL)suspend { if (suspend == suspended_) { return; } if (suspended_ == NO & suspend == YES) { suspended_ = suspend; return; } if (suspended_ == YES && suspend == NO) { suspended_ = suspend; [self updateChatRows]; } } - (int)getRowHeight:(int)rowIndex forWidth:(int)width { NSMutableDictionary *entry = (NSMutableDictionary *) [chatEntries_ objectAtIndex:rowIndex]; if (entry == nil) { return 0; } // See if the height is cached; if so return it NSString *key = [NSString stringWithFormat:@"%d", width]; NSString *value = [entry objectForKey:key]; if (value != nil) { return [value intValue]; } // Calculate what the height is, cache it, return it NSString *chat = (NSString *)[entry objectForKey:@"chat"]; CGSize sizeBox = CGSizeMake(width, 2800); CGSize size = [chat sizeWithFont:entryFont_ constrainedToSize:sizeBox lineBreakMode:NSLineBreakByWordWrapping]; value = [NSString stringWithFormat:@"%d", (int)ceilf(size.height)]; [entry setObject:value forKey:key]; return size.height; } - (void)doClear { [chatEntries_ removeAllObjects]; [chatQueue_ removeAllObjects]; // Clear occurs when switching between chat contexts (room, game, etc). // It would be nice to be able to call reloadData here. Unfortunately, // it causes "stuck chat cells" when the TableView isn't visible. As // a hack workaround, destroy the tableView_ and re-created it when // showing. During this period, queue up chat. [tableView_ removeFromSuperview]; tableView_ = nil; clear_ = YES; [self suspendUpdates:YES]; } - (void)doAddChat:(NSDictionary *)dict { [chatQueue_ addObject:dict]; [self updateChatRows]; [self scrollToBottom]; } - (void)doShow { [self show]; titleLabel_.text = title_; } - (void)doHide { if ([self superview]) [self onDone]; } - (void)doSetTitle:(NSString *)title { title_ = title; } - (void)setController:(wi::ChatController *)chatc { chatc_ = chatc; } - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)way { return UIInterfaceOrientationLandscapeRight; } // UIScrollViewDelegate // Inserting items while scrolling causes UITableView to crash. Track // when scrolling happens an cache inserts until scrolling stops. - (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView { [self suspendUpdates:YES]; return YES; } - (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView { [self suspendUpdates:NO]; } - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { [self suspendUpdates:YES]; } - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { if (decelerate == NO) { [self suspendUpdates:NO]; } } - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { [self suspendUpdates:NO]; } - (void)setFrame:(CGRect)frame { [super setFrame:frame]; [self layoutViews]; [self layoutTableView]; } @end // ++++ // This is the game thread callable class for controlling chat namespace wi { // These get called on the game thread, and do the real work on the main // thread. ChatController::ChatController(ChatView *vcchat) { vcchat_ = vcchat; [vcchat_ setController: this]; } void ChatController::Clear() { [vcchat_ performSelectorOnMainThread:@selector(doClear) withObject:nil waitUntilDone: NO]; } void ChatController::AddChat(const char *player, const char *chat) { NSString *player_s = [NSString stringWithCString:player encoding:[NSString defaultCStringEncoding]]; NSString *chat_s = [NSString stringWithCString:chat encoding:[NSString defaultCStringEncoding]]; NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys: player_s, @"player", chat_s, @"chat", (char *)NULL]; [vcchat_ performSelectorOnMainThread:@selector(doAddChat:) withObject:dict waitUntilDone: NO]; } void ChatController::Show(bool fShow) { if (fShow) { [vcchat_ performSelectorOnMainThread:@selector(doShow) withObject:nil waitUntilDone: NO]; } else { [vcchat_ performSelectorOnMainThread:@selector(doHide) withObject:nil waitUntilDone: NO]; } } void ChatController::SetTitle(const char *title) { title_ = title; NSString *title_s = [NSString stringWithCString:title encoding:[NSString defaultCStringEncoding]]; [vcchat_ performSelectorOnMainThread:@selector(doSetTitle:) withObject:title_s waitUntilDone: NO]; } const char *ChatController::GetTitle() { return title_.c_str(); } IChatControllerCallback *ChatController::SetCallback( IChatControllerCallback *pcccb) { IChatControllerCallback *old = pcccb_; pcccb_ = pcccb; return old; } void ChatController::OnMessage(base::Message *pmsg) { switch (pmsg->id) { case kidmOnDismissed: if (pcccb_ != NULL) { pcccb_->OnChatDismissed(); } break; case kidmOnSend: if (pcccb_ != NULL) { ChatParams *params = (ChatParams *)pmsg->data; pcccb_->OnChatSend(params->chat.c_str()); delete params; } break; case kidmOnPlayers: if (pcccb_ != NULL) { pcccb_->OnPlayers(); } break; } } } // namespace wi