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