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