Jelajahi Sumber

Implement NSTextInputClient protocol for IME

bruvzg 8 tahun lalu
induk
melakukan
8aa86cb9bc

+ 3 - 0
core/os/os.h

@@ -63,6 +63,8 @@ class OS {
 	void *_stack_bottom;
 
 public:
+	typedef void (*ImeCallback)(void *p_inp, String p_text, Point2 p_selection);
+
 	enum RenderThreadMode {
 
 		RENDER_THREAD_UNSAFE,
@@ -183,6 +185,7 @@ public:
 	virtual bool get_borderless_window() { return 0; }
 
 	virtual void set_ime_position(const Point2 &p_pos) {}
+	virtual void set_ime_intermediate_text_callback(ImeCallback p_callback, void *p_inp) {}
 
 	virtual Error open_dynamic_library(const String p_path, void *&p_library_handle) { return ERR_UNAVAILABLE; }
 	virtual Error close_dynamic_library(void *p_library_handle) { return ERR_UNAVAILABLE; }

+ 6 - 0
platform/osx/os_osx.h

@@ -104,6 +104,10 @@ public:
 	Size2 window_size;
 	Rect2 restore_rect;
 
+	Point2 im_position;
+	ImeCallback im_callback;
+	void *im_target;
+
 	power_osx *power_manager;
 
 	float _mouse_scale(float p_scale) {
@@ -203,6 +207,8 @@ public:
 
 	virtual void set_borderless_window(int p_borderless);
 	virtual bool get_borderless_window();
+	virtual void set_ime_position(const Point2 &p_pos);
+	virtual void set_ime_intermediate_text_callback(ImeCallback p_callback, void *p_inp);
 
 	virtual PowerState get_power_state();
 	virtual int get_power_seconds_left();

+ 136 - 10
platform/osx/os_osx.mm

@@ -261,10 +261,12 @@ static bool mouse_down_control = false;
 
 @end
 
-@interface GodotContentView : NSView {
+@interface GodotContentView : NSView <NSTextInputClient> {
 	NSTrackingArea *trackingArea;
+	NSMutableAttributedString *markedText;
+	bool imeMode;
 }
-
+- (void)cancelComposition;
 @end
 
 @implementation GodotContentView
@@ -278,16 +280,128 @@ static bool mouse_down_control = false;
 - (id)init {
 	self = [super init];
 	trackingArea = nil;
+	imeMode = false;
 	[self updateTrackingAreas];
 	[self registerForDraggedTypes:[NSArray arrayWithObject:NSFilenamesPboardType]];
+	markedText = [[NSMutableAttributedString alloc] init];
 	return self;
 }
 
 - (void)dealloc {
 	[trackingArea release];
+	[markedText release];
 	[super dealloc];
 }
 
+static const NSRange kEmptyRange = { NSNotFound, 0 };
+
+- (BOOL)hasMarkedText {
+	return (markedText.length > 0);
+}
+
+- (NSRange)markedRange {
+	return (markedText.length > 0) ? NSMakeRange(0, markedText.length - 1) : kEmptyRange;
+}
+
+- (NSRange)selectedRange {
+	return kEmptyRange;
+}
+
+- (void)setMarkedText:(id)aString selectedRange:(NSRange)selectedRange replacementRange:(NSRange)replacementRange {
+	if ([aString isKindOfClass:[NSAttributedString class]]) {
+		[markedText initWithAttributedString:aString];
+	} else {
+		[markedText initWithString:aString];
+	}
+	if (OS_OSX::singleton->im_callback) {
+		imeMode = true;
+		String ret;
+		ret.parse_utf8([[markedText mutableString] UTF8String]);
+		OS_OSX::singleton->im_callback(OS_OSX::singleton->im_target, ret, Point2(selectedRange.location, selectedRange.length));
+	}
+}
+
+- (void)doCommandBySelector:(SEL)aSelector {
+	if ([self respondsToSelector:aSelector])
+		[self performSelector:aSelector];
+}
+
+- (void)unmarkText {
+	imeMode = false;
+	[[markedText mutableString] setString:@""];
+	if (OS_OSX::singleton->im_callback)
+		OS_OSX::singleton->im_callback(OS_OSX::singleton->im_target, "", Point2());
+}
+
+- (NSArray *)validAttributesForMarkedText {
+	return [NSArray array];
+}
+
+- (NSAttributedString *)attributedSubstringForProposedRange:(NSRange)aRange actualRange:(NSRangePointer)actualRange {
+	return nil;
+}
+
+- (NSUInteger)characterIndexForPoint:(NSPoint)aPoint {
+	return 0;
+}
+
+- (NSRect)firstRectForCharacterRange:(NSRange)aRange actualRange:(NSRangePointer)actualRange {
+	const NSRect contentRect = [OS_OSX::singleton->window_view frame];
+	NSRect pointInWindowRect = NSMakeRect(OS_OSX::singleton->im_position.x / OS_OSX::singleton->display_scale, contentRect.size.height - (OS_OSX::singleton->im_position.y / OS_OSX::singleton->display_scale) - 1, 0, 0);
+	NSPoint pointOnScreen = [[OS_OSX::singleton->window_view window] convertRectToScreen:pointInWindowRect].origin;
+
+	return NSMakeRect(pointOnScreen.x, pointOnScreen.y, 0, 0);
+}
+
+- (void)cancelComposition {
+	[self unmarkText];
+	NSInputManager *currentInputManager = [NSInputManager currentInputManager];
+	[currentInputManager markedTextAbandoned:self];
+}
+
+- (void)insertText:(id)aString {
+	[self insertText:aString replacementRange:NSMakeRange(0, 0)];
+}
+
+- (void)insertText:(id)aString replacementRange:(NSRange)replacementRange {
+	NSEvent *event = [NSApp currentEvent];
+	Ref<InputEventKey> k;
+	k.instance();
+
+	get_key_modifier_state([event modifierFlags], k);
+	k->set_pressed(true);
+	k->set_echo(false);
+	k->set_scancode(0);
+
+	NSString *characters;
+	if ([aString isKindOfClass:[NSAttributedString class]]) {
+		characters = [aString string];
+	} else {
+		characters = (NSString *)aString;
+	}
+
+	NSUInteger i, length = [characters length];
+
+	NSCharacterSet *ctrlChars = [NSCharacterSet controlCharacterSet];
+	NSCharacterSet *wsnlChars = [NSCharacterSet whitespaceAndNewlineCharacterSet];
+	if ([characters rangeOfCharacterFromSet:ctrlChars].length && [characters rangeOfCharacterFromSet:wsnlChars].length == 0) {
+		NSInputManager *currentInputManager = [NSInputManager currentInputManager];
+		[currentInputManager markedTextAbandoned:self];
+		[self cancelComposition];
+		return;
+	}
+
+	for (i = 0; i < length; i++) {
+		const unichar codepoint = [characters characterAtIndex:i];
+		if ((codepoint & 0xFF00) == 0xF700)
+			continue;
+
+		k->set_unicode(codepoint);
+		OS_OSX::singleton->push_input(k);
+	}
+	[self cancelComposition];
+}
+
 - (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)sender {
 	return NSDragOperationCopy;
 }
@@ -634,15 +748,12 @@ static int translateKey(unsigned int key) {
 	NSString *characters = [event characters];
 	NSUInteger i, length = [characters length];
 
-	if (length > 0 && keycode_has_unicode(k->get_scancode())) {
-		for (i = 0; i < length; i++) {
-			k->set_unicode([characters characterAtIndex:i]);
-			OS_OSX::singleton->push_input(k);
-			k->set_scancode(0);
-		}
-	} else {
+	//disable raw input in IME mode
+	if (!imeMode)
 		OS_OSX::singleton->push_input(k);
-	}
+
+	if ((OS_OSX::singleton->im_position.x != 0) && (OS_OSX::singleton->im_position.y != 0))
+		[self interpretKeyEvents:[NSArray arrayWithObject:event]];
 }
 
 - (void)flagsChanged:(NSEvent *)event {
@@ -761,6 +872,18 @@ inline void sendScrollEvent(int button, double factor, int modifierFlags) {
 
 @end
 
+void OS_OSX::set_ime_intermediate_text_callback(ImeCallback p_callback, void *p_inp) {
+	im_callback = p_callback;
+	im_target = p_inp;
+	if (!im_callback) {
+		[window_view cancelComposition];
+	}
+}
+
+void OS_OSX::set_ime_position(const Point2 &p_pos) {
+	im_position = p_pos;
+}
+
 int OS_OSX::get_video_driver_count() const {
 	return 1;
 }
@@ -1735,6 +1858,9 @@ OS_OSX::OS_OSX() {
 	mouse_mode = OS::MOUSE_MODE_VISIBLE;
 	main_loop = NULL;
 	singleton = this;
+	im_position = Point2();
+	im_callback = NULL;
+	im_target = NULL;
 	autoreleasePool = [[NSAutoreleasePool alloc] init];
 
 	eventSource = CGEventSourceCreate(kCGEventSourceStateHIDSystemState);

+ 76 - 6
scene/gui/line_edit.cpp

@@ -641,6 +641,35 @@ void LineEdit::_notification(int p_what) {
 				if (char_ofs >= t.length())
 					break;
 
+				if (char_ofs == cursor_pos) {
+					if (ime_text.length() > 0) {
+						int ofs = 0;
+						while (true) {
+							if (ofs >= ime_text.length())
+								break;
+
+							CharType cchar = (pass && !text.empty()) ? '*' : ime_text[ofs];
+							CharType next = (pass && !text.empty()) ? '*' : ime_text[ofs + 1];
+							int im_char_width = font->get_char_size(cchar, next).width;
+
+							if ((x_ofs + im_char_width) > ofs_max)
+								break;
+
+							bool selected = ofs >= ime_selection.x && ofs < ime_selection.x + ime_selection.y;
+							if (selected) {
+								VisualServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(x_ofs, y_ofs + caret_height), Size2(im_char_width, 3)), font_color);
+							} else {
+								VisualServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(x_ofs, y_ofs + caret_height), Size2(im_char_width, 1)), font_color);
+							}
+
+							font->draw_char(ci, Point2(x_ofs, y_ofs + font_ascent), cchar, next, font_color);
+
+							x_ofs += im_char_width;
+							ofs++;
+						}
+					}
+				}
+
 				CharType cchar = (pass && !text.empty()) ? '*' : t[char_ofs];
 				CharType next = (pass && !text.empty()) ? '*' : t[char_ofs + 1];
 				int char_width = font->get_char_size(cchar, next).width;
@@ -657,24 +686,54 @@ void LineEdit::_notification(int p_what) {
 				font->draw_char(ci, Point2(x_ofs, y_ofs + font_ascent), cchar, next, selected ? font_color_selected : font_color);
 
 				if (char_ofs == cursor_pos && draw_caret) {
-					VisualServer::get_singleton()->canvas_item_add_rect(ci, Rect2(
-																					Point2(x_ofs, y_ofs), Size2(1, caret_height)),
-							cursor_color);
+					if (ime_text.length() == 0) {
+						VisualServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(x_ofs, y_ofs), Size2(1, caret_height)), cursor_color);
+					}
 				}
 
 				x_ofs += char_width;
 				char_ofs++;
 			}
 
+			if (char_ofs == cursor_pos) {
+				if (ime_text.length() > 0) {
+					int ofs = 0;
+					while (true) {
+						if (ofs >= ime_text.length())
+							break;
+
+						CharType cchar = (pass && !text.empty()) ? '*' : ime_text[ofs];
+						CharType next = (pass && !text.empty()) ? '*' : ime_text[ofs + 1];
+						int im_char_width = font->get_char_size(cchar, next).width;
+
+						if ((x_ofs + im_char_width) > ofs_max)
+							break;
+
+						bool selected = ofs >= ime_selection.x && ofs < ime_selection.x + ime_selection.y;
+						if (selected) {
+							VisualServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(x_ofs, y_ofs + caret_height), Size2(im_char_width, 3)), font_color);
+						} else {
+							VisualServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(x_ofs, y_ofs + caret_height), Size2(im_char_width, 1)), font_color);
+						}
+
+						font->draw_char(ci, Point2(x_ofs, y_ofs + font_ascent), cchar, next, font_color);
+
+						x_ofs += im_char_width;
+						ofs++;
+					}
+				}
+			}
+
 			if (char_ofs == cursor_pos && draw_caret) { //may be at the end
-				VisualServer::get_singleton()->canvas_item_add_rect(ci, Rect2(
-																				Point2(x_ofs, y_ofs), Size2(1, caret_height)),
-						cursor_color);
+				if (ime_text.length() == 0) {
+					VisualServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(x_ofs, y_ofs), Size2(1, caret_height)), cursor_color);
+				}
 			}
 
 			if (has_focus()) {
 
 				OS::get_singleton()->set_ime_position(get_global_position() + Point2(x_ofs, y_ofs + caret_height));
+				OS::get_singleton()->set_ime_intermediate_text_callback(_ime_text_callback, this);
 			}
 		} break;
 		case NOTIFICATION_FOCUS_ENTER: {
@@ -685,6 +744,7 @@ void LineEdit::_notification(int p_what) {
 
 			Point2 cursor_pos = Point2(get_cursor_pos(), 1) * get_minimum_size().height;
 			OS::get_singleton()->set_ime_position(get_global_position() + cursor_pos);
+			OS::get_singleton()->set_ime_intermediate_text_callback(_ime_text_callback, this);
 
 			if (OS::get_singleton()->has_virtual_keyboard())
 				OS::get_singleton()->show_virtual_keyboard(text, get_global_rect());
@@ -693,6 +753,9 @@ void LineEdit::_notification(int p_what) {
 		case NOTIFICATION_FOCUS_EXIT: {
 
 			OS::get_singleton()->set_ime_position(Point2());
+			OS::get_singleton()->set_ime_intermediate_text_callback(NULL, NULL);
+			ime_text = "";
+			ime_selection = Point2();
 
 			if (OS::get_singleton()->has_virtual_keyboard())
 				OS::get_singleton()->hide_virtual_keyboard();
@@ -1231,6 +1294,13 @@ bool LineEdit::get_expand_to_text_length() const {
 	return expand_to_text_length;
 }
 
+void LineEdit::_ime_text_callback(void *p_self, String p_text, Point2 p_selection) {
+	LineEdit *self = (LineEdit *)p_self;
+	self->ime_text = p_text;
+	self->ime_selection = p_selection;
+	self->update();
+}
+
 void LineEdit::_text_changed() {
 
 	if (expand_to_text_length)

+ 3 - 0
scene/gui/line_edit.h

@@ -70,6 +70,8 @@ private:
 	String text;
 	String placeholder;
 	float placeholder_alpha;
+	String ime_text;
+	Point2 ime_selection;
 
 	PopupMenu *menu;
 
@@ -92,6 +94,7 @@ private:
 
 	Timer *caret_blink_timer;
 
+	static void _ime_text_callback(void *p_self, String p_text, Point2 p_selection);
 	void _text_changed();
 	bool expand_to_text_length;
 

+ 83 - 16
scene/gui/text_edit.cpp

@@ -987,13 +987,41 @@ void TextEdit::_notification(int p_what) {
 						}
 
 						int caret_w = (str[j] == '\t') ? cache.font->get_char_size(' ').width : char_w;
-						if (draw_caret) {
-							if (insert_mode) {
-								int caret_h = (block_caret) ? 4 : 1;
-								VisualServer::get_singleton()->canvas_item_add_rect(ci, Rect2(cursor_pos, Size2i(caret_w, caret_h)), cache.caret_color);
-							} else {
-								caret_w = (block_caret) ? caret_w : 1;
-								VisualServer::get_singleton()->canvas_item_add_rect(ci, Rect2(cursor_pos, Size2i(caret_w, get_row_height())), cache.caret_color);
+						if (ime_text.length() > 0) {
+							int ofs = 0;
+							while (true) {
+								if (ofs >= ime_text.length())
+									break;
+
+								CharType cchar = ime_text[ofs];
+								CharType next = ime_text[ofs + 1];
+								int im_char_width = cache.font->get_char_size(cchar, next).width;
+
+								if ((char_ofs + char_margin + im_char_width) >= xmargin_end)
+									break;
+
+								bool selected = ofs >= ime_selection.x && ofs < ime_selection.x + ime_selection.y;
+								if (selected) {
+									VisualServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(char_ofs + char_margin, ofs_y + get_row_height()), Size2(im_char_width, 3)), color);
+								} else {
+									VisualServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(char_ofs + char_margin, ofs_y + get_row_height()), Size2(im_char_width, 1)), color);
+								}
+
+								cache.font->draw_char(ci, Point2(char_ofs + char_margin, ofs_y + ascent), cchar, next, color);
+
+								char_ofs += im_char_width;
+								ofs++;
+							}
+						}
+						if (ime_text.length() == 0) {
+							if (draw_caret) {
+								if (insert_mode) {
+									int caret_h = (block_caret) ? 4 : 1;
+									VisualServer::get_singleton()->canvas_item_add_rect(ci, Rect2(cursor_pos, Size2i(caret_w, caret_h)), cache.caret_color);
+								} else {
+									caret_w = (block_caret) ? caret_w : 1;
+									VisualServer::get_singleton()->canvas_item_add_rect(ci, Rect2(cursor_pos, Size2i(caret_w, get_row_height())), cache.caret_color);
+								}
 							}
 						}
 					}
@@ -1024,16 +1052,43 @@ void TextEdit::_notification(int p_what) {
 					if (insert_mode) {
 						cursor_pos.y += (get_row_height() - 3);
 					}
+					if (ime_text.length() > 0) {
+						int ofs = 0;
+						while (true) {
+							if (ofs >= ime_text.length())
+								break;
 
-					if (draw_caret) {
-						if (insert_mode) {
-							int char_w = cache.font->get_char_size(' ').width;
-							int caret_h = (block_caret) ? 4 : 1;
-							VisualServer::get_singleton()->canvas_item_add_rect(ci, Rect2(cursor_pos, Size2i(char_w, caret_h)), cache.caret_color);
-						} else {
-							int char_w = cache.font->get_char_size(' ').width;
-							int caret_w = (block_caret) ? char_w : 1;
-							VisualServer::get_singleton()->canvas_item_add_rect(ci, Rect2(cursor_pos, Size2i(caret_w, get_row_height())), cache.caret_color);
+							CharType cchar = ime_text[ofs];
+							CharType next = ime_text[ofs + 1];
+							int im_char_width = cache.font->get_char_size(cchar, next).width;
+
+							if ((char_ofs + char_margin + im_char_width) >= xmargin_end)
+								break;
+
+							bool selected = ofs >= ime_selection.x && ofs < ime_selection.x + ime_selection.y;
+							if (selected) {
+								VisualServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(char_ofs + char_margin, ofs_y + get_row_height()), Size2(im_char_width, 3)), color);
+							} else {
+								VisualServer::get_singleton()->canvas_item_add_rect(ci, Rect2(Point2(char_ofs + char_margin, ofs_y + get_row_height()), Size2(im_char_width, 1)), color);
+							}
+
+							cache.font->draw_char(ci, Point2(char_ofs + char_margin, ofs_y + ascent), cchar, next, color);
+
+							char_ofs += im_char_width;
+							ofs++;
+						}
+					}
+					if (ime_text.length() == 0) {
+						if (draw_caret) {
+							if (insert_mode) {
+								int char_w = cache.font->get_char_size(' ').width;
+								int caret_h = (block_caret) ? 4 : 1;
+								VisualServer::get_singleton()->canvas_item_add_rect(ci, Rect2(cursor_pos, Size2i(char_w, caret_h)), cache.caret_color);
+							} else {
+								int char_w = cache.font->get_char_size(' ').width;
+								int caret_w = (block_caret) ? char_w : 1;
+								VisualServer::get_singleton()->canvas_item_add_rect(ci, Rect2(cursor_pos, Size2i(caret_w, get_row_height())), cache.caret_color);
+							}
 						}
 					}
 				}
@@ -1197,6 +1252,7 @@ void TextEdit::_notification(int p_what) {
 
 			if (has_focus()) {
 				OS::get_singleton()->set_ime_position(get_global_position() + cursor_pos + Point2(0, get_row_height()));
+				OS::get_singleton()->set_ime_intermediate_text_callback(_ime_text_callback, this);
 			}
 		} break;
 		case NOTIFICATION_FOCUS_ENTER: {
@@ -1207,6 +1263,7 @@ void TextEdit::_notification(int p_what) {
 
 			Point2 cursor_pos = Point2(cursor_get_column(), cursor_get_line()) * get_row_height();
 			OS::get_singleton()->set_ime_position(get_global_position() + cursor_pos);
+			OS::get_singleton()->set_ime_intermediate_text_callback(_ime_text_callback, this);
 
 			if (OS::get_singleton()->has_virtual_keyboard())
 				OS::get_singleton()->show_virtual_keyboard(get_text(), get_global_rect());
@@ -1218,6 +1275,9 @@ void TextEdit::_notification(int p_what) {
 		case NOTIFICATION_FOCUS_EXIT: {
 
 			OS::get_singleton()->set_ime_position(Point2());
+			OS::get_singleton()->set_ime_intermediate_text_callback(NULL, NULL);
+			ime_text = "";
+			ime_selection = Point2();
 
 			if (OS::get_singleton()->has_virtual_keyboard())
 				OS::get_singleton()->hide_virtual_keyboard();
@@ -1228,6 +1288,13 @@ void TextEdit::_notification(int p_what) {
 	}
 }
 
+void TextEdit::_ime_text_callback(void *p_self, String p_text, Point2 p_selection) {
+	TextEdit *self = (TextEdit *)p_self;
+	self->ime_text = p_text;
+	self->ime_selection = p_selection;
+	self->update();
+}
+
 void TextEdit::_consume_pair_symbol(CharType ch) {
 
 	int cursor_position_to_move = cursor_get_column() + 1;

+ 5 - 0
scene/gui/text_edit.h

@@ -184,6 +184,9 @@ class TextEdit : public Control {
 		bool chain_backward;
 	};
 
+	String ime_text;
+	Point2 ime_selection;
+
 	TextOperation current_op;
 
 	List<TextOperation> undo_stack;
@@ -299,6 +302,8 @@ class TextEdit : public Control {
 	void _scroll_lines_up();
 	void _scroll_lines_down();
 
+	static void _ime_text_callback(void *p_self, String p_text, Point2 p_selection);
+
 	//void mouse_motion(const Point& p_pos, const Point& p_rel, int p_button_mask);
 	Size2 get_minimum_size() const;