From 039f366cbf11de2321e8d9cb81a84406afbf9a48 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sun, 6 May 2012 12:06:44 +0200 Subject: [PATCH] Add IME support to AvalonEdit --- .../ICSharpCode.AvalonEdit/Editing/Caret.cs | 3 + .../Editing/ImeNativeWrapper.cs | 156 ++++++++++++++++++ .../Editing/ImeSupport.cs | 100 +++++++++++ .../ICSharpCode.AvalonEdit.csproj | 2 + .../TextEditorOptions.cs | 17 ++ 5 files changed, 278 insertions(+) create mode 100644 src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/ImeNativeWrapper.cs create mode 100644 src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/ImeSupport.cs diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/Caret.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/Caret.cs index 5c6999c1fd..c82f906040 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/Caret.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/Caret.cs @@ -22,6 +22,7 @@ namespace ICSharpCode.AvalonEdit.Editing { readonly TextArea textArea; readonly TextView textView; + readonly ImeSupport ime; readonly CaretLayer caretAdorner; bool visible; @@ -30,6 +31,7 @@ namespace ICSharpCode.AvalonEdit.Editing this.textArea = textArea; this.textView = textArea.TextView; position = new TextViewPosition(1, 1, 0); + ime = new ImeSupport(textArea); caretAdorner = new CaretLayer(textView); textView.InsertLayer(caretAdorner, KnownLayer.Caret, LayerInsertionPosition.Replace); @@ -434,6 +436,7 @@ namespace ICSharpCode.AvalonEdit.Editing } else { caretAdorner.Hide(); } + ime.UpdateCompositionWindow(); } } diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/ImeNativeWrapper.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/ImeNativeWrapper.cs new file mode 100644 index 0000000000..9b4d40684a --- /dev/null +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/ImeNativeWrapper.cs @@ -0,0 +1,156 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security; +using System.Windows; +using System.Windows.Input; +using System.Windows.Interop; +using System.Windows.Media; +using System.Windows.Media.TextFormatting; + +using ICSharpCode.AvalonEdit; +using ICSharpCode.AvalonEdit.Document; +using ICSharpCode.AvalonEdit.Rendering; +using ICSharpCode.AvalonEdit.Utils; + +namespace ICSharpCode.AvalonEdit.Editing +{ + /// + /// Native API required for IME support. + /// + static class ImeNativeWrapper + { + [StructLayout(LayoutKind.Sequential)] + struct CompositionForm + { + public int dwStyle; + public POINT ptCurrentPos; + public RECT rcArea; + } + + [StructLayout(LayoutKind.Sequential)] + struct POINT + { + public int x; + public int y; + } + + [StructLayout(LayoutKind.Sequential)] + struct RECT + { + public int left; + public int top; + public int right; + public int bottom; + } + + [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Auto)] + class LOGFONT + { + public int lfHeight = 0; + public int lfWidth = 0; + public int lfEscapement = 0; + public int lfOrientation = 0; + public int lfWeight = 0; + public byte lfItalic = 0; + public byte lfUnderline = 0; + public byte lfStrikeOut = 0; + public byte lfCharSet = 0; + public byte lfOutPrecision = 0; + public byte lfClipPrecision = 0; + public byte lfQuality = 0; + public byte lfPitchAndFamily = 0; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst=32)] public string lfFaceName = null; + } + + const int CPS_CANCEL = 0x4; + const int NI_COMPOSITIONSTR = 0x15; + const int GCS_COMPSTR = 0x0008; + public const int WM_IME_COMPOSITION = 0x10F; + + [DllImport("imm32.dll")] + static extern IntPtr ImmAssociateContext(IntPtr hWnd, IntPtr hIMC); + [DllImport("imm32.dll")] + static extern IntPtr ImmGetContext(IntPtr hWnd); + [DllImport("imm32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + static extern bool ImmNotifyIME(IntPtr hIMC, int dwAction, int dwIndex, int dwValue = 0); + [DllImport("imm32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + static extern bool ImmReleaseContext(IntPtr hWnd, IntPtr hIMC); + [DllImport("imm32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + static extern bool ImmSetCompositionWindow(IntPtr hIMC, ref CompositionForm form); + + public static IntPtr AssociateContext(HwndSource source, IntPtr hIMC) + { + if (source == null) + throw new ArgumentNullException("source"); + return ImmAssociateContext(source.Handle, hIMC); + } + + public static bool NotifyIme(IntPtr hIMC) + { + return ImmNotifyIME(hIMC, NI_COMPOSITIONSTR, CPS_CANCEL); + } + + public static IntPtr GetContext(HwndSource source) + { + if (source == null) + return IntPtr.Zero; + return ImmGetContext(source.Handle); + } + + public static bool ReleaseContext(HwndSource source, IntPtr hIMC) + { + return source != null && hIMC != IntPtr.Zero && ImmReleaseContext(source.Handle, hIMC); + } + + public static bool SetCompositionWindow(HwndSource source, IntPtr hIMC, TextArea textArea) + { + if (textArea == null) + throw new ArgumentNullException("textArea"); + Rect textViewBounds = textArea.TextView.GetBounds(); + Rect characterBounds = textArea.TextView.GetCharacterBounds(textArea.Caret.Position, source); + if (source != null) { + Matrix transformToDevice = source.CompositionTarget.TransformToDevice; + textViewBounds.Transform(transformToDevice); + characterBounds.Transform(transformToDevice); + } + CompositionForm form = new CompositionForm(); + form.dwStyle = 0x0020; + form.ptCurrentPos.x = (int)Math.Max(characterBounds.Left, textViewBounds.Left); + form.ptCurrentPos.y = (int)Math.Max(characterBounds.Top, textViewBounds.Top); + form.rcArea.left = (int)textViewBounds.Left; + form.rcArea.top = (int)textViewBounds.Top; + form.rcArea.right = (int)textViewBounds.Right; + form.rcArea.bottom = (int)textViewBounds.Bottom; + return ImmSetCompositionWindow(hIMC, ref form); + } + + static Rect GetBounds(this TextView textView) + { + Point location = textView.TranslatePoint(new Point(0,0), textView); + return new Rect(location, new Size(textView.ActualWidth, textView.ActualHeight)); + } + + static Rect GetCharacterBounds(this TextView textView, TextViewPosition pos, HwndSource source) + { + VisualLine vl = textView.GetVisualLine(pos.Line); + if (vl == null) + throw new Exception(); + TextLine line = vl.GetTextLine(pos.VisualColumn); + double offset = vl.GetTextLineVisualYPosition(line, VisualYPosition.LineTop) - textView.ScrollOffset.Y; + Rect r = line.GetTextBounds(pos.VisualColumn, 1).First().Rectangle; + r.Offset(-textView.ScrollOffset.X, offset); + Point pointOnRootVisual = textView.TransformToAncestor(source.RootVisual).Transform(r.Location); + Point pointOnHwnd = pointOnRootVisual.TransformToDevice(source.RootVisual); + r.Location = pointOnHwnd; + return r; + } + } +} diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/ImeSupport.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/ImeSupport.cs new file mode 100644 index 0000000000..5e3082b0dd --- /dev/null +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/Editing/ImeSupport.cs @@ -0,0 +1,100 @@ +// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) +// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) + +using System; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Input; +using System.Windows.Interop; +using System.Windows.Media; +using System.Windows.Media.TextFormatting; +using ICSharpCode.AvalonEdit; +using ICSharpCode.AvalonEdit.Document; +using ICSharpCode.AvalonEdit.Rendering; + +namespace ICSharpCode.AvalonEdit.Editing +{ + class ImeSupport : IDisposable + { + TextArea textArea; + IntPtr currentContext; + IntPtr previousContext; + HwndSource hwndSource; + + public ImeSupport(TextArea textArea) + { + if (textArea == null) + throw new ArgumentNullException("textArea"); + this.textArea = textArea; + InputMethod.SetIsInputMethodSuspended(this.textArea, true); + textArea.GotKeyboardFocus += TextAreaGotKeyboardFocus; + textArea.LostKeyboardFocus += TextAreaLostKeyboardFocus; + currentContext = IntPtr.Zero; + previousContext = IntPtr.Zero; + } + + public void Dispose() + { + if (textArea != null) { + textArea.GotKeyboardFocus -= TextAreaGotKeyboardFocus; + textArea.LostKeyboardFocus -= TextAreaLostKeyboardFocus; + textArea = null; + } + ClearContext(); + } + + void ClearContext() + { + if (hwndSource != null) { + hwndSource.RemoveHook(WndProc); + ImeNativeWrapper.AssociateContext(hwndSource, previousContext); + previousContext = IntPtr.Zero; + ImeNativeWrapper.ReleaseContext(hwndSource, currentContext); + hwndSource = null; + currentContext = IntPtr.Zero; + } + } + + void TextAreaGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e) + { + if (this.textArea == null) + return; + if (e.OriginalSource != this.textArea) + return; + hwndSource = (HwndSource)PresentationSource.FromVisual(this.textArea); + if (hwndSource != null) { + currentContext = ImeNativeWrapper.GetContext(hwndSource); + previousContext = ImeNativeWrapper.AssociateContext(hwndSource, currentContext); + hwndSource.AddHook(WndProc); + } + } + + void TextAreaLostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e) + { + if (e.OriginalSource != this.textArea) + return; + if (currentContext != IntPtr.Zero) + ImeNativeWrapper.NotifyIme(currentContext); + ClearContext(); + } + + IntPtr WndProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) + { + if (msg == ImeNativeWrapper.WM_IME_COMPOSITION) + UpdateCompositionWindow(); + return IntPtr.Zero; + } + + public void UpdateCompositionWindow() + { + if (currentContext != IntPtr.Zero && textArea != null) { + ImeNativeWrapper.SetCompositionWindow(hwndSource, currentContext, textArea); + } + } + } +} \ No newline at end of file diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj index 3d644e2710..b45a6fb85a 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/ICSharpCode.AvalonEdit.csproj @@ -147,7 +147,9 @@ Selection.cs + + diff --git a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/TextEditorOptions.cs b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/TextEditorOptions.cs index df95a8d488..95ee2498c7 100644 --- a/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/TextEditorOptions.cs +++ b/src/Libraries/AvalonEdit/ICSharpCode.AvalonEdit/TextEditorOptions.cs @@ -381,5 +381,22 @@ namespace ICSharpCode.AvalonEdit } } } + + bool enableImeSupport; + + /// + /// Gets/Sets whether the support for Input Method Editors (IME) + /// for non-alphanumeric scripts (Chinese, Japanese, Korean, ...) is enabled. + /// + [DefaultValue(true)] + public virtual bool EnableImeSupport { + get { return enableImeSupport; } + set { + if (enableImeSupport != value) { + enableImeSupport = value; + OnPropertyChanged("EnableImeSupport"); + } + } + } } }