OSDN Git Service

UWP版に自動補完機能を付けた
authorkonekoneko <test2214@hotmail.co.jp>
Sat, 12 Nov 2016 08:28:51 +0000 (13:58 +0530)
committerkonekoneko <test2214@hotmail.co.jp>
Sat, 12 Nov 2016 08:28:51 +0000 (13:58 +0530)
Core/AutoCompleteBoxBase.cs [new file with mode: 0644]
Core/CompleteCollection.cs [new file with mode: 0644]
Core/CompleteCollectionHelper.cs [new file with mode: 0644]
Core/Core.projitems
Core/Document.cs
Core/ITextRender.cs
Core/Util.cs
UWP/FooEditEngine.UWP/AutoCompleteBox.cs [new file with mode: 0644]
UWP/FooEditEngine.UWP/FooEditEngine.UWP.csproj
UWP/FooEditEngine.UWP/FooTextBox.cs
UWP/Test/MainViewModel.cs

diff --git a/Core/AutoCompleteBoxBase.cs b/Core/AutoCompleteBoxBase.cs
new file mode 100644 (file)
index 0000000..88f90d3
--- /dev/null
@@ -0,0 +1,196 @@
+using System;
+using System.Linq;
+
+namespace FooEditEngine
+{
+    public sealed class ShowingCompleteBoxEventArgs : EventArgs
+    {
+        /// <summary>
+        /// 入力された文字
+        /// </summary>
+        public string KeyChar;
+        /// <summary>
+        /// 入力した単語と一致したコレクションのインデックス。一致しないなら-1をセットする
+        /// </summary>
+        public int foundIndex;
+        /// <summary>
+        /// 入力しようとした単語を設定する
+        /// </summary>
+        public string inputedWord;
+        /// <summary>
+        /// 補完対象のテキストボックス
+        /// </summary>
+        public Document textbox;
+        /// <summary>
+        /// キャレット座標
+        /// </summary>
+        public Point CaretPostion;
+        public ShowingCompleteBoxEventArgs(string keyChar, Document textbox, Point caret_pos)
+        {
+            this.inputedWord = null;
+            this.KeyChar = keyChar;
+            this.foundIndex = -1;
+            this.textbox = textbox;
+            this.CaretPostion = caret_pos;
+        }
+    }
+
+    public sealed class SelectItemEventArgs : EventArgs
+    {
+        /// <summary>
+        /// 補完候補
+        /// </summary>
+        public string word;
+        /// <summary>
+        /// 入力中の単語
+        /// </summary>
+        public string inputing_word;
+        /// <summary>
+        /// 補完対象のテキストボックス
+        /// </summary>
+        public Document textbox;
+        public SelectItemEventArgs(string word, string inputing_word, Document textbox)
+        {
+            this.word = word;
+            this.inputing_word = inputing_word;
+            this.textbox = textbox;
+        }
+    }
+
+    public delegate void SelectItemEventHandler(object sender,SelectItemEventArgs e);
+    public delegate void ShowingCompleteBoxEnventHandler(object sender, ShowingCompleteBoxEventArgs e);
+
+    public class AutoCompleteBoxBase
+    {
+        const int InputLength = 2;  //補完を開始する文字の長さ
+
+        protected Document Document
+        {
+            get;
+            private set;
+        }
+
+        /// <summary>
+        /// コンストラクター
+        /// </summary>
+        /// <param name="document">対象となるDocumentWindow</param>
+        public AutoCompleteBoxBase(Document document)
+        {
+            this.SelectItem = (s, e) => {
+                string inputing_word = e.inputing_word;
+                string word = e.word;
+
+                var doc = e.textbox;
+                //キャレットは入力された文字の後ろにあるので、一致する分だけ選択して置き換える
+                int caretIndex = doc.LayoutLines.GetIndexFromTextPoint(e.textbox.CaretPostion);
+                int start = caretIndex - inputing_word.Length;
+                if (start < 0)
+                    start = 0;
+                doc.Replace(start, inputing_word.Length, word);
+                doc.RequestRedraw();
+            };
+            this.ShowingCompleteBox = (s, e) => {
+                AutoCompleteBoxBase box = (AutoCompleteBoxBase)s;
+
+                var doc = e.textbox;
+                int caretIndex = doc.LayoutLines.GetIndexFromTextPoint(e.textbox.CaretPostion);
+                int inputingIndex = caretIndex - 1;
+                if (inputingIndex < 0)
+                    inputingIndex = 0;
+
+                e.inputedWord = CompleteHelper.GetWord(doc, inputingIndex, box.Operators) + e.KeyChar;
+
+                if (e.inputedWord == null)
+                    return;
+
+                for (int i = 0; i < box.Items.Count; i++)
+                {
+                    CompleteWord item = (CompleteWord)box.Items[i];
+                    if (item.word.StartsWith(e.inputedWord))
+                    {
+                        e.foundIndex = i;
+                        break;
+                    }
+                }
+            };
+            this.Operators = new char[] { ' ', '\t', Document.NewLine };
+            this.Document = document;
+        }
+
+        public void ParseInput(string input_text)
+        {
+            if (this.Operators == null ||
+                input_text == "\r" ||
+                input_text == "\n" ||
+                this.ShowingCompleteBox == null ||
+                (this.IsCloseCompleteBox == false && input_text == "\b"))
+                return;
+
+            this.OpenCompleteBox(input_text);
+        }
+
+        /// <summary>
+        /// 補完すべき単語が選択されたときに発生するイベント
+        /// </summary>
+        public SelectItemEventHandler SelectItem;
+        /// <summary>
+        /// UI表示前のイベント
+        /// </summary>
+        public ShowingCompleteBoxEnventHandler ShowingCompleteBox;
+
+        /// <summary>
+        /// 区切り文字のリスト
+        /// </summary>
+        public char[] Operators
+        {
+            get;
+            set;
+        }
+
+        /// <summary>
+        /// オートコンプリートの対象となる単語のリスト
+        /// </summary>
+        public virtual CompleteCollection<ICompleteItem> Items
+        {
+            get;
+            set;
+        }
+
+        internal Func<TextPoint, Point> GetPostion;
+
+        public virtual bool IsCloseCompleteBox
+        {
+            get;
+        }
+
+        protected virtual void RequestShowCompleteBox(ShowingCompleteBoxEventArgs ev)
+        {
+        }
+
+        protected virtual void RequestCloseCompleteBox()
+        {
+        }
+
+        public void OpenCompleteBox(string key_char, bool force = false)
+        {
+            if (this.GetPostion == null)
+                throw new InvalidOperationException("GetPostionがnullです");
+            Point p = this.GetPostion(this.Document.CaretPostion);
+
+            ShowingCompleteBoxEventArgs ev = new ShowingCompleteBoxEventArgs(key_char, this.Document, p);
+            ShowingCompleteBox(this, ev);
+
+            bool hasCompleteItem = ev.foundIndex != -1 && ev.inputedWord != null && ev.inputedWord != string.Empty && ev.inputedWord.Length >= InputLength;
+            System.Diagnostics.Debug.WriteLine("hasCompleteItem:{0}", hasCompleteItem);
+            if (force || hasCompleteItem)
+            {
+                RequestShowCompleteBox(ev);
+            }
+            else
+            {
+                RequestCloseCompleteBox();
+            }
+        }
+
+    }
+}
diff --git a/Core/CompleteCollection.cs b/Core/CompleteCollection.cs
new file mode 100644 (file)
index 0000000..c8a2566
--- /dev/null
@@ -0,0 +1,104 @@
+using System;
+using System.ComponentModel;
+using System.Collections.ObjectModel;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+
+namespace FooEditEngine
+{
+    public interface ICompleteItem : INotifyPropertyChanged
+    {
+        /// <summary>
+        /// 補完対象の単語を表す
+        /// </summary>
+        string word { get; }
+    }
+
+    public class CompleteWord : ICompleteItem
+    {
+        private string _word;
+        /// <summary>
+        /// コンストラクター
+        /// </summary>
+        public CompleteWord(string w)
+        {
+            this._word = w;
+            this.PropertyChanged += new PropertyChangedEventHandler((s,e)=>{});
+        }
+
+        /// <summary>
+        /// 補完候補となる単語を表す
+        /// </summary>
+        public string word
+        {
+            get { return this._word; }
+            set { this._word = value; this.OnPropertyChanged(); }
+        }
+
+        /// <summary>
+        /// プロパティが変更されたことを通知する
+        /// </summary>
+        public void OnPropertyChanged([CallerMemberName] string name = "")
+        {
+            if (this.PropertyChanged != null)
+                this.PropertyChanged(this, new PropertyChangedEventArgs(name));
+        }
+
+        /// <summary>
+        /// プロパティが変更されたことを通知する
+        /// </summary>
+        public event PropertyChangedEventHandler PropertyChanged;
+    }
+
+    public sealed class CompleteCollection<T> : ObservableCollection<T> where T : ICompleteItem
+    {
+        public const string ShowMember = "word";
+
+        /// <summary>
+        /// 補完対象の単語を表す
+        /// </summary>
+        public CompleteCollection()
+        {
+            this.LongestItem = default(T);
+        }
+
+        /// <summary>
+        /// 最も長い単語を表す
+        /// </summary>
+        public T LongestItem
+        {
+            get;
+            private set;
+        }
+
+        public void AddRange(IEnumerable<T> collection)
+        {
+            foreach (T s in collection)
+                this.Add(s);
+        }
+
+        public new void Add(T s)
+        {
+            if (this.LongestItem == null)
+                this.LongestItem = s;
+            if (s.word.Length > this.LongestItem.word.Length)
+                this.LongestItem = s;
+            base.Add(s);
+        }
+
+        public new void Insert(int index, T s)
+        {
+            if (this.LongestItem == null)
+                this.LongestItem = s;
+            if (s.word.Length > this.LongestItem.word.Length)
+                this.LongestItem = s;
+            base.Insert(index, s);
+        }
+
+        public new void Clear()
+        {
+            this.LongestItem = default(T);
+            base.Clear();
+        }
+    }
+}
diff --git a/Core/CompleteCollectionHelper.cs b/Core/CompleteCollectionHelper.cs
new file mode 100644 (file)
index 0000000..0aedb51
--- /dev/null
@@ -0,0 +1,76 @@
+using System;
+using System.Linq;
+using System.Text;
+using System.Collections.Generic;
+using FooEditEngine;
+
+namespace FooEditEngine
+{
+    /// <summary>
+    /// 補完候補作成用便利クラス
+    /// </summary>
+    public static class CompleteHelper
+    {
+        /// <summary>
+        /// KeywordManager.Operatorsで区切られた単語を補完候補に追加する
+        /// </summary>
+        /// <param name="s"></param>
+        public static void AddCompleteWords(CompleteCollection<ICompleteItem> items, IList<char> Operators, string s)
+        {
+            if (items == null || Operators == null)
+                return;
+
+            char[] seps = new char[Operators.Count];
+            Operators.CopyTo(seps, 0);
+
+            string[] words = s.Split(seps, StringSplitOptions.RemoveEmptyEntries);
+
+            foreach (string word in words)
+                CompleteHelper.AddComleteWord(items, word);
+        }
+
+        /// <summary>
+        /// 補完候補を追加する
+        /// </summary>
+        /// <param name="word"></param>
+        public static void AddComleteWord(CompleteCollection<ICompleteItem> items, string word)
+        {
+            CompleteWord newItem = new CompleteWord(word);
+            if (items.Contains(newItem) == false && CompleteHelper.IsVaildWord(word))
+                items.Add(newItem);
+        }
+
+        public static string GetWord(Document doc, int startIndex,char[] sep)
+        {
+            if (doc.Length == 0)
+                return null;
+            StringBuilder word = new StringBuilder();
+            for (int i = startIndex; i >= 0; i--)
+            {
+                if(sep.Contains(doc[i]))
+                {
+                    return word.ToString();
+                }
+                word.Insert(0,doc[i]);
+            }
+            if (word.Length > 0)
+                return word.ToString();
+            else
+                return null;
+        }
+
+        static bool IsVaildWord(string s)
+        {
+            if (s.Length == 0 || s == string.Empty)
+                return false;
+            if (!Char.IsLetter(s[0]))
+                return false;
+            for (int i = 1; i < s.Length; i++)
+            {
+                if (!Char.IsLetterOrDigit(s[i]))
+                    return false;
+            }
+            return true;
+        }
+    }
+}
index 1b027ab..509ae4f 100644 (file)
@@ -9,9 +9,12 @@
     <Import_RootNamespace>Core</Import_RootNamespace>
   </PropertyGroup>
   <ItemGroup>
