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