| 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
|
|
|