FreeRDP
Loading...
Searching...
No Matches
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 <GameController/GameController.h>
13#import <objc/runtime.h>
14#import "RDPSessionViewController.h"
15#import "RDPSessionToolbar.h"
16#import "RDPOverlayStyle.h"
17#import "RDPKeyboard.h"
18#import "Utils.h"
19#import "Toast+UIView.h"
20#import "ConnectionParams.h"
21
22#define TOOLBAR_HEIGHT 44
23#define KEYBOARD_CURSOR_MARGIN 44.0
24
25@interface RDPSessionViewController (Private)
26- (void)showSessionToolbar:(BOOL)show;
27- (void)initGestureRecognizers;
28- (void)suspendSession;
29- (void)fitSessionViewToViewport;
30- (void)centerSessionViewInViewport;
31- (void)setKeyboardOverlap:(CGFloat)overlap;
32- (CGPoint)remotePositionForSessionViewPosition:(CGPoint)position;
33- (CGPoint)sessionViewPositionForRemotePosition:(CGPoint)position;
34- (CGPoint)clampedSessionViewCursorPosition:(CGPoint)position;
35- (NSDictionary *)eventDescriptorForMouseEvent:(int)event position:(CGPoint)position;
36- (void)handleMouseMoveForPosition:(CGPoint)position;
37- (CGPoint)currentCursorViewPosition;
38- (void)moveCursorByViewportDelta:(CGPoint)delta;
39- (void)moveCursorToSessionViewPosition:(CGPoint)position;
40- (void)sendMouseButtonEvent:(int)event;
41- (BOOL)isKeyboardActive;
42- (BOOL)isLandscape;
43- (CGFloat)advancedKeyboardHeight;
44- (void)resetTextInput;
45- (void)commitTextInput;
46@end
47
48// dummy text field
49@interface RDPSessionTextField : UITextField
50@end
51
52@implementation RDPSessionTextField
53
54- (void)deleteBackward
55{
56 // send backspace to remote session if dummy text field is empty.
57 if ([[self text] length] == 0)
58 [[RDPKeyboard getSharedRDPKeyboard] sendBackspaceKeyStroke];
59 [super deleteBackward];
60}
61
62@end
63
64@implementation RDPSessionViewController
65
66#pragma mark class methods
67
68- (id)initWithNibName:(NSString *)nibNameOrNil
69 bundle:(NSBundle *)nibBundleOrNil
70 session:(RDPSession *)session
71{
72 self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
73 if (self)
74 {
75 _session = [session retain];
76 [_session setDelegate:self];
77 _session_initilized = NO;
78
79 _advanced_keyboard_view = nil;
80 _sent_text = [@"" retain];
81 _last_session_viewport_size = CGSizeZero;
82
83 _session_toolbar_visible = NO;
84
85 _cursor_view_position = CGPointZero;
86 _last_mouse_pan_location = CGPointZero;
87 _has_cursor_view_position = NO;
88 _has_user_moved_cursor = NO;
89 _mouse_pan_active = NO;
90 _long_press_active = NO;
91 _mouse_drag_active = NO;
92 _pointer_is_indirect = NO;
93 }
94
95 return self;
96}
97
98// Implement loadView to create a view hierarchy programmatically, without using a nib.
99- (void)loadView
100{
101 // load default view and set background color and resizing mask
102 [super loadView];
103
104 // let pointer input pass through the session toolbar to the remote session
105 object_setClass(_session_toolbar, [RDPSessionToolbar class]);
106 [(RDPSessionToolbar *)_session_toolbar setPassthroughView:_session_scrollview];
107 if (RDPSupportLiquidGlass() == NO)
108 {
109 // before ios 26, the gray background has been displayed in toolbar
110 UIToolbarAppearance *appearance = [[[UIToolbarAppearance alloc] init] autorelease];
111
112 // set transparent background
113 [appearance configureWithTransparentBackground];
114 [_session_toolbar setStandardAppearance:appearance];
115 [_session_toolbar setCompactAppearance:appearance];
116 [_session_toolbar setScrollEdgeAppearance:appearance];
117 [_session_toolbar setCompactScrollEdgeAppearance:appearance];
118 }
119
120 // init keyboard toolbar
121 _keyboard_toolbar = [[RDPKeyboardToolbarView alloc] initWithTarget:self];
122 [_dummy_textfield setInputAccessoryView:_keyboard_toolbar];
123
124 // the text field must not alter when typed
125 [_dummy_textfield setAutocapitalizationType:UITextAutocapitalizationTypeNone];
126 [_dummy_textfield setSpellCheckingType:UITextSpellCheckingTypeNo];
127 [_dummy_textfield setSmartQuotesType:UITextSmartQuotesTypeNo];
128 [_dummy_textfield setSmartDashesType:UITextSmartDashesTypeNo];
129 [_dummy_textfield setSmartInsertDeleteType:UITextSmartInsertDeleteTypeNo];
130 [_dummy_textfield addTarget:self
131 action:@selector(textInputChanged:)
132 forControlEvents:UIControlEventEditingChanged];
133
134 // the undo/redo/paste shortcuts of the iPad keyboard act on the hidden text field, not on
135 // the remote session, so don't show them
136 [[_dummy_textfield inputAssistantItem] setLeadingBarButtonGroups:@[]];
137 [[_dummy_textfield inputAssistantItem] setTrailingBarButtonGroups:@[]];
138
139 // init gesture recognizers
140 [self initGestureRecognizers];
141
142 // hide session toolbar
143 [_session_toolbar
144 setFrame:CGRectMake(0.0, -TOOLBAR_HEIGHT, [[self view] bounds].size.width, TOOLBAR_HEIGHT)];
145}
146
147// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
148- (void)viewDidLoad
149{
150 [super viewDidLoad];
151
152 [_session_scrollview setContentInsetAdjustmentBehavior:UIScrollViewContentInsetAdjustmentNever];
153 [_session_scrollview setContentInset:UIEdgeInsetsZero];
154 [_session_scrollview setScrollIndicatorInsets:UIEdgeInsetsZero];
155 [_session_scrollview setShowsHorizontalScrollIndicator:NO];
156 [_session_scrollview setShowsVerticalScrollIndicator:NO];
157 [_session_scrollview setAlwaysBounceHorizontal:NO];
158 [_session_scrollview setAlwaysBounceVertical:NO];
159 [_session_scrollview setBounces:NO];
160}
161
162- (void)viewDidLayoutSubviews
163{
164 [super viewDidLayoutSubviews];
165
166 CGRect viewportFrame = [[self view] bounds];
167 [_session_scrollview setFrame:viewportFrame];
168
169 CGSize viewportSize = [_session_scrollview bounds].size;
170 if (!CGSizeEqualToSize(viewportSize, _last_session_viewport_size))
171 [self fitSessionViewToViewport];
172 else
173 [self centerSessionViewInViewport];
174}
175
176- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
177{
178 return YES;
179}
180
181- (BOOL)prefersStatusBarHidden
182{
183 return YES;
184}
185
186- (UIStatusBarAnimation)preferredStatusBarUpdateAnimation
187{
188 return UIStatusBarAnimationSlide;
189}
190
191- (void)viewWillTransitionToSize:(CGSize)size
192 withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
193{
194 [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
195
196 // the system keyboard changes its height with the orientation, so follow it
197 [coordinator
198 animateAlongsideTransition:nil
199 completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
200 if (!_advanced_keyboard_view)
201 return;
202 CGRect frame = [_advanced_keyboard_view frame];
203 frame.size.height = [self advancedKeyboardHeight];
204 [_advanced_keyboard_view setFrame:frame];
205 [_dummy_textfield reloadInputViews];
206 }];
207}
208
209- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
210{
211 (void)fromInterfaceOrientation;
212 [self centerSessionViewInViewport];
213}
214
215- (void)didReceiveMemoryWarning
216{
217 // Releases the view if it doesn't have a superview.
218 [super didReceiveMemoryWarning];
219
220 // Release any cached data, images, etc. that aren't in use.
221}
222
223- (void)viewDidUnload
224{
225 [super viewDidUnload];
226 // Release any retained subviews of the main view.
227 // e.g. self.myOutlet = nil;
228}
229
230- (void)viewWillAppear:(BOOL)animated
231{
232 [super viewWillAppear:animated];
233
234 // remote screen always fit in device screen
235 [self setNeedsStatusBarAppearanceUpdate];
236 [[self navigationController] setNeedsStatusBarAppearanceUpdate];
237 [[self navigationController] setNavigationBarHidden:YES animated:animated];
238 if (@available(iOS 18.0, *))
239 [[self tabBarController] setTabBarHidden:YES animated:animated];
240 else
241 [[[self tabBarController] tabBar] setHidden:YES];
242
243 // if session is suspended - notify that we got a new bitmap context
244 if ([_session isSuspended])
245 [self sessionBitmapContextWillChange:_session];
246
247 // init keyboard
248 [[RDPKeyboard getSharedRDPKeyboard] initWithSession:_session delegate:self];
249}
250
251- (void)viewDidAppear:(BOOL)animated
252{
253 [super viewDidAppear:animated];
254
255 if (!_session_initilized)
256 {
257 if ([_session isSuspended])
258 {
259 [_session resume];
260 [self sessionBitmapContextDidChange:_session];
261 }
262 else
263 [_session connect];
264
265 _session_initilized = YES;
266 }
267}
268
269- (void)viewWillDisappear:(BOOL)animated
270{
271 [super viewWillDisappear:animated];
272 if (_mouse_drag_active)
273 [self sendMouseButtonEvent:GetLeftMouseButtonClickEvent(NO)];
274 _mouse_drag_active = NO;
275 _mouse_pan_active = NO;
276 _long_press_active = NO;
277
278 [[self navigationController] setNeedsStatusBarAppearanceUpdate];
279 [[self navigationController] setNavigationBarHidden:NO animated:animated];
280 if (@available(iOS 18.0, *))
281 [[self tabBarController] setTabBarHidden:NO animated:animated];
282 else
283 [[[self tabBarController] tabBar] setHidden:NO];
284
285 // reset all modifier keys on rdp keyboard
286 [[RDPKeyboard getSharedRDPKeyboard] reset];
287
288 // hide toolbar and keyboard
289 [self showSessionToolbar:NO];
290 [_dummy_textfield resignFirstResponder];
291}
292
293- (void)dealloc
294{
295 // remove any observers
296 [[NSNotificationCenter defaultCenter] removeObserver:self];
297
298 // the session lives on longer so set the delegate to nil
299 [_session setDelegate:nil];
300
301 [_advanced_keyboard_view release];
302 [_keyboard_toolbar release];
303 [_sent_text release];
304 [_session release];
305 [super dealloc];
306}
307
308#pragma mark -
309#pragma mark ScrollView delegate methods
310
311- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
312{
313 return _session_view;
314}
315
316- (void)scrollViewDidZoom:(UIScrollView *)scrollView
317{
318 [self centerSessionViewInViewport];
319}
320
321- (void)scrollViewDidEndZooming:(UIScrollView *)scrollView
322 withView:(UIView *)view
323 atScale:(CGFloat)scale
324{
325 NSLog(@"New zoom scale: %f", scale);
326 [self centerSessionViewInViewport];
327 [_session_view setNeedsDisplayInRemoteRect:[_session_view bounds]];
328}
329
330#pragma mark -
331#pragma mark TextField delegate methods
332- (BOOL)textFieldShouldBeginEditing:(UITextField *)textField
333{
334 return YES;
335}
336
337- (BOOL)textFieldShouldEndEditing:(UITextField *)textField
338{
339 [self destroyAdvancedKeyboard];
340 return YES;
341}
342
343- (void)textFieldDidEndEditing:(UITextField *)textField
344{
345 [self commitTextInput];
346}
347
348- (BOOL)textFieldShouldReturn:(UITextField *)textField
349{
350 [self commitTextInput];
351 [[RDPKeyboard getSharedRDPKeyboard] sendEnterKeyStroke];
352
353 // turn off default action
354 return NO;
355}
356
357// sends what changed in the text field since the last time
358- (void)textInputChanged:(UITextField *)textField
359{
360 // send marked text (chinese, japanese) only once committed, sending every change
361 // arrives out of order on the server
362 if ([textField markedTextRange] != nil)
363 return;
364
365 // a letter typed with a toolbar modifier is a shortcut (e.g. ctrl+a). read it before
366 // sending, which releases the modifiers
367 RDPKeyboard *keyboard = [RDPKeyboard getSharedRDPKeyboard];
368 BOOL modifierPressed = [keyboard ctrlPressed] || [keyboard altPressed] ||
369 [keyboard winPressed] || [keyboard altGrPressed];
370
371 NSString *text = [textField text] != nil ? [textField text] : @"";
372 NSUInteger length = MIN([text length], [_sent_text length]);
373 NSUInteger common = 0;
374
375 while (common < length)
376 {
377 if ([text characterAtIndex:common] != [_sent_text characterAtIndex:common])
378 {
379 break;
380 }
381 common++;
382 }
383
384 // don't split a multi code point character (e.g. emoji)
385 if (common < [_sent_text length])
386 common = MIN(common, [_sent_text rangeOfComposedCharacterSequenceAtIndex:common].location);
387 if (common < [text length])
388 common = MIN(common, [text rangeOfComposedCharacterSequenceAtIndex:common].location);
389
390 for (NSUInteger i = common; i < [_sent_text length];)
391 {
392 NSRange range = [_sent_text rangeOfComposedCharacterSequenceAtIndex:i];
393 [keyboard sendBackspaceKeyStroke];
394 i = NSMaxRange(range);
395 }
396
397 for (NSUInteger i = common; i < [text length]; i++)
398 {
399 [keyboard sendUnicode:[text characterAtIndex:i]];
400 }
401
402 BOOL isLastCharLetter = NO;
403 if ([text length] > 0)
404 {
405 isLastCharLetter = [[NSCharacterSet letterCharacterSet]
406 characterIsMember:[text characterAtIndex:[text length] - 1]];
407 }
408
409 // some keyboard (korean) composes with the letters before the cursor
410 // keep it
411 if (modifierPressed || !isLastCharLetter)
412 {
413 [self resetTextInput];
414 }
415 else
416 {
417 [_sent_text release];
418 _sent_text = [text copy];
419 }
420}
421
422#pragma mark -
423#pragma mark AdvancedKeyboardDelegate functions
424- (void)advancedKeyPressedVKey:(NSInteger)key
425{
426 [self commitTextInput];
427 [[RDPKeyboard getSharedRDPKeyboard] sendVirtualKeyCode:key];
428}
429
430#pragma mark - RDP keyboard handler
431
432- (void)modifiersChangedForKeyboard:(RDPKeyboard *)keyboard
433{
434 [_keyboard_toolbar updateModifiersWithKeyboard:keyboard];
435}
436
437#pragma mark -
438#pragma mark RDPSessionDelegate functions
439
440- (void)session:(RDPSession *)session didFailToConnect:(int)reason
441{
442 // remove and release connecting view
443 [_connecting_indicator_view stopAnimating];
444 [_connecting_view removeFromSuperview];
445 [_connecting_view autorelease];
446
447 // return to bookmark list
448 [[self navigationController] popViewControllerAnimated:YES];
449}
450
451- (void)sessionWillConnect:(RDPSession *)session
452{
453 // load connecting view
454 [[NSBundle mainBundle] loadNibNamed:@"RDPConnectingView" owner:self options:nil];
455
456 // set strings
457 [_lbl_connecting setText:NSLocalizedString(@"Connecting", @"Connecting progress view - label")];
458 [_cancel_connect_button setTitle:NSLocalizedString(@"Cancel", @"Cancel Button")
459 forState:UIControlStateNormal];
460
461 // center view and give it round corners
462 [_connecting_view setCenter:[[self view] center]];
463 [[_connecting_view layer] setCornerRadius:10];
464
465 // display connecting view and start indicator
466 [[self view] addSubview:_connecting_view];
467 [_connecting_indicator_view startAnimating];
468}
469
470- (void)sessionDidConnect:(RDPSession *)session
471{
472 // register keyboard notification handlers
473 [[NSNotificationCenter defaultCenter] addObserver:self
474 selector:@selector(keyboardWillShow:)
475 name:UIKeyboardWillShowNotification
476 object:nil];
477 [[NSNotificationCenter defaultCenter] addObserver:self
478 selector:@selector(keyboardWillHide:)
479 name:UIKeyboardWillHideNotification
480 object:nil];
481 [[NSNotificationCenter defaultCenter] addObserver:self
482 selector:@selector(keyboardDidHide:)
483 name:UIKeyboardDidHideNotification
484 object:nil];
485
486 // register hardware keyboard connection handlers (for ipad magic keyboard)
487 [[NSNotificationCenter defaultCenter] addObserver:self
488 selector:@selector(hardwareKeyboardChanged:)
489 name:GCKeyboardDidConnectNotification
490 object:nil];
491 [[NSNotificationCenter defaultCenter] addObserver:self
492 selector:@selector(hardwareKeyboardChanged:)
493 name:GCKeyboardDidDisconnectNotification
494 object:nil];
495 [self hardwareKeyboardChanged:nil];
496
497 // remove and release connecting view
498 [_connecting_indicator_view stopAnimating];
499 [_connecting_view removeFromSuperview];
500 [_connecting_view autorelease];
501
502 // check if session settings changed ...
503 // The 2nd width check is to ignore changes in resolution settings due to the RDVH display bug
504 // (refer to RDPSEssion.m for more details)
505 ConnectionParams *orig_params = [session params];
506 rdpSettings *sess_params = [session getSessionParams];
507 const UINT32 width = freerdp_settings_get_uint32(sess_params, FreeRDP_DesktopWidth);
508 const UINT32 height = freerdp_settings_get_uint32(sess_params, FreeRDP_DesktopHeight);
509 const UINT32 depth = freerdp_settings_get_uint32(sess_params, FreeRDP_ColorDepth);
510
511 if (([orig_params intForKey:@"width"] != width &&
512 [orig_params intForKey:@"width"] != (width + 1ul)) ||
513 [orig_params intForKey:@"height"] != height || [orig_params intForKey:@"colors"] != depth)
514 {
515 // display notification that the session params have been changed by the server
516 NSString *message =
517 [NSString stringWithFormat:NSLocalizedString(
518 @"The server changed the screen settings to %dx%dx%d",
519 @"Screen settings not supported message with width, "
520 @"height and colors parameter"),
521 width, height, depth];
522 [[self view] makeToast:message duration:ToastDurationNormal position:@"bottom"];
523 }
524}
525
526- (void)sessionWillDisconnect:(RDPSession *)session
527{
528}
529
530- (void)sessionDidDisconnect:(RDPSession *)session
531{
532 // return to bookmark list
533 [[self navigationController] popViewControllerAnimated:YES];
534}
535
536- (void)sessionBitmapContextWillChange:(RDPSession *)session
537{
538 [_session_view prepareForBitmapContextChange];
539
540 // calc new view frame
541 rdpSettings *sess_params = [session getSessionParams];
542 CGRect view_rect =
543 CGRectMake(0, 0, freerdp_settings_get_uint32(sess_params, FreeRDP_DesktopWidth),
544 freerdp_settings_get_uint32(sess_params, FreeRDP_DesktopHeight));
545
546 // set session view to its native (remote) size and update content size
547 [_session_scrollview setZoomScale:1.0];
548 [_session_view setFrame:view_rect];
549 [_session_scrollview setContentSize:view_rect.size];
550 _has_cursor_view_position = NO;
551 _has_user_moved_cursor = NO;
552 _last_session_viewport_size = CGSizeZero;
553 [self fitSessionViewToViewport];
554
555 // show toolbar
556 [_session setToolbarVisible:YES];
557 [self showSessionToolbar:[_session toolbarVisible]];
558}
559
560- (void)sessionBitmapContextDidChange:(RDPSession *)session
561{
562 // associate view with session
563 [_session_view setSession:session];
564 [_session_view setDefaultRemoteCursor];
565 if (!_has_cursor_view_position)
566 (void)[self currentCursorViewPosition];
567
568 // Upload the new desktop once; subsequent EndPaint callbacks update only dirty regions.
569 [_session_view setNeedsDisplayInRemoteRect:[_session_view bounds]];
570}
571
572- (void)session:(RDPSession *)session needsRedrawInRect:(CGRect)rect
573{
574 [_session_view setNeedsDisplayInRemoteRect:rect];
575}
576
577- (void)session:(RDPSession *)session didSetRemoteCursor:(RDPCursor *)cursor
578{
579 (void)session;
580 [_session_view setRemoteCursor:cursor];
581
582 if (!_has_user_moved_cursor &&
583 (!_has_cursor_view_position || CGPointEqualToPoint(_cursor_view_position, CGPointZero)))
584 {
585 _has_cursor_view_position = NO;
586 (void)[self currentCursorViewPosition];
587 }
588}
589
590- (void)session:(RDPSession *)session didMoveRemoteCursor:(CGPoint)position
591{
592 (void)session;
593 if (_mouse_pan_active || _long_press_active)
594 return;
595 CGPoint viewPosition = [self sessionViewPositionForRemotePosition:position];
596 if (!_has_user_moved_cursor && CGPointEqualToPoint(position, CGPointZero) &&
597 _has_cursor_view_position && !CGPointEqualToPoint(_cursor_view_position, CGPointZero))
598 return;
599
600 _cursor_view_position = viewPosition;
601 _has_cursor_view_position = YES;
602 [_session_view setRemoteCursorPosition:viewPosition];
603}
604
605- (void)sessionDidHideRemoteCursor:(RDPSession *)session
606{
607 (void)session;
608 [_session_view hideRemoteCursor];
609}
610
611- (void)sessionDidSetDefaultRemoteCursor:(RDPSession *)session
612{
613 (void)session;
614 [_session_view setDefaultRemoteCursor];
615}
616
617- (void)session:(RDPSession *)session requestsAuthenticationWithParams:(NSMutableDictionary *)params
618{
619 // custom view --> stock UIAlertController
620 __block UIAlertController *alert = [UIAlertController
621 alertControllerWithTitle:NSLocalizedString(@"Credentials", @"Credentials title")
622 message:NSLocalizedString(@"Please provide the missing user information.",
623 @"Credentials input view message")
624 preferredStyle:UIAlertControllerStyleAlert];
625
626 // username input field
627 [alert addTextFieldWithConfigurationHandler:^(UITextField *textField) {
628 [textField
629 setPlaceholder:NSLocalizedString(@"Username", @"Credentials Input Username hint")];
630 [textField setText:[params valueForKey:@"username"]];
631 [textField setAutocapitalizationType:UITextAutocapitalizationTypeNone];
632 [textField setAutocorrectionType:UITextAutocorrectionTypeNo];
633 }];
634
635 // password input field
636 [alert addTextFieldWithConfigurationHandler:^(UITextField *textField) {
637 [textField
638 setPlaceholder:NSLocalizedString(@"Password", @"Credentials Input Password hint")];
639 [textField setText:[params valueForKey:@"password"]];
640 [textField setSecureTextEntry:YES];
641 }];
642
643 // domain input field
644 [alert addTextFieldWithConfigurationHandler:^(UITextField *textField) {
645 [textField setPlaceholder:NSLocalizedString(@"Domain", @"Credentials Input Domain hint")];
646 [textField setText:[params valueForKey:@"domain"]];
647 [textField setAutocapitalizationType:UITextAutocapitalizationTypeNone];
648 [textField setAutocorrectionType:UITextAutocorrectionTypeNo];
649 }];
650
651 // cancel btn
652 UIAlertAction *cancelAction =
653 [UIAlertAction actionWithTitle:NSLocalizedString(@"Cancel", @"Cancel Button")
654 style:UIAlertActionStyleCancel
655 handler:^(UIAlertAction *action) {
656 [params setValue:[NSNumber numberWithBool:NO] forKey:@"result"];
657 [[session uiRequestCompleted] signal];
658 }];
659 [alert addAction:cancelAction];
660
661 // login btn
662 UIAlertAction *loginAction = [UIAlertAction
663 actionWithTitle:NSLocalizedString(@"Login", @"Login Button")
664 style:UIAlertActionStyleDefault
665 handler:^(UIAlertAction *action) {
666 NSArray *fields = [alert textFields];
667 [params setValue:[[fields objectAtIndex:0] text] forKey:@"username"];
668 [params setValue:[[fields objectAtIndex:1] text] forKey:@"password"];
669 [params setValue:[[fields objectAtIndex:2] text] forKey:@"domain"];
670 [params setValue:[NSNumber numberWithBool:YES] forKey:@"result"];
671 [[session uiRequestCompleted] signal];
672 }];
673 [alert addAction:loginAction];
674
675 [alert setPreferredAction:loginAction];
676
677 [self presentViewController:alert animated:YES completion:nil];
678}
679
680- (void)session:(RDPSession *)session verifyCertificateWithParams:(NSMutableDictionary *)params
681{
682 NSString *message = [NSString
683 stringWithFormat:@"%@\n\n%@ %@",
684 NSLocalizedString(@"The identity of the remote computer cannot be "
685 @"verified. Do you want to connect anyway?",
686 @"Verify certificate view message"),
687 NSLocalizedString(@"Issuer:", @"Verify certificate view issuer label"),
688 [params valueForKey:@"issuer"]];
689 UIAlertController *alert =
690 [UIAlertController alertControllerWithTitle:nil
691 message:message
692 preferredStyle:UIAlertControllerStyleAlert];
693
694 [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"No", @"No Button")
695 style:UIAlertActionStyleCancel
696 handler:^(UIAlertAction *action) {
697 [params setValue:[NSNumber numberWithBool:NO]
698 forKey:@"result"];
699 [[session uiRequestCompleted] signal];
700 }]];
701
702 [alert addAction:[UIAlertAction actionWithTitle:NSLocalizedString(@"Yes", @"Yes Button")
703 style:UIAlertActionStyleDefault
704 handler:^(UIAlertAction *action) {
705 [params setValue:[NSNumber numberWithBool:YES]
706 forKey:@"result"];
707 [[session uiRequestCompleted] signal];
708 }]];
709
710 [self presentViewController:alert animated:YES completion:nil];
711}
712
713- (CGSize)sizeForFitScreenForSession:(RDPSession *)session
714{
715 // set remote resolution that matches the on-screen viewport.
716 CGSize size = [self view].bounds.size;
717 UIScreen *screen = [[self view] window] ? [[[self view] window] screen] : [UIScreen mainScreen];
718 CGFloat scale =
719 [screen respondsToSelector:@selector(nativeScale)] ? [screen nativeScale] : [screen scale];
720 if (scale <= 0.0f)
721 scale = 1.0f;
722
723 size.width = ceilf(size.width * scale);
724 size.height = ceilf(size.height * scale);
725
726 CGFloat maxDimension = MAX(size.width, size.height);
727 if (maxDimension > 4096.0f)
728 {
729 CGFloat downscale = 4096.0f / maxDimension;
730 size.width = floorf(size.width * downscale);
731 size.height = floorf(size.height * downscale);
732 }
733
734 size.width = MAX(64.0f, size.width);
735 size.height = MAX(64.0f, size.height);
736 return size;
737}
738
739#pragma mark - Keyboard Toolbar Handlers
740
741- (void)showAdvancedKeyboardAnimated
742{
743 // already advanced keyboard activated
744 if (_advanced_keyboard_view)
745 return;
746
747 CGRect rect = CGRectMake(0, 0, [[self view] bounds].size.width, [self advancedKeyboardHeight]);
748 _advanced_keyboard_view = [[AdvancedKeyboardView alloc] initWithFrame:rect delegate:self];
749
750 [_dummy_textfield setInputView:_advanced_keyboard_view];
751 [_dummy_textfield reloadInputViews];
752 [_keyboard_toolbar setExtendedKeyboardActive:YES];
753}
754
755- (void)destroyAdvancedKeyboard
756{
757 // advanced keyboard not activated
758 if (!_advanced_keyboard_view)
759 return;
760
761 [_dummy_textfield setInputView:nil];
762 [_advanced_keyboard_view autorelease];
763 _advanced_keyboard_view = nil;
764 [_keyboard_toolbar setExtendedKeyboardActive:NO];
765}
766
767// toggle advanced keyboard (not keyboard)
768- (IBAction)toggleKeyboardWhenOtherVisible:(id)sender
769{
770 if (!_advanced_keyboard_view)
771 {
772 // show advanced keyboard
773 [self showAdvancedKeyboardAnimated];
774 }
775 else
776 {
777 // close advanced keyboard
778 [self destroyAdvancedKeyboard];
779 [_dummy_textfield reloadInputViews];
780 }
781}
782
783- (IBAction)toggleWinKey:(id)sender
784{
785 [[RDPKeyboard getSharedRDPKeyboard] toggleWinKey];
786}
787
788- (IBAction)toggleShiftKey:(id)sender
789{
790 [[RDPKeyboard getSharedRDPKeyboard] toggleShiftKey];
791}
792
793- (IBAction)toggleCtrlKey:(id)sender
794{
795 [[RDPKeyboard getSharedRDPKeyboard] toggleCtrlKey];
796}
797
798- (IBAction)toggleAltKey:(id)sender
799{
800 [[RDPKeyboard getSharedRDPKeyboard] toggleAltKey];
801}
802
803- (IBAction)toggleAltGrKey:(id)sender
804{
805 [[RDPKeyboard getSharedRDPKeyboard] toggleAltGrKey];
806}
807
808- (IBAction)pressEscKey:(id)sender
809{
810 [self commitTextInput];
811 [[RDPKeyboard getSharedRDPKeyboard] sendEscapeKeyStroke];
812}
813
814- (IBAction)pressDeleteKey:(id)sender
815{
816 [self commitTextInput];
817 [[RDPKeyboard getSharedRDPKeyboard] sendDeleteKeyStroke];
818}
819
820- (IBAction)pressTabKey:(id)sender
821{
822 [self commitTextInput];
823 [[RDPKeyboard getSharedRDPKeyboard] sendTabKeyStroke];
824}
825
826#pragma mark -
827#pragma mark event handlers
828
829- (IBAction)switchSession:(id)sender
830{
831 [self suspendSession];
832}
833
834- (IBAction)toggleKeyboard:(id)sender
835{
836 if (![self isKeyboardActive])
837 [_dummy_textfield becomeFirstResponder];
838 else
839 [_dummy_textfield resignFirstResponder];
840}
841
842- (IBAction)disconnectSession:(id)sender
843{
844 [_session disconnect];
845}
846
847- (IBAction)cancelButtonPressed:(id)sender
848{
849 [_session disconnect];
850}
851
852#pragma mark -
853#pragma mark iOS Keyboard Notification Handlers
854
855- (void)keyboardWillShow:(NSNotification *)notification
856{
857 // remember the system keyboard's height (to show advanced keyboard view)
858 if (!_advanced_keyboard_view)
859 {
860 CGRect frame =
861 [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
862 CGFloat height = frame.size.height - [_keyboard_toolbar bounds].size.height;
863
864 // 100.0 is heuristic value.
865 if (height >= 100.0)
866 {
867 if ([self isLandscape])
868 _keyboard_height_landscape = height;
869 else
870 _keyboard_height_portrait = height;
871 }
872 }
873
874 [self centerSessionViewInViewport];
875
876 // get covered area (keyboard)
877 CGRect keyboardFrame =
878 [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
879 keyboardFrame = [[self view] convertRect:keyboardFrame fromView:nil];
880 CGRect coveredArea = CGRectIntersection([[self view] bounds], keyboardFrame);
881
882 [self setKeyboardOverlap:CGRectGetHeight(coveredArea)];
883}
884
885- (void)keyboardWillHide:(NSNotification *)notification
886{
887 (void)notification;
888 [self centerSessionViewInViewport];
889 [self setKeyboardOverlap:0.0];
890}
891
892- (void)keyboardDidHide:(NSNotification *)notification
893{
894 // release adanced keyboard view
895 if (![self isKeyboardActive])
896 [self destroyAdvancedKeyboard];
897
898 // resume capturing the hardware keyboard once the on-screen keyboard is gone
899 if (![self isKeyboardActive] && [_session_view hardwareKeyboardActive])
900 [_session_view becomeFirstResponder];
901}
902
903- (void)hardwareKeyboardChanged:(NSNotification *)notification
904{
905 BOOL connected = (GCKeyboard.coalescedKeyboard != nil);
906 [_session_view setHardwareKeyboardActive:connected];
907
908 if (connected && ![self isKeyboardActive])
909 [_session_view becomeFirstResponder];
910 else if (!connected)
911 [_session_view resignFirstResponder];
912}
913
914#pragma mark -
915#pragma mark Gesture handlers
916
917- (void)handleSingleTap:(UITapGestureRecognizer *)gesture
918{
919 if (_pointer_is_indirect)
920 [self moveCursorToSessionViewPosition:[gesture locationInView:_session_view]];
921 [self sendMouseButtonEvent:GetLeftMouseButtonClickEvent(YES)];
922 [self sendMouseButtonEvent:GetLeftMouseButtonClickEvent(NO)];
923}
924
925- (void)handleSecondaryTap:(UITapGestureRecognizer *)gesture
926{
927 [self moveCursorToSessionViewPosition:[gesture locationInView:_session_view]];
928 [self sendMouseButtonEvent:GetRightMouseButtonClickEvent(YES)];
929 [self sendMouseButtonEvent:GetRightMouseButtonClickEvent(NO)];
930}
931
932- (void)handleLongPress:(UILongPressGestureRecognizer *)gesture
933{
934 if ([gesture state] == UIGestureRecognizerStateBegan)
935 {
936 _long_press_active = YES;
937 _mouse_drag_active = NO;
938 }
939 else if ([gesture state] == UIGestureRecognizerStateEnded ||
940 [gesture state] == UIGestureRecognizerStateCancelled ||
941 [gesture state] == UIGestureRecognizerStateFailed)
942 {
943 if (_mouse_drag_active)
944 [self sendMouseButtonEvent:GetLeftMouseButtonClickEvent(NO)];
945 else if ([gesture state] == UIGestureRecognizerStateEnded)
946 {
947 [self sendMouseButtonEvent:GetRightMouseButtonClickEvent(YES)];
948 [self sendMouseButtonEvent:GetRightMouseButtonClickEvent(NO)];
949 }
950
951 _mouse_drag_active = NO;
952 _long_press_active = NO;
953 }
954}
955
956- (void)handleMousePan:(UIPanGestureRecognizer *)gesture
957{
958 // A mouse/trackpad drag holds a button down, so move the cursor absolutely and
959 // keep the left button pressed for the duration of the drag.
960 if (_pointer_is_indirect)
961 {
962 [self moveCursorToSessionViewPosition:[gesture locationInView:_session_view]];
963
964 if ([gesture state] == UIGestureRecognizerStateBegan)
965 {
966 [self sendMouseButtonEvent:GetLeftMouseButtonClickEvent(YES)];
967 _mouse_drag_active = YES;
968 }
969 else if ([gesture state] == UIGestureRecognizerStateEnded ||
970 [gesture state] == UIGestureRecognizerStateCancelled ||
971 [gesture state] == UIGestureRecognizerStateFailed)
972 {
973 if (_mouse_drag_active)
974 [self sendMouseButtonEvent:GetLeftMouseButtonClickEvent(NO)];
975 _mouse_drag_active = NO;
976 }
977 return;
978 }
979
980 CGPoint location = [gesture locationInView:_session_scrollview];
981
982 if ([gesture state] == UIGestureRecognizerStateBegan)
983 {
984 _mouse_pan_active = YES;
985 _last_mouse_pan_location = location;
986 if (_long_press_active && !_mouse_drag_active)
987 {
988 [self sendMouseButtonEvent:GetLeftMouseButtonClickEvent(YES)];
989 _mouse_drag_active = YES;
990 }
991 }
992 else if ([gesture state] == UIGestureRecognizerStateChanged)
993 {
994 if (_long_press_active && !_mouse_drag_active)
995 {
996 [self sendMouseButtonEvent:GetLeftMouseButtonClickEvent(YES)];
997 _mouse_drag_active = YES;
998 }
999
1000 CGPoint delta = CGPointMake(location.x - _last_mouse_pan_location.x,
1001 location.y - _last_mouse_pan_location.y);
1002 [self moveCursorByViewportDelta:delta];
1003 _last_mouse_pan_location = location;
1004 }
1005 else if ([gesture state] == UIGestureRecognizerStateEnded ||
1006 [gesture state] == UIGestureRecognizerStateCancelled ||
1007 [gesture state] == UIGestureRecognizerStateFailed)
1008 {
1009 _mouse_pan_active = NO;
1010 }
1011}
1012
1013- (void)handleHover:(UIHoverGestureRecognizer *)gesture
1014{
1015 if ([gesture state] != UIGestureRecognizerStateBegan &&
1016 [gesture state] != UIGestureRecognizerStateChanged)
1017 return;
1018
1019 [self moveCursorToSessionViewPosition:[gesture locationInView:_session_view]];
1020}
1021
1022- (void)handleDoubleLongPress:(UILongPressGestureRecognizer *)gesture
1023{
1024 // this point is mapped against the scroll view because we want to have relative movement to the
1025 // screen/scrollview
1026 CGPoint pos = [gesture locationInView:_session_scrollview];
1027 CGPoint session_view_pos = [self currentCursorViewPosition];
1028
1029 if ([gesture state] == UIGestureRecognizerStateBegan)
1030 _prev_long_press_position = pos;
1031 else if ([gesture state] == UIGestureRecognizerStateChanged)
1032 {
1033 int delta = _prev_long_press_position.y - pos.y;
1034
1035 if (delta > GetScrollGestureDelta())
1036 {
1037 [_session sendInputEvent:[self eventDescriptorForMouseEvent:GetMouseWheelEvent(YES)
1038 position:session_view_pos]];
1039 _prev_long_press_position = pos;
1040 }
1041 else if (delta < -GetScrollGestureDelta())
1042 {
1043 [_session sendInputEvent:[self eventDescriptorForMouseEvent:GetMouseWheelEvent(NO)
1044 position:session_view_pos]];
1045 _prev_long_press_position = pos;
1046 }
1047 }
1048}
1049
1050- (void)handleScroll:(UIPanGestureRecognizer *)gesture
1051{
1052 CGFloat delta = [gesture translationInView:_session_view].y;
1053 if (fabs(delta) < GetScrollGestureDelta())
1054 return;
1055
1056 CGPoint position = [self currentCursorViewPosition];
1057 [_session sendInputEvent:[self eventDescriptorForMouseEvent:GetMouseWheelEvent(delta > 0)
1058 position:position]];
1059 [gesture setTranslation:CGPointZero inView:_session_view];
1060}
1061
1062- (void)handleSingle3FingersTap:(UITapGestureRecognizer *)gesture
1063{
1064 [_session setToolbarVisible:![_session toolbarVisible]];
1065 [self showSessionToolbar:[_session toolbarVisible]];
1066}
1067
1068- (UIPointerStyle *)pointerInteraction:(UIPointerInteraction *)interaction
1069 styleForRegion:(UIPointerRegion *)region
1070{
1071 return [UIPointerStyle hiddenPointerStyle];
1072}
1073
1074- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
1075 shouldReceiveTouch:(UITouch *)touch
1076{
1077 // the scroll recognizer only handles pointer scroll events, never touches
1078 if ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]] &&
1079 [(UIPanGestureRecognizer *)gestureRecognizer allowedScrollTypesMask] != 0)
1080 return NO;
1081
1082 _pointer_is_indirect = ([touch type] == UITouchTypeIndirectPointer);
1083
1084 // the long press maps to a right-click / drag for touch input, which a connected
1085 // mouse handles through its own buttons instead
1086 if (_pointer_is_indirect &&
1087 [gestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]])
1088 return NO;
1089
1090 return YES;
1091}
1092
1093- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer
1094 shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
1095{
1096 BOOL panAndHold =
1097 ([gestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]] &&
1098 [otherGestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]]) ||
1099 ([gestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]] &&
1100 [otherGestureRecognizer isKindOfClass:[UIPanGestureRecognizer class]]);
1101 return panAndHold;
1102}
1103
1104@end
1105
1106@implementation RDPSessionViewController (Private)
1107
1108#pragma mark -
1109#pragma mark Helper functions
1110
1111- (void)fitSessionViewToViewport
1112{
1113 CGSize viewportSize = [_session_scrollview bounds].size;
1114 CGSize remoteSize = [_session_view bounds].size;
1115 if (viewportSize.width <= 0.0f || viewportSize.height <= 0.0f || remoteSize.width <= 0.0f ||
1116 remoteSize.height <= 0.0f)
1117 return;
1118
1119 CGFloat fitScale =
1120 MIN(viewportSize.width / remoteSize.width, viewportSize.height / remoteSize.height);
1121 [_session_scrollview setMinimumZoomScale:fitScale];
1122 [_session_scrollview setMaximumZoomScale:MAX(2.0f, fitScale)];
1123 [_session_scrollview setZoomScale:fitScale animated:NO];
1124 [_session_scrollview setContentOffset:CGPointZero animated:NO];
1125 _last_session_viewport_size = viewportSize;
1126 [self centerSessionViewInViewport];
1127}
1128
1129- (void)centerSessionViewInViewport
1130{
1131 CGSize viewportSize = [_session_scrollview bounds].size;
1132 CGRect sessionFrame = [_session_view frame];
1133 if (viewportSize.width <= 0.0f || viewportSize.height <= 0.0f ||
1134 sessionFrame.size.width <= 0.0f || sessionFrame.size.height <= 0.0f)
1135 return;
1136
1137 sessionFrame.origin.x = MAX((viewportSize.width - sessionFrame.size.width) * 0.5f, 0.0f);
1138 sessionFrame.origin.y = MAX((viewportSize.height - sessionFrame.size.height) * 0.5f, 0.0f);
1139 [_session_view setFrame:sessionFrame];
1140
1141 [_session_scrollview
1142 setContentSize:CGSizeMake(MAX(viewportSize.width, sessionFrame.size.width),
1143 MAX(viewportSize.height, sessionFrame.size.height))];
1144}
1145
1146- (void)setKeyboardOverlap:(CGFloat)overlap
1147{
1148 // extend the scrollable area to push up using 2 finger
1149 UIEdgeInsets inset = [_session_scrollview contentInset];
1150 inset.bottom = overlap;
1151 [_session_scrollview setContentInset:inset];
1152
1153 CGPoint offset = [_session_scrollview contentOffset];
1154 CGFloat viewportHeight = [_session_scrollview bounds].size.height;
1155
1156 // cursor is usually located in typing area
1157 if (overlap > 0.0 && _has_cursor_view_position)
1158 {
1159 CGPoint cursor = [_session_scrollview convertPoint:_cursor_view_position
1160 fromView:_session_view];
1161 CGFloat visibleBottom = offset.y + viewportHeight - overlap;
1162 CGFloat hiddenHeight = cursor.y + KEYBOARD_CURSOR_MARGIN - visibleBottom;
1163 if (hiddenHeight > 0.0)
1164 offset.y += hiddenHeight;
1165 }
1166
1167 // stay within the scrollable range or scrolls back once the keyboard hides
1168 CGFloat maxOffsetY = [_session_scrollview contentSize].height + overlap - viewportHeight;
1169 offset.y = MAX(MIN(offset.y, maxOffsetY), 0.0);
1170 [_session_scrollview setContentOffset:offset animated:YES];
1171}
1172
1173- (void)showSessionToolbar:(BOOL)show
1174{
1175 // already shown or hidden?
1176 if (_session_toolbar_visible == show)
1177 return;
1178
1179 // Offset by the safe-area insets so the toolbar/buttons are not hidden behind
1180 // the status bar / notch / Dynamic Island on modern devices (and side notch
1181 // in landscape).
1182 UIEdgeInsets safe = [[self view] safeAreaInsets];
1183 // devices without a notch (e.g. iPad) have no top inset once the status bar is hidden,
1184 // so keep at least the system margin to the top edge
1185 safe.top = MAX(safe.top, [self systemMinimumLayoutMargins].leading);
1186 CGFloat toolbarWidth = [[self view] bounds].size.width - safe.left - safe.right;
1187
1188 if (show)
1189 {
1190 [UIView animateWithDuration:0.4
1191 delay:0.0
1192 options:UIViewAnimationOptionCurveLinear
1193 animations:^{
1194 [_session_toolbar setFrame:CGRectMake(safe.left, safe.top,
1195 toolbarWidth, TOOLBAR_HEIGHT)];
1196 }
1197 completion:nil];
1198 _session_toolbar_visible = YES;
1199 }
1200 else
1201 {
1202 [UIView animateWithDuration:0.4
1203 delay:0.0
1204 options:UIViewAnimationOptionCurveLinear
1205 animations:^{
1206 [_session_toolbar setFrame:CGRectMake(safe.left, -TOOLBAR_HEIGHT,
1207 toolbarWidth, TOOLBAR_HEIGHT)];
1208 }
1209 completion:nil];
1210 _session_toolbar_visible = NO;
1211 }
1212}
1213
1214- (void)initGestureRecognizers
1215{
1216 // single tap recognizer. A double-click is just two quick taps, which the server
1217 // coalesces on its own, so we avoid the latency of waiting for a double-tap to fail.
1218 UITapGestureRecognizer *singleTapRecognizer =
1219 [[[UITapGestureRecognizer alloc] initWithTarget:self
1220 action:@selector(handleSingleTap:)] autorelease];
1221 [singleTapRecognizer setNumberOfTouchesRequired:1];
1222 [singleTapRecognizer setNumberOfTapsRequired:1];
1223 [singleTapRecognizer setDelegate:self];
1224
1225 // long press gesture recognizer
1226 UILongPressGestureRecognizer *longPressRecognizer = [[[UILongPressGestureRecognizer alloc]
1227 initWithTarget:self
1228 action:@selector(handleLongPress:)] autorelease];
1229 [longPressRecognizer setMinimumPressDuration:0.3];
1230 [longPressRecognizer setAllowableMovement:12.0];
1231 [longPressRecognizer setDelegate:self];
1232
1233 // One-finger movement behaves like a trackpad and moves the remote cursor relatively.
1234 UIPanGestureRecognizer *mousePanRecognizer =
1235 [[[UIPanGestureRecognizer alloc] initWithTarget:self
1236 action:@selector(handleMousePan:)] autorelease];
1237 [mousePanRecognizer setMinimumNumberOfTouches:1];
1238 [mousePanRecognizer setMaximumNumberOfTouches:1];
1239 [mousePanRecognizer setDelegate:self];
1240
1241 // A connected mouse/trackpad moves the remote cursor absolutely while hovering.
1242 UIHoverGestureRecognizer *hoverRecognizer =
1243 [[[UIHoverGestureRecognizer alloc] initWithTarget:self
1244 action:@selector(handleHover:)] autorelease];
1245 [hoverRecognizer setDelegate:self];
1246
1247 // Secondary mouse/trackpad button maps to a right-click. Standard tap recognizers
1248 // only track the primary button, so this needs its own recognizer.
1249 UITapGestureRecognizer *secondaryTapRecognizer = [[[UITapGestureRecognizer alloc]
1250 initWithTarget:self
1251 action:@selector(handleSecondaryTap:)] autorelease];
1252 [secondaryTapRecognizer setButtonMaskRequired:UIEventButtonMaskSecondary];
1253 [secondaryTapRecognizer setAllowedTouchTypes:@[@(UITouchTypeIndirectPointer)]];
1254 [secondaryTapRecognizer setDelegate:self];
1255
1256 // Mouse wheel and trackpad scrolling are forwarded as remote wheel events. Setting a
1257 // scroll type mask lets this pan recognizer receive pointer scroll device events.
1258 UIPanGestureRecognizer *scrollRecognizer =
1259 [[[UIPanGestureRecognizer alloc] initWithTarget:self
1260 action:@selector(handleScroll:)] autorelease];
1261 [scrollRecognizer setAllowedScrollTypesMask:UIScrollTypeMaskAll];
1262 [scrollRecognizer setDelegate:self];
1263
1264 // double long press gesture recognizer
1265 UILongPressGestureRecognizer *doubleLongPressRecognizer = [[[UILongPressGestureRecognizer alloc]
1266 initWithTarget:self
1267 action:@selector(handleDoubleLongPress:)] autorelease];
1268 [doubleLongPressRecognizer setNumberOfTouchesRequired:2];
1269 [doubleLongPressRecognizer setMinimumPressDuration:0.3];
1270
1271 // 3 finger, single tap gesture for showing/hiding the toolbar
1272 UITapGestureRecognizer *single3FingersTapRecognizer = [[[UITapGestureRecognizer alloc]
1273 initWithTarget:self
1274 action:@selector(handleSingle3FingersTap:)] autorelease];
1275 [single3FingersTapRecognizer setNumberOfTapsRequired:1];
1276 [single3FingersTapRecognizer setNumberOfTouchesRequired:3];
1277 [singleTapRecognizer requireGestureRecognizerToFail:longPressRecognizer];
1278
1279 // Reserve one finger for the mouse; two fingers still pan/zoom the viewport.
1280 [[_session_scrollview panGestureRecognizer] setMinimumNumberOfTouches:2];
1281
1282 // add gestures to scroll view
1283 [_session_scrollview addGestureRecognizer:singleTapRecognizer];
1284 [_session_scrollview addGestureRecognizer:longPressRecognizer];
1285 [_session_scrollview addGestureRecognizer:mousePanRecognizer];
1286 [_session_scrollview addGestureRecognizer:hoverRecognizer];
1287 [_session_scrollview addGestureRecognizer:secondaryTapRecognizer];
1288 [_session_scrollview addGestureRecognizer:scrollRecognizer];
1289 [_session_scrollview addGestureRecognizer:doubleLongPressRecognizer];
1290 [_session_scrollview addGestureRecognizer:single3FingersTapRecognizer];
1291
1292 // Hide the system pointer over the session so only the remote cursor is visible.
1293 UIPointerInteraction *pointerInteraction =
1294 [[[UIPointerInteraction alloc] initWithDelegate:self] autorelease];
1295 [_session_view addInteraction:pointerInteraction];
1296}
1297
1298- (void)suspendSession
1299{
1300 // suspend session and pop navigation controller
1301 [_session suspend];
1302
1303 // pop current view controller
1304 [[self navigationController] popViewControllerAnimated:YES];
1305}
1306
1307- (NSDictionary *)eventDescriptorForMouseEvent:(int)event position:(CGPoint)position
1308{
1309 CGPoint remote_position = [self remotePositionForSessionViewPosition:position];
1310 return [NSDictionary
1311 dictionaryWithObjectsAndKeys:@"mouse", @"type", [NSNumber numberWithUnsignedShort:event],
1312 @"flags",
1313 [NSNumber numberWithUnsignedShort:lrintf(remote_position.x)],
1314 @"coord_x",
1315 [NSNumber numberWithUnsignedShort:lrintf(remote_position.y)],
1316 @"coord_y", nil];
1317}
1318
1319- (CGPoint)remotePositionForSessionViewPosition:(CGPoint)position
1320{
1321 rdpSettings *settings = [_session getSessionParams];
1322 CGSize viewSize = [_session_view bounds].size;
1323 CGFloat desktopWidth =
1324 settings ? freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth) : 0;
1325 CGFloat desktopHeight =
1326 settings ? freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight) : 0;
1327
1328 if ((viewSize.width > 0.0f) && (desktopWidth > 0.0f))
1329 position.x = position.x * desktopWidth / viewSize.width;
1330 if ((viewSize.height > 0.0f) && (desktopHeight > 0.0f))
1331 position.y = position.y * desktopHeight / viewSize.height;
1332
1333 if (desktopWidth > 0.0f)
1334 position.x = MIN(MAX(position.x, 0.0f), desktopWidth - 1.0f);
1335 if (desktopHeight > 0.0f)
1336 position.y = MIN(MAX(position.y, 0.0f), desktopHeight - 1.0f);
1337
1338 return position;
1339}
1340
1341- (CGPoint)sessionViewPositionForRemotePosition:(CGPoint)position
1342{
1343 rdpSettings *settings = [_session getSessionParams];
1344 CGSize viewSize = [_session_view bounds].size;
1345 CGFloat desktopWidth =
1346 settings ? freerdp_settings_get_uint32(settings, FreeRDP_DesktopWidth) : 0;
1347 CGFloat desktopHeight =
1348 settings ? freerdp_settings_get_uint32(settings, FreeRDP_DesktopHeight) : 0;
1349
1350 if ((desktopWidth > 0.0f) && (viewSize.width > 0.0f))
1351 position.x = position.x * viewSize.width / desktopWidth;
1352 if ((desktopHeight > 0.0f) && (viewSize.height > 0.0f))
1353 position.y = position.y * viewSize.height / desktopHeight;
1354
1355 return [self clampedSessionViewCursorPosition:position];
1356}
1357
1358- (CGPoint)clampedSessionViewCursorPosition:(CGPoint)position
1359{
1360 CGSize viewSize = [_session_view bounds].size;
1361 if (viewSize.width > 0.0f)
1362 position.x = MIN(MAX(position.x, 0.0f), viewSize.width - 1.0f);
1363 if (viewSize.height > 0.0f)
1364 position.y = MIN(MAX(position.y, 0.0f), viewSize.height - 1.0f);
1365 return position;
1366}
1367
1368- (CGPoint)currentCursorViewPosition
1369{
1370 if (!_has_cursor_view_position)
1371 {
1372 CGSize viewSize = [_session_view bounds].size;
1373 _cursor_view_position = CGPointMake(MAX(viewSize.width - 1.0, 0.0) * 0.5,
1374 MAX(viewSize.height - 1.0, 0.0) * 0.5);
1375 _has_cursor_view_position = YES;
1376 [_session_view setRemoteCursorPosition:_cursor_view_position];
1377 }
1378 [_session_view showRemoteCursor];
1379
1380 return _cursor_view_position;
1381}
1382
1383- (void)moveCursorByViewportDelta:(CGPoint)delta
1384{
1385 CGPoint position = [self currentCursorViewPosition];
1386 CGFloat zoomScale = [_session_scrollview zoomScale];
1387 if (zoomScale <= 0.0)
1388 zoomScale = 1.0;
1389
1390 position.x += delta.x / zoomScale;
1391 position.y += delta.y / zoomScale;
1392 position = [self clampedSessionViewCursorPosition:position];
1393 _cursor_view_position = position;
1394 _has_cursor_view_position = YES;
1395 _has_user_moved_cursor = YES;
1396
1397 // Local prediction keeps the cursor responsive while the RDP event is in flight.
1398 [_session_view setRemoteCursorPosition:position];
1399 [self handleMouseMoveForPosition:position];
1400}
1401
1402- (void)moveCursorToSessionViewPosition:(CGPoint)position
1403{
1404 position = [self clampedSessionViewCursorPosition:position];
1405 _cursor_view_position = position;
1406 _has_cursor_view_position = YES;
1407 _has_user_moved_cursor = YES;
1408
1409 // Local prediction keeps the cursor responsive while the RDP event is in flight.
1410 [_session_view setRemoteCursorPosition:position];
1411 [_session_view showRemoteCursor];
1412 [self handleMouseMoveForPosition:position];
1413}
1414
1415- (void)sendMouseButtonEvent:(int)event
1416{
1417 // typed text can not be changed anymore.
1418 [self commitTextInput];
1419 CGPoint position = [self currentCursorViewPosition];
1420 [_session sendInputEvent:[self eventDescriptorForMouseEvent:event position:position]];
1421}
1422
1423- (void)handleMouseMoveForPosition:(CGPoint)position
1424{
1425 [_session sendInputEvent:[self eventDescriptorForMouseEvent:PTR_FLAGS_MOVE position:position]];
1426}
1427
1428- (BOOL)isKeyboardActive
1429{
1430 return [_dummy_textfield isFirstResponder];
1431}
1432
1433- (void)resetTextInput
1434{
1435 [_dummy_textfield setText:@""];
1436 [_sent_text release];
1437 _sent_text = [@"" retain];
1438}
1439
1440- (void)commitTextInput
1441{
1442 if ([[_dummy_textfield text] length] == 0)
1443 return;
1444
1445 // send the text being composed as it is
1446 [_dummy_textfield unmarkText];
1447 [self textInputChanged:_dummy_textfield];
1448 [self resetTextInput];
1449}
1450
1451- (BOOL)isLandscape
1452{
1453 CGSize size = [[self view] bounds].size;
1454 return size.width > size.height;
1455}
1456
1457- (CGFloat)advancedKeyboardHeight
1458{
1459 // before the system keyboard was shown in this orientation, use a typical height
1460 if ([self isLandscape])
1461 return (_keyboard_height_landscape > 0.0) ? _keyboard_height_landscape
1462 : (IsPad() ? 400.0 : 200.0);
1463 return (_keyboard_height_portrait > 0.0) ? _keyboard_height_portrait
1464 : (IsPad() ? 320.0 : 290.0);
1465}
1466
1467@end
WINPR_ATTR_NODISCARD FREERDP_API UINT32 freerdp_settings_get_uint32(const rdpSettings *settings, FreeRDP_Settings_Keys_UInt32 id)
Returns a UINT32 settings value.