+    <Compile Include="$(MSBuildThisFileDirectory)AutoCompleteBoxBase.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)Automaion\FooTextBoxAutomationPeer.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)CacheManager.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)CollectionDebugView.cs" />
+    <Compile Include="$(MSBuildThisFileDirectory)CompleteCollection.cs" />
+    <Compile Include="$(MSBuildThisFileDirectory)CompleteCollectionHelper.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)Controller.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)Direct2D\CustomTextRenderer.cs" />
     <Compile Include="$(MSBuildThisFileDirectory)Direct2D\D2DRenderCommon.cs" />
index 575667b..794909c 100644 (file)
@@ -233,6 +233,12 @@ namespace FooEditEngine
             set;
         }
 
+        public AutoCompleteBoxBase AutoComplete
+        {
+            get;
+            set;
+        }
+
         public event ProgressEventHandler LoadProgress;
 
         /// <summary>
@@ -1028,8 +1034,13 @@ namespace FooEditEngine
             this.UndoManager.push(cmd);
             cmd.redo();
 
-            if (this.FireUpdateEvent && UserInput && s == Document.NewLine.ToString())
-                this.AutoIndentHook(this, null);
+            if (this.FireUpdateEvent && UserInput)
+            {
+                if(this.AutoComplete != null)
+                    this.AutoComplete.ParseInput(string.Empty); //入力は終わっているので空文字を渡す
+                if (s == Document.NewLine.ToString())
+                    this.AutoIndentHook(this, null);
+            }
         }
 
         /// <summary>
