| 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..d87e7674bdc9d29e499e1d9073299db118e45c1e
|
| --- /dev/null
|
| +++ b/remoting/ios/ui/host_view_controller.mm
|
| @@ -0,0 +1,676 @@
|
| +// Copyright 2014 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
| +
|
| +#if !defined(__has_feature) || !__has_feature(objc_arc)
|
| +#error "This file requires ARC support."
|
| +#endif
|
| +
|
| +#import "remoting/ios/ui/host_view_controller.h"
|
| +
|
| +#include <OpenGLES/ES2/gl.h>
|
| +
|
| +#import "remoting/ios/data_store.h"
|
| +
|
| +namespace {
|
| +
|
| +// TODO (aboone) Some of the layout is not yet set in stone, so variables have
|
| +// been used to position and turn items on and off. Eventually these may be
|
| +// stabilized and removed.
|
| +
|
| +// Scroll speed multiplier for mouse wheel
|
| +const static int kMouseWheelSensitivity = 20;
|
| +
|
| +// Area the navigation bar consumes when visible in pixels
|
| +const static int kTopMargin = 20;
|
| +// Area the footer consumes when visible (no footer currently exists)
|
| +const static int kBottomMargin = 0;
|
| +
|
| +} // namespace
|
| +
|
| +@interface HostViewController (Private)
|
| +- (void)setupGL;
|
| +- (void)tearDownGL;
|
| +- (void)goBack;
|
| +- (void)updateLabels;
|
| +- (BOOL)isToolbarHidden;
|
| +- (void)updatePanVelocityShouldCancel:(bool)canceled;
|
| +- (void)orientationChanged:(NSNotification*)note;
|
| +- (void)applySceneChange:(CGPoint)translation scaleBy:(float)ratio;
|
| +- (void)showToolbar:(BOOL)visible;
|
| +@end
|
| +
|
| +@implementation HostViewController
|
| +
|
| +@synthesize host = _host;
|
| +@synthesize userEmail = _userEmail;
|
| +@synthesize userAuthorizationToken = _userAuthorizationToken;
|
| +
|
| +// Override UIViewController
|
| +- (void)viewDidLoad {
|
| + [super viewDidLoad];
|
| +
|
| + _context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
|
| + DCHECK(_context);
|
| + static_cast<GLKView*>(self.view).context = _context;
|
| +
|
| + [_keyEntryView setDelegate:self];
|
| +
|
| + _clientToHostProxy = [[HostProxy alloc] init];
|
| +
|
| + // There is a 1 pixel top border which is actually the background not being
|
| + // covered. There is no obvious way to remove that pixel 'border'. Set the
|
| + // background clear, and also reset the backgroundimage and shawdowimage to an
|
| + // empty image any time the view is moved.
|
| + _hiddenToolbar.backgroundColor = [UIColor clearColor];
|
| + if ([_hiddenToolbar respondsToSelector:@selector(setBackgroundImage:
|
| + forToolbarPosition:
|
| + barMetrics:)]) {
|
| + [_hiddenToolbar setBackgroundImage:[UIImage new]
|
| + forToolbarPosition:UIToolbarPositionAny
|
| + barMetrics:UIBarMetricsDefault];
|
| + }
|
| + if ([_hiddenToolbar
|
| + respondsToSelector:@selector(setShadowImage:forToolbarPosition:)]) {
|
| + [_hiddenToolbar setShadowImage:[UIImage new]
|
| + forToolbarPosition:UIToolbarPositionAny];
|
| + }
|
| +
|
| + // 1/2 circle rotation for an icon ~ 180 degree ~ 1 radian
|
| + _barBtnNavigation.imageView.transform = CGAffineTransformMakeRotation(M_PI);
|
| +
|
| + _scene = [[SceneView alloc] init];
|
| + [_scene setMarginsFromLeft:0 right:0 top:kTopMargin bottom:kBottomMargin];
|
| + _desktop = [[DesktopTexture alloc] init];
|
| + _mouse = [[CursorTexture alloc] init];
|
| +
|
| + _glBufferLock = [[NSLock alloc] init];
|
| + _glCursorLock = [[NSLock alloc] init];
|
| +
|
| + [_scene
|
| + setContentSize:[Utility getOrientatedSize:self.view.bounds.size
|
| + shouldWidthBeLongestSide:[Utility isInLandscapeMode]]];
|
| + [self showToolbar:YES];
|
| + [self updateLabels];
|
| +
|
| + [self setupGL];
|
| +
|
| + [_singleTapRecognizer requireGestureRecognizerToFail:_twoFingerTapRecognizer];
|
| + [_twoFingerTapRecognizer
|
| + requireGestureRecognizerToFail:_threeFingerTapRecognizer];
|
| + //[_pinchRecognizer requireGestureRecognizerToFail:_twoFingerTapRecognizer];
|
| + [_panRecognizer requireGestureRecognizerToFail:_singleTapRecognizer];
|
| + [_threeFingerPanRecognizer
|
| + requireGestureRecognizerToFail:_threeFingerTapRecognizer];
|
| + //[_pinchRecognizer requireGestureRecognizerToFail:_threeFingerPanRecognizer];
|
| +
|
| + // Subscribe to changes in orientation
|
| + [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
|
| + [[NSNotificationCenter defaultCenter]
|
| + addObserver:self
|
| + selector:@selector(orientationChanged:)
|
| + name:UIDeviceOrientationDidChangeNotification
|
| + object:[UIDevice currentDevice]];
|
| +}
|
| +
|
| +- (void)setupGL {
|
| + [EAGLContext setCurrentContext:_context];
|
| +
|
| + _effect = [[GLKBaseEffect alloc] init];
|
| + [Utility logGLErrorCode:@"setupGL begin"];
|
| +
|
| + // Initialize each texture
|
| + [_desktop bindToEffect:[_effect texture2d0]];
|
| + [_mouse bindToEffect:[_effect texture2d1]];
|
| + [Utility logGLErrorCode:@"setupGL textureComplete"];
|
| +}
|
| +
|
| +// Override UIViewController
|
| +- (void)viewDidUnload {
|
| + [super viewDidUnload];
|
| + [self tearDownGL];
|
| +
|
| + if ([EAGLContext currentContext] == _context) {
|
| + [EAGLContext setCurrentContext:nil];
|
| + }
|
| + _context = nil;
|
| +}
|
| +
|
| +- (void)tearDownGL {
|
| + [EAGLContext setCurrentContext:_context];
|
| +
|
| + // Release Textures
|
| + [_desktop releaseTexture];
|
| + [_mouse releaseTexture];
|
| +}
|
| +
|
| +// Override UIViewController
|
| +- (void)viewWillAppear:(BOOL)animated {
|
| + [super viewWillAppear:NO];
|
| + [self.navigationController setNavigationBarHidden:YES animated:YES];
|
| + [self updateLabels];
|
| + if (![_clientToHostProxy isConnected]) {
|
| + [_busyIndicator startAnimating];
|
| +
|
| + [_clientToHostProxy connectToHost:_userEmail
|
| + authToken:_userAuthorizationToken
|
| + jabberId:_host.jabberId
|
| + hostId:_host.hostId
|
| + publicKey:_host.publicKey
|
| + delegate:self];
|
| + }
|
| +}
|
| +
|
| +// Override UIViewController
|
| +- (void)viewWillDisappear:(BOOL)animated {
|
| + [super viewWillDisappear:NO];
|
| + NSArray* viewControllers = self.navigationController.viewControllers;
|
| + if (viewControllers.count > 1 &&
|
| + [viewControllers objectAtIndex:viewControllers.count - 2] == self) {
|
| + // View is disappearing because a new view controller was pushed onto the
|
| + // stack
|
| + } else if ([viewControllers indexOfObject:self] == NSNotFound) {
|
| + // View is disappearing because it was popped from the stack
|
| + [_clientToHostProxy disconnectFromHost];
|
| + }
|
| +}
|
| +
|
| +// "Back" goes to the root controller for now
|
| +- (void)goBack {
|
| + [self.navigationController popToRootViewControllerAnimated:YES];
|
| +}
|
| +
|
| +// @protocol PinEntryViewControllerDelegate
|
| +// Return the PIN input by User, indicate if the User should be prompted to
|
| +// re-enter the pin in the future
|
| +- (void)connectToHostWithPin:(UIViewController*)controller
|
| + hostPin:(NSString*)hostPin
|
| + shouldPrompt:(BOOL)shouldPrompt {
|
| + const HostPreferences* hostPrefs =
|
| + [[DataStore sharedStore] getHostForId:_host.hostId];
|
| + if (!hostPrefs) {
|
| + hostPrefs = [[DataStore sharedStore] createHost:_host.hostId];
|
| + }
|
| + if (hostPrefs) {
|
| + hostPrefs.hostPin = hostPin;
|
| + hostPrefs.askForPin = [NSNumber numberWithBool:shouldPrompt];
|
| + [[DataStore sharedStore] saveChanges];
|
| + }
|
| +
|
| + [[controller presentingViewController] dismissViewControllerAnimated:NO
|
| + completion:nil];
|
| +
|
| + [_clientToHostProxy authenticationResponse:hostPin createPair:!shouldPrompt];
|
| +}
|
| +
|
| +// @protocol PinEntryViewControllerDelegate
|
| +// Returns if the user canceled while entering their PIN
|
| +- (void)cancelledConnectToHostWithPin:(UIViewController*)controller {
|
| + [[controller presentingViewController] dismissViewControllerAnimated:NO
|
| + completion:nil];
|
| +
|
| + [self goBack];
|
| +}
|
| +
|
| +- (void)setHostDetails:(Host*)host
|
| + userEmail:(NSString*)userEmail
|
| + authorizationToken:(NSString*)authorizationToken {
|
| + DCHECK(host.jabberId);
|
| + _host = host;
|
| + _userEmail = userEmail;
|
| + _userAuthorizationToken = authorizationToken;
|
| +}
|
| +
|
| +// Set various labels on the form for iPad vs iPhone, and orientation
|
| +- (void)updateLabels {
|
| + if (![Utility isPad] && ![Utility isInLandscapeMode]) {
|
| + [_barBtnDisconnect setTitle:@"" forState:(UIControlStateNormal)];
|
| + [_barBtnCtrlAltDel setTitle:@"CtAtD" forState:UIControlStateNormal];
|
| + } else {
|
| + [_barBtnCtrlAltDel setTitle:@"Ctrl+Alt+Del" forState:UIControlStateNormal];
|
| +
|
| + NSString* hostStatus = _host.hostName;
|
| + if (![_statusMessage isEqual:@"Connected"]) {
|
| + hostStatus = [NSString
|
| + stringWithFormat:@"%@ - %@", _host.hostName, _statusMessage];
|
| + }
|
| + [_barBtnDisconnect setTitle:hostStatus forState:UIControlStateNormal];
|
| + }
|
| +
|
| + [_barBtnDisconnect sizeToFit];
|
| + [_barBtnCtrlAltDel sizeToFit];
|
| +}
|
| +
|
| +// Resize the view of the desktop - Zoom in/out. This can occur during a Pan.
|
| +- (IBAction)pinchGestureTriggered:(UIPinchGestureRecognizer*)sender {
|
| + if ([sender state] == UIGestureRecognizerStateChanged) {
|
| + [self applySceneChange:CGPointMake(0.0, 0.0) scaleBy:sender.scale];
|
| +
|
| + sender.scale = 1.0; // reset scale so next iteration is a relative ratio
|
| + }
|
| +}
|
| +
|
| +- (IBAction)tapGestureTriggered:(UITapGestureRecognizer*)sender {
|
| + if ([_scene containsTouchPoint:[sender locationInView:self.view]]) {
|
| + [Utility leftClickOn:_clientToHostProxy at:_scene.mousePosition];
|
| + }
|
| +}
|
| +
|
| +// Change position of scene. This can occur during a pinch or longpress.
|
| +// Or perform a Mouse Wheel Scroll
|
| +- (IBAction)panGestureTriggered:(UIPanGestureRecognizer*)sender {
|
| + CGPoint translation = [sender translationInView:self.view];
|
| +
|
| + // If we start with 2 touches, and the pinch gesture is not in progress yet,
|
| + // then disable it, so mouse scrolling and zoom do not occur at the same
|
| + // time.
|
| + if ([sender numberOfTouches] == 2 &&
|
| + [sender state] == UIGestureRecognizerStateBegan &&
|
| + !(_pinchRecognizer.state == UIGestureRecognizerStateBegan ||
|
| + _pinchRecognizer.state == UIGestureRecognizerStateChanged)) {
|
| + _pinchRecognizer.enabled = NO;
|
| + }
|
| +
|
| + if (!_pinchRecognizer.enabled) {
|
| + // Began with 2 touches, so this is a scroll event
|
| + translation.x *= kMouseWheelSensitivity;
|
| + translation.y *= kMouseWheelSensitivity;
|
| + [Utility mouseScroll:_clientToHostProxy
|
| + at:_scene.mousePosition
|
| + delta:webrtc::DesktopVector(translation.x, translation.y)];
|
| + } else {
|
| + // Did not begin with 2 touches, doing a pan event
|
| + if ([sender state] == UIGestureRecognizerStateChanged) {
|
| + CGPoint translation = [sender translationInView:self.view];
|
| +
|
| + [self applySceneChange:translation scaleBy:1.0];
|
| +
|
| + } else if ([sender state] == UIGestureRecognizerStateEnded) {
|
| + // After user removes their fingers from the screen, apply an acceleration
|
| + // effect
|
| + [_scene setPanVelocity:[sender velocityInView:self.view]];
|
| + }
|
| + }
|
| +
|
| + // Finished the event chain
|
| + if (!([sender state] == UIGestureRecognizerStateBegan ||
|
| + [sender state] == UIGestureRecognizerStateChanged)) {
|
| + _pinchRecognizer.enabled = YES;
|
| + }
|
| +
|
| + // Reset translation so next iteration is relative.
|
| + [sender setTranslation:CGPointZero inView:self.view];
|
| +}
|
| +
|
| +// Click-Drag mouse operation. This can occur during a Pan.
|
| +- (IBAction)longPressGestureTriggered:(UILongPressGestureRecognizer*)sender {
|
| +
|
| + if ([sender state] == UIGestureRecognizerStateBegan) {
|
| + [_clientToHostProxy mouseAction:_scene.mousePosition
|
| + wheelDelta:webrtc::DesktopVector(0, 0)
|
| + whichButton:1
|
| + buttonDown:YES];
|
| + } else if (!([sender state] == UIGestureRecognizerStateBegan ||
|
| + [sender state] == UIGestureRecognizerStateChanged)) {
|
| + [_clientToHostProxy mouseAction:_scene.mousePosition
|
| + wheelDelta:webrtc::DesktopVector(0, 0)
|
| + whichButton:1
|
| + buttonDown:NO];
|
| + }
|
| +}
|
| +
|
| +- (IBAction)twoFingerTapGestureTriggered:(UITapGestureRecognizer*)sender {
|
| + if ([_scene containsTouchPoint:[sender locationInView:self.view]]) {
|
| + [Utility rightClickOn:_clientToHostProxy at:_scene.mousePosition];
|
| + }
|
| +}
|
| +
|
| +- (IBAction)threeFingerTapGestureTriggered:(UITapGestureRecognizer*)sender {
|
| +
|
| + if ([_scene containsTouchPoint:[sender locationInView:self.view]]) {
|
| + [Utility middleClickOn:_clientToHostProxy at:_scene.mousePosition];
|
| + }
|
| +}
|
| +
|
| +- (IBAction)threeFingerPanGestureTriggered:(UIPanGestureRecognizer*)sender {
|
| + if ([sender state] == UIGestureRecognizerStateChanged) {
|
| + CGPoint translation = [sender translationInView:self.view];
|
| + if (translation.y > 0) {
|
| + // Swiped down
|
| + [self showToolbar:YES];
|
| + } else if (translation.y < 0) {
|
| + // Swiped up
|
| + [_keyEntryView becomeFirstResponder];
|
| + [self updateLabels];
|
| + }
|
| + [sender setTranslation:CGPointZero inView:self.view];
|
| + }
|
| +}
|
| +
|
| +- (IBAction)barBtnNavigationBackPressed:(id)sender {
|
| + [self goBack];
|
| +}
|
| +
|
| +- (IBAction)barBtnKeyboardPressed:(id)sender {
|
| + if ([_keyEntryView isFirstResponder]) {
|
| + [_keyEntryView endEditing:NO];
|
| + } else {
|
| + [_keyEntryView becomeFirstResponder];
|
| + }
|
| +
|
| + [self updateLabels];
|
| +}
|
| +
|
| +- (IBAction)barBtnToolBarHidePressed:(id)sender {
|
| + [self showToolbar:[self isToolbarHidden]]; // Toolbar is either on
|
| + // screen or off screen
|
| +}
|
| +
|
| +- (IBAction)barBtnCtrlAltDelPressed:(id)sender {
|
| + [_keyEntryView ctrlAltDel];
|
| +}
|
| +
|
| +// Override UIResponder
|
| +// When any gesture begins, remove any acceleration effects currently being
|
| +// applied. Example, Panning view and let it shoot off into the distance, but
|
| +// then I see a spot I'm interested in so I will touch to capture that locations
|
| +// focus.
|
| +- (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
|
| + [self updatePanVelocityShouldCancel:YES];
|
| + [super touchesBegan:touches withEvent:event];
|
| +}
|
| +
|
| +// @protocol UIGestureRecognizerDelegate
|
| +// Allow panning and zooming to occur simultaneously.
|
| +// Allow panning and long press to occur simultaneously.
|
| +// Pinch requires 2 touches, and long press requires a single touch, so they are
|
| +// mutually exclusive regardless of if panning is the initiating gesture
|
| +- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
|
| + shouldRecognizeSimultaneouslyWithGestureRecognizer:
|
| + (UIGestureRecognizer*)otherGestureRecognizer {
|
| + if (gestureRecognizer == _pinchRecognizer ||
|
| + (gestureRecognizer == _panRecognizer)) {
|
| + if (otherGestureRecognizer == _pinchRecognizer ||
|
| + otherGestureRecognizer == _panRecognizer) {
|
| + return YES;
|
| + }
|
| + }
|
| +
|
| + if (gestureRecognizer == _longPressRecognizer ||
|
| + gestureRecognizer == _panRecognizer) {
|
| + if (otherGestureRecognizer == _longPressRecognizer ||
|
| + otherGestureRecognizer == _panRecognizer) {
|
| + return YES;
|
| + }
|
| + }
|
| + return NO;
|
| +}
|
| +
|
| +// @protocol ClientControllerDelegate
|
| +// Prompt the user for their PIN if pairing has not already been established
|
| +- (void)requestHostPin:(BOOL)pairingSupported {
|
| + BOOL requestPin = YES;
|
| + const HostPreferences* hostPrefs =
|
| + [[DataStore sharedStore] getHostForId:_host.hostId];
|
| + if (hostPrefs) {
|
| + requestPin = [hostPrefs.askForPin boolValue];
|
| + if (!requestPin) {
|
| + if (hostPrefs.hostPin == nil || hostPrefs.hostPin.length == 0) {
|
| + requestPin = YES;
|
| + }
|
| + }
|
| + }
|
| + if (requestPin == YES) {
|
| + PinEntryViewController* pinEntry = [[PinEntryViewController alloc] init];
|
| + [pinEntry setDelegate:self];
|
| + [pinEntry setHostName:_host.hostName];
|
| + [pinEntry setShouldPrompt:YES];
|
| + [pinEntry setPairingSupported:pairingSupported];
|
| +
|
| + [self presentViewController:pinEntry animated:YES completion:nil];
|
| + } else {
|
| + [_clientToHostProxy authenticationResponse:hostPrefs.hostPin
|
| + createPair:pairingSupported];
|
| + }
|
| +}
|
| +
|
| +// @protocol ClientControllerDelegate
|
| +// Occurs when a connection to a HOST is established successfully
|
| +- (void)connected {
|
| + // Everything is good, nothing to do
|
| +}
|
| +
|
| +// @protocol ClientControllerDelegate
|
| +- (void)connectionStatus:(NSString*)statusMessage {
|
| + _statusMessage = statusMessage;
|
| +
|
| + if ([_statusMessage isEqual:@"Connection closed"]) {
|
| + [self goBack];
|
| + } else {
|
| + [self updateLabels];
|
| + }
|
| +}
|
| +
|
| +// @protocol ClientControllerDelegate
|
| +// Occurs when a connection to a HOST has failed
|
| +- (void)connectionFailed:(NSString*)errorMessage {
|
| + [_busyIndicator stopAnimating];
|
| + NSString* errorMsg;
|
| + if ([_clientToHostProxy isConnected]) {
|
| + errorMsg = @"Lost Connection";
|
| + } else {
|
| + errorMsg = @"Unable to connect";
|
| + }
|
| + [Utility showAlert:errorMsg message:errorMessage];
|
| + [self goBack];
|
| +}
|
| +
|
| +// @protocol ClientControllerDelegate
|
| +// Copy the updated regions to a backing store to be consumed by the GL Context
|
| +// on a different thread. A region is stored in disjoint memory locations, and
|
| +// must be transformed to a contiguous memory buffer for a GL Texture write.
|
| +// /-----\
|
| +// | 2-4| This buffer is 5x3 bytes large, a region exists at bytes 2 to 4 and
|
| +// | 7-9| bytes 7 to 9. The region is extracted to a new contiguous buffer
|
| +// | | of 6 bytes in length.
|
| +// \-----/
|
| +// More than 1 region may exist in the frame from each call, in which case a new
|
| +// buffer is created for each region
|
| +- (void)applyFrame:(const webrtc::DesktopSize&)size
|
| + stride:(NSInteger)stride
|
| + data:(uint8_t*)data
|
| + regions:(const std::vector<webrtc::DesktopRect>&)regions {
|
| + [_glBufferLock lock]; // going to make changes to |_glRegions|
|
| +
|
| + if (!_scene.frameSize.equals(size)) {
|
| + // When this is the initial frame, the busyIndicator is still spinning. Now
|
| + // is a good time to stop it.
|
| + [_busyIndicator stopAnimating];
|
| +
|
| + // If the |_toolbar| is still showing, hide it.
|
| + [self showToolbar:NO];
|
| + [_scene setContentSize:
|
| + [Utility getOrientatedSize:self.view.bounds.size
|
| + shouldWidthBeLongestSide:[Utility isInLandscapeMode]]];
|
| + [_scene setFrameSize:size];
|
| + [_desktop setTextureSize:size];
|
| + [_mouse setTextureSize:size];
|
| + }
|
| +
|
| + uint32_t src_stride = stride;
|
| +
|
| + for (uint32_t i = 0; i < regions.size(); i++) {
|
| + scoped_ptr<GLRegion> region(new GLRegion());
|
| +
|
| + if (region.get()) {
|
| + webrtc::DesktopRect rect = regions.at(i);
|
| +
|
| + webrtc::DesktopSize(rect.width(), rect.height());
|
| + region->offset.reset(new webrtc::DesktopVector(rect.left(), rect.top()));
|
| + region->image.reset(new webrtc::BasicDesktopFrame(
|
| + webrtc::DesktopSize(rect.width(), rect.height())));
|
| +
|
| + if (region->image->data()) {
|
| + uint32_t bytes_per_row =
|
| + region->image->kBytesPerPixel * region->image->size().width();
|
| +
|
| + uint32_t offset =
|
| + (src_stride * region->offset->y()) + // row
|
| + (region->offset->x() * region->image->kBytesPerPixel); // column
|
| +
|
| + uint8_t* src_buffer = data + offset;
|
| + uint8_t* dst_buffer = region->image->data();
|
| +
|
| + // row by row copy
|
| + for (uint32_t j = 0; j < region->image->size().height(); j++) {
|
| + memcpy(dst_buffer, src_buffer, bytes_per_row);
|
| + dst_buffer += bytes_per_row;
|
| + src_buffer += src_stride;
|
| + }
|
| + _glRegions.push_back(region.release());
|
| + }
|
| + }
|
| + }
|
| + [_glBufferLock unlock]; // done making changes to |_glRegions|
|
| +}
|
| +
|
| +// @protocol ClientControllerDelegate
|
| +// Copy the delivered cursor to a backing store to be consumed by the GL Context
|
| +// on a different thread. Note only the most recent cursor is of importance,
|
| +// discard the previous cursor.
|
| +- (void)applyCursor:(const webrtc::DesktopSize&)size
|
| + hotspot:(const webrtc::DesktopVector&)hotspot
|
| + cursorData:(uint8_t*)data {
|
| +
|
| + [_glCursorLock lock]; // going to make changes to |_cursor|
|
| +
|
| + // MouseCursor takes ownership of DesktopFrame
|
| + [_mouse setCursor:new webrtc::MouseCursor(new webrtc::BasicDesktopFrame(size),
|
| + hotspot)];
|
| +
|
| + if (_mouse.cursor.image().data()) {
|
| + memcpy(_mouse.cursor.image().data(),
|
| + data,
|
| + size.width() * size.height() * _mouse.cursor.image().kBytesPerPixel);
|
| + } else {
|
| + [_mouse setCursor:NULL];
|
| + }
|
| +
|
| + [_glCursorLock unlock]; // done making changes to |_cursor|
|
| +}
|
| +
|
| +// @protocol GLKViewDelegate
|
| +// There is quite a few gotchas involved in working with this function. For
|
| +// sanity purposes, I've just assumed calls to the function are on a different
|
| +// thread which I've termed GL Context. Any variables consumed by this function
|
| +// should be thread safe.
|
| +//
|
| +// Clear Screen, update desktop, update cursor, define position, and finally
|
| +// present
|
| +//
|
| +// In general, avoid expensive work in this function to maximize frame rate.
|
| +- (void)glkView:(GLKView*)view drawInRect:(CGRect)rect {
|
| + [self updatePanVelocityShouldCancel:NO];
|
| +
|
| + // Clear to black, to give the background color
|
| + glClearColor(0.0, 0.0, 0.0, 1.0);
|
| + glClear(GL_COLOR_BUFFER_BIT);
|
| +
|
| + [Utility logGLErrorCode:@"drawInRect bindBuffer"];
|
| +
|
| + if (_glRegions.size() > 0 || [_desktop needDraw]) {
|
| + [_glBufferLock lock];
|
| +
|
| + for (uint32_t i = 0; i < _glRegions.size(); i++) {
|
| + // |_glRegions[i].data| has been properly ordered by [self applyFrame]
|
| + [_desktop drawRegion:_glRegions[i] rect:rect];
|
| + }
|
| +
|
| + _glRegions.clear();
|
| + [_glBufferLock unlock];
|
| + }
|
| +
|
| + if ([_mouse needDrawAtPosition:_scene.mousePosition]) {
|
| + [_glCursorLock lock];
|
| + [_mouse drawWithMousePosition:_scene.mousePosition];
|
| + [_glCursorLock unlock];
|
| + }
|
| +
|
| + [_effect transform].projectionMatrix = _scene.projectionMatrix;
|
| + [_effect transform].modelviewMatrix = _scene.modelViewMatrix;
|
| + [_effect prepareToDraw];
|
| +
|
| + [Utility logGLErrorCode:@"drawInRect prepareToDrawComplete"];
|
| +
|
| + [_scene draw];
|
| +}
|
| +
|
| +// @protocol KeyInputDelegate
|
| +- (void)keyboardDismissed {
|
| + [self updateLabels];
|
| +}
|
| +
|
| +// @protocol KeyInputDelegate
|
| +// Send keyboard input to HOST
|
| +- (void)keyboardActionKeyCode:(uint32_t)keyPressed isKeyDown:(BOOL)keyDown {
|
| + [_clientToHostProxy keyboardAction:keyPressed keyDown:keyDown];
|
| +}
|
| +
|
| +- (BOOL)isToolbarHidden {
|
| + return (_toolbar.frame.origin.y < 0);
|
| +}
|
| +
|
| +// Update the scene acceleration vector
|
| +- (void)updatePanVelocityShouldCancel:(bool)canceled {
|
| + if (canceled) {
|
| + [_scene setPanVelocity:CGPointMake(0, 0)];
|
| + }
|
| + BOOL inMotion = [_scene tickPanVelocity];
|
| +
|
| + _singleTapRecognizer.enabled = !inMotion;
|
| + _longPressRecognizer.enabled = !inMotion;
|
| +}
|
| +
|
| +- (void)applySceneChange:(CGPoint)translation scaleBy:(float)ratio {
|
| + [_scene panAndZoom:translation scaleBy:ratio];
|
| + // Notify HOST that the mouse moved
|
| + [Utility moveMouse:_clientToHostProxy at:_scene.mousePosition];
|
| +}
|
| +
|
| +// Callback from NSNotificationCenter when the User changes orientation
|
| +- (void)orientationChanged:(NSNotification*)note {
|
| + [_scene
|
| + setContentSize:[Utility getOrientatedSize:self.view.bounds.size
|
| + shouldWidthBeLongestSide:[Utility isInLandscapeMode]]];
|
| + [self showToolbar:![self isToolbarHidden]];
|
| + [self updateLabels];
|
| +}
|
| +
|
| +// Animate |_toolbar| by moving it on or offscreen
|
| +- (void)showToolbar:(BOOL)visible {
|
| + CGRect frame = [_toolbar frame];
|
| +
|
| + _toolBarYPosition.constant = -frame.size.height;
|
| + int topOffset = kTopMargin;
|
| +
|
| + if (visible) {
|
| + topOffset += frame.size.height;
|
| + _toolBarYPosition.constant = kTopMargin;
|
| + }
|
| +
|
| + _hiddenToolbarYPosition.constant = topOffset;
|
| + [_scene setMarginsFromLeft:0 right:0 top:topOffset bottom:kBottomMargin];
|
| +
|
| + // hidden when |_toolbar| is |visible|
|
| + _hiddenToolbar.hidden = (visible == YES);
|
| +
|
| + [UIView animateWithDuration:0.5
|
| + animations:^{ [self.view layoutIfNeeded]; }
|
| + completion:^(BOOL finished) {// Nothing to do for now
|
| + }];
|
| +
|
| + // Center view if needed for any reason.
|
| + // Specificallly, if the top anchor is active.
|
| + [self applySceneChange:CGPointMake(0.0, 0.0) scaleBy:1.0];
|
| +}
|
| +@end
|
|
|