summaryrefslogtreecommitdiffstats
path: root/remoting/ios/ui
diff options
context:
space:
mode:
Diffstat (limited to 'remoting/ios/ui')
-rw-r--r--remoting/ios/ui/cursor_texture.h58
-rw-r--r--remoting/ios/ui/cursor_texture.mm181
-rw-r--r--remoting/ios/ui/desktop_texture.h38
-rw-r--r--remoting/ios/ui/desktop_texture.mm83
-rw-r--r--remoting/ios/ui/help_view_controller.h17
-rw-r--r--remoting/ios/ui/help_view_controller.mm21
-rw-r--r--remoting/ios/ui/host_list_view_controller.h39
-rw-r--r--remoting/ios/ui/host_list_view_controller.mm229
-rw-r--r--remoting/ios/ui/host_list_view_controller_unittest.mm90
-rw-r--r--remoting/ios/ui/host_view_controller.h115
-rw-r--r--remoting/ios/ui/host_view_controller.mm676
-rw-r--r--remoting/ios/ui/pin_entry_view_controller.h49
-rw-r--r--remoting/ios/ui/pin_entry_view_controller.mm71
-rw-r--r--remoting/ios/ui/pin_entry_view_controller_ipad.xib103
-rw-r--r--remoting/ios/ui/pin_entry_view_controller_iphone.xib113
-rw-r--r--remoting/ios/ui/scene_view.h171
-rw-r--r--remoting/ios/ui/scene_view.mm642
-rw-r--r--remoting/ios/ui/scene_view_unittest.mm1219
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="&lt;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="&lt;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