index f141026..a688fc7 100644 (file)
@@ -13,7 +13,7 @@ using System.Collections.Generic;
 
 namespace FooEditEngine
 {
-    struct Point
+    public struct Point
     {
         public double X;
         public double Y;
index bb8ef6f..987fe51 100644 (file)
@@ -43,6 +43,14 @@ namespace FooEditEngine
             var gt = element.TransformToVisual(element);
             return gt.TransformPoint(screen);
         }
+
+        public static Point GetPointInWindow(Point client, Windows.UI.Xaml.UIElement element)
+        {
+            //ウィンドウ内での絶対座標を取得する
+            var gt = element.TransformToVisual(null);
+            return gt.TransformPoint(client);
+        }
+
         public static Point GetScreentPoint(Point client, Windows.UI.Xaml.UIElement element)
         {
             //ウィンドウ内での絶対座標を取得する
diff --git a/UWP/FooEditEngine.UWP/AutoCompleteBox.cs b/UWP/FooEditEngine.UWP/AutoCompleteBox.cs
new file mode 100644 (file)
index 0000000..bc0339c
--- /dev/null
@@ -0,0 +1,143 @@
+using System;
+using Windows.System;
+using Windows.UI.Xaml.Controls;
+using Windows.UI.Xaml.Controls.Primitives;
+using Windows.UI.Xaml.Input;
+
+namespace FooEditEngine.UWP
+{
+    public class AutoCompleteBox : AutoCompleteBoxBase
+    {
+        private string inputedWord;
+        private ListBox listBox1 = new ListBox();
+        private Popup popup = new Popup();
+        private Document doc;
+
+        public AutoCompleteBox(Document doc) : base(doc)
+        {
+            //リストボックスを追加する
+            this.popup.Child = this.listBox1;
+            this.listBox1.DoubleTapped += ListBox1_DoubleTapped;
+            this.listBox1.KeyDown += listBox1_KeyDown;
+            this.listBox1.Height = 200;
+            this.doc = doc;
+        }
+
+        /// <summary>
+        /// オートコンプリートの対象となる単語のリスト
+        /// </summary>
+        public override CompleteCollection<ICompleteItem> Items
+        {
+            get
+            {
+                return (CompleteCollection<ICompleteItem>)this.listBox1.ItemsSource;
+            }
+            set
+            {
+                this.listBox1.ItemsSource = value;
+                this.listBox1.DisplayMemberPath = CompleteCollection<ICompleteItem>.ShowMember;
+            }
+        }
+
+        public override bool IsCloseCompleteBox
+        {
+            get
+            {
+                return !this.popup.IsOpen;
+            }
+        }
+
+        protected override void RequestShowCompleteBox(ShowingCompleteBoxEventArgs ev)
+        {
+            this.inputedWord = ev.inputedWord;
+            DecideListBoxLocation(this.doc,this.listBox1, ev.CaretPostion);
+            this.listBox1.SelectedIndex = ev.foundIndex;
+            this.listBox1.ScrollIntoView(this.listBox1.SelectedItem);
+            this.popup.IsOpen = true;
+        }
+
+        protected override void RequestCloseCompleteBox()
+        {
+            this.popup.IsOpen = false;
+        }
+
+        void DecideListBoxLocation(Document doc, ListBox listbox, Point p)
+        {
+            int height = (int)doc.LayoutLines.GetLayout(doc.CaretPostion.row).Height;
+
+            if (p.Y + listbox.Height + height > doc.LayoutLines.Render.TextArea.Height)
+                p.Y -= listbox.Height;
+            else
+                p.Y += height;
+
+            Canvas.SetLeft(this.popup, p.X);
+            Canvas.SetTop(this.popup, p.Y);
+        }
+
+        public bool ProcessKeyDown(FooTextBox textbox, KeyRoutedEventArgs e,bool isCtrl,bool isShift)
+        {
+            if (this.popup.IsOpen == false)
+            {
+                if (e.Key == VirtualKey.Space && isCtrl)
+                {
+                    this.OpenCompleteBox(string.Empty);
+                    e.Handled = true;
+
+                    return true;
+                }
+                return false;
+            }
+
+            switch (e.Key)
+            {
+                case VirtualKey.Escape:
+                    this.RequestCloseCompleteBox();
+                    textbox.Focus(Windows.UI.Xaml.FocusState.Programmatic);
+                    e.Handled = true;
+                    return true;
+                case VirtualKey.Down:
+                    if (this.listBox1.SelectedIndex + 1 >= this.listBox1.Items.Count)
+                        this.listBox1.SelectedIndex = this.listBox1.Items.Count - 1;
+                    else
+                        this.listBox1.SelectedIndex++;
+                    this.listBox1.ScrollIntoView(this.listBox1.SelectedItem);
+                    e.Handled = true;
+                    return true;
+                case VirtualKey.Up:
+                    if (this.listBox1.SelectedIndex - 1 < 0)
+                        this.listBox1.SelectedIndex = 0;
+                    else
+                        this.listBox1.SelectedIndex--;
+                    this.listBox1.ScrollIntoView(this.listBox1.SelectedItem);
+                    e.Handled = true;
+                    return true;
+                case VirtualKey.Enter:
+                    this.RequestCloseCompleteBox();
+                    CompleteWord selWord = (CompleteWord)this.listBox1.SelectedItem;
+                    this.SelectItem(this, new SelectItemEventArgs(selWord.word, this.inputedWord, this.Document));
+                    e.Handled = true;
+                    return true;
+            }
+
+            return false;
+        }
+
+        private void ListBox1_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
+        {
+            this.popup.IsOpen = false;
+            CompleteWord selWord = (CompleteWord)this.listBox1.SelectedItem;
+            this.SelectItem(this, new SelectItemEventArgs(selWord.word, this.inputedWord, this.Document));
+        }
+
+        void listBox1_KeyDown(object sender, KeyRoutedEventArgs e)
+        {
+            if (e.Key == VirtualKey.Enter)
+            {
+                this.popup.IsOpen = false;
+                CompleteWord selWord = (CompleteWord)this.listBox1.SelectedItem;
+                this.SelectItem(this, new SelectItemEventArgs(selWord.word, this.inputedWord, this.Document));
+                e.Handled = true;
+            }
+        }
+    }
+}
index c488174..790c42f 100644 (file)
     <PRIResource Include="strings\en-US\Resources.resw" />
   </ItemGroup>
   <ItemGroup>
+    <Compile Include="AutoCompleteBox.cs" />
     <Compile Include="Direct2D\D2DRender.cs" />
     <Compile Include="Direct2D\D2DRenderBase.cs" />
     <Compile Include="FooTextBox.cs" />
index f27755b..d79ef69 100644 (file)
@@ -64,7 +64,7 @@ namespace FooEditEngine.UWP
         public FooTextBox()
         {
             this.DefaultStyleKey = typeof(FooTextBox);
-
+            
             this.rectangle = new Windows.UI.Xaml.Shapes.Rectangle();
             this.rectangle.Margin = this.Padding;
 #if !DUMMY_RENDER
@@ -507,6 +507,11 @@ namespace FooEditEngine.UWP
             bool isControlPressed = this.IsModiferKeyPressed(VirtualKey.Control);
             bool isShiftPressed = this.IsModiferKeyPressed(VirtualKey.Shift);
             bool isMovedCaret = false;
+
+            var autocomplete = this.Document.AutoComplete as AutoCompleteBox;
+            if (autocomplete != null && autocomplete.ProcessKeyDown(this, e, isControlPressed, isShiftPressed))
+                return;
+
             switch (e.Key)
             {
                 case VirtualKey.Up:
@@ -1269,6 +1274,11 @@ namespace FooEditEngine.UWP
                 old_doc.Update -= new DocumentUpdateEventHandler(Document_Update);
                 this._Document.SelectionChanged -= Controller_SelectionChanged;
                 this._Document.LoadProgress -= Document_LoadProgress;
+                if (this._Document.AutoComplete != null)
+                {
+                    this._Document.AutoComplete.GetPostion = null;
+                    this._Document.AutoComplete = null;
+                }
 
                 //NotifyTextChanged()を呼び出すと落ちるのでTextConextをごっそり作り替える
                 this.RemoveTextContext();
@@ -1280,6 +1290,16 @@ namespace FooEditEngine.UWP
             this._Document.LayoutLines.Render = this.Render;
             this._Document.Update += new DocumentUpdateEventHandler(Document_Update);
             this._Document.LoadProgress += Document_LoadProgress;
+            if(this._Document.AutoComplete != null)
+            {
+                this._Document.AutoComplete.GetPostion = (tp) =>
+                {
+                    var p = this.View.GetPostionFromTextPoint(tp);
+                    //AutoCompleteBox内ではCanvasで位置を指定しているので変換する必要がある
+                    var pointInWindow = Util.GetPointInWindow(p, this);
+                  return pointInWindow;
+                };
+            }
             //初期化が終わっていればすべて存在する
             if (this.Controller != null && this.View != null)
             {
index 56157b2..00c4a0e 100644 (file)
@@ -6,6 +6,7 @@ using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 using FooEditEngine;
+using FooEditEngine.UWP;
 
 namespace Test
 {
@@ -15,8 +16,23 @@ namespace Test
 
         public MainViewModel()
         {
-            this._list.Add(new Document() { Title = "test1" });
-            this._list.Add(new Document() { Title = "test2" });
+            var complete_collection = new CompleteCollection<ICompleteItem>();
+            CompleteHelper.AddComleteWord(complete_collection, "int");
+            CompleteHelper.AddComleteWord(complete_collection, "float");
+            CompleteHelper.AddComleteWord(complete_collection, "double");
+            CompleteHelper.AddComleteWord(complete_collection, "char");
+            CompleteHelper.AddComleteWord(complete_collection, "byte");
+            CompleteHelper.AddComleteWord(complete_collection, "var");
+            CompleteHelper.AddComleteWord(complete_collection, "short");
+
+            var doc = new Document() { Title = "test1" };
+            doc.AutoComplete = new AutoCompleteBox(doc);
+            doc.AutoComplete.Items = complete_collection;
+            this._list.Add(doc);
+
+            doc = new Document() { Title = "test2" };
+            this._list.Add(doc);
+
             this.CurrentDocument = this._list[0];
         }