Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(123)

Unified Diff: remoting/ios/ui/host_view_controller.mm

Issue 186733007: iOS Chromoting Client (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Created 6 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: remoting/ios/ui/host_view_controller.mm
diff --git a/remoting/ios/ui/host_view_controller.mm b/remoting/ios/ui/host_view_controller.mm
new file mode 100644
index 0000000000000000000000000000000000000000..5002eb28b5cf922b86f6b0b600a9ac2e0c3dea87
--- /dev/null
+++ b/remoting/ios/ui/host_view_controller.mm
@@ -0,0 +1,1488 @@
+// 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 possition and turn items on and off. Evantually these should be
+// stabilized and removed.
+
+// Scrool speed multiplier for swiping
+const static int kMouseSensitivity = 2.5;
+
+// Scrool speed multiplier for mouse wheel
+const static int kMouseWheelSensitivity = 20;
+
+// Input Axis inversion
+// 1 for standard, -1 for inverted
+const static int kXAxisInversion = -1;
+const static int kYAxisInversion = -1;
+
+// Area the navigation bar consumes when visible in pixels
+const static int kTopMargin = 19;
+// Area the tool bar consumes when visible in pixels
+const static int kBottomMargin = 0;
+// Experimental value for bounding the maximum zoom ratio
+const static int kMaxZoomSize = 3;
+// Distance between Button in the Tool bar in pixels
+const static int kButtonSpacer = 15;
+
+#ifdef DEBUG
+// Some basic statistics.
+// The number of canvas updates received from the network.
+static uint32_t _applyCount = 0;
+// The number of frames drawn to the GL Context
+static uint32_t _frameCount = 0;
+// The number of frames that also required canvas updates to be drawn to the GL
+// Context.
+static uint32_t _drawCount = 0;
+#endif // DEBUG
+} // namespace
+
+@interface HostViewController (Private)
+- (void)setupGL;
+- (void)tearDownGL;
+- (void)bindTextureForIOS:(GLuint)glName;
+- (void)checkConnectStatus;
+- (void)doConnectToHostWithPin:(NSString*)hostPin
+ createPairing:(BOOL)createPair;
+- (BOOL)convertTouchPointToMousePoint:(CGPoint)touchPoint
+ targetPoint:(webrtc::DesktopVector&)desktopPoint;
+- (BOOL)convertMousePointToTouchPoint:(const webrtc::DesktopVector&)mousePoint
+ targetPoint:(CGPoint&)touchPoint;
+- (void)logGLErrorCode:(NSString*)funcName;
+- (void)setFrameForControls;
+- (CGRect)getViewBounds;
+- (void)updateContentSize;
+- (void)updatePanVelocityShouldCancel:(bool)canceled;
+- (void)orientationChanged:(NSNotification*)note;
+- (webrtc::DesktopSize)getCurrentSize;
+- (void)panAndZoom:(CGPoint)translation scaleBy:(float)ratio;
++ (float)calculateScaleingDelta:(float)scale
+ size:(float)size
+ position:(float)position
+ anchor:(float)anchor;
++ (int)calculatePositionDelta:(int)position
+ length:(int)length
+ translation:(int)translation
+ scaleDelta:(int)scaleDelta
+ isAnchoredLow:(BOOL)isAnchoredLow
+ isAnchoredHigh:(BOOL)isAnchoredHigh;
++ (int)applyBoundsToFrameAxis:(float)position
+ delta:(int)delta
+ lowerBound:(int)lowerBound
+ upperBound:(int)upperBound;
++ (int)calculateMousePosition:(int)nextPosition
+ maxPosition:(int)maxPosition
+ centerPosition:(int)centerPosition
+ isAnchoredLow:(BOOL)isAnchoredLow
+ isAnchoredHigh:(BOOL)isAnchoredHigh;
+- (void)cancelVelocityWhenAtEdge;
+- (void)updateMousePositionAndAnchorsWithBounds:
+ (const webrtc::DesktopVector&)bounds
+ translation:(CGPoint)translation;
+- (BOOL)isPointInScene:(CGPoint)point;
+- (void)showToolbar:(BOOL)visible;
+@end
+
+@implementation HostViewController
+
+// Override UIViewController
+- (void)viewDidLoad {
+ [super viewDidLoad];
+
+ _context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
+
+ DCHECK(_context);
+
+ GLKView* view = static_cast<GLKView*>(self.view);
+ view.context = _context;
+
+ [_keyEntryView setDelegate:self];
+
+ _controller = [[ClientController alloc] init];
+
+ _updateDisplayTimer =
+ [NSTimer scheduledTimerWithTimeInterval:0.4
+ target:self
+ selector:@selector(checkConnectStatus)
+ userInfo:nil
+ repeats:NO];
+ _mousePosition.set(0, 0);
+ _glBufferLock = [[NSLock alloc] init];
+ _glCursorLock = [[NSLock alloc] init];
+ _needCursorRedraw = NO;
+ _cursorDrawnToGL = webrtc::DesktopRect::MakeXYWH(0, 0, 0, 0);
+ _sizeChanged = YES; // rgba
+ _frameSize.set(1, 1);
+ _scenePosition = GLKVector3Make(0, 0, 1);
+
+ [self updatePanVelocityShouldCancel:YES];
+
+ // [self updateContectSize] is implicit in this call, and required at this
+ // point
+ [self showToolbar:NO];
+
+ [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]];
+
+ [self.navigationController setNavigationBarHidden:YES animated:YES];
+
+ // We impliment UIBarPositioningDelegate to handle this class's delegate
+ [_viewToolbar setDelegate:self];
+ // Causes |_viewToolbar| to call |positionForBar| via it's delegate where we
+ // can inform it of it's relative position
+ [self setNeedsStatusBarAppearanceUpdate];
+}
+
+- (void)setupGL {
+ [EAGLContext setCurrentContext:_context];
+
+ _effect = [[GLKBaseEffect alloc] init];
+
+ [self logGLErrorCode:@"setupGL begin"];
+
+ // Create 2 textures in the array |_textureIds|
+ glGenTextures(2, _textureIds);
+
+ [self logGLErrorCode:@"setupGL init"];
+ // Initialize each texture
+ [self bindTextureForIOS:_textureIds[0]];
+ glBindTexture(GL_TEXTURE_2D, 0);
+
+ [self bindTextureForIOS:_textureIds[1]];
+ glBindTexture(GL_TEXTURE_2D, 0);
+
+ [self logGLErrorCode:@"setupGL textureComplete"];
+
+ // Texture 0 is the HOST Desktop layer, and will always replace what is in the
+ // draw context
+ [_effect texture2d0].target = GLKTextureTarget2D;
+ [_effect texture2d0].name = _textureIds[0];
+ [_effect texture2d0].envMode = GLKTextureEnvModeReplace;
+ [_effect texture2d0].enabled = GL_TRUE;
+
+ // Texuture 1 is the Cursor layer, and is stamped on top of texture 0 as a
+ // transparent image
+ [_effect texture2d1].target = GLKTextureTarget2D;
+ [_effect texture2d1].name = _textureIds[1];
+ [_effect texture2d1].envMode = GLKTextureEnvModeDecal;
+ [_effect texture2d1].enabled = GL_TRUE;
+}
+
+// Override UIViewController
+- (void)viewDidUnload {
+ [super viewDidUnload];
+ [self tearDownGL];
+
+ if ([EAGLContext currentContext] == _context) {
+ [EAGLContext setCurrentContext:nil];
+ }
+ _context = nil;
+}
+
+- (void)tearDownGL {
+ [EAGLContext setCurrentContext:_context];
+ // Release 2 GL Textures
+ glDeleteTextures(2, _textureIds);
+}
+
+// Override UIViewController
+- (void)viewWillAppear:(BOOL)animated {
+ [super viewWillAppear:NO];
+
+ [self setFrameForControls];
+}
+
+// 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
+ [_controller disconnectFromHost];
+ }
+}
+
+// GL Binding Context requires some specific flags for the type of textures
+// being drawn
+- (void)bindTextureForIOS:(GLuint)glName {
+ glBindTexture(GL_TEXTURE_2D, glName);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
+ glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
+}
+
+// Callback from |_updateDisplayTimer|. Occurs a short time after the form has
+// loaded. Begin the connection to the host.
+- (void)checkConnectStatus {
+ if (_updateDisplayTimer) {
+ [_updateDisplayTimer invalidate];
+ _updateDisplayTimer = nil;
+ }
+
+ _bbiHostAndStatus.title = _host.hostName;
+ [_busyIndicator startAnimating];
+
+ NSString* authToken = [_authorization.parameters valueForKey:@"access_token"];
+
+ if (authToken == nil) {
+ authToken = [_authorization authorizationTokenKey];
+ }
+
+ [_controller connectToHost:[_authorization userEmail]
+ authToken:authToken
+ jabberId:_host.jabberId
+ hostId:_host.hostId
+ publicKey:_host.publicKey
+ delegate:self];
+}
+
+// @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:(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];
+ }
+
+ [[_pinEntry presentingViewController] dismissViewControllerAnimated:NO
+ completion:NULL];
+ _pinEntry = nil;
+
+ [self doConnectToHostWithPin:hostPin createPairing:!shouldPrompt];
+}
+
+// @protocol PinEntryViewControllerDelegate
+// Returns if the user canceled while entering their PIN
+- (void)cancelledConnectToHostWithPin {
+ [[_pinEntry presentingViewController] dismissViewControllerAnimated:NO
+ completion:NULL];
+ _pinEntry = nil;
+
+ [self.navigationController popViewControllerAnimated:YES];
+}
+
+- (void)setHostDetails:(Host*)host
+ authorization:(GTMOAuth2Authentication*)authorization {
+ _host = host;
+ _authorization = authorization;
+}
+
+// Occurs after the user has entered thier PIN. If Pairing is already
+// established, PIN entry may be skipped.
+- (void)doConnectToHostWithPin:(NSString*)hostPin
+ createPairing:(BOOL)createPair {
+ [_controller authenticationResponse:hostPin createPair:createPair];
+}
+
+// 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 opperates in integer values.
+- (BOOL)convertTouchPointToMousePoint:(CGPoint)touchPoint
+ targetPoint:(webrtc::DesktopVector&)mousePoint {
+ if (![self isPointInScene: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 - _scenePosition.x,
+ glOrientedTouchPoint.y - _scenePosition.y);
+
+ // The perspective exists in relative to the CLIENT resoluation at 1:1, zoom
+ // our persepective so we are relative to the HOST at 1:1
+ CGPoint glOrientedPointInFrame =
+ CGPointMake(glOrientedPointInRespectToFrame.x / _scenePosition.z,
+ glOrientedPointInRespectToFrame.y / _scenePosition.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);
+}
+
+// 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 opperates in integer values.
+- (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 resoluation at 1:1, zoom
+ // our persepective so we are relative to the HOST at 1:1
+ CGPoint glOrientedPointInFrame =
+ CGPointMake(deskTopOrientedPointInFrame.x * _scenePosition.z,
+ deskTopOrientedPointInFrame.y * _scenePosition.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 + _scenePosition.x,
+ glOrientedPointInFrame.y + _scenePosition.y);
+
+ // The GL Context is upside down from the User's perspective so flip it.
+ CGPoint glOrientedTouchPoint =
+ CGPointMake(glOrientedPointInRespectToFrame.x,
+ _contentSize.height() - glOrientedPointInRespectToFrame.y);
+
+ // Convert from float to integer
+ touchPoint.x = floorf(glOrientedTouchPoint.x);
+ touchPoint.y = floorf(glOrientedTouchPoint.y);
+
+ return [self isPointInScene:touchPoint];
+}
+
+// Resize the view of the desktop - Zoon in/out. This can occur during a Pan.
+- (IBAction)pinchGestureTriggered:(UIPinchGestureRecognizer*)sender {
+ if ([sender state] == UIGestureRecognizerStateChanged) {
+ [self panAndZoom:CGPointMake(0.0, 0.0) scaleBy:sender.scale];
+
+ sender.scale = 1.0; // reset scale so next iteration is a relative ratio
+ }
+}
+
+// Left button click
+- (IBAction)tapGestureTriggered:(UITapGestureRecognizer*)sender {
+ if ([self isPointInScene:[sender locationInView:self.view]]) {
+ [Utility leftClickOn:_controller at:_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 scrool event
+ translation.x *= kMouseWheelSensitivity;
+ translation.y *= kMouseWheelSensitivity;
+ [Utility mouseScroll:_controller
+ at:_mousePosition
+ delta:webrtc::DesktopVector(translation.x, translation.y)];
+ } else {
+ // Did not begin with 2 touches, pan event
+ if ([sender state] == UIGestureRecognizerStateChanged) {
+ CGPoint translation = [sender translationInView:self.view];
+
+ [self panAndZoom:translation scaleBy:1.0];
+
+ } else if ([sender state] == UIGestureRecognizerStateEnded) {
+ // After user removes their fingers from the screen, apply an acceleration
+ // effect
+ _panVelocity = [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) {
+ [_controller mouseAction:_mousePosition
+ wheelDelta:webrtc::DesktopVector(0, 0)
+ whichButton:1
+ buttonDown:YES];
+ } else if (!([sender state] == UIGestureRecognizerStateBegan ||
+ [sender state] == UIGestureRecognizerStateChanged)) {
+ [_controller mouseAction:_mousePosition
+ wheelDelta:webrtc::DesktopVector(0, 0)
+ whichButton:1
+ buttonDown:NO];
+ }
+}
+
+// Right mouse button click
+- (IBAction)twoFingerTapGestureTriggered:(UITapGestureRecognizer*)sender {
+ if ([self isPointInScene:[sender locationInView:self.view]]) {
+ [Utility rightClickOn:_controller at:_mousePosition];
+ }
+}
+
+// Middle button click
+- (IBAction)threeFingerTapGestureTriggered:(UITapGestureRecognizer*)sender {
+
+ if ([self isPointInScene:[sender locationInView:self.view]]) {
+ [Utility middleClickOn:_controller at:_mousePosition];
+ }
+}
+
+// Show keyboard or navigation toolbar
+- (IBAction)threeFingerPanGestureTriggered:(UIPanGestureRecognizer*)sender {
+ if ([sender state] == UIGestureRecognizerStateChanged) {
+ CGPoint translation = [sender translationInView:self.view];
+ if (translation.y > 0) {
+ // Swiped down
+ [self.navigationController setNavigationBarHidden:NO animated:YES];
+ } else if (translation.y < 0) {
+ // Swiped up
+ [_keyEntryView becomeFirstResponder];
+ }
+ [sender setTranslation:CGPointZero inView:self.view];
+ }
+}
+
+// Do navigation 'back'
+- (IBAction)barBtnNavigationBackPressed:(id)sender {
+ [self.navigationController popViewControllerAnimated:YES];
+}
+
+// Display the keyboard
+- (IBAction)barBtnKeyboardPressed:(id)sender {
+ [_keyEntryView becomeFirstResponder];
+}
+
+// Display the navigation bar
+- (IBAction)barBtnToolBarHidePressed:(id)sender {
+ [self showToolbar:(_viewToolbar.frame.origin.y < 0)]; // Toolbar is either on
+ // screen or off screen
+}
+
+// @protocol UIBarPositioningDelegate
+// Place the toolbar at the top of the screen
+- (UIBarPosition)positionForBar:(id<UIBarPositioning>)bar {
+ return UIBarPositionTopAttached;
+}
+
+// Override UIResponder
+// When any gesture beings, 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 simultaniously.
+// Allow panning and long press to occur simultaniously.
+// Pinch requires 2 touches, and long press requires a single touch, so they are
+// mutually exclusive regardless of if panning is the initiating guesture
+- (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) {
+ _pinEntry = [[PinEntryViewController alloc] init];
+ if (_pinEntry) {
+ [_pinEntry setDelegate:self];
+ [_pinEntry setHostName:_host.hostName];
+ [_pinEntry setShouldPrompt:YES];
+ [_pinEntry setPairingSupported:pairingSupported];
+
+ [self presentViewController:_pinEntry animated:YES completion:nil];
+ }
+ } else {
+ [self doConnectToHostWithPin:hostPrefs.hostPin
+ createPairing:pairingSupported];
+ }
+}
+
+// @protocol ClientControllerDelegate
+// Occurs when a connection to a HOST is established successfully
+- (void)connected {
+ [_busyIndicator stopAnimating];
+}
+
+// @protocol ClientControllerDelegate
+// Occurs when a connection to a HOST has failed
+- (void)connectionFailed:(NSString*)errorMessage {
+ [_busyIndicator stopAnimating];
+ NSString* errorMsg;
+ if ([_controller isConnected]) {
+ errorMsg = @"Lost Connection";
+ } else {
+ errorMsg = @"Unable to connect";
+ }
+ [Utility showAlert:errorMsg message:errorMessage];
+ [self.navigationController popViewControllerAnimated:YES];
+}
+
+// @protocol ClientControllerDelegate
+- (void)connectionStatus:(NSString*)statusMessage {
+ NSMutableString* hostStatus = [[NSMutableString alloc] init];
+ [hostStatus appendFormat:@"%@ - %@", _host.hostName, statusMessage];
+ _bbiHostAndStatus.title = hostStatus;
+}
+
+// @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 continguous 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 {
+
+#ifdef DEBUG
+ _applyCount++;
+#endif // DEBUG
+
+ // The HOST can change resolution. This always occurs on the first call to
+ // |applyFrame|
+ if (_frameSize.width() != size.width() ||
+ _frameSize.height() != size.height()) {
+ _frameSize.set(size.width(), size.height());
+
+ _scenePosition.x = 0;
+ _scenePosition.y = 0;
+
+ CGPoint ratios = [self pixelRatio];
+
+ float verticalPixelScaleRatio = 1;
+
+ // If the new resolution is larger then the old size, increase the zoom to
+ // fit the entire vertical height in the CLIENT resolution height.
+ if (_scenePosition.z < ratios.y) {
+ verticalPixelScaleRatio = 1 / _scenePosition.z; // keep the current scale
+ } else {
+ verticalPixelScaleRatio =
+ ratios.y / _scenePosition.z; // make scale small
+ // enough so it will
+ // fit vertical height
+ }
+
+ _isAnchorLeft = YES;
+ _isAnchorBottom = YES;
+ _isAnchorTop = YES;
+ _isAnchorRight = YES;
+
+ [self panAndZoom:CGPointMake(0.0, 0.0) scaleBy:verticalPixelScaleRatio];
+ _sizeChanged = YES;
+
+ webrtc::DesktopVector newMouseLocation;
+ if ([self convertTouchPointToMousePoint:
+ CGPointMake(_contentSize.width() / 2, _contentSize.height() / 2)
+ targetPoint:newMouseLocation]) {
+ _mousePosition.set(newMouseLocation.x(), newMouseLocation.y());
+ }
+
+#if DEBUG
+ NSLog(@"resized frame:%d:%d scale:%f",
+ _frameSize.width(),
+ _frameSize.height(),
+ _scenePosition.z);
+#endif // DEBUG
+ }
+
+ [_glBufferLock lock]; // going to make changes to |_glRegions|
+
+ 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 makeing 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
+ _cursor.reset(
+ new webrtc::MouseCursor(new webrtc::BasicDesktopFrame(size), hotspot));
+
+ if (_cursor->image().data()) {
+ memcpy(_cursor->image().data(),
+ data,
+ size.width() * size.height() * _cursor->image().kBytesPerPixel);
+
+ _needCursorRedraw = YES;
+ } else {
+ _cursor.reset();
+ _needCursorRedraw = NO;
+ }
+
+ [_glCursorLock unlock]; // done makeing 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 varibles consumed by this function
+// should be thread safe.
+//
+// Clear Screen, update desktop, update cursor, define possition, 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);
+
+ [self logGLErrorCode:@"drawInRect bindBuffer"];
+
+ if (_glRegions.size() > 0 || _sizeChanged) {
+#ifdef DEBUG
+ _drawCount++;
+#endif // DEBUG
+
+ if (_sizeChanged) {
+ // Update desktop is done by regions. But for the first call and when the
+ // size changes (from the HOST changing desktop resoluation) we have to
+ // reinitialize the textures.
+ [self bindTextureForIOS:_textureIds[0]];
+
+ glTexImage2D(GL_TEXTURE_2D,
+ 0,
+ GL_RGBA,
+ _frameSize.width(),
+ _frameSize.height(),
+ 0,
+ GL_RGBA,
+ GL_UNSIGNED_BYTE,
+ NULL);
+
+ glBindTexture(GL_TEXTURE_2D, 0);
+
+ [self bindTextureForIOS:_textureIds[1]];
+
+ glTexImage2D(GL_TEXTURE_2D,
+ 0,
+ GL_RGBA,
+ _frameSize.width(),
+ _frameSize.height(),
+ 0,
+ GL_RGBA,
+ GL_UNSIGNED_BYTE,
+ NULL);
+
+ glBindTexture(GL_TEXTURE_2D, 0);
+
+ [self logGLErrorCode:@"drawInRect glTexImage2D"];
+ _sizeChanged = NO;
+ }
+
+ [self bindTextureForIOS:_textureIds[0]];
+
+ [_glBufferLock lock]; // going to make changes to |_glRegions|
+
+ for (uint32_t i = 0; i < _glRegions.size(); i++) {
+
+ GLRegion* region = _glRegions[i];
+
+ // |data| is properly ordered by |applyFrame|
+ glTexSubImage2D(GL_TEXTURE_2D,
+ 0,
+ region->offset->x(),
+ region->offset->y(),
+ region->image->size().width(),
+ region->image->size().height(),
+ GL_RGBA,
+ GL_UNSIGNED_BYTE,
+ region->image->data());
+
+ [self logGLErrorCode:@"drawInRect glTexSubImage2D"];
+ }
+
+ _glRegions.clear();
+
+ [_glBufferLock unlock]; // done makeing changes to |_glRegions|
+
+ // release bind - be nice
+ glBindTexture(GL_TEXTURE_2D, 0);
+ }
+
+ // When the cursor needs to be redraw in a different spot then we must clear
+ // the previous area.
+ if (_cursor.get() != NULL &&
+ (_needCursorRedraw == YES ||
+ _cursorDrawnToGL.left() != _mousePosition.x() - _cursor->hotspot().x() ||
+ _cursorDrawnToGL.top() != _mousePosition.y() - _cursor->hotspot().y())) {
+
+ [_glCursorLock lock]; // going to make changes to |_cursor|
+ [self bindTextureForIOS:_textureIds[1]];
+
+ if (_cursorDrawnToGL.width() > 0 && _cursorDrawnToGL.height() > 0) {
+ webrtc::BasicDesktopFrame transparentCursor(_cursorDrawnToGL.size());
+
+ if (transparentCursor.data() != NULL) {
+ CHECK(transparentCursor.kBytesPerPixel ==
+ _cursor->image().kBytesPerPixel);
+ memset(transparentCursor.data(),
+ 0,
+ transparentCursor.stride() * transparentCursor.size().height());
+
+ glTexSubImage2D(GL_TEXTURE_2D,
+ 0,
+ _cursorDrawnToGL.left(),
+ _cursorDrawnToGL.top(),
+ _cursorDrawnToGL.width(),
+ _cursorDrawnToGL.height(),
+ GL_RGBA,
+ GL_UNSIGNED_BYTE,
+ 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, _frameSize.width(), _frameSize.height());
+ CGRect cursor = CGRectMake(_mousePosition.x() - _cursor->hotspot().x(),
+ _mousePosition.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);
+
+ glTexSubImage2D(GL_TEXTURE_2D,
+ 0,
+ _cursorDrawnToGL.left(),
+ _cursorDrawnToGL.top(),
+ _cursorDrawnToGL.width(),
+ _cursorDrawnToGL.height(),
+ GL_RGBA,
+ GL_UNSIGNED_BYTE,
+ _cursor->image().data());
+
+ } else if (CGRectIntersectsRect(screen, rect)) {
+ // 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(webrtc::DesktopSize(
+ _cursorDrawnToGL.width(), _cursorDrawnToGL.height()));
+
+ if (partialCursor.data()) {
+ CHECK(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 +=
+ (static_cast<int32_t>(cursor.origin.y) - _cursorDrawnToGL.top()) *
+ src_stride;
+ source += (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;
+ }
+
+ glTexSubImage2D(GL_TEXTURE_2D,
+ 0,
+ _cursorDrawnToGL.left(),
+ _cursorDrawnToGL.top(),
+ _cursorDrawnToGL.width(),
+ _cursorDrawnToGL.height(),
+ GL_RGBA,
+ GL_UNSIGNED_BYTE,
+ partialCursor.data());
+ }
+ }
+ }
+
+ glBindTexture(GL_TEXTURE_2D, 0);
+ [_glCursorLock unlock]; // done makeing changes to |_cursor|
+
+ _needCursorRedraw = NO;
+ [self logGLErrorCode:@"drawInRect mouseDrawComplete"];
+ }
+
+ // The scene is the entire CLIENT screen
+ GLKMatrix4 projectionMatrix = GLKMatrix4MakeOrtho(
+ 0.0, _contentSize.width(), 0.0, _contentSize.height(), 1.0, -1.0);
+ [_effect transform].projectionMatrix = projectionMatrix;
+
+ // Start by using the entire scene
+ GLKMatrix4 modelMatrix = GLKMatrix4Identity;
+
+ // Position scene according to any panning or bounds
+ modelMatrix = GLKMatrix4Translate(
+ modelMatrix, _scenePosition.x, _scenePosition.y + kBottomMargin, 0.0);
+
+ webrtc::DesktopSize currentSize = [self getCurrentSize];
+ CGPoint ratios = [self pixelRatio];
+
+ // Apply zoom
+ modelMatrix = GLKMatrix4Scale(
+ modelMatrix,
+ _scenePosition.z / ratios.x,
+ _scenePosition.z / ratios.y *
+ (1.0 -
+ (static_cast<float>(kTopMargin + _toolbarHeight + kBottomMargin) /
+ static_cast<float>(currentSize.height()))),
+ 1.0);
+
+ // We are directly above the sceen and looking down.
+ GLKMatrix4 viewMatrix = GLKMatrix4MakeLookAt(
+ 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); // center view
+
+ [_effect transform].modelviewMatrix =
+ GLKMatrix4Multiply(viewMatrix, modelMatrix);
+
+ [_effect prepareToDraw];
+
+ [self logGLErrorCode:@"drawInRect prepareToDrawComplete"];
+
+ glEnableVertexAttribArray(GLKVertexAttribPosition);
+ glEnableVertexAttribArray(GLKVertexAttribTexCoord0);
+ glEnableVertexAttribArray(GLKVertexAttribTexCoord1);
+
+ // Define our scene space
+ glVertexAttribPointer(GLKVertexAttribPosition,
+ 2,
+ GL_FLOAT,
+ GL_FALSE,
+ sizeof(TexturedVertex),
+ &(_quad.bl.geometryVertex));
+ // Define the desktop plane
+ glVertexAttribPointer(GLKVertexAttribTexCoord0,
+ 2,
+ GL_FLOAT,
+ GL_FALSE,
+ sizeof(TexturedVertex),
+ &(_quad.bl.textureVertex));
+ // Define the cursor plane
+ glVertexAttribPointer(GLKVertexAttribTexCoord1,
+ 2,
+ GL_FLOAT,
+ GL_FALSE,
+ sizeof(TexturedVertex),
+ &(_quad.bl.textureVertex));
+
+ // Draw!
+ glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
+
+ [self logGLErrorCode:@"drawInRect exit"];
+
+#ifdef DEBUG
+ _frameCount++;
+
+ if (_frameCount % 150 == 0) {
+ NSLog(@"@drawInRect drawCount:%d ApplyCount%d", _drawCount, _applyCount);
+ }
+#endif // DEBUG
+}
+
+// Sometimes its necessary to read gl errors. This is called in various places
+// while working in the GL Context
+- (void)logGLErrorCode:(NSString*)funcName {
+ GLenum errorCode = 1;
+
+ while (errorCode != 0) {
+ errorCode = glGetError(); // I don't know why this is returning an error
+ // ont he first call to this function, but if I
+ // don't read it, then stuff doesn't work...
+#if DEBUG
+ if (errorCode != 0) {
+ NSLog(@"glerror in %@: %X", funcName, errorCode);
+ }
+#endif // DEBUG
+ }
+}
+
+// Position busy spinner in the middle of the frame
+- (void)setFrameForControls {
+ CGRect mainFrameForSpinner = self.view.frame;
+ CGRect spinnerFrame = _busyIndicator.frame;
+ spinnerFrame.origin.x =
+ (mainFrameForSpinner.size.width / 2) - (spinnerFrame.size.width / 2);
+ spinnerFrame.origin.y =
+ (mainFrameForSpinner.size.height / 2) - (spinnerFrame.size.height / 2);
+ _busyIndicator.frame = spinnerFrame;
+}
+
+// @protocol KeyInputDelegate
+// Send keyboard input to HOST
+- (void)keyboardActionKeyCode:(uint32_t)keyPressed isKeyDown:(BOOL)keyDown {
+ [_controller keyboardAction:keyPressed keyDown:keyDown];
+}
+
+// Return the client resolution in User's perspective
+- (CGRect)getViewBounds {
+ CGRect curBounds = self.view.bounds;
+ if ((curBounds.size.height > curBounds.size.width) &&
+ [Utility isInLandscapeMode]) {
+ float width = curBounds.size.height;
+ curBounds.size.height = curBounds.size.width;
+ curBounds.size.width = width;
+ }
+ return curBounds;
+}
+
+// Update the CLIENT resoluation and draw scene size, account for margins
+- (void)updateContentSize {
+
+ CGRect viewBounds = [self getViewBounds];
+
+ viewBounds.size.height -= (kTopMargin + _toolbarHeight + kBottomMargin);
+
+ _contentSize.set(viewBounds.size.width, viewBounds.size.height);
+
+ 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);
+
+ _quad = newQuad;
+}
+
+// Update the scene acceleration vector
+- (void)updatePanVelocityShouldCancel:(bool)canceled {
+
+ if (canceled) {
+ _panVelocity = CGPointMake(0.0, 0.0);
+ }
+
+ BOOL inMotion = ((_panVelocity.x != 0.0) || (_panVelocity.y != 0.0));
+
+ _singleTapRecognizer.enabled = !inMotion;
+ _longPressRecognizer.enabled = !inMotion;
+
+ if (inMotion) {
+
+ uint32_t divisor = 50 / _scenePosition.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 =
+ CGPointMake(_panVelocity.x * reducer, _panVelocity.y * reducer);
+ }
+}
+
+// Callback from NSNotificationCenter when the User changes orientation
+- (void)orientationChanged:(NSNotification*)note {
+ [self updateContentSize];
+ [self panAndZoom:CGPointMake(0.0, 0.0) scaleBy:1.0];
+}
+
+// Returns the number of pixels displayed per device pixel when the scaleing 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 resoluations.
+- (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;
+}
+
+// Return the FrameSize in persepective of the CLIENT resolution
+- (webrtc::DesktopSize)getCurrentSize {
+ webrtc::DesktopSize r(_frameSize.width() * _scenePosition.z,
+ _frameSize.height() * _scenePosition.z);
+ return r;
+}
+
+// Applies translation and zoom originateing from |touch|. Translation is
+// bounded to screen edges. Zooming is bounded on the lower side to the maximun
+// of width and height, and on the upper side by a constant, experimentally
+// chosen.
+- (void)panAndZoom:(CGPoint)translation scaleBy:(float)ratio {
+
+ CGPoint ratios = [self pixelRatio];
+
+ // New Scaleing factor bounded by a min and max
+ float resultScale = _scenePosition.z * ratio;
+ float scaleUpperBound = kMaxZoomSize;
+ float scaleLowerBound = MIN(ratios.x, ratios.y);
+
+ if (resultScale < scaleLowerBound) {
+ resultScale = scaleLowerBound;
+ } else if (resultScale > scaleUpperBound) {
+ resultScale = scaleUpperBound;
+ }
+
+ // The GL perspective is upside down in relation to the User's view, so flip
+ // the translation
+ translation.y = -translation.y;
+
+ // These could be user options, but just using constants for now.
+ 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 getCurrentSize];
+
+ // When bounded on the top or right, this point is where the scene must be
+ // positioned given its current deminsions
+ webrtc::DesktopVector bounds(_contentSize.width() - currentSize.width(),
+ _contentSize.height() - currentSize.height());
+
+ // There are rounding errors in the scope of this function, see the butterfly
+ // effect. In successsive 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 (_isAnchorRight) {
+ _scenePosition =
+ GLKVector3Make(bounds.x(), _scenePosition.y, _scenePosition.z);
+ }
+
+ if (_isAnchorTop) {
+ _scenePosition =
+ GLKVector3Make(_scenePosition.x, bounds.y(), _scenePosition.z);
+ }
+
+ if (_scenePosition.z != resultScale) {
+
+ // When scaling the scene, the origination of scaleing 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];
+
+ if (_isAnchorLeft) {
+ mousePositionInClientResolution.x = 0;
+ } else if (_isAnchorRight) {
+ mousePositionInClientResolution.x = _contentSize.width();
+ }
+
+ if (_isAnchorTop) {
+ mousePositionInClientResolution.y = _contentSize.height();
+ } else if (_isAnchorBottom) {
+ mousePositionInClientResolution.y = 0;
+ }
+
+ scaleDelta.x -= [HostViewController
+ calculateScaleingDelta:ratio
+ size:currentSize.width()
+ position:_scenePosition.x
+ anchor:mousePositionInClientResolution.x];
+
+ scaleDelta.y -= [HostViewController
+ calculateScaleingDelta:ratio
+ size:currentSize.height()
+ position:_scenePosition.y
+ anchor:mousePositionInClientResolution.y];
+ }
+
+ delta.x = [HostViewController
+ calculatePositionDelta:_scenePosition.x
+ length:_contentSize.width() - currentSize.width()
+ translation:translation.x
+ scaleDelta:scaleDelta.x
+ isAnchoredLow:_isAnchorLeft
+ isAnchoredHigh:_isAnchorRight];
+
+ delta.y = [HostViewController
+ calculatePositionDelta:_scenePosition.y
+ length:_contentSize.height() - currentSize.height()
+ translation:translation.y
+ scaleDelta:scaleDelta.y
+ isAnchoredLow:_isAnchorBottom
+ isAnchoredHigh:_isAnchorTop];
+
+ delta.x = [HostViewController applyBoundsToFrameAxis:_scenePosition.x
+ delta:delta.x
+ lowerBound:bounds.x() + scaleDelta.x
+ upperBound:0];
+
+ delta.y = [HostViewController applyBoundsToFrameAxis:_scenePosition.y
+ delta:delta.y
+ lowerBound:bounds.y() + scaleDelta.y
+ upperBound:0];
+
+ BOOL isLeftAndRightAnchored = _isAnchorLeft && _isAnchorRight;
+ BOOL isTopAndBottomAnchored = _isAnchorTop && _isAnchorBottom;
+
+ [self updateMousePositionAndAnchorsWithBounds:bounds translation:translation];
+
+ // If both anchors were lost, then keep the one that is easier to predict
+ if (isLeftAndRightAnchored && !_isAnchorLeft && !_isAnchorRight) {
+ delta.x = -_scenePosition.x;
+ _isAnchorLeft = YES;
+ }
+
+ // If both anchors were lost, then keep the one that is easier to predict
+ if (isTopAndBottomAnchored && !_isAnchorTop && !_isAnchorBottom) {
+ delta.y = -_scenePosition.y;
+ _isAnchorBottom = YES;
+ }
+
+ // FINALLY, update the scene's position
+ _scenePosition = GLKVector3Make(
+ _scenePosition.x + delta.x, _scenePosition.y + delta.y, resultScale);
+
+ // Notify HOST that the mouse moved
+ [Utility moveMouse:_controller at:_mousePosition];
+}
+
+// When zoom is changed the scene is also translated to keep the
+// origination point (the spot the user is touching) at the same place in the
+// User's perspective.
++ (float)calculateScaleingDelta:(float)scale
+ size:(float)size
+ position:(float)position
+ anchor:(float)anchor {
+
+ float newSize = size * scale;
+
+ float scaleXBy = fabs(position - anchor) / newSize;
+
+ float delta = (newSize - size) * scaleXBy;
+
+ return delta;
+}
+
+// Determine overall |_scenePosition| Delta for an axis
++ (int)calculatePositionDelta:(int)position
+ length:(int)length
+ translation:(int)translation
+ scaleDelta:(int)scaleDelta
+ isAnchoredLow:(BOOL)isAnchoredLow
+ isAnchoredHigh:(BOOL)isAnchoredHigh {
+
+ if (isAnchoredLow && isAnchoredHigh) {
+ // center the view
+ return (length / 2) - position;
+ } else if (isAnchoredLow) {
+ return 0;
+ } else if (isAnchoredHigh) {
+ return scaleDelta;
+ } else {
+
+ return translation + scaleDelta;
+ }
+}
+
+// |position + delta| is snapped to the bounds, return the delta in respect to
+// the bounding.
++ (int)applyBoundsToFrameAxis:(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
+ if (result < upperBound) {
+ result = upperBound;
+ } else if (result > lowerBound) {
+ result = lowerBound;
+ }
+ }
+ return result - position;
+}
+
+// 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)calculateMousePosition:(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;
+}
+
+// If the mouse is at an edge, remove any existing velocity from vector
+- (void)cancelVelocityWhenAtEdge {
+ if (_panVelocity.x != 0.0) {
+ if (_mousePosition.x() == 0 ||
+ _mousePosition.x() == _frameSize.width() - 1) {
+ _panVelocity = CGPointMake(0.0, _panVelocity.y);
+ }
+ }
+
+ if (_panVelocity.y != 0.0) {
+ if (_mousePosition.y() == 0 ||
+ _mousePosition.y() == _frameSize.height() - 1) {
+ _panVelocity = CGPointMake(_panVelocity.x, 0.0);
+ }
+ }
+}
+
+// Mouse is tracked in the prespective of the HOST desktop, but the projection
+// to the user is in the prespective 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)updateMousePositionAndAnchorsWithBounds:
+ (const webrtc::DesktopVector&)bounds
+ translation:(CGPoint)translation {
+ webrtc::DesktopVector centerMouseLocation;
+ [self convertTouchPointToMousePoint:CGPointMake(_contentSize.width() / 2,
+ _contentSize.height() / 2)
+ targetPoint:centerMouseLocation];
+
+ webrtc::DesktopVector predictedMousePosition(
+ _mousePosition.x() - translation.x, _mousePosition.y() + translation.y);
+
+ _mousePosition
+ .set([HostViewController calculateMousePosition:predictedMousePosition.x()
+ maxPosition:_frameSize.width()
+ centerPosition:centerMouseLocation.x()
+ isAnchoredLow:_isAnchorLeft
+ isAnchoredHigh:_isAnchorRight],
+ [HostViewController calculateMousePosition:predictedMousePosition.y()
+ maxPosition:_frameSize.height()
+ centerPosition:centerMouseLocation.y()
+ isAnchoredLow:_isAnchorTop
+ isAnchoredHigh:_isAnchorBottom]);
+ [self cancelVelocityWhenAtEdge];
+
+ _isAnchorLeft = (bounds.x() >= 0) ||
+ ((_scenePosition.x) == 0 &&
+ predictedMousePosition.x() < centerMouseLocation.x());
+
+ _isAnchorRight = (bounds.x() >= 0) ||
+ (_scenePosition.x == bounds.x() &&
+ predictedMousePosition.x() > centerMouseLocation.x());
+ _isAnchorTop = (bounds.y() >= 0) ||
+ (_scenePosition.y == bounds.y() &&
+ predictedMousePosition.y() < centerMouseLocation.y());
+ _isAnchorBottom = (bounds.y() >= 0) ||
+ ((_scenePosition.y) == 0 &&
+ predictedMousePosition.y() > centerMouseLocation.y());
+}
+
+- (BOOL)isPointInScene:(CGPoint)point {
+ CGRect frame = CGRectMake(0,
+ kTopMargin + _toolbarHeight,
+ _contentSize.width(),
+ _contentSize.height());
+ return CGRectContainsPoint(frame, point);
+}
+
+// Animate the toolbar moving on or offscreen
+- (void)showToolbar:(BOOL)visible {
+ if (visible && (_viewToolbar.frame.origin.y >= 0)) {
+ return;
+ }
+
+ CGRect frame = [_viewToolbar frame];
+ frame.origin.y = -frame.size.height;
+ int newToolbarHeight = 0;
+
+ if (visible) {
+ frame.origin.y = 20;
+ newToolbarHeight = 40;
+ }
+
+ _toolbarHeight = newToolbarHeight;
+
+ [UIView animateWithDuration:0.5
+ animations:^{ [_viewToolbar setFrame:frame]; }
+ completion:^(BOOL finished) {
+ // Nothing to do for now
+ }];
+
+ [self updateContentSize];
+ [self panAndZoom:CGPointMake(0.0, 0.0) scaleBy:1.0];
+}
+@end

Powered by Google App Engine
This is Rietveld 408576698