diff options
Diffstat (limited to 'remoting/ios/ui')
-rw-r--r-- | remoting/ios/ui/cursor_texture.h | 58 | ||||
-rw-r--r-- | remoting/ios/ui/cursor_texture.mm | 181 | ||||
-rw-r--r-- | remoting/ios/ui/desktop_texture.h | 38 | ||||
-rw-r--r-- | remoting/ios/ui/desktop_texture.mm | 83 | ||||
-rw-r--r-- | remoting/ios/ui/help_view_controller.h | 17 | ||||
-rw-r--r-- | remoting/ios/ui/help_view_controller.mm | 21 | ||||
-rw-r--r-- | remoting/ios/ui/host_list_view_controller.h | 39 | ||||
-rw-r--r-- | remoting/ios/ui/host_list_view_controller.mm | 229 | ||||
-rw-r--r-- | remoting/ios/ui/host_list_view_controller_unittest.mm | 90 | ||||
-rw-r--r-- | remoting/ios/ui/host_view_controller.h | 115 | ||||
-rw-r--r-- | remoting/ios/ui/host_view_controller.mm | 676 | ||||
-rw-r--r-- | remoting/ios/ui/pin_entry_view_controller.h | 49 | ||||
-rw-r--r-- | remoting/ios/ui/pin_entry_view_controller.mm | 71 | ||||
-rw-r--r-- | remoting/ios/ui/pin_entry_view_controller_ipad.xib | 103 | ||||
-rw-r--r-- | remoting/ios/ui/pin_entry_view_controller_iphone.xib | 113 | ||||
-rw-r--r-- | remoting/ios/ui/scene_view.h | 171 | ||||
-rw-r--r-- | remoting/ios/ui/scene_view.mm | 642 | ||||
-rw-r--r-- | remoting/ios/ui/scene_view_unittest.mm | 1219 |
18 files changed, 3915 insertions, 0 deletions
diff --git a/remoting/ios/ui/cursor_texture.h b/remoting/ios/ui/cursor_texture.h new file mode 100644 index 0000000..398a424 --- /dev/null +++ b/remoting/ios/ui/cursor_texture.h @@ -0,0 +1,58 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef REMOTING_IOS_UI_CURSOR_TEXTURE_H_ +#define REMOTING_IOS_UI_CURSOR_TEXTURE_H_ + +#import <Foundation/Foundation.h> +#import <GLKit/GLKit.h> + +#import "base/memory/scoped_ptr.h" + +#import "remoting/ios/utility.h" +#include "third_party/webrtc/modules/desktop_capture/desktop_geometry.h" +#include "third_party/webrtc/modules/desktop_capture/mouse_cursor.h" + +@interface CursorTexture : NSObject { + @private + // GL name + GLuint _textureId; + webrtc::DesktopSize _textureSize; + BOOL _needInitialize; + + // The current cursor + scoped_ptr<webrtc::MouseCursor> _cursor; + + BOOL _needCursorRedraw; + + // Rectangle of the most recent cursor drawn to a GL Texture. On each + // successive frame when a new cursor is available this region is cleared on + // the GL Texture, so that the GL Texture is completely transparent again, and + // the cursor is then redrawn. + webrtc::DesktopRect _cursorDrawnToGL; +} + +- (const webrtc::DesktopSize&)textureSize; + +- (void)setTextureSize:(const webrtc::DesktopSize&)size; + +- (const webrtc::MouseCursor&)cursor; + +- (void)setCursor:(webrtc::MouseCursor*)cursor; + +// bind this object to an effect's via the effects properties +- (void)bindToEffect:(GLKEffectPropertyTexture*)effectProperty; + +// True if the cursor has changed in a way that requires it to be redrawn +- (BOOL)needDrawAtPosition:(const webrtc::DesktopVector&)position; + +// needDrawAtPosition must be checked prior to calling drawWithMousePosition. +// Draw mouse at the new position. +- (void)drawWithMousePosition:(const webrtc::DesktopVector&)position; + +- (void)releaseTexture; + +@end + +#endif // REMOTING_IOS_UI_CURSOR_TEXTURE_H_
\ No newline at end of file diff --git a/remoting/ios/ui/cursor_texture.mm b/remoting/ios/ui/cursor_texture.mm new file mode 100644 index 0000000..9ffa5f7 --- /dev/null +++ b/remoting/ios/ui/cursor_texture.mm @@ -0,0 +1,181 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "remoting/ios/ui/cursor_texture.h" + +@implementation CursorTexture + +- (id)init { + self = [super init]; + if (self) { + _needCursorRedraw = NO; + _cursorDrawnToGL = webrtc::DesktopRect::MakeXYWH(0, 0, 0, 0); + } + return self; +} + +- (const webrtc::DesktopSize&)textureSize { + return _textureSize; +} + +- (void)setTextureSize:(const webrtc::DesktopSize&)size { + if (!_textureSize.equals(size)) { + _textureSize.set(size.width(), size.height()); + _needInitialize = true; + } +} + +- (const webrtc::MouseCursor&)cursor { + return *_cursor.get(); +} + +- (void)setCursor:(webrtc::MouseCursor*)cursor { + _cursor.reset(cursor); + + if (_cursor.get() != NULL && _cursor->image().data()) { + _needCursorRedraw = true; + } +} + +- (void)bindToEffect:(GLKEffectPropertyTexture*)effectProperty { + glGenTextures(1, &_textureId); + [Utility bindTextureForIOS:_textureId]; + + // This is the Cursor layer, and is stamped on top of Desktop as a + // transparent image + effectProperty.target = GLKTextureTarget2D; + effectProperty.name = _textureId; + effectProperty.envMode = GLKTextureEnvModeDecal; + effectProperty.enabled = GL_TRUE; + + [Utility logGLErrorCode:@"CursorTexture bindToTexture"]; + // Release context + glBindTexture(GL_TEXTURE_2D, 0); +} + +- (BOOL)needDrawAtPosition:(const webrtc::DesktopVector&)position { + return (_cursor.get() != NULL && + (_needInitialize || _needCursorRedraw == YES || + _cursorDrawnToGL.left() != position.x() - _cursor->hotspot().x() || + _cursorDrawnToGL.top() != position.y() - _cursor->hotspot().y())); +} + +- (void)drawWithMousePosition:(const webrtc::DesktopVector&)position { + if (_textureSize.height() == 0 && _textureSize.width() == 0) { + return; + } + + [Utility bindTextureForIOS:_textureId]; + + if (_needInitialize) { + glTexImage2D(GL_TEXTURE_2D, + 0, + GL_RGBA, + _textureSize.width(), + _textureSize.height(), + 0, + GL_RGBA, + GL_UNSIGNED_BYTE, + NULL); + + [Utility logGLErrorCode:@"CursorTexture initializeTextureSurfaceWithSize"]; + _needInitialize = false; + } + // When the cursor needs to be redraw in a different spot then we must clear + // the previous area. + + DCHECK([self needDrawAtPosition:position]); + + if (_cursorDrawnToGL.width() > 0 && _cursorDrawnToGL.height() > 0) { + webrtc::BasicDesktopFrame transparentCursor(_cursorDrawnToGL.size()); + + if (transparentCursor.data() != NULL) { + DCHECK(transparentCursor.kBytesPerPixel == + _cursor->image().kBytesPerPixel); + memset(transparentCursor.data(), + 0, + transparentCursor.stride() * transparentCursor.size().height()); + + [Utility drawSubRectToGLFromRectOfSize:_textureSize + subRect:_cursorDrawnToGL + data:transparentCursor.data()]; + + // there is no longer any cursor drawn to screen + _cursorDrawnToGL = webrtc::DesktopRect::MakeXYWH(0, 0, 0, 0); + } + } + + if (_cursor.get() != NULL) { + + CGRect screen = + CGRectMake(0.0, 0.0, _textureSize.width(), _textureSize.height()); + CGRect cursor = CGRectMake(position.x() - _cursor->hotspot().x(), + position.y() - _cursor->hotspot().y(), + _cursor->image().size().width(), + _cursor->image().size().height()); + + if (CGRectContainsRect(screen, cursor)) { + _cursorDrawnToGL = webrtc::DesktopRect::MakeXYWH(cursor.origin.x, + cursor.origin.y, + cursor.size.width, + cursor.size.height); + + [Utility drawSubRectToGLFromRectOfSize:_textureSize + subRect:_cursorDrawnToGL + data:_cursor->image().data()]; + + } else if (CGRectIntersectsRect(screen, cursor)) { + // Some of the cursor falls off screen, need to clip it + CGRect intersection = CGRectIntersection(screen, cursor); + _cursorDrawnToGL = + webrtc::DesktopRect::MakeXYWH(intersection.origin.x, + intersection.origin.y, + intersection.size.width, + intersection.size.height); + + webrtc::BasicDesktopFrame partialCursor(_cursorDrawnToGL.size()); + + if (partialCursor.data()) { + DCHECK(partialCursor.kBytesPerPixel == _cursor->image().kBytesPerPixel); + + uint32_t src_stride = _cursor->image().stride(); + uint32_t dst_stride = partialCursor.stride(); + + uint8_t* source = _cursor->image().data(); + source += abs((static_cast<int32_t>(cursor.origin.y) - + _cursorDrawnToGL.top())) * + src_stride; + source += abs((static_cast<int32_t>(cursor.origin.x) - + _cursorDrawnToGL.left())) * + _cursor->image().kBytesPerPixel; + uint8_t* dst = partialCursor.data(); + + for (uint32_t y = 0; y < _cursorDrawnToGL.height(); y++) { + memcpy(dst, source, dst_stride); + source += src_stride; + dst += dst_stride; + } + + [Utility drawSubRectToGLFromRectOfSize:_textureSize + subRect:_cursorDrawnToGL + data:partialCursor.data()]; + } + } + } + + _needCursorRedraw = false; + [Utility logGLErrorCode:@"CursorTexture drawWithMousePosition"]; + // Release context + glBindTexture(GL_TEXTURE_2D, 0); +} + +- (void)releaseTexture { + glDeleteTextures(1, &_textureId); +} + +@end diff --git a/remoting/ios/ui/desktop_texture.h b/remoting/ios/ui/desktop_texture.h new file mode 100644 index 0000000..28a330c --- /dev/null +++ b/remoting/ios/ui/desktop_texture.h @@ -0,0 +1,38 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef REMOTING_IOS_UI_DESKTOP_TEXTURE_H_ +#define REMOTING_IOS_UI_DESKTOP_TEXTURE_H_ + +#import <Foundation/Foundation.h> +#import <GLKit/GLKit.h> + +#import "remoting/ios/utility.h" +#include "third_party/webrtc/modules/desktop_capture/desktop_geometry.h" + +@interface DesktopTexture : NSObject { + @private + // GL name + GLuint _textureId; + webrtc::DesktopSize _textureSize; + BOOL _needInitialize; +} + +- (const webrtc::DesktopSize&)textureSize; + +- (void)setTextureSize:(const webrtc::DesktopSize&)size; + +// bind this object to an effect's via the effects properties +- (void)bindToEffect:(GLKEffectPropertyTexture*)effectProperty; + +- (BOOL)needDraw; + +// draw a region of the texture +- (void)drawRegion:(GLRegion*)region rect:(CGRect)rect; + +- (void)releaseTexture; + +@end + +#endif // REMOTING_IOS_UI_DESKTOP_TEXTURE_H_
\ No newline at end of file diff --git a/remoting/ios/ui/desktop_texture.mm b/remoting/ios/ui/desktop_texture.mm new file mode 100644 index 0000000..d806dee --- /dev/null +++ b/remoting/ios/ui/desktop_texture.mm @@ -0,0 +1,83 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "remoting/ios/ui/desktop_texture.h" + +@implementation DesktopTexture + +- (const webrtc::DesktopSize&)textureSize { + return _textureSize; +} + +- (void)setTextureSize:(const webrtc::DesktopSize&)size { + if (!_textureSize.equals(size)) { + _textureSize.set(size.width(), size.height()); + _needInitialize = true; + } +} + +- (void)bindToEffect:(GLKEffectPropertyTexture*)effectProperty { + glGenTextures(1, &_textureId); + [Utility bindTextureForIOS:_textureId]; + + // This is the HOST Desktop layer, and each draw will always replace what is + // currently in the draw context + effectProperty.target = GLKTextureTarget2D; + effectProperty.name = _textureId; + effectProperty.envMode = GLKTextureEnvModeReplace; + effectProperty.enabled = GL_TRUE; + + [Utility logGLErrorCode:@"DesktopTexture bindToTexture"]; + // Release context + glBindTexture(GL_TEXTURE_2D, 0); +} + +- (BOOL)needDraw { + return _needInitialize; +} + +- (void)drawRegion:(GLRegion*)region rect:(CGRect)rect { + if (_textureSize.height() == 0 && _textureSize.width() == 0) { + return; + } + + [Utility bindTextureForIOS:_textureId]; + + if (_needInitialize) { + glTexImage2D(GL_TEXTURE_2D, + 0, + GL_RGBA, + _textureSize.width(), + _textureSize.height(), + 0, + GL_RGBA, + GL_UNSIGNED_BYTE, + NULL); + + [Utility logGLErrorCode:@"DesktopTexture initializeTextureSurfaceWithSize"]; + _needInitialize = false; + } + + [Utility drawSubRectToGLFromRectOfSize:_textureSize + subRect:webrtc::DesktopRect::MakeXYWH( + region->offset->x(), + region->offset->y(), + region->image->size().width(), + region->image->size().height()) + data:region->image->data()]; + + [Utility logGLErrorCode:@"DesktopTexture drawRegion"]; + // Release context + glBindTexture(GL_TEXTURE_2D, 0); +} + +- (void)releaseTexture { + glDeleteTextures(1, &_textureId); +} + +@end diff --git a/remoting/ios/ui/help_view_controller.h b/remoting/ios/ui/help_view_controller.h new file mode 100644 index 0000000..036a7b5 --- /dev/null +++ b/remoting/ios/ui/help_view_controller.h @@ -0,0 +1,17 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef REMOTING_IOS_UI_HELP_VIEW_CONTROLLER_H_ +#define REMOTING_IOS_UI_HELP_VIEW_CONTROLLER_H_ + +#import <UIKit/UIKit.h> + +@interface HelpViewController : UIViewController { + @private + IBOutlet UIWebView* _webView; +} + +@end + +#endif // REMOTING_IOS_UI_HELP_VIEW_CONTROLLER_H_
\ No newline at end of file diff --git a/remoting/ios/ui/help_view_controller.mm b/remoting/ios/ui/help_view_controller.mm new file mode 100644 index 0000000..1a3c705 --- /dev/null +++ b/remoting/ios/ui/help_view_controller.mm @@ -0,0 +1,21 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "remoting/ios/ui/help_view_controller.h" + +@implementation HelpViewController + +// Override UIViewController +- (void)viewWillAppear:(BOOL)animated { + [self.navigationController setNavigationBarHidden:NO animated:YES]; + NSString* string = @"https://support.google.com/chrome/answer/1649523"; + NSURL* url = [NSURL URLWithString:string]; + [_webView loadRequest:[NSURLRequest requestWithURL:url]]; +} + +@end diff --git a/remoting/ios/ui/host_list_view_controller.h b/remoting/ios/ui/host_list_view_controller.h new file mode 100644 index 0000000..5f3c1bf --- /dev/null +++ b/remoting/ios/ui/host_list_view_controller.h @@ -0,0 +1,39 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef REMOTING_IOS_UI_HOST_LIST_VIEW_CONTROLLER_H_ +#define REMOTING_IOS_UI_HOST_LIST_VIEW_CONTROLLER_H_ + +#import <UIKit/UIKit.h> +#import <GLKit/GLKit.h> + +#import "host_refresh.h" + +// HostListViewController presents the user with a list of hosts which has +// been shared from other platforms to connect to +@interface HostListViewController : UIViewController<HostRefreshDelegate, + UITableViewDelegate, + UITableViewDataSource> { + @private + IBOutlet UITableView* _tableHostList; + IBOutlet UIButton* _btnAccount; + IBOutlet UIActivityIndicatorView* _refreshActivityIndicator; + IBOutlet UIBarButtonItem* _versionInfo; + + NSArray* _hostList; +} + +@property(nonatomic, readonly) GTMOAuth2Authentication* authorization; +@property(nonatomic, readonly) NSString* userEmail; + +// Triggered by UI 'refresh' button +- (IBAction)btnRefreshHostListPressed:(id)sender; +// Triggered by UI 'log in' button, if user is already logged in then the user +// is logged out and a new session begins by requesting the user to log in, +// possibly with a different account +- (IBAction)btnAccountPressed:(id)sender; + +@end + +#endif // REMOTING_IOS_UI_HOST_LIST_VIEW_CONTROLLER_H_
\ No newline at end of file diff --git a/remoting/ios/ui/host_list_view_controller.mm b/remoting/ios/ui/host_list_view_controller.mm new file mode 100644 index 0000000..7dd7fa2 --- /dev/null +++ b/remoting/ios/ui/host_list_view_controller.mm @@ -0,0 +1,229 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "remoting/ios/ui/host_list_view_controller.h" + +#import "remoting/ios/authorize.h" +#import "remoting/ios/host.h" +#import "remoting/ios/host_cell.h" +#import "remoting/ios/host_refresh.h" +#import "remoting/ios/utility.h" +#import "remoting/ios/ui/host_view_controller.h" + +@interface HostListViewController (Private) +- (void)refreshHostList; +- (void)checkUserAndRefreshHostList; +- (BOOL)isSignedIn; +- (void)signInUser; +// Callback from [Authorize createLoginController...] +- (void)viewController:(UIViewController*)viewController + finishedWithAuth:(GTMOAuth2Authentication*)authResult + error:(NSError*)error; +@end + +@implementation HostListViewController + +@synthesize userEmail = _userEmail; +@synthesize authorization = _authorization; + +// Override default setter +- (void)setAuthorization:(GTMOAuth2Authentication*)authorization { + _authorization = authorization; + if (_authorization.canAuthorize) { + _userEmail = _authorization.userEmail; + } else { + _userEmail = nil; + } + + NSString* userName = _userEmail; + + if (userName == nil) { + userName = @"Not logged in"; + } + + [_btnAccount setTitle:userName forState:UIControlStateNormal]; + + [self refreshHostList]; +} + +// Override UIViewController +// Create google+ service for google authentication and oAuth2 authorization. +- (void)viewDidLoad { + [super viewDidLoad]; + + [_tableHostList setDataSource:self]; + [_tableHostList setDelegate:self]; + + _versionInfo.title = [Utility appVersionNumberDisplayString]; +} + +// Override UIViewController +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + [self.navigationController setNavigationBarHidden:NO animated:NO]; + [self setAuthorization:[Authorize getAnyExistingAuthorization]]; +} + +// Override UIViewController +// Cancel segue when host status is not online +- (BOOL)shouldPerformSegueWithIdentifier:(NSString*)identifier + sender:(id)sender { + if ([identifier isEqualToString:@"ConnectToHost"]) { + Host* host = [self hostAtIndex:[_tableHostList indexPathForCell:sender]]; + if (![host.status isEqualToString:@"ONLINE"]) { + return NO; + } + } + return YES; +} + +// Override UIViewController +// check for segues defined in the storyboard by identifier, and set a few +// properties before transitioning +- (void)prepareForSegue:(UIStoryboardSegue*)segue sender:(id)sender { + if ([segue.identifier isEqualToString:@"ConnectToHost"]) { + // the designationViewController type is defined by the storyboard + HostViewController* hostView = + static_cast<HostViewController*>(segue.destinationViewController); + + NSString* authToken = + [_authorization.parameters valueForKey:@"access_token"]; + + if (authToken == nil) { + authToken = _authorization.authorizationTokenKey; + } + + [hostView setHostDetails:[self hostAtIndex:[_tableHostList + indexPathForCell:sender]] + userEmail:_userEmail + authorizationToken:authToken]; + } +} + +// @protocol HostRefreshDelegate, remember received host list for the table +// view to refresh from +- (void)hostListRefresh:(NSArray*)hostList + errorMessage:(NSString*)errorMessage { + if (hostList != nil) { + _hostList = hostList; + [_tableHostList reloadData]; + } + [_refreshActivityIndicator stopAnimating]; + if (errorMessage != nil) { + [Utility showAlert:@"Host Refresh Failed" message:errorMessage]; + } +} + +// @protocol UITableViewDataSource +// Only have 1 section and it contains all the hosts +- (NSInteger)tableView:(UITableView*)tableView + numberOfRowsInSection:(NSInteger)section { + return [_hostList count]; +} + +// @protocol UITableViewDataSource +// Convert a host entry to a table row +- (HostCell*)tableView:(UITableView*)tableView + cellForRowAtIndexPath:(NSIndexPath*)indexPath { + static NSString* CellIdentifier = @"HostStatusCell"; + + HostCell* cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier + forIndexPath:indexPath]; + + Host* host = [self hostAtIndex:indexPath]; + cell.labelHostName.text = host.hostName; + cell.labelStatus.text = host.status; + + UIColor* statColor = nil; + if ([host.status isEqualToString:@"ONLINE"]) { + statColor = [[UIColor alloc] initWithRed:0 green:1 blue:0 alpha:1]; + } else { + statColor = [[UIColor alloc] initWithRed:1 green:0 blue:0 alpha:1]; + } + [cell.labelStatus setTextColor:statColor]; + + return cell; +} + +// @protocol UITableViewDataSource +// Rows are not editable via standard UI mechanisms +- (BOOL)tableView:(UITableView*)tableView + canEditRowAtIndexPath:(NSIndexPath*)indexPath { + return NO; +} + +- (IBAction)btnRefreshHostListPressed:(id)sender { + [self refreshHostList]; +} + +- (IBAction)btnAccountPressed:(id)sender { + [self signInUser]; +} + +- (void)refreshHostList { + [_refreshActivityIndicator startAnimating]; + _hostList = [[NSArray alloc] init]; + [_tableHostList reloadData]; + + // Insert a small delay so the user is well informed that something is + // happening by the animating activity indicator + [self performSelector:@selector(checkUserAndRefreshHostList) + withObject:nil + afterDelay:.5]; +} + +// Most likely you want to call refreshHostList +- (void)checkUserAndRefreshHostList { + if (![self isSignedIn]) { + [self signInUser]; + } else { + HostRefresh* hostRefresh = [[HostRefresh alloc] init]; + [hostRefresh refreshHostList:_authorization delegate:self]; + } +} + +- (BOOL)isSignedIn { + return (_userEmail != nil); +} + +// Launch the google.com authentication and authorization process. If a user is +// already signed in, begin by signing out so another account could be +// signed in. +- (void)signInUser { + [self presentViewController: + [Authorize createLoginController:self + finishedSelector:@selector(viewController: + finishedWithAuth: + error:)] + animated:YES + completion:nil]; +} + +// Callback from [Authorize createLoginController...] +// Handle completion of the authentication process, and updates the service +// with the new credentials. +- (void)viewController:(UIViewController*)viewController + finishedWithAuth:(GTMOAuth2Authentication*)authResult + error:(NSError*)error { + [viewController.presentingViewController dismissViewControllerAnimated:NO + completion:nil]; + + if (error != nil) { + [Utility showAlert:@"Authentication Error" + message:error.localizedDescription]; + [self setAuthorization:nil]; + } else { + [self setAuthorization:authResult]; + } +} + +- (Host*)hostAtIndex:(NSIndexPath*)indexPath { + return [_hostList objectAtIndex:indexPath.row]; +} + +@end diff --git a/remoting/ios/ui/host_list_view_controller_unittest.mm b/remoting/ios/ui/host_list_view_controller_unittest.mm new file mode 100644 index 0000000..7e75739 --- /dev/null +++ b/remoting/ios/ui/host_list_view_controller_unittest.mm @@ -0,0 +1,90 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "remoting/ios/ui/host_list_view_controller.h" + +#import "base/compiler_specific.h" +#import "testing/gtest_mac.h" + +#import "remoting/ios/host.h" +#import "remoting/ios/host_refresh_test_helper.h" +#import "remoting/ios/ui/host_view_controller.h" + +namespace remoting { + +class HostListViewControllerTest : public ::testing::Test { + protected: + virtual void SetUp() OVERRIDE { + controller_ = [[HostListViewController alloc] init]; + SetHostByCount(1); + } + + void SetHostByCount(int numHosts) { + NSArray* array = + [Host parseListFromJSON:HostRefreshTestHelper::GetHostList(numHosts)]; + RefreshHostList(array); + } + + void SetHostByString(NSString* string) { + NSArray* array = + [Host parseListFromJSON:HostRefreshTestHelper::GetHostList(string)]; + RefreshHostList(array); + } + + void RefreshHostList(NSArray* array) { + [controller_ hostListRefresh:array errorMessage:nil]; + } + + HostListViewController* controller_; +}; + +TEST_F(HostListViewControllerTest, DefaultAuthorization) { + ASSERT_TRUE(controller_.authorization == nil); + + [controller_ viewWillAppear:YES]; + + ASSERT_TRUE(controller_.authorization != nil); +} + +TEST_F(HostListViewControllerTest, hostListRefresh) { + SetHostByCount(2); + ASSERT_EQ(2, [controller_ tableView:nil numberOfRowsInSection:0]); + + SetHostByCount(10); + ASSERT_EQ(10, [controller_ tableView:nil numberOfRowsInSection:0]); +} + +TEST_F(HostListViewControllerTest, + ShouldPerformSegueWithIdentifierOfConnectToHost) { + ASSERT_FALSE([controller_ shouldPerformSegueWithIdentifier:@"ConnectToHost" + sender:nil]); + + NSString* host = HostRefreshTestHelper::GetMultipleHosts(1); + host = [host stringByReplacingOccurrencesOfString:@"TESTING" + withString:@"ONLINE"]; + SetHostByString(host); + ASSERT_TRUE([controller_ shouldPerformSegueWithIdentifier:@"ConnectToHost" + sender:nil]); +} + +TEST_F(HostListViewControllerTest, prepareSegueWithIdentifierOfConnectToHost) { + HostViewController* destination = [[HostViewController alloc] init]; + + ASSERT_NSNE(HostRefreshTestHelper::HostNameTest, destination.host.hostName); + + UIStoryboardSegue* seque = + [[UIStoryboardSegue alloc] initWithIdentifier:@"ConnectToHost" + source:controller_ + destination:destination]; + + [controller_ prepareForSegue:seque sender:nil]; + + ASSERT_NSEQ(HostRefreshTestHelper::HostNameTest, destination.host.hostName); +} + +} // namespace remoting
\ No newline at end of file diff --git a/remoting/ios/ui/host_view_controller.h b/remoting/ios/ui/host_view_controller.h new file mode 100644 index 0000000..6e0f287 --- /dev/null +++ b/remoting/ios/ui/host_view_controller.h @@ -0,0 +1,115 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef REMOTING_IOS_UI_HOST_VIEW_CONTROLLER_H_ +#define REMOTING_IOS_UI_HOST_VIEW_CONTROLLER_H_ + +#import <GLKit/GLKit.h> + +#include "base/memory/scoped_ptr.h" +#include "base/memory/scoped_vector.h" + +#import "remoting/ios/host.h" +#import "remoting/ios/key_input.h" +#import "remoting/ios/utility.h" +#import "remoting/ios/bridge/host_proxy.h" +#import "remoting/ios/ui/desktop_texture.h" +#import "remoting/ios/ui/cursor_texture.h" +#import "remoting/ios/ui/pin_entry_view_controller.h" +#import "remoting/ios/ui/scene_view.h" + +@interface HostViewController + : GLKViewController<PinEntryViewControllerDelegate, + KeyInputDelegate, + // Communication channel from HOST to CLIENT + ClientProxyDelegate, + UIGestureRecognizerDelegate, + UIToolbarDelegate> { + @private + IBOutlet UIActivityIndicatorView* _busyIndicator; + IBOutlet UIButton* _barBtnDisconnect; + IBOutlet UIButton* _barBtnKeyboard; + IBOutlet UIButton* _barBtnNavigation; + IBOutlet UIButton* _barBtnCtrlAltDel; + IBOutlet UILongPressGestureRecognizer* _longPressRecognizer; + IBOutlet UIPanGestureRecognizer* _panRecognizer; + IBOutlet UIPanGestureRecognizer* _threeFingerPanRecognizer; + IBOutlet UIPinchGestureRecognizer* _pinchRecognizer; + IBOutlet UITapGestureRecognizer* _singleTapRecognizer; + IBOutlet UITapGestureRecognizer* _twoFingerTapRecognizer; + IBOutlet UITapGestureRecognizer* _threeFingerTapRecognizer; + IBOutlet UIToolbar* _toolbar; + IBOutlet UIToolbar* _hiddenToolbar; + IBOutlet NSLayoutConstraint* _toolBarYPosition; + IBOutlet NSLayoutConstraint* _hiddenToolbarYPosition; + + KeyInput* _keyEntryView; + NSString* _statusMessage; + + // The GLES2 context being drawn too. + EAGLContext* _context; + + // GLKBaseEffect encapsulates the GL Shaders needed to draw at most two + // textures |_textureIds| given vertex information. The draw surface consists + // of two layers (GL Textures). The bottom layer is the desktop of the HOST. + // The top layer is mostly transparent and is used to overlay the current + // cursor. + GLKBaseEffect* _effect; + + // All the details needed to draw our GL Scene, and our two textures. + SceneView* _scene; + DesktopTexture* _desktop; + CursorTexture* _mouse; + + // List of regions and data that have pending draws to |_desktop| . + ScopedVector<GLRegion> _glRegions; + + // Lock for |_glRegions|, regions are delivered from HOST on another thread, + // and drawn to |_desktop| from a GL Context thread + NSLock* _glBufferLock; + + // Lock for |_mouse.cursor|, cursor updates are delivered from HOST on another + // thread, and drawn to |_mouse| from a GL Context thread + NSLock* _glCursorLock; + + // Communication channel from CLIENT to HOST + HostProxy* _clientToHostProxy; +} + +// Details for the host and user +@property(nonatomic, readonly) Host* host; +@property(nonatomic, readonly) NSString* userEmail; +@property(nonatomic, readonly) NSString* userAuthorizationToken; + +- (void)setHostDetails:(Host*)host + userEmail:(NSString*)userEmail + authorizationToken:(NSString*)authorizationToken; + +// Zoom in/out +- (IBAction)pinchGestureTriggered:(UIPinchGestureRecognizer*)sender; +// Left mouse click, moves cursor +- (IBAction)tapGestureTriggered:(UITapGestureRecognizer*)sender; +// Scroll the view in 2d +- (IBAction)panGestureTriggered:(UIPanGestureRecognizer*)sender; +// Right mouse click and drag, moves cursor +- (IBAction)longPressGestureTriggered:(UILongPressGestureRecognizer*)sender; +// Right mouse click +- (IBAction)twoFingerTapGestureTriggered:(UITapGestureRecognizer*)sender; +// Middle mouse click +- (IBAction)threeFingerTapGestureTriggered:(UITapGestureRecognizer*)sender; +// Show hidden menus. Swipe up for keyboard, swipe down for navigation menu +- (IBAction)threeFingerPanGestureTriggered:(UIPanGestureRecognizer*)sender; + +// Do navigation 'back' +- (IBAction)barBtnNavigationBackPressed:(id)sender; +// Show keyboard +- (IBAction)barBtnKeyboardPressed:(id)sender; +// Trigger |_toolbar| animation +- (IBAction)barBtnToolBarHidePressed:(id)sender; +// Send Keys for ctrl, atl, delete +- (IBAction)barBtnCtrlAltDelPressed:(id)sender; + +@end + +#endif // REMOTING_IOS_UI_HOST_VIEW_CONTROLLER_H_ diff --git a/remoting/ios/ui/host_view_controller.mm b/remoting/ios/ui/host_view_controller.mm new file mode 100644 index 0000000..d87e767 --- /dev/null +++ b/remoting/ios/ui/host_view_controller.mm @@ -0,0 +1,676 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "remoting/ios/ui/host_view_controller.h" + +#include <OpenGLES/ES2/gl.h> + +#import "remoting/ios/data_store.h" + +namespace { + +// TODO (aboone) Some of the layout is not yet set in stone, so variables have +// been used to position and turn items on and off. Eventually these may be +// stabilized and removed. + +// Scroll speed multiplier for mouse wheel +const static int kMouseWheelSensitivity = 20; + +// Area the navigation bar consumes when visible in pixels +const static int kTopMargin = 20; +// Area the footer consumes when visible (no footer currently exists) +const static int kBottomMargin = 0; + +} // namespace + +@interface HostViewController (Private) +- (void)setupGL; +- (void)tearDownGL; +- (void)goBack; +- (void)updateLabels; +- (BOOL)isToolbarHidden; +- (void)updatePanVelocityShouldCancel:(bool)canceled; +- (void)orientationChanged:(NSNotification*)note; +- (void)applySceneChange:(CGPoint)translation scaleBy:(float)ratio; +- (void)showToolbar:(BOOL)visible; +@end + +@implementation HostViewController + +@synthesize host = _host; +@synthesize userEmail = _userEmail; +@synthesize userAuthorizationToken = _userAuthorizationToken; + +// Override UIViewController +- (void)viewDidLoad { + [super viewDidLoad]; + + _context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; + DCHECK(_context); + static_cast<GLKView*>(self.view).context = _context; + + [_keyEntryView setDelegate:self]; + + _clientToHostProxy = [[HostProxy alloc] init]; + + // There is a 1 pixel top border which is actually the background not being + // covered. There is no obvious way to remove that pixel 'border'. Set the + // background clear, and also reset the backgroundimage and shawdowimage to an + // empty image any time the view is moved. + _hiddenToolbar.backgroundColor = [UIColor clearColor]; + if ([_hiddenToolbar respondsToSelector:@selector(setBackgroundImage: + forToolbarPosition: + barMetrics:)]) { + [_hiddenToolbar setBackgroundImage:[UIImage new] + forToolbarPosition:UIToolbarPositionAny + barMetrics:UIBarMetricsDefault]; + } + if ([_hiddenToolbar + respondsToSelector:@selector(setShadowImage:forToolbarPosition:)]) { + [_hiddenToolbar setShadowImage:[UIImage new] + forToolbarPosition:UIToolbarPositionAny]; + } + + // 1/2 circle rotation for an icon ~ 180 degree ~ 1 radian + _barBtnNavigation.imageView.transform = CGAffineTransformMakeRotation(M_PI); + + _scene = [[SceneView alloc] init]; + [_scene setMarginsFromLeft:0 right:0 top:kTopMargin bottom:kBottomMargin]; + _desktop = [[DesktopTexture alloc] init]; + _mouse = [[CursorTexture alloc] init]; + + _glBufferLock = [[NSLock alloc] init]; + _glCursorLock = [[NSLock alloc] init]; + + [_scene + setContentSize:[Utility getOrientatedSize:self.view.bounds.size + shouldWidthBeLongestSide:[Utility isInLandscapeMode]]]; + [self showToolbar:YES]; + [self updateLabels]; + + [self setupGL]; + + [_singleTapRecognizer requireGestureRecognizerToFail:_twoFingerTapRecognizer]; + [_twoFingerTapRecognizer + requireGestureRecognizerToFail:_threeFingerTapRecognizer]; + //[_pinchRecognizer requireGestureRecognizerToFail:_twoFingerTapRecognizer]; + [_panRecognizer requireGestureRecognizerToFail:_singleTapRecognizer]; + [_threeFingerPanRecognizer + requireGestureRecognizerToFail:_threeFingerTapRecognizer]; + //[_pinchRecognizer requireGestureRecognizerToFail:_threeFingerPanRecognizer]; + + // Subscribe to changes in orientation + [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; + [[NSNotificationCenter defaultCenter] + addObserver:self + selector:@selector(orientationChanged:) + name:UIDeviceOrientationDidChangeNotification + object:[UIDevice currentDevice]]; +} + +- (void)setupGL { + [EAGLContext setCurrentContext:_context]; + + _effect = [[GLKBaseEffect alloc] init]; + [Utility logGLErrorCode:@"setupGL begin"]; + + // Initialize each texture + [_desktop bindToEffect:[_effect texture2d0]]; + [_mouse bindToEffect:[_effect texture2d1]]; + [Utility logGLErrorCode:@"setupGL textureComplete"]; +} + +// Override UIViewController +- (void)viewDidUnload { + [super viewDidUnload]; + [self tearDownGL]; + + if ([EAGLContext currentContext] == _context) { + [EAGLContext setCurrentContext:nil]; + } + _context = nil; +} + +- (void)tearDownGL { + [EAGLContext setCurrentContext:_context]; + + // Release Textures + [_desktop releaseTexture]; + [_mouse releaseTexture]; +} + +// Override UIViewController +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:NO]; + [self.navigationController setNavigationBarHidden:YES animated:YES]; + [self updateLabels]; + if (![_clientToHostProxy isConnected]) { + [_busyIndicator startAnimating]; + + [_clientToHostProxy connectToHost:_userEmail + authToken:_userAuthorizationToken + jabberId:_host.jabberId + hostId:_host.hostId + publicKey:_host.publicKey + delegate:self]; + } +} + +// Override UIViewController +- (void)viewWillDisappear:(BOOL)animated { + [super viewWillDisappear:NO]; + NSArray* viewControllers = self.navigationController.viewControllers; + if (viewControllers.count > 1 && + [viewControllers objectAtIndex:viewControllers.count - 2] == self) { + // View is disappearing because a new view controller was pushed onto the + // stack + } else if ([viewControllers indexOfObject:self] == NSNotFound) { + // View is disappearing because it was popped from the stack + [_clientToHostProxy disconnectFromHost]; + } +} + +// "Back" goes to the root controller for now +- (void)goBack { + [self.navigationController popToRootViewControllerAnimated:YES]; +} + +// @protocol PinEntryViewControllerDelegate +// Return the PIN input by User, indicate if the User should be prompted to +// re-enter the pin in the future +- (void)connectToHostWithPin:(UIViewController*)controller + hostPin:(NSString*)hostPin + shouldPrompt:(BOOL)shouldPrompt { + const HostPreferences* hostPrefs = + [[DataStore sharedStore] getHostForId:_host.hostId]; + if (!hostPrefs) { + hostPrefs = [[DataStore sharedStore] createHost:_host.hostId]; + } + if (hostPrefs) { + hostPrefs.hostPin = hostPin; + hostPrefs.askForPin = [NSNumber numberWithBool:shouldPrompt]; + [[DataStore sharedStore] saveChanges]; + } + + [[controller presentingViewController] dismissViewControllerAnimated:NO + completion:nil]; + + [_clientToHostProxy authenticationResponse:hostPin createPair:!shouldPrompt]; +} + +// @protocol PinEntryViewControllerDelegate +// Returns if the user canceled while entering their PIN +- (void)cancelledConnectToHostWithPin:(UIViewController*)controller { + [[controller presentingViewController] dismissViewControllerAnimated:NO + completion:nil]; + + [self goBack]; +} + +- (void)setHostDetails:(Host*)host + userEmail:(NSString*)userEmail + authorizationToken:(NSString*)authorizationToken { + DCHECK(host.jabberId); + _host = host; + _userEmail = userEmail; + _userAuthorizationToken = authorizationToken; +} + +// Set various labels on the form for iPad vs iPhone, and orientation +- (void)updateLabels { + if (![Utility isPad] && ![Utility isInLandscapeMode]) { + [_barBtnDisconnect setTitle:@"" forState:(UIControlStateNormal)]; + [_barBtnCtrlAltDel setTitle:@"CtAtD" forState:UIControlStateNormal]; + } else { + [_barBtnCtrlAltDel setTitle:@"Ctrl+Alt+Del" forState:UIControlStateNormal]; + + NSString* hostStatus = _host.hostName; + if (![_statusMessage isEqual:@"Connected"]) { + hostStatus = [NSString + stringWithFormat:@"%@ - %@", _host.hostName, _statusMessage]; + } + [_barBtnDisconnect setTitle:hostStatus forState:UIControlStateNormal]; + } + + [_barBtnDisconnect sizeToFit]; + [_barBtnCtrlAltDel sizeToFit]; +} + +// Resize the view of the desktop - Zoom in/out. This can occur during a Pan. +- (IBAction)pinchGestureTriggered:(UIPinchGestureRecognizer*)sender { + if ([sender state] == UIGestureRecognizerStateChanged) { + [self applySceneChange:CGPointMake(0.0, 0.0) scaleBy:sender.scale]; + + sender.scale = 1.0; // reset scale so next iteration is a relative ratio + } +} + +- (IBAction)tapGestureTriggered:(UITapGestureRecognizer*)sender { + if ([_scene containsTouchPoint:[sender locationInView:self.view]]) { + [Utility leftClickOn:_clientToHostProxy at:_scene.mousePosition]; + } +} + +// Change position of scene. This can occur during a pinch or longpress. +// Or perform a Mouse Wheel Scroll +- (IBAction)panGestureTriggered:(UIPanGestureRecognizer*)sender { + CGPoint translation = [sender translationInView:self.view]; + + // If we start with 2 touches, and the pinch gesture is not in progress yet, + // then disable it, so mouse scrolling and zoom do not occur at the same + // time. + if ([sender numberOfTouches] == 2 && + [sender state] == UIGestureRecognizerStateBegan && + !(_pinchRecognizer.state == UIGestureRecognizerStateBegan || + _pinchRecognizer.state == UIGestureRecognizerStateChanged)) { + _pinchRecognizer.enabled = NO; + } + + if (!_pinchRecognizer.enabled) { + // Began with 2 touches, so this is a scroll event + translation.x *= kMouseWheelSensitivity; + translation.y *= kMouseWheelSensitivity; + [Utility mouseScroll:_clientToHostProxy + at:_scene.mousePosition + delta:webrtc::DesktopVector(translation.x, translation.y)]; + } else { + // Did not begin with 2 touches, doing a pan event + if ([sender state] == UIGestureRecognizerStateChanged) { + CGPoint translation = [sender translationInView:self.view]; + + [self applySceneChange:translation scaleBy:1.0]; + + } else if ([sender state] == UIGestureRecognizerStateEnded) { + // After user removes their fingers from the screen, apply an acceleration + // effect + [_scene setPanVelocity:[sender velocityInView:self.view]]; + } + } + + // Finished the event chain + if (!([sender state] == UIGestureRecognizerStateBegan || + [sender state] == UIGestureRecognizerStateChanged)) { + _pinchRecognizer.enabled = YES; + } + + // Reset translation so next iteration is relative. + [sender setTranslation:CGPointZero inView:self.view]; +} + +// Click-Drag mouse operation. This can occur during a Pan. +- (IBAction)longPressGestureTriggered:(UILongPressGestureRecognizer*)sender { + + if ([sender state] == UIGestureRecognizerStateBegan) { + [_clientToHostProxy mouseAction:_scene.mousePosition + wheelDelta:webrtc::DesktopVector(0, 0) + whichButton:1 + buttonDown:YES]; + } else if (!([sender state] == UIGestureRecognizerStateBegan || + [sender state] == UIGestureRecognizerStateChanged)) { + [_clientToHostProxy mouseAction:_scene.mousePosition + wheelDelta:webrtc::DesktopVector(0, 0) + whichButton:1 + buttonDown:NO]; + } +} + +- (IBAction)twoFingerTapGestureTriggered:(UITapGestureRecognizer*)sender { + if ([_scene containsTouchPoint:[sender locationInView:self.view]]) { + [Utility rightClickOn:_clientToHostProxy at:_scene.mousePosition]; + } +} + +- (IBAction)threeFingerTapGestureTriggered:(UITapGestureRecognizer*)sender { + + if ([_scene containsTouchPoint:[sender locationInView:self.view]]) { + [Utility middleClickOn:_clientToHostProxy at:_scene.mousePosition]; + } +} + +- (IBAction)threeFingerPanGestureTriggered:(UIPanGestureRecognizer*)sender { + if ([sender state] == UIGestureRecognizerStateChanged) { + CGPoint translation = [sender translationInView:self.view]; + if (translation.y > 0) { + // Swiped down + [self showToolbar:YES]; + } else if (translation.y < 0) { + // Swiped up + [_keyEntryView becomeFirstResponder]; + [self updateLabels]; + } + [sender setTranslation:CGPointZero inView:self.view]; + } +} + +- (IBAction)barBtnNavigationBackPressed:(id)sender { + [self goBack]; +} + +- (IBAction)barBtnKeyboardPressed:(id)sender { + if ([_keyEntryView isFirstResponder]) { + [_keyEntryView endEditing:NO]; + } else { + [_keyEntryView becomeFirstResponder]; + } + + [self updateLabels]; +} + +- (IBAction)barBtnToolBarHidePressed:(id)sender { + [self showToolbar:[self isToolbarHidden]]; // Toolbar is either on + // screen or off screen +} + +- (IBAction)barBtnCtrlAltDelPressed:(id)sender { + [_keyEntryView ctrlAltDel]; +} + +// Override UIResponder +// When any gesture begins, remove any acceleration effects currently being +// applied. Example, Panning view and let it shoot off into the distance, but +// then I see a spot I'm interested in so I will touch to capture that locations +// focus. +- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event { + [self updatePanVelocityShouldCancel:YES]; + [super touchesBegan:touches withEvent:event]; +} + +// @protocol UIGestureRecognizerDelegate +// Allow panning and zooming to occur simultaneously. +// Allow panning and long press to occur simultaneously. +// Pinch requires 2 touches, and long press requires a single touch, so they are +// mutually exclusive regardless of if panning is the initiating gesture +- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer + shouldRecognizeSimultaneouslyWithGestureRecognizer: + (UIGestureRecognizer*)otherGestureRecognizer { + if (gestureRecognizer == _pinchRecognizer || + (gestureRecognizer == _panRecognizer)) { + if (otherGestureRecognizer == _pinchRecognizer || + otherGestureRecognizer == _panRecognizer) { + return YES; + } + } + + if (gestureRecognizer == _longPressRecognizer || + gestureRecognizer == _panRecognizer) { + if (otherGestureRecognizer == _longPressRecognizer || + otherGestureRecognizer == _panRecognizer) { + return YES; + } + } + return NO; +} + +// @protocol ClientControllerDelegate +// Prompt the user for their PIN if pairing has not already been established +- (void)requestHostPin:(BOOL)pairingSupported { + BOOL requestPin = YES; + const HostPreferences* hostPrefs = + [[DataStore sharedStore] getHostForId:_host.hostId]; + if (hostPrefs) { + requestPin = [hostPrefs.askForPin boolValue]; + if (!requestPin) { + if (hostPrefs.hostPin == nil || hostPrefs.hostPin.length == 0) { + requestPin = YES; + } + } + } + if (requestPin == YES) { + PinEntryViewController* pinEntry = [[PinEntryViewController alloc] init]; + [pinEntry setDelegate:self]; + [pinEntry setHostName:_host.hostName]; + [pinEntry setShouldPrompt:YES]; + [pinEntry setPairingSupported:pairingSupported]; + + [self presentViewController:pinEntry animated:YES completion:nil]; + } else { + [_clientToHostProxy authenticationResponse:hostPrefs.hostPin + createPair:pairingSupported]; + } +} + +// @protocol ClientControllerDelegate +// Occurs when a connection to a HOST is established successfully +- (void)connected { + // Everything is good, nothing to do +} + +// @protocol ClientControllerDelegate +- (void)connectionStatus:(NSString*)statusMessage { + _statusMessage = statusMessage; + + if ([_statusMessage isEqual:@"Connection closed"]) { + [self goBack]; + } else { + [self updateLabels]; + } +} + +// @protocol ClientControllerDelegate +// Occurs when a connection to a HOST has failed +- (void)connectionFailed:(NSString*)errorMessage { + [_busyIndicator stopAnimating]; + NSString* errorMsg; + if ([_clientToHostProxy isConnected]) { + errorMsg = @"Lost Connection"; + } else { + errorMsg = @"Unable to connect"; + } + [Utility showAlert:errorMsg message:errorMessage]; + [self goBack]; +} + +// @protocol ClientControllerDelegate +// Copy the updated regions to a backing store to be consumed by the GL Context +// on a different thread. A region is stored in disjoint memory locations, and +// must be transformed to a contiguous memory buffer for a GL Texture write. +// /-----\ +// | 2-4| This buffer is 5x3 bytes large, a region exists at bytes 2 to 4 and +// | 7-9| bytes 7 to 9. The region is extracted to a new contiguous buffer +// | | of 6 bytes in length. +// \-----/ +// More than 1 region may exist in the frame from each call, in which case a new +// buffer is created for each region +- (void)applyFrame:(const webrtc::DesktopSize&)size + stride:(NSInteger)stride + data:(uint8_t*)data + regions:(const std::vector<webrtc::DesktopRect>&)regions { + [_glBufferLock lock]; // going to make changes to |_glRegions| + + if (!_scene.frameSize.equals(size)) { + // When this is the initial frame, the busyIndicator is still spinning. Now + // is a good time to stop it. + [_busyIndicator stopAnimating]; + + // If the |_toolbar| is still showing, hide it. + [self showToolbar:NO]; + [_scene setContentSize: + [Utility getOrientatedSize:self.view.bounds.size + shouldWidthBeLongestSide:[Utility isInLandscapeMode]]]; + [_scene setFrameSize:size]; + [_desktop setTextureSize:size]; + [_mouse setTextureSize:size]; + } + + uint32_t src_stride = stride; + + for (uint32_t i = 0; i < regions.size(); i++) { + scoped_ptr<GLRegion> region(new GLRegion()); + + if (region.get()) { + webrtc::DesktopRect rect = regions.at(i); + + webrtc::DesktopSize(rect.width(), rect.height()); + region->offset.reset(new webrtc::DesktopVector(rect.left(), rect.top())); + region->image.reset(new webrtc::BasicDesktopFrame( + webrtc::DesktopSize(rect.width(), rect.height()))); + + if (region->image->data()) { + uint32_t bytes_per_row = + region->image->kBytesPerPixel * region->image->size().width(); + + uint32_t offset = + (src_stride * region->offset->y()) + // row + (region->offset->x() * region->image->kBytesPerPixel); // column + + uint8_t* src_buffer = data + offset; + uint8_t* dst_buffer = region->image->data(); + + // row by row copy + for (uint32_t j = 0; j < region->image->size().height(); j++) { + memcpy(dst_buffer, src_buffer, bytes_per_row); + dst_buffer += bytes_per_row; + src_buffer += src_stride; + } + _glRegions.push_back(region.release()); + } + } + } + [_glBufferLock unlock]; // done making changes to |_glRegions| +} + +// @protocol ClientControllerDelegate +// Copy the delivered cursor to a backing store to be consumed by the GL Context +// on a different thread. Note only the most recent cursor is of importance, +// discard the previous cursor. +- (void)applyCursor:(const webrtc::DesktopSize&)size + hotspot:(const webrtc::DesktopVector&)hotspot + cursorData:(uint8_t*)data { + + [_glCursorLock lock]; // going to make changes to |_cursor| + + // MouseCursor takes ownership of DesktopFrame + [_mouse setCursor:new webrtc::MouseCursor(new webrtc::BasicDesktopFrame(size), + hotspot)]; + + if (_mouse.cursor.image().data()) { + memcpy(_mouse.cursor.image().data(), + data, + size.width() * size.height() * _mouse.cursor.image().kBytesPerPixel); + } else { + [_mouse setCursor:NULL]; + } + + [_glCursorLock unlock]; // done making changes to |_cursor| +} + +// @protocol GLKViewDelegate +// There is quite a few gotchas involved in working with this function. For +// sanity purposes, I've just assumed calls to the function are on a different +// thread which I've termed GL Context. Any variables consumed by this function +// should be thread safe. +// +// Clear Screen, update desktop, update cursor, define position, and finally +// present +// +// In general, avoid expensive work in this function to maximize frame rate. +- (void)glkView:(GLKView*)view drawInRect:(CGRect)rect { + [self updatePanVelocityShouldCancel:NO]; + + // Clear to black, to give the background color + glClearColor(0.0, 0.0, 0.0, 1.0); + glClear(GL_COLOR_BUFFER_BIT); + + [Utility logGLErrorCode:@"drawInRect bindBuffer"]; + + if (_glRegions.size() > 0 || [_desktop needDraw]) { + [_glBufferLock lock]; + + for (uint32_t i = 0; i < _glRegions.size(); i++) { + // |_glRegions[i].data| has been properly ordered by [self applyFrame] + [_desktop drawRegion:_glRegions[i] rect:rect]; + } + + _glRegions.clear(); + [_glBufferLock unlock]; + } + + if ([_mouse needDrawAtPosition:_scene.mousePosition]) { + [_glCursorLock lock]; + [_mouse drawWithMousePosition:_scene.mousePosition]; + [_glCursorLock unlock]; + } + + [_effect transform].projectionMatrix = _scene.projectionMatrix; + [_effect transform].modelviewMatrix = _scene.modelViewMatrix; + [_effect prepareToDraw]; + + [Utility logGLErrorCode:@"drawInRect prepareToDrawComplete"]; + + [_scene draw]; +} + +// @protocol KeyInputDelegate +- (void)keyboardDismissed { + [self updateLabels]; +} + +// @protocol KeyInputDelegate +// Send keyboard input to HOST +- (void)keyboardActionKeyCode:(uint32_t)keyPressed isKeyDown:(BOOL)keyDown { + [_clientToHostProxy keyboardAction:keyPressed keyDown:keyDown]; +} + +- (BOOL)isToolbarHidden { + return (_toolbar.frame.origin.y < 0); +} + +// Update the scene acceleration vector +- (void)updatePanVelocityShouldCancel:(bool)canceled { + if (canceled) { + [_scene setPanVelocity:CGPointMake(0, 0)]; + } + BOOL inMotion = [_scene tickPanVelocity]; + + _singleTapRecognizer.enabled = !inMotion; + _longPressRecognizer.enabled = !inMotion; +} + +- (void)applySceneChange:(CGPoint)translation scaleBy:(float)ratio { + [_scene panAndZoom:translation scaleBy:ratio]; + // Notify HOST that the mouse moved + [Utility moveMouse:_clientToHostProxy at:_scene.mousePosition]; +} + +// Callback from NSNotificationCenter when the User changes orientation +- (void)orientationChanged:(NSNotification*)note { + [_scene + setContentSize:[Utility getOrientatedSize:self.view.bounds.size + shouldWidthBeLongestSide:[Utility isInLandscapeMode]]]; + [self showToolbar:![self isToolbarHidden]]; + [self updateLabels]; +} + +// Animate |_toolbar| by moving it on or offscreen +- (void)showToolbar:(BOOL)visible { + CGRect frame = [_toolbar frame]; + + _toolBarYPosition.constant = -frame.size.height; + int topOffset = kTopMargin; + + if (visible) { + topOffset += frame.size.height; + _toolBarYPosition.constant = kTopMargin; + } + + _hiddenToolbarYPosition.constant = topOffset; + [_scene setMarginsFromLeft:0 right:0 top:topOffset bottom:kBottomMargin]; + + // hidden when |_toolbar| is |visible| + _hiddenToolbar.hidden = (visible == YES); + + [UIView animateWithDuration:0.5 + animations:^{ [self.view layoutIfNeeded]; } + completion:^(BOOL finished) {// Nothing to do for now + }]; + + // Center view if needed for any reason. + // Specificallly, if the top anchor is active. + [self applySceneChange:CGPointMake(0.0, 0.0) scaleBy:1.0]; +} +@end diff --git a/remoting/ios/ui/pin_entry_view_controller.h b/remoting/ios/ui/pin_entry_view_controller.h new file mode 100644 index 0000000..aaf854a --- /dev/null +++ b/remoting/ios/ui/pin_entry_view_controller.h @@ -0,0 +1,49 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef REMOTING_IOS_UI_PIN_ENTRY_VIEW_CONTROLLER_H_ +#define REMOTING_IOS_UI_PIN_ENTRY_VIEW_CONTROLLER_H_ + +#import <UIKit/UIKit.h> + +// Contract to handle finalization for Pin Prompt +@protocol PinEntryViewControllerDelegate<NSObject> + +// Returns with user's Pin. Pin has not been validated with the server yet. +// |shouldPrompt| indicates whether a prompt should be needed for the next login +// attempt with this host. +- (void)connectToHostWithPin:(UIViewController*)controller + hostPin:(NSString*)hostPin + shouldPrompt:(BOOL)shouldPrompt; + +// Returns when the user has cancelled the input, effectively closing the +// connection attempt. +- (void)cancelledConnectToHostWithPin:(UIViewController*)controller; + +@end + +// Dialog for user's Pin input. If a host has |pairingSupported| then user has +// the option to save a token for authentication. +@interface PinEntryViewController : UIViewController<UITextFieldDelegate> { + @private + IBOutlet UIView* _controlView; + IBOutlet UIButton* _cancelButton; + IBOutlet UIButton* _connectButton; + IBOutlet UILabel* _host; + IBOutlet UISwitch* _switchAskAgain; + IBOutlet UILabel* _shouldSavePin; + IBOutlet UITextField* _hostPin; +} + +@property(weak, nonatomic) id<PinEntryViewControllerDelegate> delegate; +@property(nonatomic, copy) NSString* hostName; +@property(nonatomic) BOOL shouldPrompt; +@property(nonatomic) BOOL pairingSupported; + +- (IBAction)buttonCancelClicked:(id)sender; +- (IBAction)buttonConnectClicked:(id)sender; + +@end + +#endif // REMOTING_IOS_UI_PIN_ENTRY_VIEW_CONTROLLER_H_
\ No newline at end of file diff --git a/remoting/ios/ui/pin_entry_view_controller.mm b/remoting/ios/ui/pin_entry_view_controller.mm new file mode 100644 index 0000000..9a37677 --- /dev/null +++ b/remoting/ios/ui/pin_entry_view_controller.mm @@ -0,0 +1,71 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "remoting/ios/ui/pin_entry_view_controller.h" + +#import "remoting/ios/utility.h" + +@implementation PinEntryViewController + +@synthesize delegate = _delegate; +@synthesize shouldPrompt = _shouldPrompt; +@synthesize pairingSupported = _pairingSupported; + +// Override UIViewController +- (id)initWithNibName:(NSString*)nibNameOrNil bundle:(NSBundle*)nibBundleOrNil { + // NibName is the * part of your *.xib file + + if ([Utility isPad]) { + self = [super initWithNibName:@"pin_entry_view_controller_ipad" bundle:nil]; + } else { + self = + [super initWithNibName:@"pin_entry_view_controller_iphone" bundle:nil]; + } + if (self) { + // Custom initialization + } + return self; +} + +// Override UIViewController +// Controls are not created immediately, properties must be set before the form +// is displayed +- (void)viewWillAppear:(BOOL)animated { + _host.text = _hostName; + + [_switchAskAgain setOn:!_shouldPrompt]; + + // TODO (aboone) The switch is being hidden in all cases, this functionality + // is not scheduled for QA yet. + // if (!_pairingSupported) { + _switchAskAgain.hidden = YES; + _shouldSavePin.hidden = YES; + _switchAskAgain.enabled = NO; + //} + [_hostPin becomeFirstResponder]; +} + +// @protocol UITextFieldDelegate, called when the 'enter' key is pressed +- (BOOL)textFieldShouldReturn:(UITextField*)textField { + [textField resignFirstResponder]; + if (textField == _hostPin) + [self buttonConnectClicked:self]; + return YES; +} + +- (IBAction)buttonCancelClicked:(id)sender { + [_delegate cancelledConnectToHostWithPin:self]; +} + +- (IBAction)buttonConnectClicked:(id)sender { + [_delegate connectToHostWithPin:self + hostPin:_hostPin.text + shouldPrompt:!_switchAskAgain.isOn]; +} + +@end diff --git a/remoting/ios/ui/pin_entry_view_controller_ipad.xib b/remoting/ios/ui/pin_entry_view_controller_ipad.xib new file mode 100644 index 0000000..6846c81 --- /dev/null +++ b/remoting/ios/ui/pin_entry_view_controller_ipad.xib @@ -0,0 +1,103 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.iPad.XIB" version="3.0" toolsVersion="5053" systemVersion="13C64" targetRuntime="iOS.CocoaTouch.iPad" propertyAccessControl="none" useAutolayout="YES"> + <dependencies> + <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="3733"/> + </dependencies> + <objects> + <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="PinEntryViewController"> + <connections> + <outlet property="_cancelButton" destination="BKJ-ke-HyF" id="zYI-hk-6kg"/> + <outlet property="_connectButton" destination="Tf7-gd-ldS" id="xQf-zj-uJ9"/> + <outlet property="_controlView" destination="Cqg-ut-ayj" id="H7q-tt-WHK"/> + <outlet property="_host" destination="qjI-DX-ED7" id="vcr-tb-2Fe"/> + <outlet property="_hostPin" destination="c2o-Fx-DQH" id="A7i-R4-95W"/> + <outlet property="_shouldSavePin" destination="ZLq-E5-uGf" id="ade-Tz-kSo"/> + <outlet property="_switchAskAgain" destination="Bl9-pn-tsA" id="BxE-lI-u6t"/> + <outlet property="view" destination="2" id="3"/> + </connections> + </placeholder> + <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/> + <view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="2"> + <rect key="frame" x="0.0" y="0.0" width="768" height="1024"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + <subviews> + <view contentMode="scaleAspectFit" translatesAutoresizingMaskIntoConstraints="NO" id="Cqg-ut-ayj"> + <rect key="frame" x="192" y="127" width="384" height="205"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + <subviews> + <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="<hostname>" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="qjI-DX-ED7"> + <rect key="frame" x="142" y="20" width="225" height="21"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + <fontDescription key="fontDescription" type="system" pointSize="17"/> + <color key="textColor" cocoaTouchSystemColor="darkTextColor"/> + <nil key="highlightedColor"/> + </label> + <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Enter the host's PIN" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="VOw-pP-wKz"> + <rect key="frame" x="20" y="49" width="239" height="21"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + <fontDescription key="fontDescription" type="system" pointSize="17"/> + <nil key="highlightedColor"/> + </label> + <textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="PIN" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="c2o-Fx-DQH"> + <rect key="frame" x="20" y="78" width="351" height="30"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + <fontDescription key="fontDescription" type="system" pointSize="14"/> + <textInputTraits key="textInputTraits" autocorrectionType="no" keyboardType="numberPad" returnKeyType="go" secureTextEntry="YES"/> + <connections> + <outlet property="delegate" destination="-1" id="jaH-uT-ejT"/> + </connections> + </textField> + <switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Bl9-pn-tsA"> + <rect key="frame" x="20" y="116" width="51" height="31"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + </switch> + <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Don't ask in the future" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ZLq-E5-uGf"> + <rect key="frame" x="88" y="121" width="139" height="21"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + <fontDescription key="fontDescription" type="system" pointSize="12"/> + <nil key="highlightedColor"/> + </label> + <button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="BKJ-ke-HyF"> + <rect key="frame" x="20" y="155" width="160" height="30"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + <state key="normal" title="Cancel"> + <color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/> + </state> + <connections> + <action selector="buttonCancelClicked:" destination="-1" eventType="touchUpInside" id="oxw-Oo-Npc"/> + </connections> + </button> + <button opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="Tf7-gd-ldS"> + <rect key="frame" x="207" y="155" width="160" height="30"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + <state key="normal" title="Connect"> + <color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/> + </state> + <connections> + <action selector="buttonConnectClicked:" destination="-1" eventType="touchUpInside" id="2Fi-pu-xH9"/> + </connections> + </button> + <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Authenticate to" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Abs-bA-a7i"> + <rect key="frame" x="20" y="20" width="124" height="21"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + <fontDescription key="fontDescription" type="system" pointSize="17"/> + <nil key="highlightedColor"/> + </label> + </subviews> + <color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/> + <constraints> + <constraint firstAttribute="height" constant="205" id="Z7v-Xg-IQu"/> + <constraint firstAttribute="width" constant="384" id="clt-7j-cb7"/> + </constraints> + </view> + </subviews> + <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/> + <constraints> + <constraint firstAttribute="centerX" secondItem="Cqg-ut-ayj" secondAttribute="centerX" id="Gs7-u0-yxZ"/> + <constraint firstItem="Cqg-ut-ayj" firstAttribute="top" secondItem="2" secondAttribute="top" constant="127" id="ivh-U7-Oc1"/> + </constraints> + <simulatedStatusBarMetrics key="simulatedStatusBarMetrics" statusBarStyle="lightContent"/> + <simulatedScreenMetrics key="simulatedDestinationMetrics"/> + </view> + </objects> +</document> diff --git a/remoting/ios/ui/pin_entry_view_controller_iphone.xib b/remoting/ios/ui/pin_entry_view_controller_iphone.xib new file mode 100644 index 0000000..f7bb2a2 --- /dev/null +++ b/remoting/ios/ui/pin_entry_view_controller_iphone.xib @@ -0,0 +1,113 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="5053" systemVersion="13C64" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES"> + <dependencies> + <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="3733"/> + </dependencies> + <objects> + <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="PinEntryViewController"> + <connections> + <outlet property="_cancelButton" destination="2Vw-9K-cVY" id="1wb-If-df2"/> + <outlet property="_connectButton" destination="NLw-jM-z2p" id="q5b-w6-cxk"/> + <outlet property="_host" destination="iat-rb-As1" id="azU-LC-CEu"/> + <outlet property="_hostPin" destination="Uow-Fu-2Yx" id="8iF-9q-f4R"/> + <outlet property="_shouldSavePin" destination="OPh-84-JII" id="Zby-0g-zE0"/> + <outlet property="_switchAskAgain" destination="5pF-pi-Stf" id="Ny5-lv-bsh"/> + <outlet property="view" destination="2" id="3"/> + </connections> + </placeholder> + <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/> + <view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="2"> + <rect key="frame" x="0.0" y="0.0" width="320" height="480"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + <subviews> + <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="<hostname>" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="iat-rb-As1"> + <rect key="frame" x="155" y="104" width="70" height="15"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + <fontDescription key="fontDescription" type="system" pointSize="12"/> + <color key="textColor" cocoaTouchSystemColor="darkTextColor"/> + <nil key="highlightedColor"/> + </label> + <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Enter the host's PIN" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="a63-kY-SHe"> + <rect key="frame" x="67" y="127" width="112" height="15"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + <fontDescription key="fontDescription" type="system" pointSize="12"/> + <nil key="highlightedColor"/> + </label> + <textField opaque="NO" clipsSubviews="YES" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="PIN" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="Uow-Fu-2Yx"> + <rect key="frame" x="67" y="150" width="115" height="30"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + <constraints> + <constraint firstAttribute="width" constant="115" id="1Yc-ng-yRZ"/> + <constraint firstAttribute="height" constant="30" id="BCY-xe-HQx"/> + </constraints> + <fontDescription key="fontDescription" type="system" pointSize="14"/> + <textInputTraits key="textInputTraits" autocorrectionType="no" keyboardType="numberPad" returnKeyType="go" secureTextEntry="YES"/> + <connections> + <outlet property="delegate" destination="-1" id="eEn-tS-i45"/> + </connections> + </textField> + <switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="5pF-pi-Stf"> + <rect key="frame" x="190" y="149" width="51" height="31"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + </switch> + <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Save PIN" textAlignment="center" lineBreakMode="wordWrap" numberOfLines="2" minimumFontSize="7" adjustsLetterSpacingToFitWidth="YES" preferredMaxLayoutWidth="56" translatesAutoresizingMaskIntoConstraints="NO" id="OPh-84-JII"> + <rect key="frame" x="247" y="157" width="56" height="16"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + <fontDescription key="fontDescription" type="system" pointSize="13"/> + <nil key="highlightedColor"/> + </label> + <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="2Vw-9K-cVY"> + <rect key="frame" x="79" y="188" width="45" height="31"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + <fontDescription key="fontDescription" type="system" pointSize="14"/> + <state key="normal" title="Cancel"> + <color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/> + </state> + <connections> + <action selector="buttonCancelClicked:" destination="-1" eventType="touchUpInside" id="a0p-ci-esP"/> + </connections> + </button> + <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="NLw-jM-z2p"> + <rect key="frame" x="155" y="189" width="55" height="29"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + <fontDescription key="fontDescription" type="system" pointSize="14"/> + <state key="normal" title="Connect"> + <color key="titleShadowColor" white="0.5" alpha="1" colorSpace="calibratedWhite"/> + </state> + <connections> + <action selector="buttonConnectClicked:" destination="-1" eventType="touchUpInside" id="CG9-X4-tEa"/> + </connections> + </button> + <label opaque="NO" clipsSubviews="YES" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Authenticate to" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="G1n-4q-Knh"> + <rect key="frame" x="67" y="104" width="85" height="15"/> + <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> + <fontDescription key="fontDescription" type="system" pointSize="12"/> + <nil key="highlightedColor"/> + </label> + </subviews> + <color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/> + <constraints> + <constraint firstItem="NLw-jM-z2p" firstAttribute="top" secondItem="Uow-Fu-2Yx" secondAttribute="bottom" constant="9" id="3jN-pC-2rt"/> + <constraint firstAttribute="centerY" secondItem="Uow-Fu-2Yx" secondAttribute="centerY" constant="75" id="5pL-cU-39Z"/> + <constraint firstItem="5pF-pi-Stf" firstAttribute="leading" secondItem="Uow-Fu-2Yx" secondAttribute="trailing" constant="8" id="9Qh-pj-al1"/> + <constraint firstItem="a63-kY-SHe" firstAttribute="top" secondItem="G1n-4q-Knh" secondAttribute="bottom" constant="8" id="Aep-CT-GtV"/> + <constraint firstItem="Uow-Fu-2Yx" firstAttribute="top" secondItem="a63-kY-SHe" secondAttribute="bottom" constant="8" id="Af6-MG-6hM"/> + <constraint firstItem="Uow-Fu-2Yx" firstAttribute="centerY" secondItem="5pF-pi-Stf" secondAttribute="centerY" id="Cxm-6Z-rBa"/> + <constraint firstAttribute="centerX" secondItem="Uow-Fu-2Yx" secondAttribute="centerX" constant="36" id="L6n-kv-1cb"/> + <constraint firstItem="NLw-jM-z2p" firstAttribute="leading" secondItem="2Vw-9K-cVY" secondAttribute="trailing" constant="31" id="OGl-yE-cFq"/> + <constraint firstItem="2Vw-9K-cVY" firstAttribute="leading" secondItem="Uow-Fu-2Yx" secondAttribute="leading" constant="12" id="Onp-Z7-Xp2"/> + <constraint firstItem="iat-rb-As1" firstAttribute="centerY" secondItem="G1n-4q-Knh" secondAttribute="centerY" id="RI9-Jx-K5Z"/> + <constraint firstItem="iat-rb-As1" firstAttribute="leading" secondItem="G1n-4q-Knh" secondAttribute="trailing" constant="3" id="XQd-6a-62O"/> + <constraint firstItem="NLw-jM-z2p" firstAttribute="centerY" secondItem="2Vw-9K-cVY" secondAttribute="centerY" id="baU-9W-Ab2"/> + <constraint firstItem="2Vw-9K-cVY" firstAttribute="top" secondItem="Uow-Fu-2Yx" secondAttribute="bottom" constant="8" id="dCn-aX-MNJ"/> + <constraint firstItem="a63-kY-SHe" firstAttribute="leading" secondItem="Uow-Fu-2Yx" secondAttribute="leading" id="ddk-qx-Ldm"/> + <constraint firstItem="Uow-Fu-2Yx" firstAttribute="centerY" secondItem="OPh-84-JII" secondAttribute="centerY" id="fwM-yD-GQh"/> + <constraint firstItem="5pF-pi-Stf" firstAttribute="centerY" secondItem="OPh-84-JII" secondAttribute="centerY" id="jfD-pi-EWm"/> + <constraint firstItem="OPh-84-JII" firstAttribute="leading" secondItem="5pF-pi-Stf" secondAttribute="trailing" constant="8" id="qjc-e2-rkS"/> + <constraint firstItem="a63-kY-SHe" firstAttribute="leading" secondItem="G1n-4q-Knh" secondAttribute="leading" id="tIh-JU-ubp"/> + </constraints> + <simulatedStatusBarMetrics key="simulatedStatusBarMetrics" statusBarStyle="lightContent"/> + <simulatedScreenMetrics key="simulatedDestinationMetrics"/> + </view> + </objects> +</document> diff --git a/remoting/ios/ui/scene_view.h b/remoting/ios/ui/scene_view.h new file mode 100644 index 0000000..8f082ff --- /dev/null +++ b/remoting/ios/ui/scene_view.h @@ -0,0 +1,171 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef REMOTING_IOS_UI_SCENE_VIEW_H_ +#define REMOTING_IOS_UI_SCENE_VIEW_H_ + +#import <Foundation/Foundation.h> +#import <GLKit/GLKit.h> + +#include "third_party/webrtc/modules/desktop_capture/desktop_geometry.h" + +typedef struct { + bool left; + bool right; + bool top; + bool bottom; +} AnchorPosition; + +typedef struct { + int left; + int right; + int top; + int bottom; +} MarginQuad; + +typedef struct { + CGPoint geometryVertex; + CGPoint textureVertex; +} TexturedVertex; + +typedef struct { + TexturedVertex bl; + TexturedVertex br; + TexturedVertex tl; + TexturedVertex tr; +} TexturedQuad; + +@interface SceneView : NSObject { + @private + + // GL name + GLuint _textureId; + + GLKMatrix4 _projectionMatrix; + GLKMatrix4 _modelViewMatrix; + + // The draw surface is a triangle strip (triangles defined by the intersecting + // vertexes) to create a rectangle surface. + // 1****3 + // | / | + // | / | + // 2****4 + // This also determines the resolution of our surface, being a unit (NxN) grid + // with finite divisions. For our surface N = 1, and the number of divisions + // respects the CLIENT's desktop resolution. + TexturedQuad _glQuad; + + // Cache of the CLIENT's desktop resolution. + webrtc::DesktopSize _contentSize; + // Cache of the HOST's desktop resolution. + webrtc::DesktopSize _frameSize; + + // Location of the mouse according to the CLIENT in the prospective of the + // HOST resolution + webrtc::DesktopVector _mousePosition; + + // When a user pans they expect the view to experience acceleration after + // they release the pan gesture. We track that velocity vector as a position + // delta factored over the frame rate of the GL Context. Velocity is + // accounted as a float. + CGPoint _panVelocity; +} + +// The position of the scene is tracked in the prospective of the CLIENT +// resolution. The Z-axis is used to track the scale of the render, our scene +// never changes position on the Z-axis. +@property(nonatomic, readonly) GLKVector3 position; + +// Space around border consumed by non-scene elements, we can not draw here +@property(nonatomic, readonly) MarginQuad margin; + +@property(nonatomic, readonly) AnchorPosition anchored; + +- (const GLKMatrix4&)projectionMatrix; + +// calculate and return the current model view matrix +- (const GLKMatrix4&)modelViewMatrix; + +- (const webrtc::DesktopSize&)contentSize; + +// Update the CLIENT resolution and draw scene size, accounting for margins +- (void)setContentSize:(const CGSize&)size; + +- (const webrtc::DesktopSize&)frameSize; + +// Update the HOST resolution and reinitialize the scene positioning +- (void)setFrameSize:(const webrtc::DesktopSize&)size; + +- (const webrtc::DesktopVector&)mousePosition; + +- (void)setPanVelocity:(const CGPoint&)delta; + +- (void)setMarginsFromLeft:(int)left + right:(int)right + top:(int)top + bottom:(int)bottom; + +// Draws to a GL Context +- (void)draw; + +- (BOOL)containsTouchPoint:(CGPoint)point; + +// Applies translation and zoom. Translation is bounded to screen edges. +// Zooming is bounded on the lower side to the maximum of width and height, and +// on the upper side by a constant, experimentally chosen. +- (void)panAndZoom:(CGPoint)translation scaleBy:(float)scale; + +// Mouse is tracked in the perspective of the HOST desktop, but the projection +// to the user is in the perspective of the CLIENT resolution. Find the HOST +// position that is the center of the current CLIENT view. If the mouse is in +// the half of the CLIENT screen that is closest to an anchor, then move the +// mouse, otherwise the mouse should be centered. +- (void)updateMousePositionAndAnchorsWithTranslation:(CGPoint)translation + scale:(float)scale; + +// When zoom is changed the scene is translated to keep an anchored point +// (an anchored edge, or the spot the user is touching) at the same place in the +// User's perspective. Return the delta of the position of the lower endpoint +// of the axis ++ (float)positionDeltaFromScaling:(float)ratio + position:(float)position + length:(float)length + anchor:(float)anchor; + +// Return the delta of the position of the lower endpoint of the axis ++ (int)positionDeltaFromTranslation:(int)translation + position:(int)position + freeSpace:(int)freeSpace + scaleingPositionDelta:(int)scaleingPositionDelta + isAnchoredLow:(BOOL)isAnchoredLow + isAnchoredHigh:(BOOL)isAnchoredHigh; + +// |position + delta| is snapped to the bounds, return the delta in respect to +// the bounding. ++ (int)boundDeltaFromPosition:(float)position + delta:(int)delta + lowerBound:(int)lowerBound + upperBound:(int)upperBound; + +// Return |nextPosition| when it is anchored and still in the respective 1/2 of +// the screen. When |nextPosition| is outside scene's edge, snap to edge. +// Otherwise return |centerPosition| ++ (int)boundMouseGivenNextPosition:(int)nextPosition + maxPosition:(int)maxPosition + centerPosition:(int)centerPosition + isAnchoredLow:(BOOL)isAnchoredLow + isAnchoredHigh:(BOOL)isAnchoredHigh; + +// If the mouse is at an edge return zero, otherwise return |velocity| ++ (float)boundVelocity:(float)velocity + axisLength:(int)axisLength + mousePosition:(int)mousePosition; + +// Update the scene acceleration vector. +// Returns true if velocity before 'ticking' is non-zero. +- (BOOL)tickPanVelocity; + +@end + +#endif // REMOTING_IOS_UI_SCENE_VIEW_H_
\ No newline at end of file diff --git a/remoting/ios/ui/scene_view.mm b/remoting/ios/ui/scene_view.mm new file mode 100644 index 0000000..97a577d --- /dev/null +++ b/remoting/ios/ui/scene_view.mm @@ -0,0 +1,642 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "remoting/ios/ui/scene_view.h" + +#import "remoting/ios/utility.h" + +namespace { + +// TODO (aboone) Some of the layout is not yet set in stone, so variables have +// been used to position and turn items on and off. Eventually these may be +// stabilized and removed. + +// Scroll speed multiplier for swiping +const static int kMouseSensitivity = 2.5; + +// Input Axis inversion +// 1 for standard, -1 for inverted +const static int kXAxisInversion = -1; +const static int kYAxisInversion = -1; + +// Experimental value for bounding the maximum zoom ratio +const static int kMaxZoomSize = 3; +} // namespace + +@interface SceneView (Private) +// Returns the number of pixels displayed per device pixel when the scaling is +// such that the entire frame would fit perfectly in content. Note the ratios +// are different for width and height, some people have multiple monitors, some +// have 16:9 or 4:3 while iPad is always single screen, but different iOS +// devices have different resolutions. +- (CGPoint)pixelRatio; + +// Return the FrameSize in perspective of the CLIENT resolution +- (webrtc::DesktopSize)frameSizeToScale:(float)scale; + +// When bounded on the top and right, this point is where the scene must be +// positioned given a scene size +- (webrtc::DesktopVector)getBoundsForSize:(const webrtc::DesktopSize&)size; + +// Converts a point in the the CLIENT resolution to a similar point in the HOST +// resolution. Additionally, CLIENT resolution is expressed in float values +// while HOST operates in integer values. +- (BOOL)convertTouchPointToMousePoint:(CGPoint)touchPoint + targetPoint:(webrtc::DesktopVector&)desktopPoint; + +// Converts a point in the the HOST resolution to a similar point in the CLIENT +// resolution. Additionally, CLIENT resolution is expressed in float values +// while HOST operates in integer values. +- (BOOL)convertMousePointToTouchPoint:(const webrtc::DesktopVector&)mousePoint + targetPoint:(CGPoint&)touchPoint; +@end + +@implementation SceneView + +- (id)init { + self = [super init]; + if (self) { + + _frameSize = webrtc::DesktopSize(1, 1); + _contentSize = webrtc::DesktopSize(1, 1); + _mousePosition = webrtc::DesktopVector(0, 0); + + _position = GLKVector3Make(0, 0, 1); + _margin.left = 0; + _margin.right = 0; + _margin.top = 0; + _margin.bottom = 0; + _anchored.left = false; + _anchored.right = false; + _anchored.top = false; + _anchored.bottom = false; + } + return self; +} + +- (const GLKMatrix4&)projectionMatrix { + return _projectionMatrix; +} + +- (const GLKMatrix4&)modelViewMatrix { + // Start by using the entire scene + _modelViewMatrix = GLKMatrix4Identity; + + // Position scene according to any panning or bounds + _modelViewMatrix = GLKMatrix4Translate(_modelViewMatrix, + _position.x + _margin.left, + _position.y + _margin.bottom, + 0.0); + + // Apply zoom + _modelViewMatrix = GLKMatrix4Scale(_modelViewMatrix, + _position.z / self.pixelRatio.x, + _position.z / self.pixelRatio.y, + 1.0); + + // We are directly above the screen and looking down. + static const GLKMatrix4 viewMatrix = GLKMatrix4MakeLookAt( + 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); // center view + + _modelViewMatrix = GLKMatrix4Multiply(viewMatrix, _modelViewMatrix); + + return _modelViewMatrix; +} + +- (const webrtc::DesktopSize&)contentSize { + return _contentSize; +} + +- (void)setContentSize:(const CGSize&)size { + + _contentSize.set(size.width, size.height); + + _projectionMatrix = GLKMatrix4MakeOrtho( + 0.0, _contentSize.width(), 0.0, _contentSize.height(), 1.0, -1.0); + + TexturedQuad newQuad; + newQuad.bl.geometryVertex = CGPointMake(0.0, 0.0); + newQuad.br.geometryVertex = CGPointMake(_contentSize.width(), 0.0); + newQuad.tl.geometryVertex = CGPointMake(0.0, _contentSize.height()); + newQuad.tr.geometryVertex = + CGPointMake(_contentSize.width(), _contentSize.height()); + + newQuad.bl.textureVertex = CGPointMake(0.0, 1.0); + newQuad.br.textureVertex = CGPointMake(1.0, 1.0); + newQuad.tl.textureVertex = CGPointMake(0.0, 0.0); + newQuad.tr.textureVertex = CGPointMake(1.0, 0.0); + + _glQuad = newQuad; +} + +- (const webrtc::DesktopSize&)frameSize { + return _frameSize; +} + +- (void)setFrameSize:(const webrtc::DesktopSize&)size { + DCHECK(size.width() > 0 && size.height() > 0); + // Don't do anything if the size has not changed. + if (_frameSize.equals(size)) + return; + + _frameSize.set(size.width(), size.height()); + + _position.x = 0; + _position.y = 0; + + float verticalPixelScaleRatio = + (static_cast<float>(_contentSize.height() - _margin.top - + _margin.bottom) / + static_cast<float>(_frameSize.height())) / + _position.z; + + // Anchored at the position (0,0) + _anchored.left = YES; + _anchored.right = NO; + _anchored.top = NO; + _anchored.bottom = YES; + + [self panAndZoom:CGPointMake(0.0, 0.0) scaleBy:verticalPixelScaleRatio]; + + // Center the mouse on the CLIENT screen + webrtc::DesktopVector centerMouseLocation; + if ([self convertTouchPointToMousePoint:CGPointMake(_contentSize.width() / 2, + _contentSize.height() / 2) + targetPoint:centerMouseLocation]) { + _mousePosition.set(centerMouseLocation.x(), centerMouseLocation.y()); + } + +#if DEBUG + NSLog(@"resized frame:%d:%d scale:%f", + _frameSize.width(), + _frameSize.height(), + _position.z); +#endif // DEBUG +} + +- (const webrtc::DesktopVector&)mousePosition { + return _mousePosition; +} + +- (void)setPanVelocity:(const CGPoint&)delta { + _panVelocity.x = delta.x; + _panVelocity.y = delta.y; +} + +- (void)setMarginsFromLeft:(int)left + right:(int)right + top:(int)top + bottom:(int)bottom { + _margin.left = left; + _margin.right = right; + _margin.top = top; + _margin.bottom = bottom; +} + +- (void)draw { + glEnableVertexAttribArray(GLKVertexAttribPosition); + glEnableVertexAttribArray(GLKVertexAttribTexCoord0); + glEnableVertexAttribArray(GLKVertexAttribTexCoord1); + + // Define our scene space + glVertexAttribPointer(GLKVertexAttribPosition, + 2, + GL_FLOAT, + GL_FALSE, + sizeof(TexturedVertex), + &(_glQuad.bl.geometryVertex)); + // Define the desktop plane + glVertexAttribPointer(GLKVertexAttribTexCoord0, + 2, + GL_FLOAT, + GL_FALSE, + sizeof(TexturedVertex), + &(_glQuad.bl.textureVertex)); + // Define the cursor plane + glVertexAttribPointer(GLKVertexAttribTexCoord1, + 2, + GL_FLOAT, + GL_FALSE, + sizeof(TexturedVertex), + &(_glQuad.bl.textureVertex)); + + // Draw! + glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); + + [Utility logGLErrorCode:@"SceneView draw"]; +} + +- (CGPoint)pixelRatio { + + CGPoint r = CGPointMake(static_cast<float>(_contentSize.width()) / + static_cast<float>(_frameSize.width()), + static_cast<float>(_contentSize.height()) / + static_cast<float>(_frameSize.height())); + return r; +} + +- (webrtc::DesktopSize)frameSizeToScale:(float)scale { + return webrtc::DesktopSize(_frameSize.width() * scale, + _frameSize.height() * scale); +} + +- (webrtc::DesktopVector)getBoundsForSize:(const webrtc::DesktopSize&)size { + webrtc::DesktopVector r( + _contentSize.width() - _margin.left - _margin.right - size.width(), + _contentSize.height() - _margin.bottom - _margin.top - size.height()); + + if (r.x() > 0) { + r.set((_contentSize.width() - size.width()) / 2, r.y()); + } + + if (r.y() > 0) { + r.set(r.x(), (_contentSize.height() - size.height()) / 2); + } + + return r; +} + +- (BOOL)containsTouchPoint:(CGPoint)point { + // Here frame is from the top-left corner, most other calculations are framed + // from the bottom left. + CGRect frame = + CGRectMake(_margin.left, + _margin.top, + _contentSize.width() - _margin.left - _margin.right, + _contentSize.height() - _margin.top - _margin.bottom); + return CGRectContainsPoint(frame, point); +} + +- (BOOL)convertTouchPointToMousePoint:(CGPoint)touchPoint + targetPoint:(webrtc::DesktopVector&)mousePoint { + if (![self containsTouchPoint:touchPoint]) { + return NO; + } + // A touch location occurs in respect to the user's entire view surface. + + // The GL Context is upside down from the User's perspective so flip it. + CGPoint glOrientedTouchPoint = + CGPointMake(touchPoint.x, _contentSize.height() - touchPoint.y); + + // The GL surface generally is not at the same origination point as the touch, + // so translate by the scene's position. + CGPoint glOrientedPointInRespectToFrame = + CGPointMake(glOrientedTouchPoint.x - _position.x, + glOrientedTouchPoint.y - _position.y); + + // The perspective exists in relative to the CLIENT resolution at 1:1, zoom + // our perspective so we are relative to the HOST at 1:1 + CGPoint glOrientedPointInFrame = + CGPointMake(glOrientedPointInRespectToFrame.x / _position.z, + glOrientedPointInRespectToFrame.y / _position.z); + + // Finally, flip the perspective back over to the Users, but this time in + // respect to the HOST desktop. Floor to ensure the result is always in + // frame. + CGPoint deskTopOrientedPointInFrame = + CGPointMake(floorf(glOrientedPointInFrame.x), + floorf(_frameSize.height() - glOrientedPointInFrame.y)); + + // Convert from float to integer + mousePoint.set(deskTopOrientedPointInFrame.x, deskTopOrientedPointInFrame.y); + + return CGRectContainsPoint( + CGRectMake(0, 0, _frameSize.width(), _frameSize.height()), + deskTopOrientedPointInFrame); +} + +- (BOOL)convertMousePointToTouchPoint:(const webrtc::DesktopVector&)mousePoint + targetPoint:(CGPoint&)touchPoint { + // A mouse point is in respect to the desktop frame. + + // Flip the perspective back over to the Users, in + // respect to the HOST desktop. + CGPoint deskTopOrientedPointInFrame = + CGPointMake(mousePoint.x(), _frameSize.height() - mousePoint.y()); + + // The perspective exists in relative to the CLIENT resolution at 1:1, zoom + // our perspective so we are relative to the HOST at 1:1 + CGPoint glOrientedPointInFrame = + CGPointMake(deskTopOrientedPointInFrame.x * _position.z, + deskTopOrientedPointInFrame.y * _position.z); + + // The GL surface generally is not at the same origination point as the touch, + // so translate by the scene's position. + CGPoint glOrientedPointInRespectToFrame = + CGPointMake(glOrientedPointInFrame.x + _position.x, + glOrientedPointInFrame.y + _position.y); + + // Convert from float to integer + touchPoint.x = floorf(glOrientedPointInRespectToFrame.x); + touchPoint.y = floorf(glOrientedPointInRespectToFrame.y); + + return [self containsTouchPoint:touchPoint]; +} + +- (void)panAndZoom:(CGPoint)translation scaleBy:(float)ratio { + CGPoint ratios = [self pixelRatio]; + + // New Scaling factor bounded by a min and max + float resultScale = _position.z * ratio; + float scaleUpperBound = MAX(ratios.x, MAX(ratios.y, kMaxZoomSize)); + float scaleLowerBound = MIN(ratios.x, ratios.y); + + if (resultScale < scaleLowerBound) { + resultScale = scaleLowerBound; + } else if (resultScale > scaleUpperBound) { + resultScale = scaleUpperBound; + } + + DCHECK(isnormal(resultScale) && resultScale > 0); + + // The GL perspective is upside down in relation to the User's view, so flip + // the translation + translation.y = -translation.y; + + // The constants here could be user options later. + translation.x = + translation.x * kXAxisInversion * (1 / (ratios.x * kMouseSensitivity)); + translation.y = + translation.y * kYAxisInversion * (1 / (ratios.y * kMouseSensitivity)); + + CGPoint delta = CGPointMake(0, 0); + CGPoint scaleDelta = CGPointMake(0, 0); + + webrtc::DesktopSize currentSize = [self frameSizeToScale:_position.z]; + + { + // Closure for this variable, so the variable is not available to the rest + // of this function + webrtc::DesktopVector currentBounds = [self getBoundsForSize:currentSize]; + // There are rounding errors in the scope of this function, see the + // butterfly effect. In successive calls, the resulting position isn't + // always exactly the calculated position. If we know we are Anchored, then + // go ahead and reposition it to the values above. + if (_anchored.right) { + _position.x = currentBounds.x(); + } + + if (_anchored.top) { + _position.y = currentBounds.y(); + } + } + + if (_position.z != resultScale) { + // When scaling the scene, the origination of scaling is the mouse's + // location. But when the frame is anchored, adjust the origination to the + // anchor point. + + CGPoint mousePositionInClientResolution; + [self convertMousePointToTouchPoint:_mousePosition + targetPoint:mousePositionInClientResolution]; + + // Prefer to zoom based on the left anchor when there is a choice + if (_anchored.left) { + mousePositionInClientResolution.x = 0; + } else if (_anchored.right) { + mousePositionInClientResolution.x = _contentSize.width(); + } + + // Prefer to zoom out from the top anchor when there is a choice + if (_anchored.top) { + mousePositionInClientResolution.y = _contentSize.height(); + } else if (_anchored.bottom) { + mousePositionInClientResolution.y = 0; + } + + scaleDelta.x -= + [SceneView positionDeltaFromScaling:ratio + position:_position.x + length:currentSize.width() + anchor:mousePositionInClientResolution.x]; + + scaleDelta.y -= + [SceneView positionDeltaFromScaling:ratio + position:_position.y + length:currentSize.height() + anchor:mousePositionInClientResolution.y]; + } + + delta.x = [SceneView + positionDeltaFromTranslation:translation.x + position:_position.x + freeSpace:_contentSize.width() - currentSize.width() + scaleingPositionDelta:scaleDelta.x + isAnchoredLow:_anchored.left + isAnchoredHigh:_anchored.right]; + + delta.y = [SceneView + positionDeltaFromTranslation:translation.y + position:_position.y + freeSpace:_contentSize.height() - currentSize.height() + scaleingPositionDelta:scaleDelta.y + isAnchoredLow:_anchored.bottom + isAnchoredHigh:_anchored.top]; + { + // Closure for this variable, so the variable is not available to the rest + // of this function + webrtc::DesktopVector bounds = + [self getBoundsForSize:[self frameSizeToScale:resultScale]]; + + delta.x = [SceneView boundDeltaFromPosition:_position.x + delta:delta.x + lowerBound:bounds.x() + upperBound:0]; + + delta.y = [SceneView boundDeltaFromPosition:_position.y + delta:delta.y + lowerBound:bounds.y() + upperBound:0]; + } + + BOOL isLeftAndRightAnchored = _anchored.left && _anchored.right; + BOOL isTopAndBottomAnchored = _anchored.top && _anchored.bottom; + + [self updateMousePositionAndAnchorsWithTranslation:translation + scale:resultScale]; + + // If both anchors were lost, then keep the one that is easier to predict + if (isLeftAndRightAnchored && !_anchored.left && !_anchored.right) { + delta.x = -_position.x; + _anchored.left = YES; + } + + // If both anchors were lost, then keep the one that is easier to predict + if (isTopAndBottomAnchored && !_anchored.top && !_anchored.bottom) { + delta.y = -_position.y; + _anchored.bottom = YES; + } + + // FINALLY, update the scene's position + _position.x += delta.x; + _position.y += delta.y; + _position.z = resultScale; +} + +- (void)updateMousePositionAndAnchorsWithTranslation:(CGPoint)translation + scale:(float)scale { + webrtc::DesktopVector centerMouseLocation; + [self convertTouchPointToMousePoint:CGPointMake(_contentSize.width() / 2, + _contentSize.height() / 2) + targetPoint:centerMouseLocation]; + + webrtc::DesktopVector currentBounds = + [self getBoundsForSize:[self frameSizeToScale:_position.z]]; + webrtc::DesktopVector nextBounds = + [self getBoundsForSize:[self frameSizeToScale:scale]]; + + webrtc::DesktopVector predictedMousePosition( + _mousePosition.x() - translation.x, _mousePosition.y() + translation.y); + + _mousePosition.set( + [SceneView boundMouseGivenNextPosition:predictedMousePosition.x() + maxPosition:_frameSize.width() + centerPosition:centerMouseLocation.x() + isAnchoredLow:_anchored.left + isAnchoredHigh:_anchored.right], + [SceneView boundMouseGivenNextPosition:predictedMousePosition.y() + maxPosition:_frameSize.height() + centerPosition:centerMouseLocation.y() + isAnchoredLow:_anchored.top + isAnchoredHigh:_anchored.bottom]); + + _panVelocity.x = [SceneView boundVelocity:_panVelocity.x + axisLength:_frameSize.width() + mousePosition:_mousePosition.x()]; + _panVelocity.y = [SceneView boundVelocity:_panVelocity.y + axisLength:_frameSize.height() + mousePosition:_mousePosition.y()]; + + _anchored.left = (nextBounds.x() >= 0) || + (_position.x == 0 && + predictedMousePosition.x() <= centerMouseLocation.x()); + + _anchored.right = + (nextBounds.x() >= 0) || + (_position.x == currentBounds.x() && + predictedMousePosition.x() >= centerMouseLocation.x()) || + (_mousePosition.x() == _frameSize.width() - 1 && !_anchored.left); + + _anchored.bottom = (nextBounds.y() >= 0) || + (_position.y == 0 && + predictedMousePosition.y() >= centerMouseLocation.y()); + + _anchored.top = + (nextBounds.y() >= 0) || + (_position.y == currentBounds.y() && + predictedMousePosition.y() <= centerMouseLocation.y()) || + (_mousePosition.y() == _frameSize.height() - 1 && !_anchored.bottom); +} + ++ (float)positionDeltaFromScaling:(float)ratio + position:(float)position + length:(float)length + anchor:(float)anchor { + float newSize = length * ratio; + float scaleXBy = fabs(position - anchor) / length; + float delta = (newSize - length) * scaleXBy; + return delta; +} + ++ (int)positionDeltaFromTranslation:(int)translation + position:(int)position + freeSpace:(int)freeSpace + scaleingPositionDelta:(int)scaleingPositionDelta + isAnchoredLow:(BOOL)isAnchoredLow + isAnchoredHigh:(BOOL)isAnchoredHigh { + if (isAnchoredLow && isAnchoredHigh) { + // center the view + return (freeSpace / 2) - position; + } else if (isAnchoredLow) { + return 0; + } else if (isAnchoredHigh) { + return scaleingPositionDelta; + } else { + return translation + scaleingPositionDelta; + } +} + ++ (int)boundDeltaFromPosition:(float)position + delta:(int)delta + lowerBound:(int)lowerBound + upperBound:(int)upperBound { + int result = position + delta; + + if (lowerBound < upperBound) { // the view is larger than the bounds + if (result > upperBound) { + result = upperBound; + } else if (result < lowerBound) { + result = lowerBound; + } + } else { + // the view is smaller than the bounds so we'll always be at the lowerBound + result = lowerBound; + } + return result - position; +} + ++ (int)boundMouseGivenNextPosition:(int)nextPosition + maxPosition:(int)maxPosition + centerPosition:(int)centerPosition + isAnchoredLow:(BOOL)isAnchoredLow + isAnchoredHigh:(BOOL)isAnchoredHigh { + if (nextPosition < 0) { + return 0; + } + if (nextPosition > maxPosition - 1) { + return maxPosition - 1; + } + + if ((isAnchoredLow && nextPosition <= centerPosition) || + (isAnchoredHigh && nextPosition >= centerPosition)) { + return nextPosition; + } + + return centerPosition; +} + ++ (float)boundVelocity:(float)velocity + axisLength:(int)axisLength + mousePosition:(int)mousePosition { + if (velocity != 0) { + if (mousePosition <= 0 || mousePosition >= (axisLength - 1)) { + return 0; + } + } + + return velocity; +} + +- (BOOL)tickPanVelocity { + BOOL inMotion = ((_panVelocity.x != 0.0) || (_panVelocity.y != 0.0)); + + if (inMotion) { + + uint32_t divisor = 50 / _position.z; + float reducer = .95; + + if (_panVelocity.x != 0.0 && ABS(_panVelocity.x) < divisor) { + _panVelocity = CGPointMake(0.0, _panVelocity.y); + } + + if (_panVelocity.y != 0.0 && ABS(_panVelocity.y) < divisor) { + _panVelocity = CGPointMake(_panVelocity.x, 0.0); + } + + [self panAndZoom:CGPointMake(_panVelocity.x / divisor, + _panVelocity.y / divisor) + scaleBy:1.0]; + + _panVelocity.x *= reducer; + _panVelocity.y *= reducer; + } + + return inMotion; +} + +@end
\ No newline at end of file diff --git a/remoting/ios/ui/scene_view_unittest.mm b/remoting/ios/ui/scene_view_unittest.mm new file mode 100644 index 0000000..d1dfabc --- /dev/null +++ b/remoting/ios/ui/scene_view_unittest.mm @@ -0,0 +1,1219 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#if !defined(__has_feature) || !__has_feature(objc_arc) +#error "This file requires ARC support." +#endif + +#import "remoting/ios/ui/scene_view.h" + +#import "base/compiler_specific.h" +#import "testing/gtest_mac.h" + +namespace remoting { + +namespace { +const int kClientWidth = 200; +const int kClientHeight = 100; +const webrtc::DesktopSize kClientSize(kClientWidth, kClientHeight); +// Smaller then ClientSize +const webrtc::DesktopSize kSmall(50, 75); +// Inverted - The vertical is closer to an edge than the horizontal +const webrtc::DesktopSize kSmallInversed(175, 50); +// Larger then ClientSize +const webrtc::DesktopSize kLarge(800, 125); +const webrtc::DesktopSize kLargeInversed(225, 400); +} // namespace + +class SceneViewTest : public ::testing::Test { + protected: + virtual void SetUp() OVERRIDE { + scene_ = [[SceneView alloc] init]; + [scene_ + setContentSize:CGSizeMake(kClientSize.width(), kClientSize.height())]; + [scene_ setFrameSize:kLarge]; + } + + void MakeLarge() { [scene_ setFrameSize:kLarge]; } + + SceneView* scene_; +}; + +TEST(SceneViewTest_Property, ContentSize) { + SceneView* scene = [[SceneView alloc] init]; + + [scene setContentSize:CGSizeMake(0, 0)]; + EXPECT_EQ(0, scene.contentSize.width()); + EXPECT_EQ(0, scene.contentSize.height()); + float zeros[16] = {1.0f / 0.0f, 0, 0, 0, 0, 1.0f / 0.0f, 0, 0, + 0, 0, 1, 0, 0.0f / 0.0f, 0.0f / 0.0f, 0, 1}; + + ASSERT_TRUE(memcmp(zeros, scene.projectionMatrix.m, 16 * sizeof(float)) == 0); + + [scene setContentSize:CGSizeMake(kClientSize.width(), kClientSize.height())]; + EXPECT_EQ(kClientSize.width(), scene.contentSize.width()); + EXPECT_EQ(kClientSize.height(), scene.contentSize.height()); + + EXPECT_TRUE(memcmp(GLKMatrix4MakeOrtho( + 0.0, kClientWidth, 0.0, kClientHeight, 1.0, -1.0).m, + scene.projectionMatrix.m, + 16 * sizeof(float)) == 0); +} + +TEST(SceneViewTest_Property, FrameSizeInit) { + SceneView* scene = [[SceneView alloc] init]; + [scene setContentSize:CGSizeMake(kClientSize.width(), kClientSize.height())]; + + [scene setFrameSize:webrtc::DesktopSize(1, 1)]; + EXPECT_EQ(1, scene.frameSize.width()); + EXPECT_EQ(1, scene.frameSize.height()); + + EXPECT_EQ(0, scene.position.x); + EXPECT_EQ(0, scene.position.y); + EXPECT_EQ(1, scene.position.z); + + EXPECT_FALSE(scene.anchored.left); + EXPECT_FALSE(scene.anchored.right); + EXPECT_FALSE(scene.anchored.top); + EXPECT_FALSE(scene.anchored.bottom); + + EXPECT_EQ(0, scene.mousePosition.x()); + EXPECT_EQ(0, scene.mousePosition.y()); +} + +TEST(SceneViewTest_Property, FrameSizeLarge) { + SceneView* scene = [[SceneView alloc] init]; + [scene setContentSize:CGSizeMake(kClientSize.width(), kClientSize.height())]; + [scene setFrameSize:kLarge]; + EXPECT_EQ(kLarge.width(), scene.frameSize.width()); + EXPECT_EQ(kLarge.height(), scene.frameSize.height()); + + // Screen is positioned in the lower,left corner, zoomed until the vertical + // fits exactly, and then centered horizontally + // HOST + // CLIENT ------------------------------------------------ + // ------------ | | + // | | | | + // | | | | + // | | | | + // ------------ ------------------------------------------------ + // RESULT - ONSCREEN is completely covered, with some of the HOST off screen + // (-.-) the mouse cursor + // ----------------------------------------- + // | ONSCREEN | OFFSCREEN | + // | -.- | | + // | | | + // ----------------------------------------- + float scale = static_cast<float>(kClientSize.height()) / + static_cast<float>(kLarge.height()); + // vertical fits exactly + EXPECT_EQ(scale, scene.position.z); + + // sitting on both Axis + EXPECT_EQ(0, scene.position.x); + EXPECT_EQ(0, scene.position.y); + + // bound on 3 sides, not on the right + EXPECT_TRUE(scene.anchored.left); + EXPECT_FALSE(scene.anchored.right); + EXPECT_TRUE(scene.anchored.top); + EXPECT_TRUE(scene.anchored.bottom); + + // mouse is off center on the left horizontal + EXPECT_EQ(kClientSize.width() / (scale * 2), scene.mousePosition.x()); + // mouse is centered vertical + EXPECT_EQ(kLarge.height() / 2, scene.mousePosition.y()); +} + +TEST(SceneViewTest_Property, FrameSizeLargeInversed) { + SceneView* scene = [[SceneView alloc] init]; + [scene setContentSize:CGSizeMake(kClientSize.width(), kClientSize.height())]; + [scene setFrameSize:kLargeInversed]; + EXPECT_EQ(kLargeInversed.width(), scene.frameSize.width()); + EXPECT_EQ(kLargeInversed.height(), scene.frameSize.height()); + + // Screen is positioned in the lower,left corner, zoomed until the vertical + // fits exactly, and then centered horizontally + // HOST + // --------------- + // | | + // | | + // | | + // | | + // | | + // | | + // | | + // CLIENT | | + // ------------- | | + // | | | | + // | | | | + // | | | | + // ------------- --------------- + // RESULT, entire HOST is on screen + // (-.-) the mouse cursor, XX is black backdrop + // ------------- + // |XX| |XX| + // |XX| -.- |XX| + // |XX| |XX| + // ------------- + float scale = static_cast<float>(kClientSize.height()) / + static_cast<float>(kLargeInversed.height()); + // Vertical fits exactly + EXPECT_EQ(scale, scene.position.z); + + // centered + EXPECT_EQ( + (kClientSize.width() - static_cast<int>(scale * kLargeInversed.width())) / + 2, + scene.position.x); + // sits on Axis + EXPECT_EQ(0, scene.position.y); + + // bound on all 4 sides + EXPECT_TRUE(scene.anchored.left); + EXPECT_TRUE(scene.anchored.right); + EXPECT_TRUE(scene.anchored.top); + EXPECT_TRUE(scene.anchored.bottom); + + // mouse is in centered both vertical and horizontal + EXPECT_EQ(kLargeInversed.width() / 2, scene.mousePosition.x()); + EXPECT_EQ(kLargeInversed.height() / 2, scene.mousePosition.y()); +} + +TEST(SceneViewTest_Property, FrameSizeSmall) { + SceneView* scene = [[SceneView alloc] init]; + [scene setContentSize:CGSizeMake(kClientSize.width(), kClientSize.height())]; + [scene setFrameSize:kSmall]; + EXPECT_EQ(kSmall.width(), scene.frameSize.width()); + EXPECT_EQ(kSmall.height(), scene.frameSize.height()); + + // Screen is positioned in the lower,left corner, zoomed until the vertical + // fits exactly, and then centered horizontally + // CLIENT + // --------------------------- + // | | HOST + // | | ------- + // | | | | + // | | | | + // | | | | + // | | | | + // | | | | + // --------------------------- ------- + // RESULT, entire HOST is on screen + // (-.-) the mouse cursor, XX is black backdrop + // --------------------------- + // |XXXXXXXXX| |XXXXXXXXX| + // |XXXXXXXXX| |XXXXXXXXX| + // |XXXXXXXXX| |XXXXXXXXX| + // |XXXXXXXXX| -.- |XXXXXXXXX| + // |XXXXXXXXX| |XXXXXXXXX| + // |XXXXXXXXX| |XXXXXXXXX| + // |XXXXXXXXX| |XXXXXXXXX| + // --------------------------- + float scale = static_cast<float>(kClientSize.height()) / + static_cast<float>(kSmall.height()); + // Vertical fits exactly + EXPECT_EQ(scale, scene.position.z); + + // centered + EXPECT_EQ( + (kClientSize.width() - static_cast<int>(scale * kSmall.width())) / 2, + scene.position.x); + // sits on Axis + EXPECT_EQ(0, scene.position.y); + + // bound on all 4 sides + EXPECT_TRUE(scene.anchored.left); + EXPECT_TRUE(scene.anchored.right); + EXPECT_TRUE(scene.anchored.top); + EXPECT_TRUE(scene.anchored.bottom); + + // mouse is in centered both vertical and horizontal + EXPECT_EQ((kSmall.width() / 2) - 1, // -1 for pixel rounding + scene.mousePosition.x()); + EXPECT_EQ(kSmall.height() / 2, scene.mousePosition.y()); +} + +TEST(SceneViewTest_Property, FrameSizeSmallInversed) { + SceneView* scene = [[SceneView alloc] init]; + [scene setContentSize:CGSizeMake(kClientSize.width(), kClientSize.height())]; + [scene setFrameSize:kSmallInversed]; + EXPECT_EQ(kSmallInversed.width(), scene.frameSize.width()); + EXPECT_EQ(kSmallInversed.height(), scene.frameSize.height()); + + // Screen is positioned in the lower,left corner, zoomed until the vertical + // fits exactly, and then centered horizontally + // CLIENT + // --------------------------- + // | | + // | | + // | | HOST + // | | ---------------------- + // | | | | + // | | | | + // | | | | + // --------------------------- ---------------------- + // RESULT - ONSCREEN is completely covered, with some of the HOST off screen + // (-.-) the mouse cursor + // -------------------------------------------- + // | ONSCREEN | OFFSCREEN | + // | | | + // | | | + // | -.- | | + // | | | + // | | | + // | | | + // -------------------------------------------- + float scale = static_cast<float>(kClientSize.height()) / + static_cast<float>(kSmallInversed.height()); + // vertical fits exactly + EXPECT_EQ(scale, scene.position.z); + + // sitting on both Axis + EXPECT_EQ(0, scene.position.x); + EXPECT_EQ(0, scene.position.y); + + // bound on 3 sides, not on the right + EXPECT_TRUE(scene.anchored.left); + EXPECT_FALSE(scene.anchored.right); + EXPECT_TRUE(scene.anchored.top); + EXPECT_TRUE(scene.anchored.bottom); + + // mouse is off center on the left horizontal + EXPECT_EQ(kClientSize.width() / (scale * 2), scene.mousePosition.x()); + // mouse is centered vertical + EXPECT_EQ(kSmallInversed.height() / 2, scene.mousePosition.y()); +} + +TEST_F(SceneViewTest, ContainsTouchPoint) { + int midWidth = kClientWidth / 2; + int midHeight = kClientHeight / 2; + // left + EXPECT_FALSE([scene_ containsTouchPoint:CGPointMake(-1, midHeight)]); + EXPECT_TRUE([scene_ containsTouchPoint:CGPointMake(0, midHeight)]); + // right + EXPECT_FALSE( + [scene_ containsTouchPoint:CGPointMake(kClientWidth, midHeight)]); + EXPECT_TRUE( + [scene_ containsTouchPoint:CGPointMake(kClientWidth - 1, midHeight)]); + // top + EXPECT_FALSE( + [scene_ containsTouchPoint:CGPointMake(midWidth, kClientHeight)]); + EXPECT_TRUE( + [scene_ containsTouchPoint:CGPointMake(midWidth, kClientHeight - 1)]); + // bottom + EXPECT_FALSE([scene_ containsTouchPoint:CGPointMake(midWidth, -1)]); + EXPECT_TRUE([scene_ containsTouchPoint:CGPointMake(midWidth, 0)]); + + [scene_ setMarginsFromLeft:10 right:10 top:10 bottom:10]; + + // left + EXPECT_FALSE([scene_ containsTouchPoint:CGPointMake(9, midHeight)]); + EXPECT_TRUE([scene_ containsTouchPoint:CGPointMake(10, midHeight)]); + // right + EXPECT_FALSE( + [scene_ containsTouchPoint:CGPointMake(kClientWidth - 10, midHeight)]); + EXPECT_TRUE( + [scene_ containsTouchPoint:CGPointMake(kClientWidth - 11, midHeight)]); + // top + EXPECT_FALSE( + [scene_ containsTouchPoint:CGPointMake(midWidth, kClientHeight - 10)]); + EXPECT_TRUE( + [scene_ containsTouchPoint:CGPointMake(midWidth, kClientHeight - 11)]); + // bottom + EXPECT_FALSE([scene_ containsTouchPoint:CGPointMake(midWidth, 9)]); + EXPECT_TRUE([scene_ containsTouchPoint:CGPointMake(midWidth, 10)]); +} + +TEST_F(SceneViewTest, + UpdateMousePositionAndAnchorsWithTranslationNoMovement) { + + webrtc::DesktopVector originalPosition = scene_.mousePosition; + AnchorPosition originalAnchors = scene_.anchored; + + [scene_ updateMousePositionAndAnchorsWithTranslation:CGPointMake(0, 0) + scale:1]; + + webrtc::DesktopVector newPosition = scene_.mousePosition; + + EXPECT_EQ(0, abs(originalPosition.x() - newPosition.x())); + EXPECT_EQ(0, abs(originalPosition.y() - newPosition.y())); + + EXPECT_EQ(originalAnchors.right, scene_.anchored.right); + EXPECT_EQ(originalAnchors.top, scene_.anchored.top); + EXPECT_EQ(originalAnchors.left, scene_.anchored.left); + EXPECT_EQ(originalAnchors.bottom, scene_.anchored.bottom); + + EXPECT_FALSE(scene_.tickPanVelocity); +} + +TEST_F(SceneViewTest, + UpdateMousePositionAndAnchorsWithTranslationTowardLeftAndTop) { + // Translation is in a coordinate space where (0,0) is the bottom left of the + // view. Mouse position in in a coordinate space where (0,0) is the top left + // of the view. So |y| is moved in the negative direction. + + webrtc::DesktopVector originalPosition = scene_.mousePosition; + + [scene_ setPanVelocity:CGPointMake(1, 1)]; + [scene_ updateMousePositionAndAnchorsWithTranslation:CGPointMake(2, -1) + scale:1]; + + webrtc::DesktopVector newPosition = scene_.mousePosition; + + // We could do these checks as a single test, for a positive vs negative + // difference. But this style has a clearer meaning that the position moved + // toward or away from the origin. + EXPECT_LT(newPosition.x(), originalPosition.x()); + EXPECT_LT(newPosition.y(), originalPosition.y()); + EXPECT_EQ(2, abs(originalPosition.x() - newPosition.x())); + EXPECT_EQ(1, abs(originalPosition.y() - newPosition.y())); + + EXPECT_TRUE(scene_.anchored.left); + EXPECT_TRUE(scene_.anchored.top); + + EXPECT_FALSE(scene_.anchored.right); + EXPECT_FALSE(scene_.anchored.bottom); + + EXPECT_TRUE(scene_.tickPanVelocity); + + // move much further than the bounds allow + [scene_ setPanVelocity:CGPointMake(1, 1)]; + [scene_ + updateMousePositionAndAnchorsWithTranslation:CGPointMake(10000, -10000) + scale:1]; + + newPosition = scene_.mousePosition; + + EXPECT_EQ(0, newPosition.x()); + EXPECT_EQ(0, newPosition.y()); + + EXPECT_TRUE(scene_.anchored.left); + EXPECT_TRUE(scene_.anchored.top); + + EXPECT_FALSE(scene_.anchored.right); + EXPECT_FALSE(scene_.anchored.bottom); + + EXPECT_FALSE(scene_.tickPanVelocity); +} + +TEST_F(SceneViewTest, + UpdateMousePositionAndAnchorsWithTranslationTowardLeftAndBottom) { + webrtc::DesktopVector originalPosition = scene_.mousePosition; + + // see notes for Test + // UpdateMousePositionAndAnchorsWithTranslationTowardLeftAndTop + [scene_ setPanVelocity:CGPointMake(1, 1)]; + [scene_ updateMousePositionAndAnchorsWithTranslation:CGPointMake(2, 1) + scale:1]; + webrtc::DesktopVector newPosition = scene_.mousePosition; + + EXPECT_LT(newPosition.x(), originalPosition.x()); + EXPECT_GT(newPosition.y(), originalPosition.y()); + EXPECT_EQ(2, abs(originalPosition.x() - newPosition.x())); + EXPECT_EQ(1, abs(originalPosition.y() - newPosition.y())); + + EXPECT_TRUE(scene_.anchored.left); + EXPECT_TRUE(scene_.anchored.bottom); + + EXPECT_FALSE(scene_.anchored.right); + EXPECT_FALSE(scene_.anchored.top); + + EXPECT_TRUE(scene_.tickPanVelocity); + + [scene_ setPanVelocity:CGPointMake(1, 1)]; + [scene_ updateMousePositionAndAnchorsWithTranslation:CGPointMake(10000, 10000) + scale:1]; + newPosition = scene_.mousePosition; + + EXPECT_EQ(0, newPosition.x()); + EXPECT_EQ(scene_.frameSize.height() - 1, newPosition.y()); + + EXPECT_TRUE(scene_.anchored.left); + EXPECT_TRUE(scene_.anchored.bottom); + + EXPECT_FALSE(scene_.anchored.right); + EXPECT_FALSE(scene_.anchored.top); + + EXPECT_FALSE(scene_.tickPanVelocity); +} + +TEST_F(SceneViewTest, + UpdateMousePositionAndAnchorsWithTranslationTowardRightAndTop) { + webrtc::DesktopVector originalPosition = scene_.mousePosition; + + // see notes for Test + // UpdateMousePositionAndAnchorsWithTranslationTowardLeftAndTop + + // When moving to the right the mouse remains centered since the horizontal + // display space is larger than the view space + [scene_ setPanVelocity:CGPointMake(1, 1)]; + [scene_ updateMousePositionAndAnchorsWithTranslation:CGPointMake(-2, -1) + scale:1]; + webrtc::DesktopVector newPosition = scene_.mousePosition; + + EXPECT_LT(newPosition.y(), originalPosition.y()); + EXPECT_EQ(0, abs(originalPosition.x() - newPosition.x())); + EXPECT_EQ(1, abs(originalPosition.y() - newPosition.y())); + + EXPECT_TRUE(scene_.anchored.top); + + EXPECT_FALSE(scene_.anchored.left); + EXPECT_FALSE(scene_.anchored.right); + EXPECT_FALSE(scene_.anchored.bottom); + + EXPECT_TRUE(scene_.tickPanVelocity); + + [scene_ setPanVelocity:CGPointMake(1, 1)]; + [scene_ + updateMousePositionAndAnchorsWithTranslation:CGPointMake(-10000, -10000) + scale:1]; + newPosition = scene_.mousePosition; + + EXPECT_EQ(scene_.frameSize.width() - 1, newPosition.x()); + EXPECT_EQ(0, newPosition.y()); + + EXPECT_TRUE(scene_.anchored.right); + EXPECT_TRUE(scene_.anchored.top); + + EXPECT_FALSE(scene_.anchored.left); + EXPECT_FALSE(scene_.anchored.bottom); + + EXPECT_FALSE(scene_.tickPanVelocity); +} + +TEST_F(SceneViewTest, + UpdateMousePositionAndAnchorsWithTranslationTowardRightAndBottom) { + webrtc::DesktopVector originalPosition = scene_.mousePosition; + + // see notes for Test + // UpdateMousePositionAndAnchorsWithTranslationTowardLeftAndTop + + // When moving to the right the mouse remains centered since the horizontal + // display space is larger than the view space + [scene_ setPanVelocity:CGPointMake(1, 1)]; + [scene_ updateMousePositionAndAnchorsWithTranslation:CGPointMake(-2, 1) + scale:1]; + webrtc::DesktopVector newPosition = scene_.mousePosition; + + EXPECT_GT(newPosition.y(), originalPosition.y()); + EXPECT_EQ(0, abs(originalPosition.x() - newPosition.x())); + EXPECT_EQ(1, abs(originalPosition.y() - newPosition.y())); + + EXPECT_TRUE(scene_.anchored.bottom); + + EXPECT_FALSE(scene_.anchored.left); + EXPECT_FALSE(scene_.anchored.right); + EXPECT_FALSE(scene_.anchored.top); + + EXPECT_TRUE(scene_.tickPanVelocity); + + [scene_ setPanVelocity:CGPointMake(1, 1)]; + [scene_ + updateMousePositionAndAnchorsWithTranslation:CGPointMake(-10000, 10000) + scale:1]; + newPosition = scene_.mousePosition; + + EXPECT_EQ(scene_.frameSize.width() - 1, newPosition.x()); + EXPECT_EQ(scene_.frameSize.height() - 1, newPosition.y()); + + EXPECT_TRUE(scene_.anchored.right); + EXPECT_TRUE(scene_.anchored.bottom); + + EXPECT_FALSE(scene_.anchored.left); + EXPECT_FALSE(scene_.anchored.top); + + EXPECT_FALSE(scene_.tickPanVelocity); +} + +TEST(SceneViewTest_Static, PositionDeltaFromScaling) { + + // Legend: + // * anchored point or end point + // | unanchored endpoint + // - onscreen + // # offscreen + + // *---| + // *-------| + EXPECT_EQ( + 0, + [SceneView positionDeltaFromScaling:2.0F position:0 length:100 anchor:0]); + // *---| + // *-| + EXPECT_EQ( + 0, + [SceneView positionDeltaFromScaling:0.5F position:0 length:100 anchor:0]); + // |---* + // |-------* + EXPECT_EQ(100, + [SceneView positionDeltaFromScaling:2.0F + position:0 + length:100 + anchor:100]); + // |----* + // |--* + EXPECT_EQ(-50, + [SceneView positionDeltaFromScaling:0.5F + position:0 + length:100 + anchor:100]); + // |*---| + // |-*-------| + EXPECT_EQ(25, + [SceneView positionDeltaFromScaling:2.0F + position:0 + length:100 + anchor:25]); + // |-*--| + // |*-| + EXPECT_EQ(-12.5, + [SceneView positionDeltaFromScaling:0.5F + position:0 + length:100 + anchor:25]); + // |---*| + // |------*-| + EXPECT_EQ(75, + [SceneView positionDeltaFromScaling:2.0F + position:0 + length:100 + anchor:75]); + // |--*-| + // |-*| + EXPECT_EQ(-37.5, + [SceneView positionDeltaFromScaling:0.5F + position:0 + length:100 + anchor:75]); + // |-*-| + // |---*---| + EXPECT_EQ(50, + [SceneView positionDeltaFromScaling:2.0F + position:0 + length:100 + anchor:50]); + // |--*--| + // |*| + EXPECT_EQ(-25, + [SceneView positionDeltaFromScaling:0.5F + position:0 + length:100 + anchor:50]); + ////////////////////////////////// + // Change position to 50, anchor is relatively the same + ////////////////////////////////// + EXPECT_EQ(0, + [SceneView positionDeltaFromScaling:2.0F + position:50 + length:100 + anchor:50]); + EXPECT_EQ(0, + [SceneView positionDeltaFromScaling:0.5F + position:50 + length:100 + anchor:50]); + EXPECT_EQ(100, + [SceneView positionDeltaFromScaling:2.0F + position:50 + length:100 + anchor:150]); + EXPECT_EQ(-50, + [SceneView positionDeltaFromScaling:0.5F + position:50 + length:100 + anchor:150]); + EXPECT_EQ(25, + [SceneView positionDeltaFromScaling:2.0F + position:50 + length:100 + anchor:75]); + EXPECT_EQ(-12.5, + [SceneView positionDeltaFromScaling:0.5F + position:50 + length:100 + anchor:75]); + EXPECT_EQ(75, + [SceneView positionDeltaFromScaling:2.0F + position:50 + length:100 + anchor:125]); + EXPECT_EQ(-37.5, + [SceneView positionDeltaFromScaling:0.5F + position:50 + length:100 + anchor:125]); + EXPECT_EQ(50, + [SceneView positionDeltaFromScaling:2.0F + position:50 + length:100 + anchor:100]); + EXPECT_EQ(-25, + [SceneView positionDeltaFromScaling:0.5F + position:50 + length:100 + anchor:100]); + + ////////////////////////////////// + // Change position to -50, length to 200, anchor is relatively the same + ////////////////////////////////// + EXPECT_EQ(0, + [SceneView positionDeltaFromScaling:2.0F + position:-50 + length:200 + anchor:-50]); + EXPECT_EQ(0, + [SceneView positionDeltaFromScaling:0.5F + position:-50 + length:200 + anchor:-50]); + EXPECT_EQ(200, + [SceneView positionDeltaFromScaling:2.0F + position:-50 + length:200 + anchor:150]); + EXPECT_EQ(-100, + [SceneView positionDeltaFromScaling:0.5F + position:-50 + length:200 + anchor:150]); + EXPECT_EQ(50, + [SceneView positionDeltaFromScaling:2.0F + position:-50 + length:200 + anchor:0]); + EXPECT_EQ(-25, + [SceneView positionDeltaFromScaling:0.5F + position:-50 + length:200 + anchor:0]); + EXPECT_EQ(150, + [SceneView positionDeltaFromScaling:2.0F + position:-50 + length:200 + anchor:100]); + EXPECT_EQ(-75, + [SceneView positionDeltaFromScaling:0.5F + position:-50 + length:200 + anchor:100]); + EXPECT_EQ(100, + [SceneView positionDeltaFromScaling:2.0F + position:-50 + length:200 + anchor:50]); + EXPECT_EQ(-50, + [SceneView positionDeltaFromScaling:0.5F + position:-50 + length:200 + anchor:50]); +} + +TEST(SceneViewTest_Static, PositionDeltaFromTranslation) { + // Anchored on both sides. Center it by using 1/2 the free space, offset by + // the current position + EXPECT_EQ(50, + [SceneView positionDeltaFromTranslation:0 + position:0 + freeSpace:100 + scaleingPositionDelta:0 + isAnchoredLow:YES + isAnchoredHigh:YES]); + EXPECT_EQ(50, + [SceneView positionDeltaFromTranslation:100 + position:0 + freeSpace:100 + scaleingPositionDelta:0 + isAnchoredLow:YES + isAnchoredHigh:YES]); + EXPECT_EQ(-50, + [SceneView positionDeltaFromTranslation:0 + position:100 + freeSpace:100 + scaleingPositionDelta:0 + isAnchoredLow:YES + isAnchoredHigh:YES]); + EXPECT_EQ(50, + [SceneView positionDeltaFromTranslation:0 + position:0 + freeSpace:100 + scaleingPositionDelta:100 + isAnchoredLow:YES + isAnchoredHigh:YES]); + EXPECT_EQ(100, + [SceneView positionDeltaFromTranslation:0 + position:0 + freeSpace:200 + scaleingPositionDelta:0 + isAnchoredLow:YES + isAnchoredHigh:YES]); + + // Anchored only on the left. Don't move it + EXPECT_EQ(0, + [SceneView positionDeltaFromTranslation:0 + position:0 + freeSpace:100 + scaleingPositionDelta:0 + isAnchoredLow:YES + isAnchoredHigh:NO]); + EXPECT_EQ(0, + [SceneView positionDeltaFromTranslation:100 + position:0 + freeSpace:100 + scaleingPositionDelta:0 + isAnchoredLow:YES + isAnchoredHigh:NO]); + EXPECT_EQ(0, + [SceneView positionDeltaFromTranslation:0 + position:100 + freeSpace:100 + scaleingPositionDelta:0 + isAnchoredLow:YES + isAnchoredHigh:NO]); + EXPECT_EQ(0, + [SceneView positionDeltaFromTranslation:0 + position:0 + freeSpace:200 + scaleingPositionDelta:100 + isAnchoredLow:YES + isAnchoredHigh:NO]); + EXPECT_EQ(0, + [SceneView positionDeltaFromTranslation:0 + position:0 + freeSpace:200 + scaleingPositionDelta:0 + isAnchoredLow:YES + isAnchoredHigh:NO]); + // Anchored only on the right. Move by the scaling delta + EXPECT_EQ(25, + [SceneView positionDeltaFromTranslation:0 + position:0 + freeSpace:100 + scaleingPositionDelta:25 + isAnchoredLow:NO + isAnchoredHigh:YES]); + EXPECT_EQ(50, + [SceneView positionDeltaFromTranslation:100 + position:0 + freeSpace:100 + scaleingPositionDelta:50 + isAnchoredLow:NO + isAnchoredHigh:YES]); + EXPECT_EQ(75, + [SceneView positionDeltaFromTranslation:0 + position:100 + freeSpace:100 + scaleingPositionDelta:75 + isAnchoredLow:NO + isAnchoredHigh:YES]); + EXPECT_EQ(100, + [SceneView positionDeltaFromTranslation:0 + position:0 + freeSpace:100 + scaleingPositionDelta:100 + isAnchoredLow:NO + isAnchoredHigh:YES]); + EXPECT_EQ(125, + [SceneView positionDeltaFromTranslation:0 + position:0 + freeSpace:200 + scaleingPositionDelta:125 + isAnchoredLow:NO + isAnchoredHigh:YES]); + // Not anchored, translate and move by the scaling delta + EXPECT_EQ(0, + [SceneView positionDeltaFromTranslation:0 + position:0 + freeSpace:100 + scaleingPositionDelta:0 + isAnchoredLow:NO + isAnchoredHigh:NO]); + EXPECT_EQ(25, + [SceneView positionDeltaFromTranslation:25 + position:0 + freeSpace:100 + scaleingPositionDelta:0 + isAnchoredLow:NO + isAnchoredHigh:NO]); + EXPECT_EQ(50, + [SceneView positionDeltaFromTranslation:50 + position:100 + freeSpace:100 + scaleingPositionDelta:0 + isAnchoredLow:NO + isAnchoredHigh:NO]); + EXPECT_EQ(175, + [SceneView positionDeltaFromTranslation:75 + position:0 + freeSpace:100 + scaleingPositionDelta:100 + isAnchoredLow:NO + isAnchoredHigh:NO]); + EXPECT_EQ(100, + [SceneView positionDeltaFromTranslation:100 + position:0 + freeSpace:200 + scaleingPositionDelta:0 + isAnchoredLow:NO + isAnchoredHigh:NO]); +} + +TEST(SceneViewTest_Static, BoundDeltaFromPosition) { + // Entire entity fits in our view, lower bound is not less than the + // upperBound. The delta is bounded to the lowerBound. + EXPECT_EQ(200, + [SceneView boundDeltaFromPosition:0 + delta:0 + lowerBound:200 + upperBound:100]); + EXPECT_EQ(100, + [SceneView boundDeltaFromPosition:100 + delta:0 + lowerBound:200 + upperBound:100]); + EXPECT_EQ(200, + [SceneView boundDeltaFromPosition:0 + delta:100 + lowerBound:200 + upperBound:100]); + EXPECT_EQ(150, + [SceneView boundDeltaFromPosition:50 + delta:100 + lowerBound:200 + upperBound:200]); + // Entity does not fit in our view. The result would be out of bounds on the + // high bound. The delta is bounded to the upper bound and the delta from the + // position is returned. + EXPECT_EQ(100, + [SceneView boundDeltaFromPosition:0 + delta:1000 + lowerBound:0 + upperBound:100]); + EXPECT_EQ(99, + [SceneView boundDeltaFromPosition:1 + delta:1000 + lowerBound:0 + upperBound:100]); + EXPECT_EQ(-50, + [SceneView boundDeltaFromPosition:150 + delta:1000 + lowerBound:50 + upperBound:100]); + EXPECT_EQ(100, + [SceneView boundDeltaFromPosition:100 + delta:1000 + lowerBound:0 + upperBound:200]); + // Entity does not fit in our view. The result would be out of bounds on the + // low bound. The delta is bounded to the lower bound and the delta from the + // position is returned. + EXPECT_EQ(0, + [SceneView boundDeltaFromPosition:0 + delta:-1000 + lowerBound:0 + upperBound:100]); + EXPECT_EQ(-20, + [SceneView boundDeltaFromPosition:20 + delta:-1000 + lowerBound:0 + upperBound:100]); + EXPECT_EQ(21, + [SceneView boundDeltaFromPosition:29 + delta:-1000 + lowerBound:50 + upperBound:100]); + EXPECT_EQ(1, + [SceneView boundDeltaFromPosition:-1 + delta:-1000 + lowerBound:0 + upperBound:200]); + // Entity does not fit in our view. The result is in bounds. The delta is + // returned unchanged. + EXPECT_EQ(50, + [SceneView boundDeltaFromPosition:0 + delta:50 + lowerBound:0 + upperBound:100]); + EXPECT_EQ(-10, + [SceneView boundDeltaFromPosition:20 + delta:-10 + lowerBound:0 + upperBound:100]); + EXPECT_EQ(31, + [SceneView boundDeltaFromPosition:29 + delta:31 + lowerBound:50 + upperBound:100]); + EXPECT_EQ(50, + [SceneView boundDeltaFromPosition:100 + delta:50 + lowerBound:0 + upperBound:200]); +} + +TEST(SceneViewTest_Static, BoundMouseGivenNextPosition) { + // Mouse would move off screen in the negative + EXPECT_EQ(0, + [SceneView boundMouseGivenNextPosition:-1 + maxPosition:50 + centerPosition:2 + isAnchoredLow:YES + isAnchoredHigh:YES]); + EXPECT_EQ(0, + [SceneView boundMouseGivenNextPosition:-1 + maxPosition:25 + centerPosition:99 + isAnchoredLow:NO + isAnchoredHigh:NO]); + EXPECT_EQ(0, + [SceneView boundMouseGivenNextPosition:-11 + maxPosition:0 + centerPosition:-52 + isAnchoredLow:YES + isAnchoredHigh:YES]); + EXPECT_EQ(0, + [SceneView boundMouseGivenNextPosition:-11 + maxPosition:-100 + centerPosition:44 + isAnchoredLow:NO + isAnchoredHigh:NO]); + EXPECT_EQ(0, + [SceneView boundMouseGivenNextPosition:-1 + maxPosition:50 + centerPosition:-20 + isAnchoredLow:YES + isAnchoredHigh:YES]); + + // Mouse would move off screen in the positive + EXPECT_EQ(49, + [SceneView boundMouseGivenNextPosition:50 + maxPosition:50 + centerPosition:2 + isAnchoredLow:YES + isAnchoredHigh:YES]); + EXPECT_EQ(24, + [SceneView boundMouseGivenNextPosition:26 + maxPosition:25 + centerPosition:99 + isAnchoredLow:NO + isAnchoredHigh:NO]); + EXPECT_EQ(-1, + [SceneView boundMouseGivenNextPosition:1 + maxPosition:0 + centerPosition:-52 + isAnchoredLow:YES + isAnchoredHigh:YES]); + EXPECT_EQ(-101, + [SceneView boundMouseGivenNextPosition:0 + maxPosition:-100 + centerPosition:44 + isAnchoredLow:NO + isAnchoredHigh:NO]); + EXPECT_EQ(49, + [SceneView boundMouseGivenNextPosition:60 + maxPosition:50 + centerPosition:-20 + isAnchoredLow:YES + isAnchoredHigh:YES]); + + // Mouse is not out of bounds, and not anchored. The Center is returned. + EXPECT_EQ(2, + [SceneView boundMouseGivenNextPosition:0 + maxPosition:100 + centerPosition:2 + isAnchoredLow:NO + isAnchoredHigh:NO]); + EXPECT_EQ(99, + [SceneView boundMouseGivenNextPosition:25 + maxPosition:100 + centerPosition:99 + isAnchoredLow:NO + isAnchoredHigh:NO]); + EXPECT_EQ(-52, + [SceneView boundMouseGivenNextPosition:99 + maxPosition:100 + centerPosition:-52 + isAnchoredLow:NO + isAnchoredHigh:NO]); + EXPECT_EQ(44, + [SceneView boundMouseGivenNextPosition:120 + maxPosition:200 + centerPosition:44 + isAnchoredLow:NO + isAnchoredHigh:NO]); + EXPECT_EQ(-20, + [SceneView boundMouseGivenNextPosition:180 + maxPosition:200 + centerPosition:-20 + isAnchoredLow:NO + isAnchoredHigh:NO]); + + // Mouse is not out of bounds, and anchored. The position closest + // to the anchor is returned. + EXPECT_EQ(0, + [SceneView boundMouseGivenNextPosition:0 + maxPosition:100 + centerPosition:2 + isAnchoredLow:YES + isAnchoredHigh:NO]); + EXPECT_EQ(25, + [SceneView boundMouseGivenNextPosition:25 + maxPosition:100 + centerPosition:99 + isAnchoredLow:YES + isAnchoredHigh:NO]); + EXPECT_EQ(-52, + [SceneView boundMouseGivenNextPosition:99 + maxPosition:100 + centerPosition:-52 + isAnchoredLow:YES + isAnchoredHigh:NO]); + EXPECT_EQ(44, + [SceneView boundMouseGivenNextPosition:120 + maxPosition:200 + centerPosition:44 + isAnchoredLow:YES + isAnchoredHigh:NO]); + EXPECT_EQ(-20, + [SceneView boundMouseGivenNextPosition:180 + maxPosition:200 + centerPosition:-20 + isAnchoredLow:YES + isAnchoredHigh:NO]); + EXPECT_EQ(2, + [SceneView boundMouseGivenNextPosition:0 + maxPosition:100 + centerPosition:2 + isAnchoredLow:NO + isAnchoredHigh:YES]); + EXPECT_EQ(99, + [SceneView boundMouseGivenNextPosition:25 + maxPosition:100 + centerPosition:99 + isAnchoredLow:NO + isAnchoredHigh:YES]); + EXPECT_EQ(99, + [SceneView boundMouseGivenNextPosition:99 + maxPosition:100 + centerPosition:-52 + isAnchoredLow:NO + isAnchoredHigh:YES]); + EXPECT_EQ(120, + [SceneView boundMouseGivenNextPosition:120 + maxPosition:200 + centerPosition:44 + isAnchoredLow:NO + isAnchoredHigh:YES]); + EXPECT_EQ(180, + [SceneView boundMouseGivenNextPosition:180 + maxPosition:200 + centerPosition:-20 + isAnchoredLow:NO + isAnchoredHigh:YES]); + EXPECT_EQ(0, + [SceneView boundMouseGivenNextPosition:0 + maxPosition:100 + centerPosition:2 + isAnchoredLow:YES + isAnchoredHigh:YES]); + EXPECT_EQ(25, + [SceneView boundMouseGivenNextPosition:25 + maxPosition:100 + centerPosition:99 + isAnchoredLow:YES + isAnchoredHigh:YES]); + EXPECT_EQ(99, + [SceneView boundMouseGivenNextPosition:99 + maxPosition:100 + centerPosition:-52 + isAnchoredLow:YES + isAnchoredHigh:YES]); + EXPECT_EQ(120, + [SceneView boundMouseGivenNextPosition:120 + maxPosition:200 + centerPosition:44 + isAnchoredLow:YES + isAnchoredHigh:YES]); + EXPECT_EQ(180, + [SceneView boundMouseGivenNextPosition:180 + maxPosition:200 + centerPosition:-20 + isAnchoredLow:YES + isAnchoredHigh:YES]); +} + +TEST(SceneViewTest_Static, BoundVelocity) { + // Outside bounds of the axis + EXPECT_EQ(0, [SceneView boundVelocity:5.0f axisLength:100 mousePosition:0]); + EXPECT_EQ(0, [SceneView boundVelocity:5.0f axisLength:100 mousePosition:99]); + EXPECT_EQ(0, [SceneView boundVelocity:5.0f axisLength:200 mousePosition:200]); + // Not outside bounds of the axis + EXPECT_EQ(5.0f, + [SceneView boundVelocity:5.0f axisLength:100 mousePosition:1]); + EXPECT_EQ(5.0f, + [SceneView boundVelocity:5.0f axisLength:100 mousePosition:98]); + EXPECT_EQ(5.0f, + [SceneView boundVelocity:5.0f axisLength:200 mousePosition:100]); +} + +TEST_F(SceneViewTest, TickPanVelocity) { + // We are in the large frame, which can pan left and right but not up and + // down. Start by resizing it to allow panning up and down. + + [scene_ panAndZoom:CGPointMake(0, 0) scaleBy:2.0f]; + + // Going up and right + [scene_ setPanVelocity:CGPointMake(1000, 1000)]; + [scene_ tickPanVelocity]; + + webrtc::DesktopVector pos = scene_.mousePosition; + int loopLimit = 0; + bool didMove = false; + bool inMotion = true; + + while (inMotion && loopLimit < 100) { + inMotion = [scene_ tickPanVelocity]; + if (inMotion) { + ASSERT_TRUE(pos.x() <= scene_.mousePosition.x()) << " after " << loopLimit + << " iterations."; + ASSERT_TRUE(pos.y() <= scene_.mousePosition.y()) << " after " << loopLimit + << " iterations."; + didMove = true; + } + pos = scene_.mousePosition; + loopLimit++; + } + + EXPECT_LT(1, loopLimit); + EXPECT_TRUE(!inMotion); + EXPECT_TRUE(didMove); + + // Going down and left + [scene_ setPanVelocity:CGPointMake(-1000, -1000)]; + [scene_ tickPanVelocity]; + + pos = scene_.mousePosition; + loopLimit = 0; + didMove = false; + inMotion = true; + + while (inMotion && loopLimit < 100) { + inMotion = [scene_ tickPanVelocity]; + if (inMotion) { + ASSERT_TRUE(pos.x() >= scene_.mousePosition.x()) << " after " << loopLimit + << " iterations."; + ASSERT_TRUE(pos.y() >= scene_.mousePosition.y()) << " after " << loopLimit + << " iterations."; + didMove = true; + } + pos = scene_.mousePosition; + loopLimit++; + } + + EXPECT_LT(1, loopLimit); + EXPECT_TRUE(!inMotion); + EXPECT_TRUE(didMove); +} + +} // namespace remoting
\ No newline at end of file |