hostile-takeover/game/iphone/chatviewcontroller.mm
2016-01-20 11:01:08 -08:00

705 lines
22 KiB
Plaintext

#import "game/iphone/chatviewcontroller.h"
#import "game/iphone/chatcell.h"
@implementation ChatViewController
#define TEXTFIELDWIDTH_LANDSCAPE 397
#define TEXTFIELDWIDTH_PORTRAIT 238
#define TEXTFIELDHEIGHT_LANDSCAPE 24
#define TEXTFIELDHEIGHT_PORTRAIT 29
#define TEXTFIELDFONTSIZE_LANDSCAPE 16
#define TEXTFIELDFONTSIZE_PORTRAIT 20
#define TITLELABELWIDTH_LANDSCAPE 335
#define CHAT_HISTORY 100
#define CHAT_QUEUE 100
- (id)init:(id<ChatViewControllerDelegate>)delegate parent:(UIView *)parent {
parent_ = parent;
[parent_ retain];
delegate_ = delegate;
[delegate_ retain];
view_ = 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 = [@" " sizeWithAttributes:@{NSFontAttributeName: entryFont_}];
width10Spaces_ = ceilf(size10Spaces.width);
chatc_ = NULL;
title_ = @"Chat";
[title_ retain];
titleLabel_ = nil;
toolbar2_ = nil;
clear_ = YES;
[self registerNotifications];
[self loadView];
return self;
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
[tableView_ release];
[toolbar_ release];
[textField_ release];
[chatEntries_ release];
[chatQueue_ release];
[title_ release];
[toolbar2_ release];
[titleLabel_ release];
[view_ release];
[delegate_ release];
[parent_ release];
[super dealloc];
}
- (void)testChat
{
#if 0
[self addChat:@"it is raining today" user:@"scott"];
[self addChat:@"it will be sunny tomorrow" user:@"valerie"];
[self addChat:@"the lazy fox jumped over the brown cow's back and sang the pledge of allegiance more words please word wrap yes please do it now asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf asdf" user:@"scott"];
[self addChat:@"I don't believe you." user:@"valerie"];
#endif
}
- (void)registerNotifications
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onKeyboardShown:)
name:UIKeyboardDidShowNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onKeyboardHidden:)
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, parent_.frame.size.width,
parent_.frame.size.height);
view_ = [[UIView alloc] initWithFrame:frame];
view_.autoresizesSubviews = YES;
view_.autoresizingMask = UIViewAutoresizingFlexibleHeight |
UIViewAutoresizingFlexibleWidth;
// Create toolbar, add to parent
toolbar_ = [[UIToolbar alloc] initWithFrame:CGRectZero];
toolbar_.autoresizingMask = UIViewAutoresizingFlexibleTopMargin |
UIViewAutoresizingFlexibleWidth;
toolbar_.autoresizesSubviews = YES;
[view_ 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 = UITextAutocorrectionTypeNo;
//textField_.keyboardType = UIKeyboardTypeDefault;
textField_.keyboardType = UIKeyboardTypeASCIICapable;
textField_.returnKeyType = UIReturnKeyDefault;
textField_.clearButtonMode = UITextFieldViewModeWhileEditing;
textField_.delegate = self;
// toolbar2 is the navigation header
toolbar2_ = [[UIToolbar alloc] initWithFrame:CGRectZero];
toolbar2_.autoresizingMask = UIViewAutoresizingFlexibleTopMargin |
UIViewAutoresizingFlexibleWidth;
toolbar2_.autoresizesSubviews = YES;
[view_ 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];
}
- (void)reCreateTableView {
[tableView_ removeFromSuperview];
[tableView_ release];
tableView_ = nil;
// Create tableview and add to parent
tableView_ = [[UITableView alloc] initWithFrame:CGRectZero
style:UITableViewStylePlain];
tableView_.delegate = self;
tableView_.dataSource = self;
tableView_.separatorStyle = UITableViewCellSeparatorStyleNone;
tableView_.autoresizesSubviews = YES;
tableView_.autoresizingMask = UIViewAutoresizingFlexibleHeight |
UIViewAutoresizingFlexibleWidth;
[view_ 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;
if (view_.bounds.size.width <= 320) {
rcTextField.size.height = TEXTFIELDHEIGHT_PORTRAIT;
rcTextField.size.width = TEXTFIELDWIDTH_PORTRAIT;
textField_.font = [UIFont systemFontOfSize:
TEXTFIELDFONTSIZE_PORTRAIT];
} else {
rcTextField.size.height = TEXTFIELDHEIGHT_LANDSCAPE;
rcTextField.size.width = TEXTFIELDWIDTH_LANDSCAPE / 480.0f * view_.bounds.size.width;;
textField_.font = [UIFont systemFontOfSize:
TEXTFIELDFONTSIZE_LANDSCAPE];
}
textField_.frame = rcTextField;
UIBarButtonItem *textFieldButton = [[[UIBarButtonItem alloc]
initWithCustomView:textField_] autorelease];
UIBarButtonItem *sendButton = [[[UIBarButtonItem alloc]
initWithTitle:@"Send" style:UIBarButtonItemStyleDone
target:self action:@selector(onSend)] autorelease];
NSArray *array = [NSArray arrayWithObjects: textFieldButton, sendButton,
(char *)NULL];
[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 = view_.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 / 480.0f * view_.bounds.size.width;
titleLabel_.frame = frame;
// Toolbar2 goes at the top
UIBarButtonItem *playersButton = [[[UIBarButtonItem alloc]
initWithTitle:@"Players"
style:UIBarButtonItemStyleDone
target:self action:@selector(onPlayers)] autorelease];
UIBarButtonItem *doneButton = [[[UIBarButtonItem alloc]
initWithTitle:@"Done"
style:UIBarButtonItemStyleDone
target:self action:@selector(onDone)] autorelease];
UIBarButtonItem *titleItem = [[[UIBarButtonItem alloc]
initWithCustomView:titleLabel_] autorelease];
array = [NSArray arrayWithObjects: doneButton, titleItem,
playersButton, (char *)NULL];
[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;
}
- (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 = (int)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 = (int)[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];
[self layoutTableView];
clear_ = NO;
created = YES;
}
[parent_ addSubview:view_];
[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
{
[view_ removeFromSuperview];
if (delegate_ != nil) {
[delegate_ onDone:self];
}
if (chatc_ != NULL) {
chatc_->thread().Post(kidmOnDismissed, chatc_);
}
}
- (void)onKeyboardShown:(NSNotification *)notification
{
[self scrollToBottom];
}
- (void)onKeyboardHidden:(NSNotification *)notification
{
[self scrollToBottom];
}
- (void)keyboardWillChangeFrame:(NSNotification *)notification {
NSDictionary *info = [notification userInfo];
UIApplication *app = [UIApplication sharedApplication];
CGRect rcEnd = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
CGRect rcToolbar = [toolbar_ frame];
int nMajorOSVersion = [[[UIDevice currentDevice] systemVersion] intValue];
if (nMajorOSVersion <= 7) {
// The rcEnd rect is based on portrait, but self.frame and its
// subviews' frames are landscape.
if ([app statusBarOrientation] == UIDeviceOrientationLandscapeRight) {
rcToolbar.origin.y = rcEnd.origin.x - toolbar_.frame.size.height;
}
if ([app statusBarOrientation] == UIDeviceOrientationLandscapeLeft) {
rcToolbar.origin.y = view_.frame.size.height - rcEnd.origin.x - rcEnd.size.width - toolbar_.frame.size.height;
}
} else {
rcToolbar.origin.y = rcEnd.origin.y - rcToolbar.size.height;
}
[toolbar_ setFrame:rcToolbar];
[view_ 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 = view_.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] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:@"chatcell"] autorelease];
}
[self initializeChatCell:(int)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:(int)indexPath.row
forWidth:view_.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 sizeWithAttributes:@{NSFontAttributeName: entryFont_}];
CGFloat ratio = ceilf(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:(BOOL)scroll {
if (suspended_ == YES) {
return;
}
NSArray *indexPaths = [self updateChatModel];
if ([indexPaths count] != 0) {
[tableView_ beginUpdates];
[tableView_ insertRowsAtIndexPaths:indexPaths
withRowAnimation:UITableViewRowAnimationBottom];
[tableView_ endUpdates];
}
if (scroll == YES) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:
[chatEntries_ count] - 1 inSection:0];
[tableView_ scrollToRowAtIndexPath:indexPath
atScrollPosition:UITableViewScrollPositionBottom
animated:NO];
}
}
- (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:NO];
}
}
- (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"];
CGRect rect = [chat boundingRectWithSize:CGSizeMake(width, CGFLOAT_MAX)
options: NSStringDrawingUsesLineFragmentOrigin
attributes: @{ NSFontAttributeName: entryFont_ }
context:nil];
CGFloat height = ceilf(rect.size.height);
value = [NSString stringWithFormat:@"%d", (int)height];
[entry setObject:value forKey:key];
return 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_ release];
tableView_ = nil;
clear_ = YES;
[self suspendUpdates:YES];
}
- (void)doAddChat:(NSDictionary *)dict
{
[chatQueue_ addObject:dict];
[self updateChatRows:YES];
}
- (void)doShow:(UINavigationController *)vcwi
{
[self show];
titleLabel_.text = title_;
}
- (void)doHide
{
[self onDone];
}
- (void)doSetTitle:(NSString *)title
{
[title_ release];
title_ = title;
[title_ retain];
}
- (void)setController:(wi::ChatController *)chatc
{
chatc_ = chatc;
}
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)way {
return way == 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];
}
@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(WiViewController *vcwi,
ChatViewController *vcchat) : vcwi_(vcwi), vcchat_(vcchat),
pcccb_(NULL) {
[vcchat_ setController: this];
}
void ChatController::Clear() {
[vcchat_ performSelectorOnMainThread:@selector(doClear)
withObject:nil waitUntilDone: NO];
}
void ChatController::AddChat(const char *player, const char *chat) {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
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];
[pool release];
}
void ChatController::Show(bool fShow) {
if (fShow) {
[vcchat_ performSelectorOnMainThread:@selector(doShow:)
withObject:vcwi_ 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:
[vcwi_ becomeFirstResponder];
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