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