iOS平台FreeRDP中文直接输入支持
FreeRDP在iOS平台不支持中文输入,本人提交了一个中文支持的patch,官方已经合并了提交的代码。
1.官方对这一问题的处理状态。
查看地址:https://github.com/FreeRDP/FreeRDP/pull/1211
当前状态截图:
2.代码下载
现在可用的代码官方还没有公开,不过可以在我的代码分支上去下载源代码。
下载地址:https://github.com/zhanleewo
3.缺陷和Bug
但是当前我所做出的修改有如下几个缺陷:
1). 只能处理中文宽字符,不能处理其他语言的宽字符。
2). 当切换到中文键盘(非手写中文键盘)时,只能输入中文,以及中文标点符号,无法中文和英文混杂输入。
3). 切换到手写中文键盘时,只能输入中文,以及中文标点符号,无法中文和英文混杂输入。
4.修改的地方:
1).文件RDPSessionViewController.h, 63行,添加变量声明:BOOL _keyboard_has_display;
2).文件RDPSessionViewController.m,57行,添加变量初始化:_keyboard_has_display = NO;
3).文件RDPSessionViewController.m,625行,添加变量设置:_keyboard_has_display = YES;
4).文件RDPSessionViewController.m,625行,添加变量设置:_keyboard_has_display = NO;
5).文件RDPSessionViewController.m,227-257行,添加内容:
1 - (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string 2 { 3 if(string == nil || [string length] == 0) { // key touched is backspace 4 if(range.length > 0 && textField.text.length <= [@"UserInput" length]) { 5 [[RDPKeyboard getSharedRDPKeyboard] sendBackspaceKeyStroke]; 6 return NO; 7 } 8 return YES; 9 } 10 11 if([[UITextInputMode currentInputMode].primaryLanguage isEqualToString:@"zh-Hans"] 12 && (([string characterAtIndex:0] >= 'a' && [string characterAtIndex:0] <= 'z') 13 || ([string characterAtIndex:0] >= 'A' && [string characterAtIndex:0] <= 'Z'))) { 14 for(int i = 0 ; i < [string length] ; i++) { 15 unichar curChar = [string characterAtIndex:i]; 16 if(curChar == '\n') 17 [[RDPKeyboard getSharedRDPKeyboard] sendEnterKeyStroke]; 18 } 19 return YES; 20 } 21 22 for(int i = 0 ; i < [string length] ; i++) { 23 unichar curChar = [string characterAtIndex:i]; 24 if(curChar == '\n') 25 [[RDPKeyboard getSharedRDPKeyboard] sendEnterKeyStroke]; 26 else 27 [[RDPKeyboard getSharedRDPKeyboard] sendUnicode:curChar]; 28 } 29 textField.text = @"UserInput"; 30 return NO; 31 }
5.被修改之后的源文件:
1).RDPSessionViewController.h
1 /* 2 RDP Session View Controller 3 Copyright 2013 Thinstuff Technologies GmbH, Author: Martin Fleisz 4 This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. 5 If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 */ 7 8 #import <UIKit/UIKit.h> 9 #import "RDPSession.h" 10 #import "RDPKeyboard.h" 11 #import "RDPSessionView.h" 12 #import "TouchPointerView.h" 13 #import "AdvancedKeyboardView.h" 14 15 @interface RDPSessionViewController : UIViewController <RDPSessionDelegate, TouchPointerDelegate, AdvancedKeyboardDelegate, RDPKeyboardDelegate, UIScrollViewDelegate, UITextFieldDelegate, UIAlertViewDelegate> 16 { 17 // scrollview that hosts the rdp session view 18 IBOutlet UIScrollView* _session_scrollview; 19 20 // rdp session view 21 IBOutlet RDPSessionView* _session_view; 22 23 // touch pointer view 24 IBOutlet TouchPointerView* _touchpointer_view; 25 BOOL _autoscroll_with_touchpointer; 26 BOOL _is_autoscrolling; 27 28 // rdp session toolbar 29 IBOutlet UIToolbar* _session_toolbar; 30 BOOL _session_toolbar_visible; 31 32 // dummy text field used to display the keyboard 33 IBOutlet UITextField* _dummy_textfield; 34 35 // connecting view and the controls within that view 36 IBOutlet UIView* _connecting_view; 37 IBOutlet UILabel* _lbl_connecting; 38 IBOutlet UIActivityIndicatorView* _connecting_indicator_view; 39 IBOutlet UIButton* _cancel_connect_button; 40 41 // extended keyboard toolbar 42 UIToolbar* _keyboard_toolbar; 43 44 // rdp session 45 RDPSession* _session; 46 BOOL _session_initilized; 47 48 // flag that indicates whether the keyboard is visible or not 49 BOOL _keyboard_visible; 50 51 // flag to switch between left/right mouse button mode 52 BOOL _toggle_mouse_button; 53 54 // keyboard extension view 55 AdvancedKeyboardView* _advanced_keyboard_view; 56 BOOL _advanced_keyboard_visible; 57 BOOL _requesting_advanced_keyboard; 58 59 60 // flag that indicates whether the keyboard is visible or not 61 BOOL _keyboard_has_display; 62 63 // delayed mouse move event sending 64 NSTimer* _mouse_move_event_timer; 65 int _mouse_move_events_skipped; 66 CGPoint _prev_long_press_position; 67 } 68 69 - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil session:(RDPSession*)session; 70 71 @end
2).RDPSessionViewController.m
1 /* 2 RDP Session View Controller 3 Copyright 2013 Thinstuff Technologies GmbH, Author: Martin Fleisz 4 This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. 5 If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. 6 */ 7 8 #import <QuartzCore/QuartzCore.h> 9 #import "RDPSessionViewController.h" 10 #import "RDPKeyboard.h" 11 #import "Utils.h" 12 #import "Toast+UIView.h" 13 #import "ConnectionParams.h" 14 #import "CredentialsInputController.h" 15 #import "VerifyCertificateController.h" 16 17 #define TOOLBAR_HEIGHT 30 18 19 #define AUTOSCROLLDISTANCE 20 20 #define AUTOSCROLLTIMEOUT 0.05 21 @interface RDPSessionViewController (Private) 22 -(void)showSessionToolbar:(BOOL)show; 23 -(UIToolbar*)keyboardToolbar; 24 -(void)initGestureRecognizers; 25 - (void)suspendSession; 26 - (NSDictionary*)eventDescriptorForMouseEvent:(int)event position:(CGPoint)position; 27 - (void)handleMouseMoveForPosition:(CGPoint)position; 28 @end 29 30 31 @implementation RDPSessionViewController 32 33 #pragma mark class methods 34 35 - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil session:(RDPSession *)session 36 { 37 self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; 38 if (self) 39 { 40 _session = [session retain]; 41 [_session setDelegate:self]; 42 _session_initilized = NO; 43 44 _mouse_move_events_skipped = 0; 45 _mouse_move_event_timer = nil; 46 47 _advanced_keyboard_view = nil; 48 _advanced_keyboard_visible = NO; 49 _requesting_advanced_keyboard = NO; 50 51 _session_toolbar_visible = NO; 52 53 _toggle_mouse_button = NO; 54 55 _keyboard_has_display = NO; 56 57 _autoscroll_with_touchpointer = [[NSUserDefaults standardUserDefaults] boolForKey:@"ui.auto_scroll_touchpointer"]; 58 _is_autoscrolling = NO; 59 [UIView setAnimationDelegate:self]; 60 [UIView setAnimationDidStopSelector:@selector(animationStopped:finished:context:)]; 61 } 62 63 return self; 64 } 65 66 // Implement loadView to create a view hierarchy programmatically, without using a nib. 67 - (void)loadView 68 { 69 // load default view and set background color and resizing mask 70 [super loadView]; 71 72 // init keyboard handling vars and register required notification handlers 73 _keyboard_visible = NO; 74 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillShow:) name: UIKeyboardWillShowNotification object:nil]; 75 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidShow:) name: UIKeyboardDidShowNotification object:nil]; 76 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name: UIKeyboardWillHideNotification object:nil]; 77 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidHide:) name: UIKeyboardDidHideNotification object:nil]; 78 79 // init keyboard toolbar 80 _keyboard_toolbar = [[self keyboardToolbar] retain]; 81 [_dummy_textfield setInputAccessoryView:_keyboard_toolbar]; 82 83 // init gesture recognizers 84 [self initGestureRecognizers]; 85 86 // hide session toolbar 87 [_session_toolbar setFrame:CGRectMake(0.0, -TOOLBAR_HEIGHT, [[self view] bounds].size.width, TOOLBAR_HEIGHT)]; 88 } 89 90 91 // Implement viewDidLoad to do additional setup after loading the view, typically from a nib. 92 - (void)viewDidLoad 93 { 94 [super viewDidLoad]; 95 } 96 97 98 - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation { 99 return YES; 100 } 101 102 - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation 103 { 104 if (![_touchpointer_view isHidden]) 105 [_touchpointer_view ensurePointerIsVisible]; 106 } 107 108 - (void)didReceiveMemoryWarning { 109 // Releases the view if it doesn't have a superview. 110 [super didReceiveMemoryWarning]; 111 112 // Release any cached data, images, etc. that aren't in use. 113 } 114 115 - (void)viewDidUnload { 116 [super viewDidUnload]; 117 // Release any retained subviews of the main view. 118 // e.g. self.myOutlet = nil; 119 } 120 121 - (void)viewWillAppear:(BOOL)animated 122 { 123 [super viewWillAppear:animated]; 124 125 // hide navigation bar and (if enabled) the status bar 126 if ([[NSUserDefaults standardUserDefaults] boolForKey:@"ui.hide_status_bar"]) 127 { 128 if(animated == YES) 129 [[UIApplication sharedApplication] setStatusBarHidden:YES withAnimation:UIStatusBarAnimationSlide]; 130 else 131 [[UIApplication sharedApplication] setStatusBarHidden:YES withAnimation:UIStatusBarAnimationNone]; 132 } 133 [[self navigationController] setNavigationBarHidden:YES animated:animated]; 134 135 // if sesssion is suspended - notify that we got a new bitmap context 136 if ([_session isSuspended]) 137 [self sessionBitmapContextWillChange:_session]; 138 139 // init keyboard 140 [[RDPKeyboard getSharedRDPKeyboard] initWithSession:_session delegate:self]; 141 } 142 143 - (void)viewDidAppear:(BOOL)animated 144 { 145 [super viewDidAppear:animated]; 146 147 if (!_session_initilized) 148 { 149 if ([_session isSuspended]) 150 { 151 [_session resume]; 152 [self sessionBitmapContextDidChange:_session]; 153 [_session_view setNeedsDisplay]; 154 } 155 else 156 [_session connect]; 157 158 _session_initilized = YES; 159 } 160 } 161 162 - (void)viewWillDisappear:(BOOL)animated 163 { 164 [super viewWillDisappear:animated]; 165 166 // show navigation and status bar again 167 if(animated == YES) 168 [[UIApplication sharedApplication] setStatusBarHidden:NO withAnimation:UIStatusBarAnimationSlide]; 169 else 170 [[UIApplication sharedApplication] setStatusBarHidden:NO withAnimation:UIStatusBarAnimationNone]; 171 [[self navigationController] setNavigationBarHidden:NO animated:animated]; 172 173 // reset all modifier keys on rdp keyboard 174 [[RDPKeyboard getSharedRDPKeyboard] reset]; 175 176 // hide toolbar and keyboard 177 [self showSessionToolbar:NO]; 178 [_dummy_textfield resignFirstResponder]; 179 } 180 181 182 - (void)dealloc { 183 // remove any observers 184 [[NSNotificationCenter defaultCenter] removeObserver:self]; 185 186 // the session lives on longer so set the delegate to nil 187 [_session setDelegate:nil]; 188 189 [_advanced_keyboard_view release]; 190 [_keyboard_toolbar release]; 191 [_session release]; 192 [super dealloc]; 193 } 194 195 #pragma mark - 196 #pragma mark ScrollView delegate methods 197 198 - (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView 199 { 200 return _session_view; 201 } 202 203 -(void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(float)scale 204 { 205 NSLog(@"New zoom scale: %f", scale); 206 [_session_view setNeedsDisplay]; 207 } 208 209 #pragma mark - 210 #pragma mark TextField delegate methods 211 -(BOOL)textFieldShouldBeginEditing:(UITextField *)textField 212 { 213 _keyboard_visible = YES; 214 _advanced_keyboard_visible = NO; 215 return YES; 216 } 217 218 -(BOOL)textFieldShouldEndEditing:(UITextField *)textField 219 { 220 _keyboard_visible = NO; 221 _advanced_keyboard_visible = NO; 222 return YES; 223 } 224 225 - (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string 226 { 227 if(string == nil || [string length] == 0) { // key touched is backspace 228 if(range.length > 0 && textField.text.length <= [@"UserInput" length]) { 229 [[RDPKeyboard getSharedRDPKeyboard] sendBackspaceKeyStroke]; 230 return NO; 231 } 232 return YES; 233 } 234 235 if([[UITextInputMode currentInputMode].primaryLanguage isEqualToString:@"zh-Hans"] 236 && (([string characterAtIndex:0] >= 'a' && [string characterAtIndex:0] <= 'z') 237 || ([string characterAtIndex:0] >= 'A' && [string characterAtIndex:0] <= 'Z'))) { 238 for(int i = 0 ; i < [string length] ; i++) { 239 unichar curChar = [string characterAtIndex:i]; 240 if(curChar == '\n') 241 [[RDPKeyboard getSharedRDPKeyboard] sendEnterKeyStroke]; 242 } 243 return YES; 244 } 245 246 for(int i = 0 ; i < [string length] ; i++) { 247 unichar curChar = [string characterAtIndex:i]; 248 if(curChar == '\n') 249 [[RDPKeyboard getSharedRDPKeyboard] sendEnterKeyStroke]; 250 else 251 [[RDPKeyboard getSharedRDPKeyboard] sendUnicode:curChar]; 252 } 253 textField.text = @"UserInput"; 254 return NO; 255 } 256 257 258 #pragma mark - 259 #pragma mark AdvancedKeyboardDelegate functions 260 -(void)advancedKeyPressedVKey:(int)key 261 { 262 [[RDPKeyboard getSharedRDPKeyboard] sendVirtualKeyCode:key]; 263 } 264 265 -(void)advancedKeyPressedUnicode:(int)key 266 { 267 [[RDPKeyboard getSharedRDPKeyboard] sendUnicode:key]; 268 } 269 270 #pragma mark - RDP keyboard handler 271 272 - (void)modifiersChangedForKeyboard:(RDPKeyboard *)keyboard 273 { 274 UIBarButtonItem* curItem; 275 276 // shift button (only on iPad) 277 int objectIdx = 0; 278 if (IsPad()) 279 { 280 objectIdx = 2; 281 curItem = (UIBarButtonItem*)[[_keyboard_toolbar items] objectAtIndex:objectIdx]; 282 [curItem setStyle:[keyboard shiftPressed] ? UIBarButtonItemStyleDone : UIBarButtonItemStyleBordered]; 283 } 284 285 // ctrl button 286 objectIdx += 2; 287 curItem = (UIBarButtonItem*)[[_keyboard_toolbar items] objectAtIndex:objectIdx]; 288 [curItem setStyle:[keyboard ctrlPressed] ? UIBarButtonItemStyleDone : UIBarButtonItemStyleBordered]; 289 290 // win button 291 objectIdx += 2; 292 curItem = (UIBarButtonItem*)[[_keyboard_toolbar items] objectAtIndex:objectIdx]; 293 [curItem setStyle:[keyboard winPressed] ? UIBarButtonItemStyleDone : UIBarButtonItemStyleBordered]; 294 295 // alt button 296 objectIdx += 2; 297 curItem = (UIBarButtonItem*)[[_keyboard_toolbar items] objectAtIndex:objectIdx]; 298 [curItem setStyle:[keyboard altPressed] ? UIBarButtonItemStyleDone : UIBarButtonItemStyleBordered]; 299 } 300 301 #pragma mark - 302 #pragma mark RDPSessionDelegate functions 303 304 - (void)session:(RDPSession*)session didFailToConnect:(int)reason 305 { 306 // remove and release connecting view 307 [_connecting_indicator_view stopAnimating]; 308 [_connecting_view removeFromSuperview]; 309 [_connecting_view autorelease]; 310 311 // return to bookmark list 312 [[self navigationController] popViewControllerAnimated:YES]; 313 } 314 315 - (void)sessionWillConnect:(RDPSession*)session 316 { 317 // load connecting view 318 [[NSBundle mainBundle] loadNibNamed:@"RDPConnectingView" owner:self options:nil]; 319 320 // set strings 321 [_lbl_connecting setText:NSLocalizedString(@"Connecting", @"Connecting progress view - label")]; 322 [_cancel_connect_button setTitle:NSLocalizedString(@"Cancel", @"Cancel Button") forState:UIControlStateNormal]; 323 324 // center view and give it round corners 325 [_connecting_view setCenter:[[self view] center]]; 326 [[_connecting_view layer] setCornerRadius:10]; 327 328 // display connecting view and start indicator 329 [[self view] addSubview:_connecting_view]; 330 [_connecting_indicator_view startAnimating]; 331 } 332 333 - (void)sessionDidConnect:(RDPSession*)session 334 { 335 // remove and release connecting view 336 [_connecting_indicator_view stopAnimating]; 337 [_connecting_view removeFromSuperview]; 338 [_connecting_view autorelease]; 339 340 // check if session settings changed ... 341 // The 2nd width check is to ignore changes in resolution settings due to the RDVH display bug (refer to RDPSEssion.m for more details) 342 ConnectionParams* orig_params = [session params]; 343 rdpSettings* sess_params = [session getSessionParams]; 344 if (([orig_params intForKey:@"width"] != sess_params->DesktopWidth && [orig_params intForKey:@"width"] != (sess_params->DesktopWidth + 1)) || 345 [orig_params intForKey:@"height"] != sess_params->DesktopHeight || [orig_params intForKey:@"colors"] != sess_params->ColorDepth) 346 { 347 // display notification that the session params have been changed by the server 348 NSString* message = [NSString stringWithFormat:NSLocalizedString(@"The server changed the screen settings to %dx%dx%d", @"Screen settings not supported message with width, height and colors parameter"), sess_params->DesktopWidth, sess_params->DesktopHeight, sess_params->ColorDepth]; 349 [[self view] makeToast:message duration:ToastDurationNormal position:@"bottom"]; 350 } 351 } 352 353 - (void)sessionWillDisconnect:(RDPSession*)session 354 { 355 356 } 357 358 - (void)sessionDidDisconnect:(RDPSession*)session 359 { 360 // return to bookmark list 361 [[self navigationController] popViewControllerAnimated:YES]; 362 } 363 364 - (void)sessionBitmapContextWillChange:(RDPSession*)session 365 { 366 // calc new view frame 367 rdpSettings* sess_params = [session getSessionParams]; 368 CGRect view_rect = CGRectMake(0, 0, sess_params->DesktopWidth, sess_params->DesktopHeight); 369 370 // reset zoom level and update content size 371 [_session_scrollview setZoomScale:1.0]; 372 [_session_scrollview setContentSize:view_rect.size]; 373 374 // set session view size 375 [_session_view setFrame:view_rect]; 376 377 // show/hide toolbar 378 [_session setToolbarVisible:![[NSUserDefaults standardUserDefaults] boolForKey:@"ui.hide_tool_bar"]]; 379 [self showSessionToolbar:[_session toolbarVisible]]; 380 } 381 382 - (void)sessionBitmapContextDidChange:(RDPSession*)session 383 { 384 // associate view with session 385 [_session_view setSession:session]; 386 387 // issue an update (this might be needed in case we had a resize for instance) 388 [_session_view setNeedsDisplay]; 389 } 390 391 - (void)session:(RDPSession*)session needsRedrawInRect:(CGRect)rect 392 { 393 [_session_view setNeedsDisplayInRect:rect]; 394 } 395 396 - (void)session:(RDPSession *)session requestsAuthenticationWithParams:(NSMutableDictionary *)params 397 { 398 CredentialsInputController* view_controller = [[[CredentialsInputController alloc] initWithNibName:@"CredentialsInputView" bundle:nil session:_session params:params] autorelease]; 399 [self presentModalViewController:view_controller animated:YES]; 400 } 401 402 - (void)session:(RDPSession *)session verifyCertificateWithParams:(NSMutableDictionary *)params 403 { 404 VerifyCertificateController* view_controller = [[[VerifyCertificateController alloc] initWithNibName:@"VerifyCertificateView" bundle:nil session:_session params:params] autorelease]; 405 [self presentModalViewController:view_controller animated:YES]; 406 } 407 408 - (CGSize)sizeForFitScreenForSession:(RDPSession*)session 409 { 410 if (IsPad()) 411 return [self view].bounds.size; 412 else 413 { 414 // on phones make a resolution that has a 16:10 ratio with the phone's height 415 CGSize size = [self view].bounds.size; 416 CGFloat maxSize = (size.width > size.height) ? size.width : size.height; 417 return CGSizeMake(maxSize * 1.6f, maxSize); 418 } 419 } 420 421 - (void)showGoProScreen:(RDPSession*)session 422 { 423 UIAlertView* alertView = [[[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Pro Version", @"Pro version dialog title") 424 message:NSLocalizedString(@"Do you want to buy Thinstuff RDC Pro and enable the full RDP Experience", @"Pro version dialog message") delegate:self cancelButtonTitle:NSLocalizedString(@"No", @"No Button title") otherButtonTitles:NSLocalizedString(@"Yes", @"Yes button title"), nil] autorelease]; 425 [alertView show]; 426 } 427 428 #pragma mark - Keyboard Toolbar Handlers 429 430 -(void)showAdvancedKeyboardAnimated 431 { 432 // calc initial and final rect of the advanced keyboard view 433 CGRect rect = [[_keyboard_toolbar superview] bounds]; 434 rect.origin.y = [_keyboard_toolbar bounds].size.height; 435 rect.size.height -= rect.origin.y; 436 437 // create new view (hidden) and add to host-view of keyboard toolbar 438 _advanced_keyboard_view = [[AdvancedKeyboardView alloc] initWithFrame:CGRectMake(rect.origin.x, 439 [[_keyboard_toolbar superview] bounds].size.height, 440 rect.size.width, rect.size.height) delegate:self]; 441 [[_keyboard_toolbar superview] addSubview:_advanced_keyboard_view]; 442 // we set autoresize to YES for the keyboard toolbar's superview so that our adv. keyboard view gets properly resized 443 [[_keyboard_toolbar superview] setAutoresizesSubviews:YES]; 444 445 // show view with animation 446 [UIView beginAnimations:nil context:NULL]; 447 [_advanced_keyboard_view setFrame:rect]; 448 [UIView commitAnimations]; 449 } 450 451 -(IBAction)toggleKeyboardWhenOtherVisible:(id)sender 452 { 453 if(_advanced_keyboard_visible == NO) 454 { 455 [self showAdvancedKeyboardAnimated]; 456 } 457 else 458 { 459 // hide existing view 460 [UIView beginAnimations:@"hide_advanced_keyboard_view" context:NULL]; 461 CGRect rect = [_advanced_keyboard_view frame]; 462 rect.origin.y = [[_keyboard_toolbar superview] bounds].size.height; 463 [_advanced_keyboard_view setFrame:rect]; 464 [UIView commitAnimations]; 465 466 // the view is released in the animationDidStop selector registered in init 467 } 468 469 // toggle flag 470 _advanced_keyboard_visible = !_advanced_keyboard_visible; 471 } 472 473 -(IBAction)toggleWinKey:(id)sender 474 { 475 [[RDPKeyboard getSharedRDPKeyboard] toggleWinKey]; 476 } 477 478 -(IBAction)toggleShiftKey:(id)sender 479 { 480 [[RDPKeyboard getSharedRDPKeyboard] toggleShiftKey]; 481 } 482 483 -(IBAction)toggleCtrlKey:(id)sender 484 { 485 [[RDPKeyboard getSharedRDPKeyboard] toggleCtrlKey]; 486 } 487 488 -(IBAction)toggleAltKey:(id)sender 489 { 490 [[RDPKeyboard getSharedRDPKeyboard] toggleAltKey]; 491 } 492 493 -(IBAction)pressEscKey:(id)sender 494 { 495 [[RDPKeyboard getSharedRDPKeyboard] sendEscapeKeyStroke]; 496 } 497 498 #pragma mark - 499 #pragma mark event handlers 500 501 - (void)animationStopped:(NSString*)animationID finished:(NSNumber*)finished context:(void*)context 502 { 503 if ([animationID isEqualToString:@"hide_advanced_keyboard_view"]) 504 { 505 // cleanup advanced keyboard view 506 [_advanced_keyboard_view removeFromSuperview]; 507 [_advanced_keyboard_view autorelease]; 508 _advanced_keyboard_view = nil; 509 } 510 } 511 512 - (IBAction)switchSession:(id)sender 513 { 514 [self suspendSession]; 515 } 516 517 - (IBAction)toggleKeyboard:(id)sender 518 { 519 if(!_keyboard_visible) 520 [_dummy_textfield becomeFirstResponder]; 521 else 522 [_dummy_textfield resignFirstResponder]; 523 } 524 525 - (IBAction)toggleExtKeyboard:(id)sender 526 { 527 // if the sys kb is shown but not the advanced kb then toggle the advanced kb 528 if(_keyboard_visible && !_advanced_keyboard_visible) 529 [self toggleKeyboardWhenOtherVisible:nil]; 530 else 531 { 532 // if not visible request the advanced keyboard view 533 if(_advanced_keyboard_visible == NO) 534 _requesting_advanced_keyboard = YES; 535 [self toggleKeyboard:nil]; 536 } 537 } 538 539 - (IBAction)toggleTouchPointer:(id)sender 540 { 541 BOOL toggle_visibilty = ![_touchpointer_view isHidden]; 542 [_touchpointer_view setHidden:toggle_visibilty]; 543 if(toggle_visibilty) 544 [_session_scrollview setContentInset:UIEdgeInsetsZero]; 545 else 546 [_session_scrollview setContentInset:[_touchpointer_view getEdgeInsets]]; 547 } 548 549 - (IBAction)disconnectSession:(id)sender 550 { 551 [_session disconnect]; 552 } 553 554 555 -(IBAction)cancelButtonPressed:(id)sender 556 { 557 [_session disconnect]; 558 } 559 560 #pragma mark In-App purchase transaction notification handlers 561 562 - (void)onTransactionSuccess:(NSNotification*)notification 563 { 564 UIAlertView* alertView = [[[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Transaction Succeeded", @"Pro version bought dialog title") 565 message:NSLocalizedString(@"Thanks for buying Thinstuff RDC Pro. In order for the purchase to take effect please reconnect your current session.", @"Pro version bought dialog message") delegate:nil cancelButtonTitle:NSLocalizedString(@"OK", @"OK Button title") otherButtonTitles:nil] autorelease]; 566 [alertView show]; 567 } 568 569 - (void)onTransactionFailed:(NSNotification*)notification 570 { 571 UIAlertView* alertView = [[[UIAlertView alloc] initWithTitle:NSLocalizedString(@"Transaction Failed", @"Pro version buy failed dialog title") 572 message:NSLocalizedString(@"The transaction did not complete successfully!", @"Pro version buy failed dialog message") delegate:nil cancelButtonTitle:NSLocalizedString(@"OK", @"OK Button title") otherButtonTitles:nil] autorelease]; 573 [alertView show]; 574 } 575 576 #pragma mark - 577 #pragma mark iOS Keyboard Notification Handlers 578 579 - (void)keyboardWillShow:(NSNotification *)notification 580 { 581 if([[UITextInputMode currentInputMode].primaryLanguage isEqualToString:@"zh-Hans"] && _keyboard_has_display) { 582 return; 583 } 584 585 CGRect keyboardEndFrame = [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]; 586 [UIView beginAnimations:nil context:NULL]; 587 [UIView setAnimationCurve:[[[notification userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey] intValue]]; 588 [UIView setAnimationDuration:[[[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]]; 589 CGRect frame = [_session_scrollview frame]; 590 frame.size.height -= [[self view] convertRect:keyboardEndFrame toView:nil].size.height; 591 [_session_scrollview setFrame:frame]; 592 [_touchpointer_view setFrame:frame]; 593 [UIView commitAnimations]; 594 _keyboard_has_display = YES; 595 596 // NSLog(@"Invoke %s", __func__); 597 [_touchpointer_view ensurePointerIsVisible]; 598 } 599 600 - (void)keyboardDidShow:(NSNotification *)notification 601 { 602 if(_requesting_advanced_keyboard) 603 { 604 [self showAdvancedKeyboardAnimated]; 605 _advanced_keyboard_visible = YES; 606 _requesting_advanced_keyboard = NO; 607 } 608 } 609 610 - (void)keyboardWillHide:(NSNotification *)notification 611 { 612 CGRect keyboardEndFrame = [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]; 613 614 [UIView beginAnimations:nil context:NULL]; 615 [UIView setAnimationCurve:[[[notification userInfo] objectForKey:UIKeyboardAnimationCurveUserInfoKey] intValue]]; 616 [UIView setAnimationDuration:[[[notification userInfo] objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue]]; 617 CGRect frame = [_session_scrollview frame]; 618 frame.size.height += [[self view] convertRect:keyboardEndFrame toView:nil].size.height; 619 [_session_scrollview setFrame:frame]; 620 [_touchpointer_view setFrame:frame]; 621 [UIView commitAnimations]; 622 623 _keyboard_has_display = NO; 624 } 625 626 - (void)keyboardDidHide:(NSNotification*)notification 627 { 628 // release adanced keyboard view 629 if(_advanced_keyboard_visible == YES) 630 { 631 _advanced_keyboard_visible = NO; 632 [_advanced_keyboard_view removeFromSuperview]; 633 [_advanced_keyboard_view autorelease]; 634 _advanced_keyboard_view = nil; 635 } 636 } 637 638 #pragma mark - 639 #pragma mark Gesture handlers 640 641 - (void)handleSingleTap:(UITapGestureRecognizer*)gesture 642 { 643 CGPoint pos = [gesture locationInView:_session_view]; 644 if (_toggle_mouse_button) 645 { 646 [_session sendInputEvent:[self eventDescriptorForMouseEvent:GetRightMouseButtonClickEvent(YES) position:pos]]; 647 [_session sendInputEvent:[self eventDescriptorForMouseEvent:GetRightMouseButtonClickEvent(NO) position:pos]]; 648 } 649 else 650 { 651 [_session sendInputEvent:[self eventDescriptorForMouseEvent:GetLeftMouseButtonClickEvent(YES) position:pos]]; 652 [_session sendInputEvent:[self eventDescriptorForMouseEvent:GetLeftMouseButtonClickEvent(NO) position:pos]]; 653 } 654 655 _toggle_mouse_button = NO; 656 } 657 658 - (void)handleDoubleTap:(UITapGestureRecognizer*)gesture 659 { 660 CGPoint pos = [gesture locationInView:_session_view]; 661 [_session sendInputEvent:[self eventDescriptorForMouseEvent:GetLeftMouseButtonClickEvent(YES) position:pos]]; 662 [_session sendInputEvent:[self eventDescriptorForMouseEvent:GetLeftMouseButtonClickEvent(NO) position:pos]]; 663 [_session sendInputEvent:[self eventDescriptorForMouseEvent:GetLeftMouseButtonClickEvent(YES) position:pos]]; 664 [_session sendInputEvent:[self eventDescriptorForMouseEvent:GetLeftMouseButtonClickEvent(NO) position:pos]]; 665 _toggle_mouse_button = NO; 666 } 667 668 - (void)handleLongPress:(UILongPressGestureRecognizer*)gesture 669 { 670 CGPoint pos = [gesture locationInView:_session_view]; 671 672 if([gesture state] == UIGestureRecognizerStateBegan) 673 [_session sendInputEvent:[self eventDescriptorForMouseEvent:GetLeftMouseButtonClickEvent(YES) position:pos]]; 674 else if([gesture state] == UIGestureRecognizerStateChanged) 675 [self handleMouseMoveForPosition:pos]; 676 else if([gesture state] == UIGestureRecognizerStateEnded) 677 [_session sendInputEvent:[self eventDescriptorForMouseEvent:GetLeftMouseButtonClickEvent(NO) position:pos]]; 678 } 679 680 681 - (void)handleDoubleLongPress:(UILongPressGestureRecognizer*)gesture 682 { 683 // this point is mapped against the scroll view because we want to have relative movement to the screen/scrollview 684 CGPoint pos = [gesture locationInView:_session_scrollview]; 685 686 if([gesture state] == UIGestureRecognizerStateBegan) 687 _prev_long_press_position = pos; 688 else if([gesture state] == UIGestureRecognizerStateChanged) 689 { 690 int delta = _prev_long_press_position.y - pos.y; 691 692 if(delta > GetScrollGestureDelta()) 693 { 694 [_session sendInputEvent:[self eventDescriptorForMouseEvent:GetMouseWheelEvent(YES) position:pos]]; 695 _prev_long_press_position = pos; 696 } 697 else if(delta < -GetScrollGestureDelta()) 698 { 699 [_session sendInputEvent:[self eventDescriptorForMouseEvent:GetMouseWheelEvent(NO) position:pos]]; 700 _prev_long_press_position = pos; 701 } 702 } 703 } 704 705 -(void)handleSingle2FingersTap:(UITapGestureRecognizer*)gesture 706 { 707 _toggle_mouse_button = !_toggle_mouse_button; 708 } 709 710 -(void)handleSingle3FingersTap:(UITapGestureRecognizer*)gesture 711 { 712 [_session setToolbarVisible:![_session toolbarVisible]]; 713 [self showSessionToolbar:[_session toolbarVisible]]; 714 } 715 716 #pragma mark - 717 #pragma mark Touch Pointer delegates 718 // callback if touch pointer should be closed 719 -(void)touchPointerClose 720 { 721 [self toggleTouchPointer:nil]; 722 } 723 724 // callback for a left click action 725 -(void)touchPointerLeftClick:(CGPoint)pos down:(BOOL)down 726 { 727 CGPoint session_view_pos = [_touchpointer_view convertPoint:pos toView:_session_view]; 728 [_session sendInputEvent:[self eventDescriptorForMouseEvent:GetLeftMouseButtonClickEvent(down) position:session_view_pos]]; 729 } 730 731 // callback for a right click action 732 -(void)touchPointerRightClick:(CGPoint)pos down:(BOOL)down 733 { 734 CGPoint session_view_pos = [_touchpointer_view convertPoint:pos toView:_session_view]; 735 [_session sendInputEvent:[self eventDescriptorForMouseEvent:GetRightMouseButtonClickEvent(down) position:session_view_pos]]; 736 } 737 738 - (void)doAutoScrolling 739 { 740 int scrollX = 0; 741 int scrollY = 0; 742 CGPoint curPointerPos = [_touchpointer_view getPointerPosition]; 743 CGRect viewBounds = [_touchpointer_view bounds]; 744 CGRect scrollBounds = [_session_view bounds]; 745 746 // add content insets to scroll bounds 747 scrollBounds.size.width += [_session_scrollview contentInset].right; 748 scrollBounds.size.height += [_session_scrollview contentInset].bottom; 749 750 // add zoom factor 751 scrollBounds.size.width *= [_session_scrollview zoomScale]; 752 scrollBounds.size.height *= [_session_scrollview zoomScale]; 753 754 if (curPointerPos.x > (viewBounds.size.width - [_touchpointer_view getPointerWidth])) 755 scrollX = AUTOSCROLLDISTANCE; 756 else if (curPointerPos.x < 0) 757 scrollX = -AUTOSCROLLDISTANCE; 758 759 if (curPointerPos.y > (viewBounds.size.height - [_touchpointer_view getPointerHeight])) 760 scrollY = AUTOSCROLLDISTANCE; 761 else if (curPointerPos.y < (_session_toolbar_visible ? TOOLBAR_HEIGHT : 0)) 762 scrollY = -AUTOSCROLLDISTANCE; 763 764 CGPoint newOffset = [_session_scrollview contentOffset]; 765 newOffset.x += scrollX; 766 newOffset.y += scrollY; 767 768 // if offset is going off screen - stop scrolling in that direction 769 if (newOffset.x < 0) 770 { 771 scrollX = 0; 772 newOffset.x = 0; 773 } 774 else if (newOffset.x > (scrollBounds.size.width - viewBounds.size.width)) 775 { 776 scrollX = 0; 777 newOffset.x = MAX(scrollBounds.size.width - viewBounds.size.width, 0); 778 } 779 if (newOffset.y < 0) 780 { 781 scrollY = 0; 782 newOffset.y = 0; 783 } 784 else if (newOffset.y > (scrollBounds.size.height - viewBounds.size.height)) 785 { 786 scrollY = 0; 787 newOffset.y = MAX(scrollBounds.size.height - viewBounds.size.height, 0); 788 } 789 790 // perform scrolling 791 [_session_scrollview setContentOffset:newOffset]; 792 793 // continue scrolling? 794 if (scrollX != 0 || scrollY != 0) 795 [self performSelector:@selector(doAutoScrolling) withObject:nil afterDelay:AUTOSCROLLTIMEOUT]; 796 else 797 _is_autoscrolling = NO; 798 } 799 800 // callback for a right click action 801 -(void)touchPointerMove:(CGPoint)pos 802 { 803 CGPoint session_view_pos = [_touchpointer_view convertPoint:pos toView:_session_view]; 804 [self handleMouseMoveForPosition:session_view_pos]; 805 806 if (_autoscroll_with_touchpointer && !_is_autoscrolling) 807 { 808 _is_autoscrolling = YES; 809 [self performSelector:@selector(doAutoScrolling) withObject:nil afterDelay:AUTOSCROLLTIMEOUT]; 810 } 811 } 812 813 // callback if scrolling is performed 814 -(void)touchPointerScrollDown:(BOOL)down 815 { 816 [_session sendInputEvent:[self eventDescriptorForMouseEvent:GetMouseWheelEvent(down) position:CGPointZero]]; 817 } 818 819 // callback for toggling the standard keyboard 820 -(void)touchPointerToggleKeyboard 821 { 822 if(_advanced_keyboard_visible) 823 [self toggleKeyboardWhenOtherVisible:nil]; 824 else 825 [self toggleKeyboard:nil]; 826 } 827 828 // callback for toggling the extended keyboard 829 -(void)touchPointerToggleExtendedKeyboard 830 { 831 [self toggleExtKeyboard:nil]; 832 } 833 834 // callback for reset view 835 -(void)touchPointerResetSessionView 836 { 837 [_session_scrollview setZoomScale:1.0 animated:YES]; 838 } 839 840 @end 841 842 843 @implementation RDPSessionViewController (Private) 844 845 #pragma mark - 846 #pragma mark Helper functions 847 848 -(void)showSessionToolbar:(BOOL)show 849 { 850 // already shown or hidden? 851 if (_session_toolbar_visible == show) 852 return; 853 854 if(show) 855 { 856 [UIView beginAnimations:@"showToolbar" context:nil]; 857 [UIView setAnimationDuration:.4]; 858 [UIView setAnimationCurve:UIViewAnimationCurveLinear]; 859 [_session_toolbar setFrame:CGRectMake(0.0, 0.0, [[self view] bounds].size.width, TOOLBAR_HEIGHT)]; 860 [UIView commitAnimations]; 861 _session_toolbar_visible = YES; 862 } 863 else 864 { 865 [UIView beginAnimations:@"hideToolbar" context:nil]; 866 [UIView setAnimationDuration:.4]; 867 [UIView setAnimationCurve:UIViewAnimationCurveLinear]; 868 [_session_toolbar setFrame:CGRectMake(0.0, -TOOLBAR_HEIGHT, [[self view] bounds].size.width, TOOLBAR_HEIGHT)]; 869 [UIView commitAnimations]; 870 _session_toolbar_visible = NO; 871 } 872 } 873 874 -(UIToolbar*)keyboardToolbar 875 { 876 UIToolbar* keyboard_toolbar = [[[UIToolbar alloc] initWithFrame:CGRectNull] autorelease]; 877 [keyboard_toolbar setBarStyle:UIBarStyleBlackOpaque]; 878 879 UIBarButtonItem* esc_btn = [[[UIBarButtonItem alloc] initWithTitle:@"Esc" style:UIBarButtonItemStyleBordered target:self action:@selector(pressEscKey:)] autorelease]; 880 UIImage* win_icon = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"toolbar_icon_win" ofType:@"png"]]; 881 UIBarButtonItem* win_btn = [[[UIBarButtonItem alloc] initWithImage:win_icon style:UIBarButtonItemStyleBordered target:self action:@selector(toggleWinKey:)] autorelease]; 882 UIBarButtonItem* ctrl_btn = [[[UIBarButtonItem alloc] initWithTitle:@"Ctrl" style:UIBarButtonItemStyleBordered target:self action:@selector(toggleCtrlKey:)] autorelease]; 883 UIBarButtonItem* alt_btn = [[[UIBarButtonItem alloc] initWithTitle:@"Alt" style:UIBarButtonItemStyleBordered target:self action:@selector(toggleAltKey:)] autorelease]; 884 UIBarButtonItem* ext_btn = [[[UIBarButtonItem alloc] initWithTitle:@"Ext" style:UIBarButtonItemStyleBordered target:self action:@selector(toggleKeyboardWhenOtherVisible:)] autorelease]; 885 UIBarButtonItem* done_btn = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(toggleKeyboard:)] autorelease]; 886 UIBarButtonItem* flex_spacer = [[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil] autorelease]; 887 888 // iPad gets a shift button, iphone doesn't (there's just not enough space ...) 889 NSArray* items; 890 if(IsPad()) 891 { 892 UIBarButtonItem* shift_btn = [[[UIBarButtonItem alloc] initWithTitle:@"Shift" style:UIBarButtonItemStyleBordered target:self action:@selector(toggleShiftKey:)] autorelease]; 893 items = [NSArray arrayWithObjects:esc_btn, flex_spacer, 894 shift_btn, flex_spacer, 895 ctrl_btn, flex_spacer, 896 win_btn, flex_spacer, 897 alt_btn, flex_spacer, 898 ext_btn, flex_spacer, done_btn, nil]; 899 } 900 else 901 { 902 items = [NSArray arrayWithObjects:esc_btn, flex_spacer, ctrl_btn, flex_spacer, win_btn, flex_spacer, alt_btn, flex_spacer, ext_btn, flex_spacer, done_btn, nil]; 903 } 904 905 [keyboard_toolbar setItems:items]; 906 [keyboard_toolbar sizeToFit]; 907 return keyboard_toolbar; 908 } 909 910 - (void)initGestureRecognizers 911 { 912 // single and double tap recognizer 913 UITapGestureRecognizer* doubleTapRecognizer = [[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleDoubleTap:)] autorelease]; 914 [doubleTapRecognizer setNumberOfTouchesRequired:1]; 915 [doubleTapRecognizer setNumberOfTapsRequired:2]; 916 917 UITapGestureRecognizer* singleTapRecognizer = [[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleSingleTap:)] autorelease]; 918 [singleTapRecognizer requireGestureRecognizerToFail:doubleTapRecognizer]; 919 [singleTapRecognizer setNumberOfTouchesRequired:1]; 920 [singleTapRecognizer setNumberOfTapsRequired:1]; 921 922 // 2 fingers - tap recognizer 923 UITapGestureRecognizer* single2FingersTapRecognizer = [[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleSingle2FingersTap:)] autorelease]; 924 [single2FingersTapRecognizer setNumberOfTouchesRequired:2]; 925 [single2FingersTapRecognizer setNumberOfTapsRequired:1]; 926 927 // long press gesture recognizer 928 UILongPressGestureRecognizer* longPressRecognizer = [[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)] autorelease]; 929 [longPressRecognizer setMinimumPressDuration:0.5]; 930 931 // double long press gesture recognizer 932 UILongPressGestureRecognizer* doubleLongPressRecognizer = [[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleDoubleLongPress:)] autorelease]; 933 [doubleLongPressRecognizer setNumberOfTouchesRequired:2]; 934 [doubleLongPressRecognizer setMinimumPressDuration:0.5]; 935 936 // 3 finger, single tap gesture for showing/hiding the toolbar 937 UITapGestureRecognizer* single3FingersTapRecognizer = [[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleSingle3FingersTap:)] autorelease]; 938 [single3FingersTapRecognizer setNumberOfTapsRequired:1]; 939 [single3FingersTapRecognizer setNumberOfTouchesRequired:3]; 940 941 // add gestures to scroll view 942 [_session_scrollview addGestureRecognizer:singleTapRecognizer]; 943 [_session_scrollview addGestureRecognizer:doubleTapRecognizer]; 944 [_session_scrollview addGestureRecognizer:single2FingersTapRecognizer]; 945 [_session_scrollview addGestureRecognizer:longPressRecognizer]; 946 [_session_scrollview addGestureRecognizer:doubleLongPressRecognizer]; 947 [_session_scrollview addGestureRecognizer:single3FingersTapRecognizer]; 948 } 949 950 - (void)suspendSession 951 { 952 // suspend session and pop navigation controller 953 [_session suspend]; 954 955 // pop current view controller 956 [[self navigationController] popViewControllerAnimated:YES]; 957 } 958 959 - (NSDictionary*)eventDescriptorForMouseEvent:(int)event position:(CGPoint)position 960 { 961 return [NSDictionary dictionaryWithObjectsAndKeys: 962 @"mouse", @"type", 963 [NSNumber numberWithUnsignedShort:event], @"flags", 964 [NSNumber numberWithUnsignedShort:lrintf(position.x)], @"coord_x", 965 [NSNumber numberWithUnsignedShort:lrintf(position.y)], @"coord_y", 966 nil]; 967 } 968 969 - (void)sendDelayedMouseEventWithTimer:(NSTimer*)timer 970 { 971 _mouse_move_event_timer = nil; 972 NSDictionary* event = [timer userInfo]; 973 [_session sendInputEvent:event]; 974 [timer autorelease]; 975 } 976 977 - (void)handleMouseMoveForPosition:(CGPoint)position 978 { 979 NSDictionary* event = [self eventDescriptorForMouseEvent:PTR_FLAGS_MOVE position:position]; 980 981 // cancel pending mouse move events 982 [_mouse_move_event_timer invalidate]; 983 _mouse_move_events_skipped++; 984 985 if (_mouse_move_events_skipped >= 5) 986 { 987 [_session sendInputEvent:event]; 988 _mouse_move_events_skipped = 0; 989 } 990 else 991 { 992 [_mouse_move_event_timer autorelease]; 993 _mouse_move_event_timer = [[NSTimer scheduledTimerWithTimeInterval:0.05 target:self selector:@selector(sendDelayedMouseEventWithTimer:) userInfo:event repeats:NO] retain]; 994 } 995 } 996 997 @end
6.结束语
如果各位朋友有更好的解决办法,并且愿意分享你的智慧,请将你修改后的内容提交至FreeRDP官方,为开源事业贡献一份力量。