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 |