#region Disclaimer / License // Copyright (C) 2009, Kenneth Skovhede // http://www.hexad.dk, opensource@hexad.dk // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; either // version 2.1 of the License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA // #endregion using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Text; using System.Windows.Forms; using OSGeo.MapGuide.MaestroAPI; namespace OSGeo.MapGuide.Maestro { /* * Intellisense overview: * * The intellisense of this expression editor consists of the following parts: * - An ImageListBox which is filled with auto-complete suggestions * - A System.Windows.Forms.ToolTip which is shown when an auto-complete choice is highlighted (but not selected) * * In order to invoke intellisense, we listen for the KeyUp and KeyDown events * on the textbox to determine what actions to take. Some actions include: * * Key Up: * - Comma: Show auto-complete with all suggestions * - Quotes (Single or Double): Insert an extra quote of that type * - Up/Down: Move the auto-complete selection up/down one item if the auto-complete box is visible. * - Backspace: Invoke auto-complete with suggestions if there is a context buffer, otherwise hide auto-complete. * - Alt + Right: Invoke auto-complete with all suggestions * - Alphanumeric (no modifiers): Invoke auto-complete with suggestions * * Key Down: * - Escape: Hide auto-complete * - Enter: Hide auto-complete * * As part of the loading process, a full list of auto-complete items (functions/properties) is constructed (sorted by name) * Everytime intellisense is invoked, this list is queried for possible suggestions. * * In order to determine what items to suggest, the editor builds a context buffer from the current position of the caret * in the textbox. The context buffer algorithm is as follows: * * 1 - Start from caret position * 2 - Can we move back one char? * 2.1 - Get this char. * 2.2 - If alpha numeric, goto 2. * 3 - Get the string that represents the uninterrupted alphanumeric string sequence that ends at the caret position * 4 - Get the list of completable items that starts with this alphanumeric string * 5 - Add these items to the auto-complete box. * 6 - Show the auto-complete box */ public partial class FormExpression : Form { private FeatureSourceDescription.FeatureSourceSchema m_schema; private string m_providername; private ServerConnectionI m_connection; private string m_featureSource = null; public FormExpression() { InitializeComponent(); InitAutoComplete(); } public string Expression { get { return ExpressionText.Text; } set { ExpressionText.Text = value; } } public void SetupForm(ServerConnectionI connection, FeatureSourceDescription.FeatureSourceSchema schema, string provider, string featuresSourceId) { try { m_schema = schema; m_providername = provider; m_connection = connection; m_featureSource = featuresSourceId; FdoProviderCapabilities caps = m_connection.GetProviderCapabilities(provider); //TODO: Perhaps add column type and indication of primary key SortedList sortedCols = new SortedList(); foreach (FeatureSetColumn col in m_schema.Columns) { sortedCols.Add(col.Name, col); } ColumnName.Items.Clear(); ColumnName.Tag = sortedCols; foreach (FeatureSetColumn col in sortedCols.Values) { string name = col.Name; ToolStripButton btn = new ToolStripButton(); btn.Name = name; btn.Text = name; btn.Click += delegate { InsertText(name); }; btnProperties.DropDown.Items.Add(btn); ColumnName.Items.Add(name); } if (ColumnName.Items.Count > 0) ColumnName.SelectedIndex = 0; LoadCompletableProperties(m_schema.Columns); //TODO: Figure out how to translate the enums into something usefull //Functions SortedList sortedFuncs = new SortedList(); foreach (FdoProviderCapabilitiesExpressionFunctionDefinition func in caps.Expression.FunctionDefinitionList) { sortedFuncs.Add(func.Name, func); } foreach (FdoProviderCapabilitiesExpressionFunctionDefinition func in sortedFuncs.Values) { string name = func.Name; ToolStripButton btn = new ToolStripButton(); btn.Name = name; btn.Text = name; btn.ToolTipText = func.Description; string fmt = "{0}({1})"; List args = new List(); foreach (FdoProviderCapabilitiesExpressionFunctionDefinitionArgumentDefinition argDef in func.ArgumentDefinitionList) { args.Add(argDef.Name.Trim()); } string expr = string.Format(fmt, name, string.Join(", ", args.ToArray())); btn.Click += delegate { InsertText(expr); }; btnFunctions.DropDown.Items.Add(btn); } LoadCompletableFunctions(caps.Expression.FunctionDefinitionList); //Spatial Operators foreach (FdoProviderCapabilitiesFilterOperation op in caps.Filter.Spatial) { string name = op.ToString().ToUpper(); ToolStripButton btn = new ToolStripButton(); btn.Name = btn.Text = btn.ToolTipText = op.ToString(); btn.Click += delegate { InsertFilter(name); }; btnSpatial.DropDown.Items.Add(btn); } //Distance Operators foreach (FdoProviderCapabilitiesFilterOperation1 op in caps.Filter.Distance) { string name = op.ToString().ToUpper(); ToolStripButton btn = new ToolStripButton(); btn.Name = btn.Text = btn.ToolTipText = op.ToString(); btn.Click += delegate { InsertFilter(name); }; btnDistance.DropDown.Items.Add(btn); } //Conditional Operators foreach (FdoProviderCapabilitiesFilterOperation op in caps.Filter.Condition) { string name = op.ToString().ToUpper(); ToolStripButton btn = new ToolStripButton(); btn.Name = btn.Text = btn.ToolTipText = op.ToString(); btn.Click += delegate { InsertFilter(name); }; btnCondition.DropDown.Items.Add(btn); } /*try { /*FdoProviderCapabilities cap = m_connection.GetProviderCapabilities(m_providername); foreach (FdoProviderCapabilitiesFilterType cmd in cap.Filter.Condition) FunctionCombo.Items.Add(cmd.ToString()); FunctionLabel.Enabled = FunctionCombo.Enabled = true; } catch { FunctionLabel.Enabled = FunctionCombo.Enabled = false; }*/ } catch { } } private void InsertText(string exprText) { int index = ExpressionText.SelectionStart; if (ExpressionText.SelectionLength > 0) { ExpressionText.SelectedText = exprText; ExpressionText.SelectionStart = index; } else { if (index > 0) { string text = ExpressionText.Text; ExpressionText.Text = text.Insert(index, exprText); ExpressionText.SelectionStart = index; } else { ExpressionText.Text = exprText; ExpressionText.SelectionStart = index; } } } private void InsertFilter(string op) { if (!string.IsNullOrEmpty(op)) { string filterTemplate = " {0} GeomFromText('')"; string exprText = string.Format(filterTemplate, op); InsertText(exprText); } } private void OKBtn_Click(object sender, EventArgs e) { this.DialogResult = DialogResult.OK; this.Close(); } private SortedList _autoCompleteItems = new SortedList(); private ImageListBox _autoBox; enum AutoCompleteItemType : int { Property = 0, Function = 1, } /// /// Base auto-complete item /// abstract class AutoCompleteItem { public abstract AutoCompleteItemType Type { get; } public abstract string Name { get; } public abstract string ToolTipText { get; } public abstract string AutoCompleteText { get; } } /// /// Property auto-complete item /// class PropertyItem : AutoCompleteItem { private FeatureSetColumn _propDef; public PropertyItem(FeatureSetColumn pd) { _propDef = pd; } public override AutoCompleteItemType Type { get { return AutoCompleteItemType.Property; } } public override string Name { get { return _propDef.Name; } } private string _ttText; public override string ToolTipText { get { if (string.IsNullOrEmpty(_ttText)) { _ttText = string.Format(Strings.FormExpression.PropertyTooltip, _propDef.Name, _propDef.Type.Name); } return _ttText; } } public override string AutoCompleteText { get { return this.Name; } } } /// /// Function auto-complete item /// class FunctionItem : AutoCompleteItem { private FdoProviderCapabilitiesExpressionFunctionDefinition _func; public FunctionItem(FdoProviderCapabilitiesExpressionFunctionDefinition fd) { _func = fd; } public override AutoCompleteItemType Type { get { return AutoCompleteItemType.Function; } } public override string Name { get { return _func.Name; } } private string _ttText; public override string ToolTipText { get { if (string.IsNullOrEmpty(_ttText)) _ttText = string.Format(Strings.FormExpression.FunctionTooltip, GetReturnTypeString(), _func.Name, GetArgumentString(), _func.Description); return _ttText; } } private string _argStr; private string GetArgumentString() { if (string.IsNullOrEmpty(_argStr)) { List tokens = new List(); foreach (FdoProviderCapabilitiesExpressionFunctionDefinitionArgumentDefinition argDef in _func.ArgumentDefinitionList) { tokens.Add("[" + argDef.Name.Trim() + "]"); } _argStr = string.Join(", ", tokens.ToArray()); } return _argStr; } private string GetReturnTypeString() { return _func.ReturnType; } public override string AutoCompleteText { get { return this.Name + "(" + GetArgumentString() + ")"; } } } private void InitAutoComplete() { _autoBox = new ImageListBox(); _autoBox.Visible = false; _autoBox.ImageList = new ImageList(); _autoBox.ImageList.Images.Add(Properties.Resources.table); //Property _autoBox.ImageList.Images.Add(Properties.Resources.bricks); //Function _autoBox.DoubleClick += new EventHandler(OnAutoCompleteDoubleClick); _autoBox.SelectedIndexChanged += new EventHandler(OnAutoCompleteSelectedIndexChanged); _autoBox.KeyDown += new KeyEventHandler(OnAutoCompleteKeyDown); _autoBox.KeyUp += new KeyEventHandler(OnAutoCompleteKeyUp); _autoBox.ValueMember = "Name"; _autoBox.Font = new Font(FontFamily.GenericMonospace, 10.0f); ExpressionText.Controls.Add(_autoBox); } void OnAutoCompleteKeyDown(object sender, KeyEventArgs e) { ExpressionText.Focus(); } void OnAutoCompleteKeyUp(object sender, KeyEventArgs e) { if (e.KeyCode == Keys.Return || e.KeyCode == Keys.Enter) { PutAutoCompleteSuggestion(); _autoBox.Hide(); _autoCompleteTooltip.Hide(this); } } void OnAutoCompleteSelectedIndexChanged(object sender, EventArgs e) { ExpressionText.Focus(); if (_autoBox.Visible && _autoBox.SelectedIndex >= 0 && _autoBox.Items.Count > 0) { string tt = ((_autoBox.SelectedItem as ImageListBoxItem).Tag as AutoCompleteItem).ToolTipText; Point pt = GetCaretPoint(); pt.X += _autoBox.Width + 10; pt.Y += 65; _autoCompleteTooltip.Show(tt, this, pt.X, pt.Y); } } void OnAutoCompleteDoubleClick(object sender, EventArgs e) { PutAutoCompleteSuggestion(); _autoBox.Hide(); _autoCompleteTooltip.Hide(this); } private void MoveAutoCompleteSelectionDown() { if (_autoBox.SelectedIndex < 0) { _autoBox.SelectedIndex = 0; } else { int idx = _autoBox.SelectedIndex; if ((idx + 1) <= _autoBox.Items.Count - 1) { _autoBox.SelectedIndex = idx + 1; } } } private void MoveAutoCompleteSelectionUp() { if (_autoBox.SelectedIndex < 0) { _autoBox.SelectedIndex = 0; } else { int idx = _autoBox.SelectedIndex; if ((idx - 1) >= 0) { _autoBox.SelectedIndex = idx - 1; } } } private void LoadCompletableProperties(IEnumerable cols) { foreach (FeatureSetColumn col in cols) { _autoCompleteItems[col.Name] = new PropertyItem(col); } } private void LoadCompletableFunctions(FdoProviderCapabilitiesExpressionFunctionDefinitionCollection funcs) { foreach (FdoProviderCapabilitiesExpressionFunctionDefinition func in funcs) { _autoCompleteItems[func.Name] = new FunctionItem(func); } } private void PutAutoCompleteSuggestion() { if (_autoBox.SelectedItems.Count == 1) { int pos = ExpressionText.SelectionStart; string context; char? c = GetContextBuffer(out context); AutoCompleteItem aci = (_autoBox.SelectedItem as ImageListBoxItem).Tag as AutoCompleteItem; string fullText = aci.AutoCompleteText; int start = pos - context.Length; int newPos = start + fullText.Length; int selLength = -1; //if it's a function, highlight the parameter (or the first parameter if there is multiple arguments if (aci.Type == AutoCompleteItemType.Function) { newPos = start + aci.Name.Length + 1; //Position the caret just after the opening bracket //Has at least two arguments int idx = fullText.IndexOf(","); if (idx > 0) selLength = idx - aci.Name.Length - 1; else selLength = fullText.IndexOf(")") - fullText.IndexOf("(") - 1; } string prefix = ExpressionText.Text.Substring(0, start); string suffix = ExpressionText.Text.Substring(pos, ExpressionText.Text.Length - pos); ExpressionText.Text = prefix + fullText + suffix; ExpressionText.SelectionStart = newPos; if (selLength > 0) { ExpressionText.SelectionLength = selLength; } ExpressionText.ScrollToCaret(); } } private Point GetCaretPoint() { Point pt = ExpressionText.GetPositionFromCharIndex(ExpressionText.SelectionStart); pt.Y += (int)Math.Ceiling(ExpressionText.Font.GetHeight()) + 2; pt.X += 2; // for Courier, may need a better method return pt; } private char? GetContextBuffer(out string buffer) { buffer = string.Empty; int caretPos = ExpressionText.SelectionStart; int currentPos = caretPos; char? res = null; if (caretPos > 0) { //Walk backwards caretPos--; char c = ExpressionText.Text[caretPos]; while (Char.IsLetterOrDigit(c)) { caretPos--; if (caretPos < 0) break; c = ExpressionText.Text[caretPos]; } if (caretPos > 0) { res = ExpressionText.Text[caretPos]; } buffer = ExpressionText.Text.Substring(caretPos + 1, currentPos - caretPos - 1); } return res; } private void HandleKeyDown(KeyEventArgs e) { Keys code = e.KeyCode; if (code == Keys.Escape) { if (_autoBox.Visible) { e.SuppressKeyPress = true; _autoBox.Hide(); _autoCompleteTooltip.Hide(this); } } else if (code == Keys.Enter || code == Keys.Return) { if (_autoBox.Visible && _autoBox.SelectedItems.Count == 1) { e.SuppressKeyPress = true; PutAutoCompleteSuggestion(); _autoBox.Hide(); _autoCompleteTooltip.Hide(this); } } } private void HandleKeyUp(KeyEventArgs e) { Keys code = e.KeyCode; if (code == Keys.Oemcomma || code == Keys.OemOpenBrackets) { Complete(string.Empty); } else if (code == Keys.OemQuotes) { if (e.Modifiers == Keys.Shift) // " InsertText("\""); else // ' InsertText("'"); } else if (code == Keys.D9 && e.Modifiers == Keys.Shift) // ( { InsertText(")"); } else if (code == Keys.Up || code == Keys.Down) { if (_autoBox.Visible) { if (code == Keys.Up) { MoveAutoCompleteSelectionUp(); } else { MoveAutoCompleteSelectionDown(); } } } else if (code == Keys.Back) { string context; char? c = GetContextBuffer(out context); if (!string.IsNullOrEmpty(context)) { Complete(context); } else { if (_autoBox.Visible) { _autoBox.Hide(); _autoCompleteTooltip.Hide(this); } } } else if (e.Modifiers == Keys.Alt && e.KeyCode == Keys.Right) { string context; char? c = GetContextBuffer(out context); Complete(context); } else { if (e.Modifiers == Keys.None) { bool alpha = (code >= Keys.A && code <= Keys.Z); bool numeric = (code >= Keys.D0 && code <= Keys.D9) || (code >= Keys.NumPad0 && code <= Keys.NumPad9); if (alpha || numeric) { string context; char? c = GetContextBuffer(out context); Complete(context); } } } } private List GetItemsStartingWith(string text) { List ati = new List(); foreach (string key in _autoCompleteItems.Keys) { if (key.ToLower().StartsWith(text.Trim().ToLower())) { ati.Add(_autoCompleteItems[key]); } } return ati; } private void Complete(string text) { List items = GetItemsStartingWith(text); _autoBox.Items.Clear(); int width = 0; foreach (AutoCompleteItem it in items) { ImageListBoxItem litem = new ImageListBoxItem(); litem.Text = it.Name; litem.ImageIndex = (int)it.Type; litem.Tag = it; _autoBox.Items.Add(litem); int length = TextRenderer.MeasureText(it.Name, _autoBox.Font).Width + 30; //For icon size if (length > width) width = length; } _autoBox.Width = width; if (!_autoBox.Visible) { if (_autoBox.Items.Count > 0) { _autoBox.BringToFront(); _autoBox.Show(); } } Point pt = GetCaretPoint(); _autoBox.Location = pt; } private void ExpressionText_KeyDown(object sender, KeyEventArgs e) { HandleKeyDown(e); } private void ExpressionText_KeyUp(object sender, KeyEventArgs e) { HandleKeyUp(e); } private void ColumnName_Click(object sender, EventArgs e) { } private void ColumnName_SelectedIndexChanged(object sender, EventArgs e) { ColumnValue.Enabled = false; LookupValues.Enabled = ColumnName.SelectedIndex >= 0; } private void LookupValues_Click(object sender, EventArgs e) { using(new WaitCursor(this)) try { SortedList cols = (SortedList)ColumnName.Tag; FeatureSetColumn col = cols[ColumnName.Text]; bool retry = true; Exception rawEx = null; string filter = null; SortedList values = new SortedList(); bool hasNull = false; while (retry) try { retry = false; using (FeatureSetReader rd = m_connection.QueryFeatureSource(m_featureSource, m_schema.FullnameDecoded, filter, new string[] { ColumnName.Text })) while (rd.Read()) if (rd.Row.IsValueNull(ColumnName.Text)) hasNull = true; else values[Convert.ToString(rd.Row[ColumnName.Text], System.Globalization.CultureInfo.InvariantCulture)] = null; } catch (Exception ex) { if (filter == null && ex.Message.IndexOf("MgNullPropertyValueException") >= 0) { hasNull = true; rawEx = ex; retry = true; filter = ColumnName.Text + " != NULL"; } else if (rawEx != null) throw rawEx; else throw; } ColumnValue.Items.Clear(); if (hasNull) ColumnValue.Items.Add("NULL"); foreach (string s in values.Keys) ColumnValue.Items.Add(s); ColumnValue.Tag = col.Type; if (ColumnValue.Items.Count == 0) MessageBox.Show(this, Strings.FormExpression.NoColumnValuesError, Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Information); else { ColumnValue.Enabled = true; ColumnValue.SelectedIndex = -1; ColumnValue.DroppedDown = true; } } catch (Exception ex) { string msg = NestedExceptionMessageProcessor.GetFullMessage(ex); MessageBox.Show(this, string.Format(Strings.FormExpression.ColumnValueError, msg), Application.ProductName, MessageBoxButtons.OK, MessageBoxIcon.Information); } } private void ColumnValue_SelectedIndexChanged(object sender, EventArgs e) { if (ColumnValue.SelectedIndex >= 0 && ColumnValue.Tag as Type != null) { if (ColumnValue.Tag == typeof(string) && (ColumnValue.SelectedIndex != 0 || ColumnValue.Text != "NULL")) ExpressionText.SelectedText = " '" + ColumnValue.Text + "' "; else ExpressionText.SelectedText = " " + ColumnValue.Text + " "; } } } // ImageListBoxItem class public class ImageListBoxItem { private string _myText; private int _myImageIndex; // properties public string Text { get { return _myText; } set { _myText = value; } } public int ImageIndex { get { return _myImageIndex; } set { _myImageIndex = value; } } //constructor public ImageListBoxItem(string text, int index) { _myText = text; _myImageIndex = index; } public ImageListBoxItem(string text) : this(text, -1) { } public ImageListBoxItem() : this("") { } private object _tag; public object Tag { get { return _tag; } set { _tag = value; } } public override string ToString() { return _myText; } }//End of ImageListBoxItem class // ImageListBox class // // Based on GListBox // // http://www.codeproject.com/KB/combobox/glistbox.aspx public class ImageListBox : ListBox { private ImageList _myImageList; public ImageList ImageList { get { return _myImageList; } set { _myImageList = value; } } public ImageListBox() { // Set owner draw mode this.DrawMode = DrawMode.OwnerDrawFixed; } protected override void OnDrawItem(System.Windows.Forms.DrawItemEventArgs e) { e.DrawBackground(); e.DrawFocusRectangle(); ImageListBoxItem item; Rectangle bounds = e.Bounds; Size imageSize = _myImageList.ImageSize; try { item = (ImageListBoxItem)Items[e.Index]; if (item.ImageIndex != -1) { _myImageList.Draw(e.Graphics, bounds.Left, bounds.Top, item.ImageIndex); e.Graphics.DrawString(item.Text, e.Font, new SolidBrush(e.ForeColor), bounds.Left + imageSize.Width, bounds.Top); } else { e.Graphics.DrawString(item.Text, e.Font, new SolidBrush(e.ForeColor), bounds.Left, bounds.Top); } } catch { if (e.Index != -1) { e.Graphics.DrawString(Items[e.Index].ToString(), e.Font, new SolidBrush(e.ForeColor), bounds.Left, bounds.Top); } else { e.Graphics.DrawString(Text, e.Font, new SolidBrush(e.ForeColor), bounds.Left, bounds.Top); } } base.OnDrawItem(e); } }//End of ImageListBox class }