FreeRDP
RDPSessionViewController.m
1 /*
2  RDP Session View Controller
3 
4  Copyright 2013 Thincast Technologies GmbH, Author: Martin Fleisz
5 
6  This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
7  If a copy of the MPL was not distributed with this file, You can obtain one at
8  http://mozilla.org/MPL/2.0/.
9  */
10 
11 #import <QuartzCore/QuartzCore.h>
12 #import "RDPSessionViewController.h"
13 #import "RDPKeyboard.h"
14 #import "Utils.h"
15 #import "Toast+UIView.h"
16 #import "ConnectionParams.h"
17 #import "CredentialsInputController.h"
18 #import "VerifyCertificateController.h"
19 #import "BlockAlertView.h"
20 
21 #define TOOLBAR_HEIGHT 30
22 
23 #define AUTOSCROLLDISTANCE 20
24 #define AUTOSCROLLTIMEOUT 0.05
25 
26 @interface RDPSessionViewController (Private)
27 - (void)showSessionToolbar:(BOOL)show;
28 - (UIToolbar *)keyboardToolbar;
29 - (void)initGestureRecognizers;
30 - (void)suspendSession;
31 - (NSDictionary *)eventDescriptorForMouseEvent:(int)event position:(CGPoint)position;
32 - (void)handleMouseMoveForPosition:(CGPoint)position;
33 @end
34 
35 @implementation RDPSessionViewController
36 
37 #pragma mark class methods
38 
39 - (id)initWithNibName:(NSString *)nibNameOrNil
40  bundle:(NSBundle *)nibBundleOrNil
41  session:(RDPSession *)session
42 {
43  self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
44  if (self)
45  {
46  _session = [session retain];
47  [_session setDelegate:self];
48  _session_initilized = NO;
49 
50  _mouse_move_events_skipped = 0;
51  _mouse_move_event_timer = nil;
52 
53  _advanced_keyboard_view = nil;
54  _advanced_keyboard_visible = NO;
55  _requesting_advanced_keyboard = NO;
56  _keyboard_last_height = 0;
57 
58  _session_toolbar_visible = NO;
59 
60  _toggle_mouse_button = NO;
61 
62  _autoscroll_with_touchpointer =
63  [[NSUserDefaults standardUserDefaults] boolForKey:@"ui.auto_scroll_touchpointer"];
64  _is_autoscrolling = NO;
65 
66  [UIView setAnimationDelegate:self];
67  [UIView setAnimationDidStopSelector:@selector(animationStopped:finished:context:)];
68  }
69 
70  return self;
71 }
72 
73 // Implement loadView to create a view hierarchy programmatically, without using a nib.
74 - (void)loadView
75 {
76  // load default view and set background color and resizing mask
77  [super loadView];
78 
79  // init keyboard handling vars
80  _keyboard_visible = NO;
81 
82  // init keyboard toolbar
83  _keyboard_toolbar = [[self keyboardToolbar] retain];
84  [_dummy_textfield setInputAccessoryView:_keyboard_toolbar];
85 
86  // init gesture recognizers
87  [self initGestureRecognizers];
88 
89  // hide session toolbar
90  [_session_toolbar
91  setFrame:CGRectMake(0.0, -TOOLBAR_HEIGHT, [[self view] bounds].size.width, TOOLBAR_HEIGHT)];
92 }
93 
94 // Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
95 - (void)viewDidLoad
96 {
97  [super viewDidLoad];
98 }
99 
100 - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
101 {
102  return YES;
103 }
104 
105 - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
106 {
107  if (![_touchpointer_view isHidden])
108  [_touchpointer_view ensurePointerIsVisible];
109 }
110 
111 - (void)didReceiveMemoryWarning
112 {
113  // Releases the view if it doesn't have a superview.
114  [super didReceiveMemoryWarning];
115 
116  // Release any cached data, images, etc. that aren't in use.
117 }
118 
119 - (void)viewDidUnload
120 {
121  [super viewDidUnload];
122  // Release any retained subviews of the main view.
123  // e.g. self.myOutlet = nil;
124 }
125 
126 - (void)viewWillAppear:(BOOL)animated
127 {
128  [super viewWillAppear:animated];
129 
130  // hide navigation bar and (if enabled) the status bar
131  if ([[NSUserDefaults standardUserDefaults] boolForKey:@"ui.hide_status_bar"])
132  {
133  if (animated == YES)
134  [[UIApplication sharedApplication] setStatusBarHidden:YES
135  withAnimation:UIStatusBarAnimationSlide];
136  else
137  [[UIApplication sharedApplication] setStatusBarHidden:YES
138  withAnimation:UIStatusBarAnimationNone];
139  }
140  [[self navigationController] setNavigationBarHidden:YES animated:animated];
141 
142  // if sesssion is suspended - notify that we got a new bitmap context
143  if ([_session isSuspended])
144  [self sessionBitmapContextWillChange:_session];
145 
146  // init keyboard
147  [[RDPKeyboard getSharedRDPKeyboard] initWithSession:_session delegate:self];
148 }
149 
150 - (void)viewDidAppear:(BOOL)animated
151 {
152  [super viewDidAppear:animated];
153 
154  if (!_session_initilized)
155  {
156  if ([_session isSuspended])
157  {
158  [_session resume];
159  [self sessionBitmapContextDidChange:_session];
160  [_session_view setNeedsDisplay];
161  }
162  else
163  [_session connect];
164 
165  _session_initilized = YES;
166  }
167 }
168 
169 - (void)viewWillDisappear:(BOOL)animated
170 {
171  [super viewWillDisappear:animated];
172 
173  // show navigation and status bar again
174  if (animated == YES)
175  [[UIApplication sharedApplication] setStatusBarHidden:NO
176  withAnimation:UIStatusBarAnimationSlide];
177  else
178  [[UIApplication sharedApplication] setStatusBarHidden:NO
179  withAnimation:UIStatusBarAnimationNone];
180  [[self navigationController] setNavigationBarHidden:NO animated:animated];
181 
182  // reset all modifier keys on rdp keyboard
183  [[RDPKeyboard getSharedRDPKeyboard] reset];
184 
185  // hide toolbar and keyboard
186  [self showSessionToolbar:NO];
187  [_dummy_textfield resignFirstResponder];
188 }
189 
190 - (void)dealloc
191 {
192  // remove any observers
193  [[NSNotificationCenter defaultCenter] removeObserver:self];
194 
195  // the session lives on longer so set the delegate to nil
196  [_session setDelegate:nil];
197 
198  [_advanced_keyboard_view release];
199  [_keyboard_toolbar release];
200  [_session release];
201  [super dealloc];
202 }
203 
204 #pragma mark -
205 #pragma mark ScrollView delegate methods
206 
207 - (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
208 {
209  return _session_view;
210 }
211 
212 - (void)scrollViewDidEndZooming:(UIScrollView *)scrollView
213  withView:(UIView *)view
214  atScale:(float)scale
215 {
216  NSLog(@"New zoom scale: %f", scale);
217  [_session_view setNeedsDisplay];
218 }
219 
220 #pragma mark -
221 #pragma mark TextField delegate methods
222 - (BOOL)textFieldShouldBeginEditing:(UITextField *)textField
223 {
224  _keyboard_visible = YES;
225  _advanced_keyboard_visible = NO;
226  return YES;
227 }
228 
229 - (BOOL)textFieldShouldEndEditing:(UITextField *)textField
230 {
231  _keyboard_visible = NO;
232  _advanced_keyboard_visible = NO;
233  return YES;
234 }
235 
236 - (BOOL)textField:(UITextField *)textField
237  shouldChangeCharactersInRange:(NSRange)range
238  replacementString:(NSString *)string
239 {
240  if ([string length] > 0)
241  {
242  for (int i = 0; i < [string length]; i++)
243  {
244  unichar curChar = [string characterAtIndex:i];
245 
246  // special handling for return/enter key
247  if (curChar == '\n')
248  [[RDPKeyboard getSharedRDPKeyboard] sendEnterKeyStroke];
249  else
250  [[RDPKeyboard getSharedRDPKeyboard] sendUnicode:curChar];
251  }
252  }
253  else
254  {
255  [[RDPKeyboard getSharedRDPKeyboard] sendBackspaceKeyStroke];
256  }
257 
258  return NO;
259 }
260 
261 #pragma mark -
262 #pragma mark AdvancedKeyboardDelegate functions
263 - (void)advancedKeyPressedVKey:(int)key
264 {
265  [[RDPKeyboard getSharedRDPKeyboard] sendVirtualKeyCode:key];
266 }
267 
268 - (void)advancedKeyPressedUnicode:(int)key
269 {
270  [[RDPKeyboard getSharedRDPKeyboard] sendUnicode:key];
271 }
272 
273 #pragma mark - RDP keyboard handler
274 
275 - (void)modifiersChangedForKeyboard:(RDPKeyboard *)keyboard
276 {
277  UIBarButtonItem *curItem;
278 
279  // shift button (only on iPad)
280  int objectIdx = 0;
281  if (IsPad())
282  {
283  objectIdx = 2;
284  curItem = (UIBarButtonItem *)[[_keyboard_toolbar items] objectAtIndex:objectIdx];
285  [curItem setStyle:[keyboard shiftPressed] ? UIBarButtonItemStyleDone
286  : UIBarButtonItemStyleBordered];
287  }
288 
289  // ctrl button
290  objectIdx += 2;
291  curItem = (UIBarButtonItem *)[[_keyboard_toolbar items] objectAtIndex:objectIdx];
292  [curItem
293  setStyle:[keyboard ctrlPressed] ? UIBarButtonItemStyleDone : UIBarButtonItemStyleBordered];
294 
295  // win button
296  objectIdx += 2;
297  curItem = (UIBarButtonItem *)[[_keyboard_toolbar items] objectAtIndex:objectIdx];
298  [curItem
299  setStyle:[keyboard winPressed] ? UIBarButtonItemStyleDone : UIBarButtonItemStyleBordered];
300 
301  // alt button
302  objectIdx += 2;
303  curItem = (UIBarButtonItem *)[[_keyboard_toolbar items] objectAtIndex:objectIdx];
304  [curItem
305  setStyle:[keyboard altPressed] ? UIBarButtonItemStyleDone : UIBarButtonItemStyleBordered];
306 }
307 
308 #pragma mark -
309 #pragma mark RDPSessionDelegate functions
310 
311 - (void)session:(RDPSession *)session didFailToConnect:(int)reason
312 {
313  // remove and release connecting view
314  [_connecting_indicator_view stopAnimating];
315  [_connecting_view removeFromSuperview];
316  [_connecting_view autorelease];
317 
318  // return to bookmark list
319  [[self navigationController] popViewControllerAnimated:YES];
320 }
321 
322 - (void)sessionWillConnect:(RDPSession *)session
323 {
324  // load connecting view
325  [[NSBundle mainBundle] loadNibNamed:@"RDPConnectingView" owner:self options:nil];
326 
327  // set strings
328  [_lbl_connecting setText:NSLocalizedString(@"Connecting", @"Connecting progress view - label")];
329  [_cancel_connect_button setTitle:NSLocalizedString(@"Cancel", @"Cancel Button")
330  forState:UIControlStateNormal];
331 
332  // center view and give it round corners
333  [_connecting_view setCenter:[[self view] center]];
334  [[_connecting_view layer] setCornerRadius:10];
335 
336  // display connecting view and start indicator
337  [[self view] addSubview:_connecting_view];
338  [_connecting_indicator_view startAnimating];
339 }
340 
341 - (void)sessionDidConnect:(RDPSession *)session
342 {
343  // register keyboard notification handlers
344  [[NSNotificationCenter defaultCenter] addObserver:self
345  selector:@selector(keyboardWillShow:)
346  name:UIKeyboardWillShowNotification
347  object:nil];
348  [[NSNotificationCenter defaultCenter] addObserver:self
349  selector:@selector(keyboardDidShow:)
350  name:UIKeyboardDidShowNotification
351  object:nil];
352  [[NSNotificationCenter defaultCenter] addObserver:self
353  selector:@selector(keyboardWillHide:)
354  name:UIKeyboardWillHideNotification
355  object:nil];
356  [[NSNotificationCenter defaultCenter] addObserver:self
357  selector:@selector(keyboardDidHide:)
358  name:UIKeyboardDidHideNotification
359  object:nil];
360 
361  // remove and release connecting view
362  [_connecting_indicator_view stopAnimating];
363  [_connecting_view removeFromSuperview];
364  [_connecting_view autorelease];
365 
366  // check if session settings changed ...
367  // The 2nd width check is to ignore changes in resolution settings due to the RDVH display bug
368  // (refer to RDPSEssion.m for more details)
369  ConnectionParams *orig_params = [session params];
370  rdpSettings *sess_params = [session getSessionParams];
371  if (([orig_params intForKey:@"width"] != sess_params->DesktopWidth &&
372  [orig_params intForKey:@"width"] != (sess_params->DesktopWidth + 1)) ||
373  [orig_params intForKey:@"height"] != sess_params->DesktopHeight ||
374  [orig_params intForKey:@"colors"] != sess_params->ColorDepth)
375  {
376  // display notification that the session params have been changed by the server
377  NSString *message =
378  [NSString stringWithFormat:NSLocalizedString(
379  @"The server changed the screen settings to %dx%dx%d",
380  @"Screen settings not supported message with width, "
381  @"height and colors parameter"),
382  sess_params->DesktopWidth, sess_params->DesktopHeight,
383  sess_params->ColorDepth];
384  [[self view] makeToast:message duration:ToastDurationNormal position:@"bottom"];
385  }
386 }
387 
388 - (void)sessionWillDisconnect:(RDPSession *)session
389 {
390 }
391 
392 - (void)sessionDidDisconnect:(RDPSession *)session
393 {
394  // return to bookmark list
395  [[self navigationController] popViewControllerAnimated:YES];
396 }
397 
398 - (void)sessionBitmapContextWillChange:(RDPSession *)session
399 {
400  // calc new view frame
401  rdpSettings *sess_params = [session getSessionParams];
402  CGRect view_rect = CGRectMake(0, 0, sess_params->DesktopWidth, sess_params->DesktopHeight);
403 
404  // reset zoom level and update content size
405  [_session_scrollview setZoomScale:1.0];
406  [_session_scrollview setContentSize:view_rect.size];
407 
408  // set session view size
409  [_session_view setFrame:view_rect];
410 
411  // show/hide toolbar
412  [_session
413  setToolbarVisible:![[NSUserDefaults standardUserDefaults] boolForKey:@"ui.hide_tool_bar"]];
414  [self showSessionToolbar:[_session toolbarVisible]];
415 }
416 
417 - (void)sessionBitmapContextDidChange:(RDPSession *)session
418 {
419  // associate view with session
420  [_session_view setSession:session];
421 
422  // issue an update (this might be needed in case we had a resize for instance)
423  [_session_view setNeedsDisplay];
424 }
425 
426 - (void)session:(RDPSession *)session needsRedrawInRect:(CGRect)rect
427 {
428  [_session_view setNeedsDisplayInRect:rect];
429 }
430 
431 - (void)session:(RDPSession *)session requestsAuthenticationWithParams:(NSMutableDictionary *)params
432 {
433  CredentialsInputController *view_controller =
434  [[[CredentialsInputController alloc] initWithNibName:@"CredentialsInputView"
435  bundle:nil
436  session:_session
437  params:params] autorelease];
438  [self presentModalViewController:view_controller animated:YES];
439 }
440 
441 - (void)session:(RDPSession *)session verifyCertificateWithParams:(NSMutableDictionary *)params
442 {
443  VerifyCertificateController *view_controller =
444  [[[VerifyCertificateController alloc] initWithNibName:@"VerifyCertificateView"
445  bundle:nil
446  session:_session
447  params:params] autorelease];
448  [self presentModalViewController:view_controller animated:YES];
449 }
450 
451 - (CGSize)sizeForFitScreenForSession:(RDPSession *)session
452 {
453  if (IsPad())
454  return [self view].bounds.size;
455  else
456  {
457  // on phones make a resolution that has a 16:10 ratio with the phone's height
458  CGSize size = [self view].bounds.size;
459  CGFloat maxSize = (size.width > size.height) ? size.width : size.height;
460  return CGSizeMake(maxSize * 1.6f, maxSize);
461  }
462 }
463 
464 #pragma mark - Keyboard Toolbar Handlers
465 
466 - (void)showAdvancedKeyboardAnimated
467 {
468  // calc initial and final rect of the advanced keyboard view
469  CGRect rect = [[_keyboard_toolbar superview] bounds];
470  rect.origin.y = [_keyboard_toolbar bounds].size.height;
471  rect.size.height -= rect.origin.y;
472 
473  // create new view (hidden) and add to host-view of keyboard toolbar
474  _advanced_keyboard_view = [[AdvancedKeyboardView alloc]
475  initWithFrame:CGRectMake(rect.origin.x, [[_keyboard_toolbar superview] bounds].size.height,
476  rect.size.width, rect.size.height)
477  delegate:self];
478  [[_keyboard_toolbar superview] addSubview:_advanced_keyboard_view];
479  // we set autoresize to YES for the keyboard toolbar's superview so that our adv. keyboard view
480  // gets properly resized
481  [[_keyboard_toolbar superview] setAutoresizesSubviews:YES];
482 
483  // show view with animation
484  [UIView beginAnimations:nil context:NULL];
485  [_advanced_keyboard_view setFrame:rect];
486  [UIView commitAnimations];
487 }
488 
489 - (IBAction)toggleKeyboardWhenOtherVisible:(id)sender
490 {
491  if (_advanced_keyboard_visible == NO)
492  {
493  [self showAdvancedKeyboardAnimated];
494  }
495  else
496  {
497  // hide existing view
498  [UIView beginAnimations:@"hide_advanced_keyboard_view" context:NULL];
499  CGRect rect = [_advanced_keyboard_view frame];
500  rect.origin.y = [[_keyboard_toolbar superview] bounds].size.height;
501  [_advanced_keyboard_view setFrame:rect];
502  [UIView commitAnimations];
503 
504  // the view is released in the animationDidStop selector registered in init
505  }
506 
507  // toggle flag
508  _advanced_keyboard_visible = !_advanced_keyboard_visible;
509 }
510 
511 - (IBAction)toggleWinKey:(id)sender
512 {
513  [[RDPKeyboard getSharedRDPKeyboard] toggleWinKey];
514 }
515 
516 - (IBAction)toggleShiftKey:(id)sender
517 {
518  [[RDPKeyboard getSharedRDPKeyboard] toggleShiftKey];
519 }
520 
521 - (IBAction)toggleCtrlKey:(id)sender
522 {
523  [[RDPKeyboard getSharedRDPKeyboard] toggleCtrlKey];
524 }
525 
526 - (IBAction)toggleAltKey:(id)sender
527 {
528  [[RDPKeyboard getSharedRDPKeyboard] toggleAltKey];
529 }
530 
531 - (IBAction)pressEscKey:(id)sender
532 {
533  [[RDPKeyboard getSharedRDPKeyboard] sendEscapeKeyStroke];
534 }
535 
536 #pragma mark -
537 #pragma mark event handlers
538 
539 - (void)animationStopped:(NSString *)animationID
540  finished:(NSNumber *)finished
541  context:(void *)context
542 {
543  if ([animationID isEqualToString:@"hide_advanced_keyboard_view"])
544  {
545  // cleanup advanced keyboard view
546  [_advanced_keyboard_view removeFromSuperview];
547  [_advanced_keyboard_view autorelease];
548  _advanced_keyboard_view = nil;
549  }
550 }
551 
552 - (IBAction)switchSession:(id)sender
553 {
554  [self suspendSession];
555 }
556 
557 - (IBAction)toggleKeyboard:(id)sender
558 {
559  if (!_keyboard_visible)
560  [_dummy_textfield becomeFirstResponder];
561  else
562  [_dummy_textfield resignFirstResponder];
563 }
564 
565 - (IBAction)toggleExtKeyboard:(id)sender
566 {
567  // if the sys kb is shown but not the advanced kb then toggle the advanced kb
568  if (_keyboard_visible && !_advanced_keyboard_visible)
569  [self toggleKeyboardWhenOtherVisible:nil];
570  else
571  {
572  // if not visible request the advanced keyboard view
573  if (_advanced_keyboard_visible == NO)
574  _requesting_advanced_keyboard = YES;
575  [self toggleKeyboard:nil];
576  }
577 }
578 
579 - (IBAction)toggleTouchPointer:(id)sender
580 {
581  BOOL toggle_visibilty = ![_touchpointer_view isHidden];
582  [_touchpointer_view setHidden:toggle_visibilty];
583  if (toggle_visibilty)
584  [_session_scrollview setContentInset:UIEdgeInsetsZero];
585  else
586  [_session_scrollview setContentInset:[_touchpointer_view getEdgeInsets]];
587 }
588 
589 - (IBAction)disconnectSession:(id)sender
590 {
591  [_session disconnect];
592 }
593 
594 - (IBAction)cancelButtonPressed:(id)sender
595 {
596  [_session disconnect];
597 }
598 
599 #pragma mark -
600 #pragma mark iOS Keyboard Notification Handlers
601 
602 // the keyboard is given in a portrait frame of reference
603 - (BOOL)isLandscape
604 {
605 
606  UIInterfaceOrientation ori = [[UIApplication sharedApplication] statusBarOrientation];
607  return (ori == UIInterfaceOrientationLandscapeLeft ||
608  ori == UIInterfaceOrientationLandscapeRight);
609 }
610 
611 - (void)shiftKeyboard:(NSNotification *)notification
612 {
613 
614  CGRect keyboardEndFrame =
615  [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
616 
617  CGFloat previousHeight = _keyboard_last_height;
618 
619  if ([self isLandscape])
620  {
621  // landscape has the keyboard based on x, so x can go negative
622  _keyboard_last_height = keyboardEndFrame.size.width + keyboardEndFrame.origin.x;
623  }
624  else
625  {
626  // portrait has the keyboard based on the difference of the height and the frames y.
627  CGFloat height = [[UIScreen mainScreen] bounds].size.height;
628  _keyboard_last_height = height - keyboardEndFrame.origin.y;
629  }
630 
631  CGFloat shiftHeight = _keyboard_last_height - previousHeight;
632 
633  [UIView beginAnimations:nil context:NULL];
634  [UIView setAnimationCurve:[[[notification userInfo]
635  objectForKey:UIKeyboardAnimationCurveUserInfoKey] intValue]];
636  [UIView
637  setAnimationDuration:[[[notification userInfo]
638  objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]];
639  CGRect frame = [_session_scrollview frame];
640  frame.size.height -= shiftHeight;
641  [_session_scrollview setFrame:frame];
642  [_touchpointer_view setFrame:frame];
643  [UIView commitAnimations];
644 }
645 
646 - (void)keyboardWillShow:(NSNotification *)notification
647 {
648  [self shiftKeyboard:notification];
649 
650  [_touchpointer_view ensurePointerIsVisible];
651 }
652 
653 - (void)keyboardDidShow:(NSNotification *)notification
654 {
655  if (_requesting_advanced_keyboard)
656  {
657  [self showAdvancedKeyboardAnimated];
658  _advanced_keyboard_visible = YES;
659  _requesting_advanced_keyboard = NO;
660  }
661 }
662 
663 - (void)keyboardWillHide:(NSNotification *)notification
664 {
665 
666  [self shiftKeyboard:notification];
667 }
668 
669 - (void)keyboardDidHide:(NSNotification *)notification
670 {
671  // release adanced keyboard view
672  if (_advanced_keyboard_visible == YES)
673  {
674  _advanced_keyboard_visible = NO;
675  [_advanced_keyboard_view removeFromSuperview];
676  [_advanced_keyboard_view autorelease];
677  _advanced_keyboard_view = nil;
678  }
679 }
680 
681 #pragma mark -
682 #pragma mark Gesture handlers
683 
684 - (void)handleSingleTap:(UITapGestureRecognizer *)gesture
685 {
686  CGPoint pos = [gesture locationInView:_session_view];
687  if (_toggle_mouse_button)
688  {
689  [_session
690  sendInputEvent:[self eventDescriptorForMouseEvent:GetRightMouseButtonClickEvent(YES)
691  position:pos]];
692  [_session
693  sendInputEvent:[self eventDescriptorForMouseEvent:GetRightMouseButtonClickEvent(NO)
694  position:pos]];
695  }
696  else
697  {
698  [_session
699  sendInputEvent:[self eventDescriptorForMouseEvent:GetLeftMouseButtonClickEvent(YES)
700  position:pos]];
701  [_session sendInputEvent:[self eventDescriptorForMouseEvent:GetLeftMouseButtonClickEvent(NO)
702  position:pos]];
703  }
704 
705  _toggle_mouse_button = NO;
706 }
707 
708 - (void)handleDoubleTap:(UITapGestureRecognizer *)gesture
709 {
710  CGPoint pos = [gesture locationInView:_session_view];
711  [_session sendInputEvent:[self eventDescriptorForMouseEvent:GetLeftMouseButtonClickEvent(YES)
712  position:pos]];
713  [_session sendInputEvent:[self eventDescriptorForMouseEvent:GetLeftMouseButtonClickEvent(NO)
714  position:pos]];
715  [_session sendInputEvent:[self eventDescriptorForMouseEvent:GetLeftMouseButtonClickEvent(YES)
716  position:pos]];
717  [_session sendInputEvent:[self eventDescriptorForMouseEvent:GetLeftMouseButtonClickEvent(NO)
718  position:pos]];
719  _toggle_mouse_button = NO;
720 }
721 
722 - (void)handleLongPress:(UILongPressGestureRecognizer *)gesture
723 {
724  CGPoint pos = [gesture locationInView:_session_view];
725 
726  if ([gesture state] == UIGestureRecognizerStateBegan)
727  [_session
728  sendInputEvent:[self eventDescriptorForMouseEvent:GetLeftMouseButtonClickEvent(YES)
729  position:pos]];
730  else if ([gesture state] == UIGestureRecognizerStateChanged)
731  [self handleMouseMoveForPosition:pos];
732  else if ([gesture state] == UIGestureRecognizerStateEnded)
733  [_session sendInputEvent:[self eventDescriptorForMouseEvent:GetLeftMouseButtonClickEvent(NO)
734  position:pos]];
735 }
736 
737 - (void)handleDoubleLongPress:(UILongPressGestureRecognizer *)gesture
738 {
739  // this point is mapped against the scroll view because we want to have relative movement to the
740  // screen/scrollview
741  CGPoint pos = [gesture locationInView:_session_scrollview];
742 
743  if ([gesture state] == UIGestureRecognizerStateBegan)
744  _prev_long_press_position = pos;
745  else if ([gesture state] == UIGestureRecognizerStateChanged)
746  {
747  int delta = _prev_long_press_position.y - pos.y;
748 
749  if (delta > GetScrollGestureDelta())
750  {
751  [_session sendInputEvent:[self eventDescriptorForMouseEvent:GetMouseWheelEvent(YES)
752  position:pos]];
753  _prev_long_press_position = pos;
754  }
755  else if (delta < -GetScrollGestureDelta())
756  {
757  [_session sendInputEvent:[self eventDescriptorForMouseEvent:GetMouseWheelEvent(NO)
758  position:pos]];
759  _prev_long_press_position = pos;
760  }
761  }
762 }
763 
764 - (void)handleSingle2FingersTap:(UITapGestureRecognizer *)gesture
765 {
766  _toggle_mouse_button = !_toggle_mouse_button;
767 }
768 
769 - (void)handleSingle3FingersTap:(UITapGestureRecognizer *)gesture
770 {
771  [_session setToolbarVisible:![_session toolbarVisible]];
772  [self showSessionToolbar:[_session toolbarVisible]];
773 }
774 
775 #pragma mark -
776 #pragma mark Touch Pointer delegates
777 // callback if touch pointer should be closed
778 - (void)touchPointerClose
779 {
780  [self toggleTouchPointer:nil];
781 }
782 
783 // callback for a left click action
784 - (void)touchPointerLeftClick:(CGPoint)pos down:(BOOL)down
785 {
786  CGPoint session_view_pos = [_touchpointer_view convertPoint:pos toView:_session_view];
787  [_session sendInputEvent:[self eventDescriptorForMouseEvent:GetLeftMouseButtonClickEvent(down)
788  position:session_view_pos]];
789 }
790 
791 // callback for a right click action
792 - (void)touchPointerRightClick:(CGPoint)pos down:(BOOL)down
793 {
794  CGPoint session_view_pos = [_touchpointer_view convertPoint:pos toView:_session_view];
795  [_session sendInputEvent:[self eventDescriptorForMouseEvent:GetRightMouseButtonClickEvent(down)
796  position:session_view_pos]];
797 }
798 
799 - (void)doAutoScrolling
800 {
801  int scrollX = 0;
802  int scrollY = 0;
803  CGPoint curPointerPos = [_touchpointer_view getPointerPosition];
804  CGRect viewBounds = [_touchpointer_view bounds];
805  CGRect scrollBounds = [_session_view bounds];
806 
807  // add content insets to scroll bounds
808  scrollBounds.size.width += [_session_scrollview contentInset].right;
809  scrollBounds.size.height += [_session_scrollview contentInset].bottom;
810 
811  // add zoom factor
812  scrollBounds.size.width *= [_session_scrollview zoomScale];
813  scrollBounds.size.height *= [_session_scrollview zoomScale];
814 
815  if (curPointerPos.x > (viewBounds.size.width - [_touchpointer_view getPointerWidth]))
816  scrollX = AUTOSCROLLDISTANCE;
817  else if (curPointerPos.x < 0)
818  scrollX = -AUTOSCROLLDISTANCE;
819 
820  if (curPointerPos.y > (viewBounds.size.height - [_touchpointer_view getPointerHeight]))
821  scrollY = AUTOSCROLLDISTANCE;
822  else if (curPointerPos.y < (_session_toolbar_visible ? TOOLBAR_HEIGHT : 0))
823  scrollY = -AUTOSCROLLDISTANCE;
824 
825  CGPoint newOffset = [_session_scrollview contentOffset];
826  newOffset.x += scrollX;
827  newOffset.y += scrollY;
828 
829  // if offset is going off screen - stop scrolling in that direction
830  if (newOffset.x < 0)
831  {
832  scrollX = 0;
833  newOffset.x = 0;
834  }
835  else if (newOffset.x > (scrollBounds.size.width - viewBounds.size.width))
836  {
837  scrollX = 0;
838  newOffset.x = MAX(scrollBounds.size.width - viewBounds.size.width, 0);
839  }
840  if (newOffset.y < 0)
841  {
842  scrollY = 0;
843  newOffset.y = 0;
844  }
845  else if (newOffset.y > (scrollBounds.size.height - viewBounds.size.height))
846  {
847  scrollY = 0;
848  newOffset.y = MAX(scrollBounds.size.height - viewBounds.size.height, 0);
849  }
850 
851  // perform scrolling
852  [_session_scrollview setContentOffset:newOffset];
853 
854  // continue scrolling?
855  if (scrollX != 0 || scrollY != 0)
856  [self performSelector:@selector(doAutoScrolling)
857  withObject:nil
858  afterDelay:AUTOSCROLLTIMEOUT];
859  else
860  _is_autoscrolling = NO;
861 }
862 
863 // callback for a right click action
864 - (void)touchPointerMove:(CGPoint)pos
865 {
866  CGPoint session_view_pos = [_touchpointer_view convertPoint:pos toView:_session_view];
867  [self handleMouseMoveForPosition:session_view_pos];
868 
869  if (_autoscroll_with_touchpointer && !_is_autoscrolling)
870  {
871  _is_autoscrolling = YES;
872  [self performSelector:@selector(doAutoScrolling)
873  withObject:nil
874  afterDelay:AUTOSCROLLTIMEOUT];
875  }
876 }
877 
878 // callback if scrolling is performed
879 - (void)touchPointerScrollDown:(BOOL)down
880 {
881  [_session sendInputEvent:[self eventDescriptorForMouseEvent:GetMouseWheelEvent(down)
882  position:CGPointZero]];
883 }
884 
885 // callback for toggling the standard keyboard
886 - (void)touchPointerToggleKeyboard
887 {
888  if (_advanced_keyboard_visible)
889  [self toggleKeyboardWhenOtherVisible:nil];
890  else
891  [self toggleKeyboard:nil];
892 }
893 
894 // callback for toggling the extended keyboard
895 - (void)touchPointerToggleExtendedKeyboard
896 {
897  [self toggleExtKeyboard:nil];
898 }
899 
900 // callback for reset view
901 - (void)touchPointerResetSessionView
902 {
903  [_session_scrollview setZoomScale:1.0 animated:YES];
904 }
905 
906 @end
907 
908 @implementation RDPSessionViewController (Private)
909 
910 #pragma mark -
911 #pragma mark Helper functions
912 
913 - (void)showSessionToolbar:(BOOL)show
914 {
915  // already shown or hidden?
916  if (_session_toolbar_visible == show)
917  return;
918 
919  if (show)
920  {
921  [UIView beginAnimations:@"showToolbar" context:nil];
922  [UIView setAnimationDuration:.4];
923  [UIView setAnimationCurve:UIViewAnimationCurveLinear];
924  [_session_toolbar
925  setFrame:CGRectMake(0.0, 0.0, [[self view] bounds].size.width, TOOLBAR_HEIGHT)];
926  [UIView commitAnimations];
927  _session_toolbar_visible = YES;
928  }
929  else
930  {
931  [UIView beginAnimations:@"hideToolbar" context:nil];
932  [UIView setAnimationDuration:.4];
933  [UIView setAnimationCurve:UIViewAnimationCurveLinear];
934  [_session_toolbar setFrame:CGRectMake(0.0, -TOOLBAR_HEIGHT, [[self view] bounds].size.width,
935  TOOLBAR_HEIGHT)];
936  [UIView commitAnimations];
937  _session_toolbar_visible = NO;
938  }
939 }
940 
941 - (UIToolbar *)keyboardToolbar
942 {
943  UIToolbar *keyboard_toolbar = [[[UIToolbar alloc] initWithFrame:CGRectNull] autorelease];
944  [keyboard_toolbar setBarStyle:UIBarStyleBlackOpaque];
945 
946  UIBarButtonItem *esc_btn =
947  [[[UIBarButtonItem alloc] initWithTitle:@"Esc"
948  style:UIBarButtonItemStyleBordered
949  target:self
950  action:@selector(pressEscKey:)] autorelease];
951  UIImage *win_icon =
952  [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"toolbar_icon_win"
953  ofType:@"png"]];
954  UIBarButtonItem *win_btn =
955  [[[UIBarButtonItem alloc] initWithImage:win_icon
956  style:UIBarButtonItemStyleBordered
957  target:self
958  action:@selector(toggleWinKey:)] autorelease];
959  UIBarButtonItem *ctrl_btn =
960  [[[UIBarButtonItem alloc] initWithTitle:@"Ctrl"
961  style:UIBarButtonItemStyleBordered
962  target:self
963  action:@selector(toggleCtrlKey:)] autorelease];
964  UIBarButtonItem *alt_btn =
965  [[[UIBarButtonItem alloc] initWithTitle:@"Alt"
966  style:UIBarButtonItemStyleBordered
967  target:self
968  action:@selector(toggleAltKey:)] autorelease];
969  UIBarButtonItem *ext_btn = [[[UIBarButtonItem alloc]
970  initWithTitle:@"Ext"
971  style:UIBarButtonItemStyleBordered
972  target:self
973  action:@selector(toggleKeyboardWhenOtherVisible:)] autorelease];
974  UIBarButtonItem *done_btn = [[[UIBarButtonItem alloc]
975  initWithBarButtonSystemItem:UIBarButtonSystemItemDone
976  target:self
977  action:@selector(toggleKeyboard:)] autorelease];
978  UIBarButtonItem *flex_spacer =
979  [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
980  target:nil
981  action:nil] autorelease];
982 
983  // iPad gets a shift button, iphone doesn't (there's just not enough space ...)
984  NSArray *items;
985  if (IsPad())
986  {
987  UIBarButtonItem *shift_btn =
988  [[[UIBarButtonItem alloc] initWithTitle:@"Shift"
989  style:UIBarButtonItemStyleBordered
990  target:self
991  action:@selector(toggleShiftKey:)] autorelease];
992  items = [NSArray arrayWithObjects:esc_btn, flex_spacer, shift_btn, flex_spacer, ctrl_btn,
993  flex_spacer, win_btn, flex_spacer, alt_btn, flex_spacer,
994  ext_btn, flex_spacer, done_btn, nil];
995  }
996  else
997  {
998  items = [NSArray arrayWithObjects:esc_btn, flex_spacer, ctrl_btn, flex_spacer, win_btn,
999  flex_spacer, alt_btn, flex_spacer, ext_btn, flex_spacer,
1000  done_btn, nil];
1001  }
1002 
1003  [keyboard_toolbar setItems:items];
1004  [keyboard_toolbar sizeToFit];
1005  return keyboard_toolbar;
1006 }
1007 
1008 - (void)initGestureRecognizers
1009 {
1010  // single and double tap recognizer
1011  UITapGestureRecognizer *doubleTapRecognizer =
1012  [[[UITapGestureRecognizer alloc] initWithTarget:self
1013  action:@selector(handleDoubleTap:)] autorelease];
1014  [doubleTapRecognizer setNumberOfTouchesRequired:1];
1015  [doubleTapRecognizer setNumberOfTapsRequired:2];
1016 
1017  UITapGestureRecognizer *singleTapRecognizer =
1018  [[[UITapGestureRecognizer alloc] initWithTarget:self
1019  action:@selector(handleSingleTap:)] autorelease];
1020  [singleTapRecognizer requireGestureRecognizerToFail:doubleTapRecognizer];
1021  [singleTapRecognizer setNumberOfTouchesRequired:1];
1022  [singleTapRecognizer setNumberOfTapsRequired:1];
1023 
1024  // 2 fingers - tap recognizer
1025  UITapGestureRecognizer *single2FingersTapRecognizer = [[[UITapGestureRecognizer alloc]
1026  initWithTarget:self
1027  action:@selector(handleSingle2FingersTap:)] autorelease];
1028  [single2FingersTapRecognizer setNumberOfTouchesRequired:2];
1029  [single2FingersTapRecognizer setNumberOfTapsRequired:1];
1030 
1031  // long press gesture recognizer
1032  UILongPressGestureRecognizer *longPressRecognizer = [[[UILongPressGestureRecognizer alloc]
1033  initWithTarget:self
1034  action:@selector(handleLongPress:)] autorelease];
1035  [longPressRecognizer setMinimumPressDuration:0.5];
1036 
1037  // double long press gesture recognizer
1038  UILongPressGestureRecognizer *doubleLongPressRecognizer = [[[UILongPressGestureRecognizer alloc]
1039  initWithTarget:self
1040  action:@selector(handleDoubleLongPress:)] autorelease];
1041  [doubleLongPressRecognizer setNumberOfTouchesRequired:2];
1042  [doubleLongPressRecognizer setMinimumPressDuration:0.5];
1043 
1044  // 3 finger, single tap gesture for showing/hiding the toolbar
1045  UITapGestureRecognizer *single3FingersTapRecognizer = [[[UITapGestureRecognizer alloc]
1046  initWithTarget:self
1047  action:@selector(handleSingle3FingersTap:)] autorelease];
1048  [single3FingersTapRecognizer setNumberOfTapsRequired:1];
1049  [single3FingersTapRecognizer setNumberOfTouchesRequired:3];
1050 
1051  // add gestures to scroll view
1052  [_session_scrollview addGestureRecognizer:singleTapRecognizer];
1053  [_session_scrollview addGestureRecognizer:doubleTapRecognizer];
1054  [_session_scrollview addGestureRecognizer:single2FingersTapRecognizer];
1055  [_session_scrollview addGestureRecognizer:longPressRecognizer];
1056  [_session_scrollview addGestureRecognizer:doubleLongPressRecognizer];
1057  [_session_scrollview addGestureRecognizer:single3FingersTapRecognizer];
1058 }
1059 
1060 - (void)suspendSession
1061 {
1062  // suspend session and pop navigation controller
1063  [_session suspend];
1064 
1065  // pop current view controller
1066  [[self navigationController] popViewControllerAnimated:YES];
1067 }
1068 
1069 - (NSDictionary *)eventDescriptorForMouseEvent:(int)event position:(CGPoint)position
1070 {
1071  return [NSDictionary
1072  dictionaryWithObjectsAndKeys:@"mouse", @"type", [NSNumber numberWithUnsignedShort:event],
1073  @"flags",
1074  [NSNumber numberWithUnsignedShort:lrintf(position.x)],
1075  @"coord_x",
1076  [NSNumber numberWithUnsignedShort:lrintf(position.y)],
1077  @"coord_y", nil];
1078 }
1079 
1080 - (void)sendDelayedMouseEventWithTimer:(NSTimer *)timer
1081 {
1082  _mouse_move_event_timer = nil;
1083  NSDictionary *event = [timer userInfo];
1084  [_session sendInputEvent:event];
1085  [timer autorelease];
1086 }
1087 
1088 - (void)handleMouseMoveForPosition:(CGPoint)position
1089 {
1090  NSDictionary *event = [self eventDescriptorForMouseEvent:PTR_FLAGS_MOVE position:position];
1091 
1092  // cancel pending mouse move events
1093  [_mouse_move_event_timer invalidate];
1094  _mouse_move_events_skipped++;
1095 
1096  if (_mouse_move_events_skipped >= 5)
1097  {
1098  [_session sendInputEvent:event];
1099  _mouse_move_events_skipped = 0;
1100  }
1101  else
1102  {
1103  [_mouse_move_event_timer autorelease];
1104  _mouse_move_event_timer =
1105  [[NSTimer scheduledTimerWithTimeInterval:0.05
1106  target:self
1107  selector:@selector(sendDelayedMouseEventWithTimer:)
1108  userInfo:event
1109  repeats:NO] retain];
1110  }
1111 }
1112 
1113 @end