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