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 
 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 
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 
  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官方,为开源事业贡献一份力量。


转载于:https://www.cnblogs.com/kappia/archive/2013/04/26/freerdp_chinese_input_directly.html

你可能感兴趣的:(iOS平台FreeRDP中文直接输入支持)