Home | History | Annotate | Download | only in ui
      1 // Copyright 2014 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 #if !defined(__has_feature) || !__has_feature(objc_arc)
      6 #error "This file requires ARC support."
      7 #endif
      8 
      9 #import "remoting/ios/ui/host_view_controller.h"
     10 
     11 #include <OpenGLES/ES2/gl.h>
     12 
     13 #import "remoting/ios/data_store.h"
     14 
     15 namespace {
     16 
     17 // TODO (aboone) Some of the layout is not yet set in stone, so variables have
     18 // been used to position and turn items on and off.  Eventually these may be
     19 // stabilized and removed.
     20 
     21 // Scroll speed multiplier for mouse wheel
     22 const static int kMouseWheelSensitivity = 20;
     23 
     24 // Area the navigation bar consumes when visible in pixels
     25 const static int kTopMargin = 20;
     26 // Area the footer consumes when visible (no footer currently exists)
     27 const static int kBottomMargin = 0;
     28 
     29 }  // namespace
     30 
     31 @interface HostViewController (Private)
     32 - (void)setupGL;
     33 - (void)tearDownGL;
     34 - (void)goBack;
     35 - (void)updateLabels;
     36 - (BOOL)isToolbarHidden;
     37 - (void)updatePanVelocityShouldCancel:(bool)canceled;
     38 - (void)orientationChanged:(NSNotification*)note;
     39 - (void)applySceneChange:(CGPoint)translation scaleBy:(float)ratio;
     40 - (void)showToolbar:(BOOL)visible;
     41 @end
     42 
     43 @implementation HostViewController
     44 
     45 @synthesize host = _host;
     46 @synthesize userEmail = _userEmail;
     47 @synthesize userAuthorizationToken = _userAuthorizationToken;
     48 
     49 // Override UIViewController
     50 - (void)viewDidLoad {
     51   [super viewDidLoad];
     52 
     53   _context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
     54   DCHECK(_context);
     55   static_cast<GLKView*>(self.view).context = _context;
     56 
     57   [_keyEntryView setDelegate:self];
     58 
     59   _clientToHostProxy = [[HostProxy alloc] init];
     60 
     61   // There is a 1 pixel top border which is actually the background not being
     62   // covered.  There is no obvious way to remove that pixel 'border'.  Set the
     63   // background clear, and also reset the backgroundimage and shawdowimage to an
     64   // empty image any time the view is moved.
     65   _hiddenToolbar.backgroundColor = [UIColor clearColor];
     66   if ([_hiddenToolbar respondsToSelector:@selector(setBackgroundImage:
     67                                                    forToolbarPosition:
     68                                                            barMetrics:)]) {
     69     [_hiddenToolbar setBackgroundImage:[UIImage new]
     70                     forToolbarPosition:UIToolbarPositionAny
     71                             barMetrics:UIBarMetricsDefault];
     72   }
     73   if ([_hiddenToolbar
     74           respondsToSelector:@selector(setShadowImage:forToolbarPosition:)]) {
     75     [_hiddenToolbar setShadowImage:[UIImage new]
     76                 forToolbarPosition:UIToolbarPositionAny];
     77   }
     78 
     79   // 1/2 circle rotation for an icon ~ 180 degree ~ 1 radian
     80   _barBtnNavigation.imageView.transform = CGAffineTransformMakeRotation(M_PI);
     81 
     82   _scene = [[SceneView alloc] init];
     83   [_scene setMarginsFromLeft:0 right:0 top:kTopMargin bottom:kBottomMargin];
     84   _desktop = [[DesktopTexture alloc] init];
     85   _mouse = [[CursorTexture alloc] init];
     86 
     87   _glBufferLock = [[NSLock alloc] init];
     88   _glCursorLock = [[NSLock alloc] init];
     89 
     90   [_scene
     91       setContentSize:[Utility getOrientatedSize:self.view.bounds.size
     92                          shouldWidthBeLongestSide:[Utility isInLandscapeMode]]];
     93   [self showToolbar:YES];
     94   [self updateLabels];
     95 
     96   [self setupGL];
     97 
     98   [_singleTapRecognizer requireGestureRecognizerToFail:_twoFingerTapRecognizer];
     99   [_twoFingerTapRecognizer
    100       requireGestureRecognizerToFail:_threeFingerTapRecognizer];
    101   //[_pinchRecognizer requireGestureRecognizerToFail:_twoFingerTapRecognizer];
    102   [_panRecognizer requireGestureRecognizerToFail:_singleTapRecognizer];
    103   [_threeFingerPanRecognizer
    104       requireGestureRecognizerToFail:_threeFingerTapRecognizer];
    105   //[_pinchRecognizer requireGestureRecognizerToFail:_threeFingerPanRecognizer];
    106 
    107   // Subscribe to changes in orientation
    108   [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
    109   [[NSNotificationCenter defaultCenter]
    110       addObserver:self
    111          selector:@selector(orientationChanged:)
    112              name:UIDeviceOrientationDidChangeNotification
    113            object:[UIDevice currentDevice]];
    114 }
    115 
    116 - (void)setupGL {
    117   [EAGLContext setCurrentContext:_context];
    118 
    119   _effect = [[GLKBaseEffect alloc] init];
    120   [Utility logGLErrorCode:@"setupGL begin"];
    121 
    122   // Initialize each texture
    123   [_desktop bindToEffect:[_effect texture2d0]];
    124   [_mouse bindToEffect:[_effect texture2d1]];
    125   [Utility logGLErrorCode:@"setupGL textureComplete"];
    126 }
    127 
    128 // Override UIViewController
    129 - (void)viewDidUnload {
    130   [super viewDidUnload];
    131   [self tearDownGL];
    132 
    133   if ([EAGLContext currentContext] == _context) {
    134     [EAGLContext setCurrentContext:nil];
    135   }
    136   _context = nil;
    137 }
    138 
    139 - (void)tearDownGL {
    140   [EAGLContext setCurrentContext:_context];
    141 
    142   // Release Textures
    143   [_desktop releaseTexture];
    144   [_mouse releaseTexture];
    145 }
    146 
    147 // Override UIViewController
    148 - (void)viewWillAppear:(BOOL)animated {
    149   [super viewWillAppear:NO];
    150   [self.navigationController setNavigationBarHidden:YES animated:YES];
    151   [self updateLabels];
    152   if (![_clientToHostProxy isConnected]) {
    153     [_busyIndicator startAnimating];
    154 
    155     [_clientToHostProxy connectToHost:_userEmail
    156                             authToken:_userAuthorizationToken
    157                              jabberId:_host.jabberId
    158                                hostId:_host.hostId
    159                             publicKey:_host.publicKey
    160                              delegate:self];
    161   }
    162 }
    163 
    164 // Override UIViewController
    165 - (void)viewWillDisappear:(BOOL)animated {
    166   [super viewWillDisappear:NO];
    167   NSArray* viewControllers = self.navigationController.viewControllers;
    168   if (viewControllers.count > 1 &&
    169       [viewControllers objectAtIndex:viewControllers.count - 2] == self) {
    170     // View is disappearing because a new view controller was pushed onto the
    171     // stack
    172   } else if ([viewControllers indexOfObject:self] == NSNotFound) {
    173     // View is disappearing because it was popped from the stack
    174     [_clientToHostProxy disconnectFromHost];
    175   }
    176 }
    177 
    178 // "Back" goes to the root controller for now
    179 - (void)goBack {
    180   [self.navigationController popToRootViewControllerAnimated:YES];
    181 }
    182 
    183 // @protocol PinEntryViewControllerDelegate
    184 // Return the PIN input by User, indicate if the User should be prompted to
    185 // re-enter the pin in the future
    186 - (void)connectToHostWithPin:(UIViewController*)controller
    187                      hostPin:(NSString*)hostPin
    188                 shouldPrompt:(BOOL)shouldPrompt {
    189   const HostPreferences* hostPrefs =
    190       [[DataStore sharedStore] getHostForId:_host.hostId];
    191   if (!hostPrefs) {
    192     hostPrefs = [[DataStore sharedStore] createHost:_host.hostId];
    193   }
    194   if (hostPrefs) {
    195     hostPrefs.hostPin = hostPin;
    196     hostPrefs.askForPin = [NSNumber numberWithBool:shouldPrompt];
    197     [[DataStore sharedStore] saveChanges];
    198   }
    199 
    200   [[controller presentingViewController] dismissViewControllerAnimated:NO
    201                                                             completion:nil];
    202 
    203   [_clientToHostProxy authenticationResponse:hostPin createPair:!shouldPrompt];
    204 }
    205 
    206 // @protocol PinEntryViewControllerDelegate
    207 // Returns if the user canceled while entering their PIN
    208 - (void)cancelledConnectToHostWithPin:(UIViewController*)controller {
    209   [[controller presentingViewController] dismissViewControllerAnimated:NO
    210                                                             completion:nil];
    211 
    212   [self goBack];
    213 }
    214 
    215 - (void)setHostDetails:(Host*)host
    216              userEmail:(NSString*)userEmail
    217     authorizationToken:(NSString*)authorizationToken {
    218   DCHECK(host.jabberId);
    219   _host = host;
    220   _userEmail = userEmail;
    221   _userAuthorizationToken = authorizationToken;
    222 }
    223 
    224 // Set various labels on the form for iPad vs iPhone, and orientation
    225 - (void)updateLabels {
    226   if (![Utility isPad] && ![Utility isInLandscapeMode]) {
    227     [_barBtnDisconnect setTitle:@"" forState:(UIControlStateNormal)];
    228     [_barBtnCtrlAltDel setTitle:@"CtAtD" forState:UIControlStateNormal];
    229   } else {
    230     [_barBtnCtrlAltDel setTitle:@"Ctrl+Alt+Del" forState:UIControlStateNormal];
    231 
    232     NSString* hostStatus = _host.hostName;
    233     if (![_statusMessage isEqual:@"Connected"]) {
    234       hostStatus = [NSString
    235           stringWithFormat:@"%@ - %@", _host.hostName, _statusMessage];
    236     }
    237     [_barBtnDisconnect setTitle:hostStatus forState:UIControlStateNormal];
    238   }
    239 
    240   [_barBtnDisconnect sizeToFit];
    241   [_barBtnCtrlAltDel sizeToFit];
    242 }
    243 
    244 // Resize the view of the desktop - Zoom in/out.  This can occur during a Pan.
    245 - (IBAction)pinchGestureTriggered:(UIPinchGestureRecognizer*)sender {
    246   if ([sender state] == UIGestureRecognizerStateChanged) {
    247     [self applySceneChange:CGPointMake(0.0, 0.0) scaleBy:sender.scale];
    248 
    249     sender.scale = 1.0;  // reset scale so next iteration is a relative ratio
    250   }
    251 }
    252 
    253 - (IBAction)tapGestureTriggered:(UITapGestureRecognizer*)sender {
    254   if ([_scene containsTouchPoint:[sender locationInView:self.view]]) {
    255     [Utility leftClickOn:_clientToHostProxy at:_scene.mousePosition];
    256   }
    257 }
    258 
    259 // Change position of scene.  This can occur during a pinch or longpress.
    260 // Or perform a Mouse Wheel Scroll
    261 - (IBAction)panGestureTriggered:(UIPanGestureRecognizer*)sender {
    262   CGPoint translation = [sender translationInView:self.view];
    263 
    264   // If we start with 2 touches, and the pinch gesture is not in progress yet,
    265   // then disable it, so mouse scrolling and zoom do not occur at the same
    266   // time.
    267   if ([sender numberOfTouches] == 2 &&
    268       [sender state] == UIGestureRecognizerStateBegan &&
    269       !(_pinchRecognizer.state == UIGestureRecognizerStateBegan ||
    270         _pinchRecognizer.state == UIGestureRecognizerStateChanged)) {
    271     _pinchRecognizer.enabled = NO;
    272   }
    273 
    274   if (!_pinchRecognizer.enabled) {
    275     // Began with 2 touches, so this is a scroll event
    276     translation.x *= kMouseWheelSensitivity;
    277     translation.y *= kMouseWheelSensitivity;
    278     [Utility mouseScroll:_clientToHostProxy
    279                       at:_scene.mousePosition
    280                    delta:webrtc::DesktopVector(translation.x, translation.y)];
    281   } else {
    282     // Did not begin with 2 touches, doing a pan event
    283     if ([sender state] == UIGestureRecognizerStateChanged) {
    284       CGPoint translation = [sender translationInView:self.view];
    285 
    286       [self applySceneChange:translation scaleBy:1.0];
    287 
    288     } else if ([sender state] == UIGestureRecognizerStateEnded) {
    289       // After user removes their fingers from the screen, apply an acceleration
    290       // effect
    291       [_scene setPanVelocity:[sender velocityInView:self.view]];
    292     }
    293   }
    294 
    295   // Finished the event chain
    296   if (!([sender state] == UIGestureRecognizerStateBegan ||
    297         [sender state] == UIGestureRecognizerStateChanged)) {
    298     _pinchRecognizer.enabled = YES;
    299   }
    300 
    301   // Reset translation so next iteration is relative.
    302   [sender setTranslation:CGPointZero inView:self.view];
    303 }
    304 
    305 // Click-Drag mouse operation.  This can occur during a Pan.
    306 - (IBAction)longPressGestureTriggered:(UILongPressGestureRecognizer*)sender {
    307 
    308   if ([sender state] == UIGestureRecognizerStateBegan) {
    309     [_clientToHostProxy mouseAction:_scene.mousePosition
    310                          wheelDelta:webrtc::DesktopVector(0, 0)
    311                         whichButton:1
    312                          buttonDown:YES];
    313   } else if (!([sender state] == UIGestureRecognizerStateBegan ||
    314                [sender state] == UIGestureRecognizerStateChanged)) {
    315     [_clientToHostProxy mouseAction:_scene.mousePosition
    316                          wheelDelta:webrtc::DesktopVector(0, 0)
    317                         whichButton:1
    318                          buttonDown:NO];
    319   }
    320 }
    321 
    322 - (IBAction)twoFingerTapGestureTriggered:(UITapGestureRecognizer*)sender {
    323   if ([_scene containsTouchPoint:[sender locationInView:self.view]]) {
    324     [Utility rightClickOn:_clientToHostProxy at:_scene.mousePosition];
    325   }
    326 }
    327 
    328 - (IBAction)threeFingerTapGestureTriggered:(UITapGestureRecognizer*)sender {
    329 
    330   if ([_scene containsTouchPoint:[sender locationInView:self.view]]) {
    331     [Utility middleClickOn:_clientToHostProxy at:_scene.mousePosition];
    332   }
    333 }
    334 
    335 - (IBAction)threeFingerPanGestureTriggered:(UIPanGestureRecognizer*)sender {
    336   if ([sender state] == UIGestureRecognizerStateChanged) {
    337     CGPoint translation = [sender translationInView:self.view];
    338     if (translation.y > 0) {
    339       // Swiped down
    340       [self showToolbar:YES];
    341     } else if (translation.y < 0) {
    342       // Swiped up
    343       [_keyEntryView becomeFirstResponder];
    344       [self updateLabels];
    345     }
    346     [sender setTranslation:CGPointZero inView:self.view];
    347   }
    348 }
    349 
    350 - (IBAction)barBtnNavigationBackPressed:(id)sender {
    351   [self goBack];
    352 }
    353 
    354 - (IBAction)barBtnKeyboardPressed:(id)sender {
    355   if ([_keyEntryView isFirstResponder]) {
    356     [_keyEntryView endEditing:NO];
    357   } else {
    358     [_keyEntryView becomeFirstResponder];
    359   }
    360 
    361   [self updateLabels];
    362 }
    363 
    364 - (IBAction)barBtnToolBarHidePressed:(id)sender {
    365   [self showToolbar:[self isToolbarHidden]];  // Toolbar is either on
    366                                               // screen or off screen
    367 }
    368 
    369 - (IBAction)barBtnCtrlAltDelPressed:(id)sender {
    370   [_keyEntryView ctrlAltDel];
    371 }
    372 
    373 // Override UIResponder
    374 // When any gesture begins, remove any acceleration effects currently being
    375 // applied.  Example, Panning view and let it shoot off into the distance, but
    376 // then I see a spot I'm interested in so I will touch to capture that locations
    377 // focus.
    378 - (void)touchesBegan:(NSSet*)touches withEvent:(UIEvent*)event {
    379   [self updatePanVelocityShouldCancel:YES];
    380   [super touchesBegan:touches withEvent:event];
    381 }
    382 
    383 // @protocol UIGestureRecognizerDelegate
    384 // Allow panning and zooming to occur simultaneously.
    385 // Allow panning and long press to occur simultaneously.
    386 // Pinch requires 2 touches, and long press requires a single touch, so they are
    387 // mutually exclusive regardless of if panning is the initiating gesture
    388 - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
    389     shouldRecognizeSimultaneouslyWithGestureRecognizer:
    390         (UIGestureRecognizer*)otherGestureRecognizer {
    391   if (gestureRecognizer == _pinchRecognizer ||
    392       (gestureRecognizer == _panRecognizer)) {
    393     if (otherGestureRecognizer == _pinchRecognizer ||
    394         otherGestureRecognizer == _panRecognizer) {
    395       return YES;
    396     }
    397   }
    398 
    399   if (gestureRecognizer == _longPressRecognizer ||
    400       gestureRecognizer == _panRecognizer) {
    401     if (otherGestureRecognizer == _longPressRecognizer ||
    402         otherGestureRecognizer == _panRecognizer) {
    403       return YES;
    404     }
    405   }
    406   return NO;
    407 }
    408 
    409 // @protocol ClientControllerDelegate
    410 // Prompt the user for their PIN if pairing has not already been established
    411 - (void)requestHostPin:(BOOL)pairingSupported {
    412   BOOL requestPin = YES;
    413   const HostPreferences* hostPrefs =
    414       [[DataStore sharedStore] getHostForId:_host.hostId];
    415   if (hostPrefs) {
    416     requestPin = [hostPrefs.askForPin boolValue];
    417     if (!requestPin) {
    418       if (hostPrefs.hostPin == nil || hostPrefs.hostPin.length == 0) {
    419         requestPin = YES;
    420       }
    421     }
    422   }
    423   if (requestPin == YES) {
    424     PinEntryViewController* pinEntry = [[PinEntryViewController alloc] init];
    425     [pinEntry setDelegate:self];
    426     [pinEntry setHostName:_host.hostName];
    427     [pinEntry setShouldPrompt:YES];
    428     [pinEntry setPairingSupported:pairingSupported];
    429 
    430     [self presentViewController:pinEntry animated:YES completion:nil];
    431   } else {
    432     [_clientToHostProxy authenticationResponse:hostPrefs.hostPin
    433                                     createPair:pairingSupported];
    434   }
    435 }
    436 
    437 // @protocol ClientControllerDelegate
    438 // Occurs when a connection to a HOST is established successfully
    439 - (void)connected {
    440   // Everything is good, nothing to do
    441 }
    442 
    443 // @protocol ClientControllerDelegate
    444 - (void)connectionStatus:(NSString*)statusMessage {
    445   _statusMessage = statusMessage;
    446 
    447   if ([_statusMessage isEqual:@"Connection closed"]) {
    448     [self goBack];
    449   } else {
    450     [self updateLabels];
    451   }
    452 }
    453 
    454 // @protocol ClientControllerDelegate
    455 // Occurs when a connection to a HOST has failed
    456 - (void)connectionFailed:(NSString*)errorMessage {
    457   [_busyIndicator stopAnimating];
    458   NSString* errorMsg;
    459   if ([_clientToHostProxy isConnected]) {
    460     errorMsg = @"Lost Connection";
    461   } else {
    462     errorMsg = @"Unable to connect";
    463   }
    464   [Utility showAlert:errorMsg message:errorMessage];
    465   [self goBack];
    466 }
    467 
    468 // @protocol ClientControllerDelegate
    469 // Copy the updated regions to a backing store to be consumed by the GL Context
    470 // on a different thread.  A region is stored in disjoint memory locations, and
    471 // must be transformed to a contiguous memory buffer for a GL Texture write.
    472 // /-----\
    473 // |  2-4|  This buffer is 5x3 bytes large, a region exists at bytes 2 to 4 and
    474 // |  7-9|  bytes 7 to 9.  The region is extracted to a new contiguous buffer
    475 // |     |  of 6 bytes in length.
    476 // \-----/
    477 // More than 1 region may exist in the frame from each call, in which case a new
    478 // buffer is created for each region
    479 - (void)applyFrame:(const webrtc::DesktopSize&)size
    480             stride:(NSInteger)stride
    481               data:(uint8_t*)data
    482            regions:(const std::vector<webrtc::DesktopRect>&)regions {
    483   [_glBufferLock lock];  // going to make changes to |_glRegions|
    484 
    485   if (!_scene.frameSize.equals(size)) {
    486     // When this is the initial frame, the busyIndicator is still spinning. Now
    487     // is a good time to stop it.
    488     [_busyIndicator stopAnimating];
    489 
    490     // If the |_toolbar| is still showing, hide it.
    491     [self showToolbar:NO];
    492     [_scene setContentSize:
    493                 [Utility getOrientatedSize:self.view.bounds.size
    494                     shouldWidthBeLongestSide:[Utility isInLandscapeMode]]];
    495     [_scene setFrameSize:size];
    496     [_desktop setTextureSize:size];
    497     [_mouse setTextureSize:size];
    498   }
    499 
    500   uint32_t src_stride = stride;
    501 
    502   for (uint32_t i = 0; i < regions.size(); i++) {
    503     scoped_ptr<GLRegion> region(new GLRegion());
    504 
    505     if (region.get()) {
    506       webrtc::DesktopRect rect = regions.at(i);
    507 
    508       webrtc::DesktopSize(rect.width(), rect.height());
    509       region->offset.reset(new webrtc::DesktopVector(rect.left(), rect.top()));
    510       region->image.reset(new webrtc::BasicDesktopFrame(
    511           webrtc::DesktopSize(rect.width(), rect.height())));
    512 
    513       if (region->image->data()) {
    514         uint32_t bytes_per_row =
    515             region->image->kBytesPerPixel * region->image->size().width();
    516 
    517         uint32_t offset =
    518             (src_stride * region->offset->y()) +                    // row
    519             (region->offset->x() * region->image->kBytesPerPixel);  // column
    520 
    521         uint8_t* src_buffer = data + offset;
    522         uint8_t* dst_buffer = region->image->data();
    523 
    524         // row by row copy
    525         for (uint32_t j = 0; j < region->image->size().height(); j++) {
    526           memcpy(dst_buffer, src_buffer, bytes_per_row);
    527           dst_buffer += bytes_per_row;
    528           src_buffer += src_stride;
    529         }
    530         _glRegions.push_back(region.release());
    531       }
    532     }
    533   }
    534   [_glBufferLock unlock];  // done making changes to |_glRegions|
    535 }
    536 
    537 // @protocol ClientControllerDelegate
    538 // Copy the delivered cursor to a backing store to be consumed by the GL Context
    539 // on a different thread.  Note only the most recent cursor is of importance,
    540 // discard the previous cursor.
    541 - (void)applyCursor:(const webrtc::DesktopSize&)size
    542             hotspot:(const webrtc::DesktopVector&)hotspot
    543          cursorData:(uint8_t*)data {
    544 
    545   [_glCursorLock lock];  // going to make changes to |_cursor|
    546 
    547   // MouseCursor takes ownership of DesktopFrame
    548   [_mouse setCursor:new webrtc::MouseCursor(new webrtc::BasicDesktopFrame(size),
    549                                             hotspot)];
    550 
    551   if (_mouse.cursor.image().data()) {
    552     memcpy(_mouse.cursor.image().data(),
    553            data,
    554            size.width() * size.height() * _mouse.cursor.image().kBytesPerPixel);
    555   } else {
    556     [_mouse setCursor:NULL];
    557   }
    558 
    559   [_glCursorLock unlock];  // done making changes to |_cursor|
    560 }
    561 
    562 // @protocol GLKViewDelegate
    563 // There is quite a few gotchas involved in working with this function.  For
    564 // sanity purposes, I've just assumed calls to the function are on a different
    565 // thread which I've termed GL Context.  Any variables consumed by this function
    566 // should be thread safe.
    567 //
    568 // Clear Screen, update desktop, update cursor, define position, and finally
    569 // present
    570 //
    571 // In general, avoid expensive work in this function to maximize frame rate.
    572 - (void)glkView:(GLKView*)view drawInRect:(CGRect)rect {
    573   [self updatePanVelocityShouldCancel:NO];
    574 
    575   // Clear to black, to give the background color
    576   glClearColor(0.0, 0.0, 0.0, 1.0);
    577   glClear(GL_COLOR_BUFFER_BIT);
    578 
    579   [Utility logGLErrorCode:@"drawInRect bindBuffer"];
    580 
    581   if (_glRegions.size() > 0 || [_desktop needDraw]) {
    582     [_glBufferLock lock];
    583 
    584     for (uint32_t i = 0; i < _glRegions.size(); i++) {
    585       // |_glRegions[i].data| has been properly ordered by [self applyFrame]
    586       [_desktop drawRegion:_glRegions[i] rect:rect];
    587     }
    588 
    589     _glRegions.clear();
    590     [_glBufferLock unlock];
    591   }
    592 
    593   if ([_mouse needDrawAtPosition:_scene.mousePosition]) {
    594     [_glCursorLock lock];
    595     [_mouse drawWithMousePosition:_scene.mousePosition];
    596     [_glCursorLock unlock];
    597   }
    598 
    599   [_effect transform].projectionMatrix = _scene.projectionMatrix;
    600   [_effect transform].modelviewMatrix = _scene.modelViewMatrix;
    601   [_effect prepareToDraw];
    602 
    603   [Utility logGLErrorCode:@"drawInRect prepareToDrawComplete"];
    604 
    605   [_scene draw];
    606 }
    607 
    608 // @protocol KeyInputDelegate
    609 - (void)keyboardDismissed {
    610   [self updateLabels];
    611 }
    612 
    613 // @protocol KeyInputDelegate
    614 // Send keyboard input to HOST
    615 - (void)keyboardActionKeyCode:(uint32_t)keyPressed isKeyDown:(BOOL)keyDown {
    616   [_clientToHostProxy keyboardAction:keyPressed keyDown:keyDown];
    617 }
    618 
    619 - (BOOL)isToolbarHidden {
    620   return (_toolbar.frame.origin.y < 0);
    621 }
    622 
    623 // Update the scene acceleration vector
    624 - (void)updatePanVelocityShouldCancel:(bool)canceled {
    625   if (canceled) {
    626     [_scene setPanVelocity:CGPointMake(0, 0)];
    627   }
    628   BOOL inMotion = [_scene tickPanVelocity];
    629 
    630   _singleTapRecognizer.enabled = !inMotion;
    631   _longPressRecognizer.enabled = !inMotion;
    632 }
    633 
    634 - (void)applySceneChange:(CGPoint)translation scaleBy:(float)ratio {
    635   [_scene panAndZoom:translation scaleBy:ratio];
    636   // Notify HOST that the mouse moved
    637   [Utility moveMouse:_clientToHostProxy at:_scene.mousePosition];
    638 }
    639 
    640 // Callback from NSNotificationCenter when the User changes orientation
    641 - (void)orientationChanged:(NSNotification*)note {
    642   [_scene
    643       setContentSize:[Utility getOrientatedSize:self.view.bounds.size
    644                          shouldWidthBeLongestSide:[Utility isInLandscapeMode]]];
    645   [self showToolbar:![self isToolbarHidden]];
    646   [self updateLabels];
    647 }
    648 
    649 // Animate |_toolbar| by moving it on or offscreen
    650 - (void)showToolbar:(BOOL)visible {
    651   CGRect frame = [_toolbar frame];
    652 
    653   _toolBarYPosition.constant = -frame.size.height;
    654   int topOffset = kTopMargin;
    655 
    656   if (visible) {
    657     topOffset += frame.size.height;
    658     _toolBarYPosition.constant = kTopMargin;
    659   }
    660 
    661   _hiddenToolbarYPosition.constant = topOffset;
    662   [_scene setMarginsFromLeft:0 right:0 top:topOffset bottom:kBottomMargin];
    663 
    664   // hidden when |_toolbar| is |visible|
    665   _hiddenToolbar.hidden = (visible == YES);
    666 
    667   [UIView animateWithDuration:0.5
    668       animations:^{ [self.view layoutIfNeeded]; }
    669       completion:^(BOOL finished) {// Nothing to do for now
    670                  }];
    671 
    672   // Center view if needed for any reason.
    673   // Specificallly, if the top anchor is active.
    674   [self applySceneChange:CGPointMake(0.0, 0.0) scaleBy:1.0];
    675 }
    676 @end
    677