diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj index d0e5f88..e9e4b33 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj @@ -226,9 +226,13 @@ Component + + Component + Component + UserControl diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KAbstractComboBox.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KAbstractComboBox.cs index d335623..7d7fd52 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KAbstractComboBox.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KAbstractComboBox.cs @@ -1,4 +1,5 @@ using Acacia.Native; +using Acacia.Utils; using System; using System.Collections.Generic; using System.ComponentModel; @@ -11,10 +12,13 @@ using System.Windows.Forms; namespace Acacia.Controls { - public abstract class KAbstractComboBox : ContainerControl, IMessageFilter + public abstract class KAbstractComboBox : ContainerControl { #region Properties + /// + /// Hide the AutoSize property, it is always enabled + /// [Browsable(false), EditorBrowsable(EditorBrowsableState.Never)] override public bool AutoSize { get { return base.AutoSize; } set { base.AutoSize = value; } } @@ -40,12 +44,41 @@ namespace Acacia.Controls set { _edit.PlaceholderFont = value; } } + /// + /// The control to set in the drop-down + /// + protected Control DropControl + { + get + { + return _dropDown?.Control; + } + set + { + _dropDown = new DropDown(this, value); + } + } - #endregion + protected int _settingText = 0; - #region Components - - private KTextBox _edit; + override public string Text + { + get { return _edit.Text; } + set + { + ++_settingText; + try + { + _edit.Text = value; + // Set the cursor after the text + _edit.Select(_edit.Text.Length, 0); + } + finally + { + --_settingText; + } + } + } #endregion @@ -55,28 +88,24 @@ namespace Acacia.Controls { AutoSize = true; SetupRenderer(); - - _edit = new KTextBox(); - _edit.BorderStyle = BorderStyle.None; - Controls.Add(_edit); - _state.AddControl(_edit); - _edit.TextChanged += _edit_TextChanged; + SetupEdit(); } - #endregion #region Text edit - private void _edit_TextChanged(object sender, EventArgs e) - { - OnTextChanged(new EventArgs()); - } + private KTextBox _edit; - override public string Text + private void SetupEdit() { - get { return _edit.Text; } - set { _edit.Text = value; } + _edit = new KTextBox(); + _edit.BorderStyle = BorderStyle.None; + Controls.Add(_edit); + _state.AddControl(_edit); + _edit.TextChanged += _edit_TextChanged; + _edit.LostFocus += _edit_LostFocus; + _edit.PreviewKeyDown += _edit_PreviewKeyDown; } public void FocusEdit() @@ -84,138 +113,177 @@ namespace Acacia.Controls _edit.Select(); } + private void _edit_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e) + { + switch (e.KeyCode) + { + case Keys.Escape: + // Escape closes the drop-down + if (DroppedDown) + { + DroppedDown = false; + // Grab the keypress to prevent closing a dialog + e.IsInputKey = true; + return; + } + break; + case Keys.Down: + // Down opens the drop down + if (!DroppedDown) + { + DroppedDown = true; + e.IsInputKey = true; + return; + } + break; + } + OnPreviewKeyDown(e); + } + + private void _edit_LostFocus(object sender, EventArgs e) + { + // Close the drop down when losing focus. This also handles the case when another window is selected, + // as that causes the focus to be taken away + DroppedDown = false; + } + + private void _edit_TextChanged(object sender, EventArgs e) + { + OnTextChanged(new EventArgs()); + } + #endregion #region Drop down - public Control DropControl + /// + /// Custom drop down. Registers a message filter when shown to close on clicks outside the drop-down. + /// This is required as the default AutoClose behaviour consumes all keyboard events. + /// + private class DropDown : ToolStripDropDown, IMessageFilter { - get + /// + /// Custom renderer that renders the border using the combo focus style. + /// + private class DropDownRenderer : ToolStripRenderer { - return _dropControl; + private readonly KVisualStyle.Part _style; + + public DropDownRenderer(KVisualStyle.Part style) + { + this._style = style; + } + + protected override void OnRenderToolStripBorder(ToolStripRenderEventArgs e) + { + _style.DrawBackground(e.Graphics, State.Pressed, e.AffectedBounds); + } } - set + + private readonly KAbstractComboBox _owner; + + public Control Control { - _dropControl = value; - SetupDropDown(); + get { return ControlHost.Control; } } - } - private ToolStripDropDown _dropDown; - private Control _dropControl; - private ToolStripControlHost _dropListHost; - - private void SetupDropDown() - { - _dropListHost = new ToolStripControlHost(_dropControl); - _dropListHost.Padding = new Padding(0); - _dropListHost.Margin = new Padding(0); - _dropListHost.AutoSize = false; - - _dropDown = new ToolStripDropDown(); - _dropDown.Padding = new Padding(0); - _dropDown.Margin = new Padding(0); - _dropDown.AutoSize = true; - _dropDown.DropShadowEnabled = false; - _dropDown.Items.Add(_dropListHost); - _dropDown.Closed += _dropDown_Closed; - _dropDown.AutoClose = false; - - Application.AddMessageFilter(this); - } - - protected override void OnHandleDestroyed(EventArgs e) - { - Application.RemoveMessageFilter(this); - base.OnHandleDestroyed(e); - } - - public bool PreFilterMessage(ref Message m) - { - switch ((WM)m.Msg) + public ToolStripControlHost ControlHost { - case WM.KEYDOWN: - System.Diagnostics.Trace.WriteLine("KEYMESSAGE: " + m); - switch((VirtualKeys)m.WParam.ToInt32()) - { - case VirtualKeys.Escape: - // Escape closes the popup - if (DroppedDown) - { - DroppedDown = false; - return true; - } - break; - case VirtualKeys.Down: - // Down opens the drop down - if (!DroppedDown) - { - DroppedDown = true; - return true; - } - break; - } - break; - case WM.CHAR: - case WM.KEYUP: - System.Diagnostics.Trace.WriteLine("KEYMESSAGE: " + m); - break; - case WM.LBUTTONDOWN: - case WM.RBUTTONDOWN: - case WM.MBUTTONDOWN: - case WM.NCLBUTTONDOWN: - case WM.NCRBUTTONDOWN: - case WM.NCMBUTTONDOWN: - if (_dropDown.Visible) - { - // - // When a mouse button is pressed, we should determine if it is within the client coordinates - // of the active dropdown. If not, we should dismiss it. - // - int i = unchecked((int)(long)m.LParam); - short x = (short)(i & 0xFFFF); - short y = (short)((i >> 16) & 0xffff); - Point pt = new Point(x, y); + get { return (ToolStripControlHost)Items[0]; } + } - // Map to global coordinates - User32.MapWindowPoints(m.HWnd, IntPtr.Zero, ref pt, 1); - System.Diagnostics.Trace.WriteLine(string.Format("MOUSE: {0} - {1}", pt, _dropDown.Bounds)); - if (!_dropDown.Bounds.Contains(pt)) + public DropDown(KAbstractComboBox owner, Control control) + { + this._owner = owner; + + KVisualStyle.Part style = owner._style[COMBOBOXPARTS.CP_BORDER]; + Renderer = new DropDownRenderer(style); + using (Graphics graphics = CreateGraphics()) + { + Padding = style?.GetMargins(graphics, State.Pressed) ?? new Padding(); + } + + Margin = new Padding(0); + AutoSize = true; + DropShadowEnabled = false; + AutoClose = false; + + // Add a host for the control + ToolStripControlHost host = new ToolStripControlHost(control); + host.Padding = new Padding(0); + host.Margin = new Padding(0); + host.AutoSize = true; + Items.Add(host); + } + + protected override void OnVisibleChanged(EventArgs e) + { + base.OnVisibleChanged(e); + + // Only register the message filter when it can do something useful + if (Visible) + Application.AddMessageFilter(this); + else + Application.RemoveMessageFilter(this); + } + + public bool PreFilterMessage(ref Message m) + { + // Handle mouse clicks to close the popup + switch ((WM)m.Msg) + { + case WM.LBUTTONDOWN: + case WM.RBUTTONDOWN: + case WM.MBUTTONDOWN: + return CheckMouseDown(m, false); + case WM.NCLBUTTONDOWN: + case WM.NCRBUTTONDOWN: + case WM.NCMBUTTONDOWN: + return CheckMouseDown(m, true); + } + return false; + } + + private bool CheckMouseDown(Message m, bool nonClient) + { + Point pt = User32.GetPointLParam(m.LParam); + Point ptOrig = pt; + if (!nonClient) + { + // Map to global coordinates, non-client ones already are + User32.MapWindowPoints(m.HWnd, IntPtr.Zero, ref pt, 1); + } + + // Check if the click was inside the drop-down + if (!Bounds.Contains(pt)) + { + // Outside the drop-down, check if it was inside the combo box + + // Map to the combo box coordinates + User32.MapWindowPoints(IntPtr.Zero, _owner.Handle, ref pt, 1); + if (_owner.ClientRectangle.Contains(pt)) + { + // Clicked inside the combo box. If the click was on the button, return true to prevent opening + // the popup again. + if (_owner._stateButton.Rectangle.Contains(pt)) { - // the user has clicked outside the dropdown - User32.MapWindowPoints(m.HWnd, Handle, ref pt, 1); - if (!ClientRectangle.Contains(pt)) - { - // the user has clicked outside the combo - DroppedDown = false; - } + return true; } } - break; + else + { + // Outside the drop-down, close it + Close(); + } + } + return false; } - return false; } - // Cannot use visibility of _dropDown to keep the open state, as clicking on the button already - // hides the popup before the event handler is shown. - private bool _isDroppedDown; - private bool _clickedButton; - - private void _dropDown_Closed(object sender, ToolStripDropDownClosedEventArgs e) - { - /*if (_stateButton.IsMouseOver) - { - _clickedButton = true; - }*/ - _isDroppedDown = false; - } + private DropDown _dropDown; private void Button_Clicked() { - /*if (_clickedButton) - _clickedButton = false; - else - DroppedDown = true;*/ DroppedDown = !DroppedDown; this._edit.Focus(); } @@ -225,30 +293,54 @@ namespace Acacia.Controls { get { - return _isDroppedDown; + return _dropDown?.Visible == true; } set { - if (value != _isDroppedDown) + if (value != DroppedDown) { if (value) { - _dropListHost.Control.Width = this.Width; - _dropListHost.Control.Height = 200; - _dropListHost.Control.Refresh(); - _dropDown.Show(this.PointToScreen(new Point(0, Height))); - _dropDown.Capture = true; + ShowDropDown(); } else { _dropDown.Close(); } - _isDroppedDown = value; } } } + private void ShowDropDown() + { + UpdateDropDownLayout(); + + // Show the drop down below the current control + _dropDown.Show(this.PointToScreen(new Point(0, Height - 1))); + } + + protected void UpdateDropDownLayout() + { + if (_dropDown == null) + return; + + // Calculate the dimensions of the drop-down + int maxHeight = GetDropDownHeightMax(); + int minHeight = GetDropDownHeightMin(); + + Size prefSize = DropControl.GetPreferredSize(new Size(Width - _dropDown.Padding.Horizontal, maxHeight - _dropDown.Padding.Vertical)); + int width = Util.Bound(prefSize.Width, Width - _dropDown.Padding.Horizontal, Width * 2); + int height = Util.Bound(prefSize.Height, minHeight, maxHeight); + + DropControl.MaximumSize = DropControl.MinimumSize = new Size(width, height); + + _dropDown.Control.Bounds = _dropDown.ControlHost.Bounds; + } + + protected abstract int GetDropDownHeightMax(); + protected abstract int GetDropDownHeightMin(); + #endregion #region Rendering @@ -306,7 +398,6 @@ namespace Acacia.Controls protected override void OnPaint(PaintEventArgs e) { _style[COMBOBOXPARTS.CP_BORDER]?.DrawBackground(e.Graphics, _state.Root.State, ClientRectangle); - System.Diagnostics.Trace.WriteLine(string.Format("BUTTON: {0}", _stateButton.State)); _style[COMBOBOXPARTS.CP_DROPDOWNBUTTON]?.DrawBackground(e.Graphics, _stateButton.State, _stateButton.Rectangle); } diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KComboBox.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KComboBox.cs index 463d29a..3d613d5 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KComboBox.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KComboBox.cs @@ -1,4 +1,6 @@ -using System; +using Acacia.Native; +using Acacia.Utils; +using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; @@ -12,7 +14,108 @@ namespace Acacia.Controls { public class KComboBox : KAbstractComboBox { - private readonly ListBox _list; + #region Drop-down list + + /// + /// Custom list for the drop-down. Performs a few functions: + /// - Prevents grabbing the focus away from the edit when clicked + /// - Adds hover highlighting + /// - Only commits selection when clicked or externally (through enter in the edit). + /// This prevents updating the text and associated filters when scrolling through the combo. + /// + private class DropList : ListBox + { + private readonly KComboBox _owner; + private int _committedIndex = -1; + public int ItemWidth { get; set; } + + public DropList(KComboBox owner, bool ownerDraw) + { + this._owner = owner; + SetStyle(ControlStyles.OptimizedDoubleBuffer, true); + SetStyle(ControlStyles.Selectable, false); + BorderStyle = BorderStyle.None; + + if (ownerDraw) + { + DrawMode = DrawMode.OwnerDrawFixed; + } + } + + protected override void OnDrawItem(DrawItemEventArgs e) + { + _owner.OnDrawItem(e); + } + + protected override void OnMouseMove(MouseEventArgs e) + { + // Perform the select to highlight + SelectedIndex = IndexFromPoint(PointToClient(Cursor.Position)); + } + + protected override void OnMouseLeave(EventArgs e) + { + base.OnMouseLeave(e); + ResetSelectedIndex(); + } + + protected override void OnVisibleChanged(EventArgs e) + { + base.OnVisibleChanged(e); + } + + private void ResetSelectedIndex() + { + SelectedIndex = _committedIndex >= Items.Count ? -1 : _committedIndex; + } + + protected override void OnMouseDown(MouseEventArgs e) + { + // Select the item under the mouse and commit + SelectedIndex = IndexFromPoint(PointToClient(Cursor.Position)); + CommitSelection(); + } + + protected override void DefWndProc(ref Message m) + { + switch ((WM)m.Msg) + { + // Prevent mouse activity from grabbing the focus away from the edit + case WM.MOUSEACTIVATE: + m.Result = (IntPtr)MA.NOACTIVATE; + return; + } + base.DefWndProc(ref m); + } + + public override Size GetPreferredSize(Size proposedSize) + { + // Preferred size is simply the size of the (maximum) number of items + Size prefSize = base.GetPreferredSize(proposedSize); + int w = Math.Max(prefSize.Width, ItemWidth); + return new Size(w, ItemHeight * Math.Min(Items.Count, _owner.MaxDropDownItems)); + } + + public void CommitSelection() + { + _committedIndex = SelectedIndex; + base.OnSelectedIndexChanged(new EventArgs()); + } + + protected override void OnSelectedIndexChanged(EventArgs e) + { + // Don't notify until committed + } + + public void ItemsChanged(int selectIndex) + { + _committedIndex = SelectedIndex = selectIndex; + } + } + + private readonly DropList _list; + + #endregion #region Items properties @@ -26,13 +129,6 @@ namespace Acacia.Controls [Category("Behavior")] public int ItemHeight { get { return _list.ItemHeight; } set { _list.ItemHeight = value; } } - [DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] - [Editor("System.Windows.Forms.Design.ListControlStringCollectionEditor, System.Design, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", typeof(UITypeEditor))] - [Localizable(true)] - [MergableProperty(false)] - [Category("Behavior")] - public ListBox.ObjectCollection Items { get { return _list.Items; } } - [DefaultValue(8)] [Localizable(true)] [Category("Behavior")] @@ -40,37 +136,262 @@ namespace Acacia.Controls #endregion - public KComboBox() + private DisplayItem _selectedItem; + + public KComboBox() : this(false) { + } + + protected internal KComboBox(bool ownerDraw) + { MaxDropDownItems = 8; - _list = new ListBox(); + _list = new DropList(this, ownerDraw); _list.IntegralHeight = true; + _list.TabStop = false; + _list.SelectedIndexChanged += _list_SelectedIndexChanged; DropControl = _list; - _list.DisplayMember = "DisplayName"; // TODO: remove from here } - public void BeginUpdate() + private void _list_SelectedIndexChanged(object sender, EventArgs e) { + if (_list.SelectedIndex >= 0) + { + _selectedItem = (DisplayItem)_list.SelectedItem; + Text = _selectedItem.ToString(); + } + else + { + Text = ""; + _selectedItem = null; + } + OnSelectedItemChanged(); + } + + public DisplayItem SelectedItem + { + get { return _selectedItem; } + } + + public void Select(object data) + { + _list.SelectedIndex = -1; + Text = null; + _selectedItem = null; + if (data != null) + { + foreach (DisplayItem item in DisplayItems) + { + if (item.Item.Equals(data)) + { + _list.SelectedItem = item; + _selectedItem = item; + break; + } + } + } + } + + public event EventHandler SelectedItemChanged; + + protected virtual void OnSelectedItemChanged() + { + SelectedItemChanged?.Invoke(this, new EventArgs()); + } + + + /// + /// Wrapper for list items to use custom string formatting + /// + public class DisplayItem + { + private readonly KComboBox _owner; + public readonly object Item; + + public DisplayItem(KComboBox owner, object item) + { + this._owner = owner; + this.Item = item; + } + + public override string ToString() + { + return _owner.DataSource.GetItemText(Item); + } + + public override bool Equals(object obj) + { + return obj is DisplayItem && ((DisplayItem)obj).Item == Item; + } + + public override int GetHashCode() + { + return Item.GetHashCode(); + } + } + + private KDataSourceRaw _dataSource; + public KDataSourceRaw DataSource + { + get { return _dataSource; } + set + { + if (_dataSource != value) + { + _dataSource = value; + _displayItemCache.Clear(); + UpdateItems(); + } + } + } + + private readonly Dictionary _displayItemCache = new Dictionary(); + + private void UpdateItems() + { + int oldCount = _list.Items.Count; _list.BeginUpdate(); + try + { + _list.Items.Clear(); + int selected = -1; + foreach (object item in _dataSource.FilteredItems) + { + DisplayItem displayItem; + if (!_displayItemCache.TryGetValue(item, out displayItem)) + { + displayItem = new DisplayItem(this, item); + _displayItemCache.Add(item, displayItem); + } + + if (displayItem == _selectedItem) + selected = _list.Items.Count; + _list.Items.Add(displayItem); + } + + if (_list.Items.Count == 0) + { + // Create a not-found item if requested + object item = _dataSource.NotFoundItem; + if (item != null) + { + _list.Items.Add(new DisplayItem(this, item)); + } + } + + // Select the current item only if new number of items is smaller. This means we don't keep selection + // when the user is removing text, only when they are typing more. + _list.ItemsChanged(_list.Items.Count < oldCount ? selected : -1); + + MeasureItems(); + UpdateDropDownLayout(); + } + finally + { + _list.EndUpdate(); + } } - public void EndUpdate() - { - _list.EndUpdate(); - } - - public object DataSource + protected IEnumerable DisplayItems { get { - return _list.DataSource; + foreach (object item in _list.Items) + yield return (DisplayItem)item; } + } - set + protected DisplayItem GetDisplayItem(int index) + { + return (DisplayItem)_list.Items[index]; + } + + protected int DisplayItemCount + { + get { return _list.Items.Count; } + } + + virtual protected void OnDrawItem(DrawItemEventArgs e) { } + + protected virtual void MeasureItems() + { + // Virtual placeholder + } + + protected void SetItemSize(Size size) + { + ItemHeight = size.Height; + _list.ItemWidth = size.Width; + } + + protected override void OnTextChanged(EventArgs e) + { + base.OnTextChanged(e); + + // Update the filter + if (DataSource != null) { - _list.BindingContext = new BindingContext(); - _list.DataSource = value; + DataSource.Filter = new KDataFilter(Text); + UpdateItems(); + + if (_settingText == 0) + { + DroppedDown = true; + } } } + + protected override int GetDropDownHeightMax() + { + return Util.Bound(_list.Items.Count, 1, MaxDropDownItems) * ItemHeight + _list.Margin.Vertical; + } + + protected override int GetDropDownHeightMin() + { + return ItemHeight; + } + + protected override void OnPreviewKeyDown(PreviewKeyDownEventArgs e) + { + switch(e.KeyCode) + { + // Forward cursor keys to the list + case Keys.Down: + case Keys.Up: + case Keys.PageDown: + case Keys.PageUp: + case Keys.Home: + case Keys.End: + User32.SendMessage(_list.Handle, (int)WM.KEYDOWN, new IntPtr((int)e.KeyCode), IntPtr.Zero); + e.IsInputKey = true; + break; + + // Enter commits the selected index and closes the drop down + case Keys.Enter: + case Keys.Tab: + if (DroppedDown) + { + if (_list.SelectedIndex >= 0) + _list.CommitSelection(); + DroppedDown = false; + } + e.IsInputKey = e.KeyCode == Keys.Enter; + break; + default: + base.OnPreviewKeyDown(e); + break; + } + } + + protected override void DefWndProc(ref Message m) + { + switch ((WM)m.Msg) + { + // Forward mouse wheel messages to the list + case WM.MOUSEWHEEL: + m.Result = (IntPtr) User32.SendMessage(_list.Handle, m.Msg, m.WParam, m.LParam); + return; + } + base.DefWndProc(ref m); + } } } diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KComboBoxCustomDraw.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KComboBoxCustomDraw.cs new file mode 100644 index 0000000..5f5f86a --- /dev/null +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KComboBoxCustomDraw.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace Acacia.Controls +{ + abstract public class KComboBoxCustomDraw : KComboBox + { + public class MeasureItemEventArgs : EventArgs + { + public readonly Graphics Graphics; + public readonly DisplayItem DisplayItem; + public object Item { get { return DisplayItem.Item; } } + public int ItemWidth { get; set; } + public int ItemHeight { get; set; } + + public MeasureItemEventArgs(Graphics graphics, DisplayItem item) + { + this.Graphics = graphics; + this.DisplayItem = item; + } + } + + public class DrawItemEventArgs : System.Windows.Forms.DrawItemEventArgs + { + public readonly DisplayItem DisplayItem; + + public object Item { get { return DisplayItem.Item; } } + + public DrawItemEventArgs(System.Windows.Forms.DrawItemEventArgs e, DisplayItem item) + : + base(e.Graphics, e.Font, e.Bounds, e.Index, e.State, e.ForeColor, e.BackColor) + { + DisplayItem = item; + } + + } + + public KComboBoxCustomDraw() : base(true) + { + } + + sealed protected override void OnDrawItem(System.Windows.Forms.DrawItemEventArgs e) + { + OnDrawItem(new DrawItemEventArgs(e, GetDisplayItem(e.Index))); + } + + protected abstract void OnDrawItem(DrawItemEventArgs e); + + protected abstract void OnMeasureItem(MeasureItemEventArgs e); + + private readonly Dictionary _sizeCache = new Dictionary(); + + protected override void MeasureItems() + { + int maxWidth = 0, maxHeight = 0; + using (Graphics graphics = CreateGraphics()) + { + foreach (DisplayItem item in DisplayItems) + { + Size s; + if (!_sizeCache.TryGetValue(item, out s)) + { + MeasureItemEventArgs e = new MeasureItemEventArgs(graphics, item); + OnMeasureItem(e); + s = new Size(e.ItemWidth, e.ItemHeight); + _sizeCache.Add(item, s); + } + + maxWidth = Math.Max(maxWidth, s.Width); + maxHeight = Math.Max(maxHeight, s.Height); + } + } + + if (maxHeight > 0) + { + SetItemSize(new Size(maxWidth, maxHeight)); + } + } + } +} diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KDataSource.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KDataSource.cs new file mode 100644 index 0000000..6c416c5 --- /dev/null +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KDataSource.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Acacia.Controls +{ + public class KDataFilter + { + public readonly string FilterText; + + public KDataFilter(string filterText) + { + this.FilterText = filterText; + } + } + + public interface KDataSourceRaw + { + System.Collections.IEnumerable Items { get; } + System.Collections.IEnumerable FilteredItems { get; } + KDataFilter Filter { get; set; } + string GetItemText(object item); + object NotFoundItem { get; } + } + + abstract public class KDataSource : KDataSourceRaw + { + /// + /// Returns all the items + /// + abstract public IEnumerable Items + { + get; + } + + public IEnumerable FilteredItems + { + get + { + if (string.IsNullOrWhiteSpace(Filter?.FilterText)) + return Items; + + return ApplyFilter(); + } + } + + private IEnumerable ApplyFilter() + { + foreach (T item in Items) + { + if (MatchesFilter(item)) + yield return item; + } + } + + virtual protected bool MatchesFilter(T item) + { + return GetItemText(item).StartsWith(Filter.FilterText); + } + + abstract protected string GetItemText(T item); + + public string GetItemText(object item) + { + return GetItemText((T)item); + } + + public KDataFilter Filter + { + get; + set; + } + + virtual public object NotFoundItem + { + get { return null; } + } + + IEnumerable KDataSourceRaw.Items { get{return Items;}} + IEnumerable KDataSourceRaw.FilteredItems { get { return FilteredItems; } } + } +} diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KUIUtil.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KUIUtil.cs index 2e4d15f..07ad8be 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KUIUtil.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KUIUtil.cs @@ -24,7 +24,7 @@ using System.Windows.Forms; namespace Acacia.Controls { - internal static class KUIUtil + public static class KUIUtil { #region Geometry @@ -126,6 +126,11 @@ namespace Acacia.Controls return r; } + public static Point TopLeft(this Rectangle _this) + { + return new Point(_this.Left, _this.Top); + } + #endregion } } diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/SharedFoldersDialog.Designer.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/SharedFoldersDialog.Designer.cs index 479ae06..8834769 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/SharedFoldersDialog.Designer.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/SharedFoldersDialog.Designer.cs @@ -35,25 +35,23 @@ this._layoutMain = new System.Windows.Forms.TableLayoutPanel(); this._layoutSelectUser = new System.Windows.Forms.TableLayoutPanel(); this.labelSelectUser = new System.Windows.Forms.Label(); - this.buttonOpenUser = new System.Windows.Forms.Button(); - this._layoutCenterGABLookup = new System.Windows.Forms.TableLayoutPanel(); this.gabLookup = new Acacia.UI.GABLookupControl(); + this.buttonOpenUser = new System.Windows.Forms.Button(); this.kTreeFolders = new Acacia.Controls.KTree(); this._layoutOptions = new System.Windows.Forms.TableLayoutPanel(); this._labelName = new System.Windows.Forms.Label(); this.textName = new System.Windows.Forms.TextBox(); this._labelSendAs = new System.Windows.Forms.Label(); this.checkSendAs = new System.Windows.Forms.CheckBox(); - this._labelPermissions = new System.Windows.Forms.Label(); - this.labelPermissionsValue = new System.Windows.Forms.Label(); this._labelReminders = new System.Windows.Forms.Label(); this.checkReminders = new System.Windows.Forms.CheckBox(); + this._labelPermissions = new System.Windows.Forms.Label(); + this.labelPermissionsValue = new System.Windows.Forms.Label(); this.dialogButtons = new Acacia.Controls.KDialogButtons(); this._layout.SuspendLayout(); this._mainBusyHider.SuspendLayout(); this._layoutMain.SuspendLayout(); this._layoutSelectUser.SuspendLayout(); - this._layoutCenterGABLookup.SuspendLayout(); this._layoutOptions.SuspendLayout(); this.SuspendLayout(); // @@ -85,8 +83,8 @@ // resources.ApplyResources(this._layoutSelectUser, "_layoutSelectUser"); this._layoutSelectUser.Controls.Add(this.labelSelectUser, 0, 0); + this._layoutSelectUser.Controls.Add(this.gabLookup, 1, 0); this._layoutSelectUser.Controls.Add(this.buttonOpenUser, 2, 0); - this._layoutSelectUser.Controls.Add(this._layoutCenterGABLookup, 1, 0); this._layoutSelectUser.Name = "_layoutSelectUser"; // // labelSelectUser @@ -94,19 +92,6 @@ resources.ApplyResources(this.labelSelectUser, "labelSelectUser"); this.labelSelectUser.Name = "labelSelectUser"; // - // buttonOpenUser - // - resources.ApplyResources(this.buttonOpenUser, "buttonOpenUser"); - this.buttonOpenUser.Name = "buttonOpenUser"; - this.buttonOpenUser.UseVisualStyleBackColor = true; - this.buttonOpenUser.Click += new System.EventHandler(this.buttonOpenUser_Click); - // - // _layoutCenterGABLookup - // - resources.ApplyResources(this._layoutCenterGABLookup, "_layoutCenterGABLookup"); - this._layoutCenterGABLookup.Controls.Add(this.gabLookup, 0, 1); - this._layoutCenterGABLookup.Name = "_layoutCenterGABLookup"; - // // gabLookup // this.gabLookup.DataSource = null; @@ -114,9 +99,18 @@ this.gabLookup.DroppedDown = false; this.gabLookup.GAB = null; this.gabLookup.Name = "gabLookup"; + this.gabLookup.PlaceholderColor = System.Drawing.Color.Gray; + this.gabLookup.PlaceholderFont = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); this.gabLookup.SelectedUser = null; this.gabLookup.SelectedUserChanged += new Acacia.UI.GABLookupControl.SelectedUserEventHandler(this.gabLookup_SelectedUserChanged); // + // buttonOpenUser + // + resources.ApplyResources(this.buttonOpenUser, "buttonOpenUser"); + this.buttonOpenUser.Name = "buttonOpenUser"; + this.buttonOpenUser.UseVisualStyleBackColor = true; + this.buttonOpenUser.Click += new System.EventHandler(this.buttonOpenUser_Click); + // // kTreeFolders // this.kTreeFolders.BackColor = System.Drawing.SystemColors.Window; @@ -140,10 +134,10 @@ this._layoutOptions.Controls.Add(this.textName, 1, 0); this._layoutOptions.Controls.Add(this._labelSendAs, 0, 1); this._layoutOptions.Controls.Add(this.checkSendAs, 1, 1); - this._layoutOptions.Controls.Add(this._labelPermissions, 0, 3); - this._layoutOptions.Controls.Add(this.labelPermissionsValue, 1, 3); this._layoutOptions.Controls.Add(this._labelReminders, 0, 2); this._layoutOptions.Controls.Add(this.checkReminders, 1, 2); + this._layoutOptions.Controls.Add(this._labelPermissions, 0, 3); + this._layoutOptions.Controls.Add(this.labelPermissionsValue, 1, 3); this._layoutOptions.Name = "_layoutOptions"; // // _labelName @@ -170,16 +164,6 @@ this.checkSendAs.UseVisualStyleBackColor = true; this.checkSendAs.CheckedChanged += new System.EventHandler(this.checkSendAs_CheckedChanged); // - // _labelPermissions - // - resources.ApplyResources(this._labelPermissions, "_labelPermissions"); - this._labelPermissions.Name = "_labelPermissions"; - // - // labelPermissionsValue - // - resources.ApplyResources(this.labelPermissionsValue, "labelPermissionsValue"); - this.labelPermissionsValue.Name = "labelPermissionsValue"; - // // _labelReminders // resources.ApplyResources(this._labelReminders, "_labelReminders"); @@ -192,6 +176,16 @@ this.checkReminders.UseVisualStyleBackColor = true; this.checkReminders.CheckedChanged += new System.EventHandler(this.checkReminders_CheckedChanged); // + // _labelPermissions + // + resources.ApplyResources(this._labelPermissions, "_labelPermissions"); + this._labelPermissions.Name = "_labelPermissions"; + // + // labelPermissionsValue + // + resources.ApplyResources(this.labelPermissionsValue, "labelPermissionsValue"); + this.labelPermissionsValue.Name = "labelPermissionsValue"; + // // dialogButtons // resources.ApplyResources(this.dialogButtons, "dialogButtons"); @@ -219,7 +213,6 @@ this._layoutMain.PerformLayout(); this._layoutSelectUser.ResumeLayout(false); this._layoutSelectUser.PerformLayout(); - this._layoutCenterGABLookup.ResumeLayout(false); this._layoutOptions.ResumeLayout(false); this._layoutOptions.PerformLayout(); this.ResumeLayout(false); @@ -243,7 +236,6 @@ private System.Windows.Forms.Label labelPermissionsValue; private Controls.KBusyHider _mainBusyHider; private Controls.KDialogButtons dialogButtons; - private System.Windows.Forms.TableLayoutPanel _layoutCenterGABLookup; private UI.GABLookupControl gabLookup; private System.Windows.Forms.Label _labelReminders; private System.Windows.Forms.CheckBox checkReminders; diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/SharedFoldersDialog.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/SharedFoldersDialog.cs index 091fdff..bbf8edf 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/SharedFoldersDialog.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/SharedFoldersDialog.cs @@ -148,8 +148,12 @@ namespace Acacia.Features.SharedFolders }; FocusNode(node); } + kTreeFolders.Focus(); + } + else + { + gabLookup.FocusEdit(); } - kTreeFolders.Focus(); } private void dialogButtons_Apply(object sender, EventArgs e) diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/SharedFoldersDialog.resx b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/SharedFoldersDialog.resx index 9d25f1f..0e2826d 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/SharedFoldersDialog.resx +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/SharedFoldersDialog.resx @@ -148,7 +148,7 @@ 3, 0 - 105, 31 + 105, 29 0 @@ -171,6 +171,39 @@ 0 + + Fill + + + 114, 3 + + + 200, 0 + + + The user was not found + + + Start typing name + + + 260, 23 + + + 0 + + + gabLookup + + + Acacia.UI.GABLookupControl, Kopano, Version=0.1.0.0, Culture=neutral, PublicKeyToken=null + + + _layoutSelectUser + + + 1 + True @@ -193,10 +226,10 @@ 8, 0, 8, 0 - 59, 25 + 59, 23 - 1 + 2 Open @@ -211,74 +244,8 @@ _layoutSelectUser - 1 - - - True - - - 1 - - - Fill - - - 3, 3 - - - 200, 0 - - - 256, 21 - - - 0 - - - gabLookup - - - Acacia.UI.GABLookupControl, Kopano, Version=0.1.0.0, Culture=neutral, PublicKeyToken=null - - - _layoutCenterGABLookup - - - 0 - - - Fill - - - 113, 2 - - - 2, 2, 2, 2 - - - 3 - - - 262, 27 - - 2 - - _layoutCenterGABLookup - - - System.Windows.Forms.TableLayoutPanel, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - _layoutSelectUser - - - 2 - - - <?xml version="1.0" encoding="utf-16"?><TableLayoutSettings><Controls><Control Name="gabLookup" Row="1" RowSpan="1" Column="0" ColumnSpan="1" /></Controls><Columns Styles="Percent,100" /><Rows Styles="Percent,50,AutoSize,0,Percent,50" /></TableLayoutSettings> - Fill @@ -289,7 +256,7 @@ 1 - 442, 31 + 442, 29 0 @@ -307,16 +274,16 @@ 0 - <?xml version="1.0" encoding="utf-16"?><TableLayoutSettings><Controls><Control Name="labelSelectUser" Row="0" RowSpan="1" Column="0" ColumnSpan="1" /><Control Name="buttonOpenUser" Row="0" RowSpan="1" Column="2" ColumnSpan="1" /><Control Name="_layoutCenterGABLookup" Row="0" RowSpan="1" Column="1" ColumnSpan="1" /></Controls><Columns Styles="AutoSize,0,Percent,100,AutoSize,0" /><Rows Styles="Percent,100,Absolute,31" /></TableLayoutSettings> + <?xml version="1.0" encoding="utf-16"?><TableLayoutSettings><Controls><Control Name="labelSelectUser" Row="0" RowSpan="1" Column="0" ColumnSpan="1" /><Control Name="gabLookup" Row="0" RowSpan="1" Column="1" ColumnSpan="1" /><Control Name="buttonOpenUser" Row="0" RowSpan="1" Column="2" ColumnSpan="1" /></Controls><Columns Styles="AutoSize,0,Percent,100,AutoSize,0" /><Rows Styles="Percent,100,Absolute,29" /></TableLayoutSettings> Fill - 3, 40 + 3, 38 - 442, 275 + 442, 277 0 @@ -468,6 +435,72 @@ 3 + + True + + + Fill + + + 3, 53 + + + 82, 27 + + + 6 + + + Show reminders + + + MiddleLeft + + + _labelReminders + + + System.Windows.Forms.Label, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + _layoutOptions + + + 4 + + + True + + + Left + + + 94, 57 + + + 6, 4, 3, 3 + + + 0, 3, 0, 3 + + + 15, 20 + + + 7 + + + checkReminders + + + System.Windows.Forms.CheckBox, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + _layoutOptions + + + 5 + True @@ -502,7 +535,7 @@ _layoutOptions - 4 + 6 True @@ -541,72 +574,6 @@ _layoutOptions - 5 - - - True - - - Fill - - - 3, 53 - - - 82, 27 - - - 6 - - - Show reminders - - - MiddleLeft - - - _labelReminders - - - System.Windows.Forms.Label, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - _layoutOptions - - - 6 - - - True - - - Left - - - 94, 57 - - - 6, 4, 3, 3 - - - 0, 3, 0, 3 - - - 15, 20 - - - 7 - - - checkReminders - - - System.Windows.Forms.CheckBox, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - _layoutOptions - - 7 @@ -640,7 +607,7 @@ 2 - <?xml version="1.0" encoding="utf-16"?><TableLayoutSettings><Controls><Control Name="_labelName" Row="0" RowSpan="1" Column="0" ColumnSpan="1" /><Control Name="textName" Row="0" RowSpan="1" Column="1" ColumnSpan="1" /><Control Name="_labelSendAs" Row="1" RowSpan="1" Column="0" ColumnSpan="1" /><Control Name="checkSendAs" Row="1" RowSpan="1" Column="1" ColumnSpan="1" /><Control Name="_labelPermissions" Row="3" RowSpan="1" Column="0" ColumnSpan="1" /><Control Name="labelPermissionsValue" Row="3" RowSpan="1" Column="1" ColumnSpan="1" /><Control Name="_labelReminders" Row="2" RowSpan="1" Column="0" ColumnSpan="1" /><Control Name="checkReminders" Row="2" RowSpan="1" Column="1" ColumnSpan="1" /></Controls><Columns Styles="AutoSize,0,Percent,100" /><Rows Styles="AutoSize,0,AutoSize,0,AutoSize,0,AutoSize,0" /></TableLayoutSettings> + <?xml version="1.0" encoding="utf-16"?><TableLayoutSettings><Controls><Control Name="_labelName" Row="0" RowSpan="1" Column="0" ColumnSpan="1" /><Control Name="textName" Row="0" RowSpan="1" Column="1" ColumnSpan="1" /><Control Name="_labelSendAs" Row="1" RowSpan="1" Column="0" ColumnSpan="1" /><Control Name="checkSendAs" Row="1" RowSpan="1" Column="1" ColumnSpan="1" /><Control Name="_labelReminders" Row="2" RowSpan="1" Column="0" ColumnSpan="1" /><Control Name="checkReminders" Row="2" RowSpan="1" Column="1" ColumnSpan="1" /><Control Name="_labelPermissions" Row="3" RowSpan="1" Column="0" ColumnSpan="1" /><Control Name="labelPermissionsValue" Row="3" RowSpan="1" Column="1" ColumnSpan="1" /></Controls><Columns Styles="AutoSize,0,Percent,100" /><Rows Styles="AutoSize,0,AutoSize,0,AutoSize,0,AutoSize,0" /></TableLayoutSettings> Fill @@ -655,7 +622,7 @@ 448, 418 - 3 + 0 _layoutMain @@ -682,7 +649,7 @@ 448, 418 - 4 + 0 @@ -718,7 +685,7 @@ 450, 35 - 0 + 1 dialogButtons diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/User32.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/User32.cs index c8f0687..9a00021 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/User32.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/User32.cs @@ -78,6 +78,21 @@ namespace Acacia.Native [DllImport("user32.dll", ExactSpelling = true, CharSet = CharSet.Auto)] public static extern int MapWindowPoints(IntPtr hWndFrom, IntPtr hWndTo, [In, Out] ref Point pt, int cPoints); + public static int GetXLParam(IntPtr lParam) + { + return lParam.ToInt32() & 0xFFFF; + } + + public static int GetYLParam(IntPtr lParam) + { + return (lParam.ToInt32() >> 16) & 0xFFFF; + } + + public static Point GetPointLParam(IntPtr lParam) + { + return new Point(GetXLParam(lParam), GetYLParam(lParam)); + } + #endregion #region Messages diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/WM.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/WM.cs index ab75ce2..f7c1f8b 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/WM.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/WM.cs @@ -23,8 +23,15 @@ using System.Text; namespace Acacia.Native { + public enum MA : int + { + NOACTIVATE = 0x0003 + } + public enum WM : int { + MOUSEACTIVATE = 0x0021, + NCHITTEST = 0x0084, NCPAINT = 0x0085, @@ -38,7 +45,9 @@ namespace Acacia.Native LBUTTONDOWN = 0x0201, RBUTTONDOWN = 0x0204, - MBUTTONDOWN = 0x0207 + MBUTTONDOWN = 0x0207, + + MOUSEWHEEL = 0x020A, } } diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/GABLookupControl.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/GABLookupControl.cs index d7c5b89..2eb4053 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/GABLookupControl.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/UI/GABLookupControl.cs @@ -1,4 +1,4 @@ -/// Copyright 2016 Kopano b.v. +/// Copyright 2017 Kopano b.v. /// /// This program is free software: you can redistribute it and/or modify /// it under the terms of the GNU Affero General Public License, version 3, @@ -31,8 +31,74 @@ using Acacia.Controls; namespace Acacia.UI { - public partial class GABLookupControl : KComboBox + public partial class GABLookupControl : KComboBoxCustomDraw { + private class NotFoundGABUser : GABUser + { + public NotFoundGABUser(string userName) : base(userName) + { + } + } + + private class GABDataSource : KDataSource + { + private readonly GABHandler _gab; + private readonly List _users; + + public GABDataSource(GABHandler gab) + { + this._gab = gab; + + _users = new List(); + foreach (IItem item in _gab.Contacts.Items.Sort("FullName", false)) + { + if (item is IContactItem) + _users.Add(new GABUser((IContactItem)item)); + } + } + + public override IEnumerable Items + { + get + { + return _users; + } + } + + protected override string GetItemText(GABUser item) + { + // If there is a filter, try to complete that + if (!string.IsNullOrEmpty(Filter?.FilterText)) + { + string s = Filter?.FilterText.ToLower(); + if (item.UserName?.ToLower().StartsWith(s) == true) + return item.UserName; + else if (item.FullName?.ToLower().StartsWith(s) == true) + return item.FullName; + else if (item.EmailAddress?.ToLower().StartsWith(s) == true) + return item.EmailAddress; + } + return item.UserName; + } + + protected override bool MatchesFilter(GABUser item) + { + string s = Filter.FilterText.ToLower(); + return + item.FullName?.ToLower().StartsWith(s) == true || + item.UserName?.ToLower().StartsWith(s) == true || + item.EmailAddress?.ToLower().StartsWith(s) == true; + } + + public override object NotFoundItem + { + get + { + return new NotFoundGABUser(Filter.FilterText); + } + } + } + public GABLookupControl() : this(null) { } @@ -40,11 +106,20 @@ namespace Acacia.UI public GABLookupControl(GABHandler gab) { InitializeComponent(); - this.GAB = gab; + GAB = gab; } #region Properties and events + + [Category("Appearance")] + [Localizable(true)] + public string NotFoundText + { + get; + set; + } + #region SelectedUser public class SelectedUserEventArgs : EventArgs @@ -64,40 +139,31 @@ namespace Acacia.UI [Category("Behavior")] public event SelectedUserEventHandler SelectedUserChanged; + private GABUser _selectedUser; public GABUser SelectedUser { get { - /*if (SelectedValue == null) - return new GABUser(Text, Text); - else - return (GABUser)SelectedValue;*/ - return null; + return _selectedUser; } set { - /*if (value == null) - { - SelectedIndex = -1; - Text = ""; - } - else - { - - }*/ + _selectedUser = null; + Select(value); } } - private void SetSelectedUser(GABUser user, bool isChosen) + protected override void OnTextChanged(EventArgs e) { - if (SelectedUser != user || isChosen) - { - System.Diagnostics.Trace.WriteLine(string.Format("SELECT: {0} -> {1} : {2}", SelectedUser, user, isChosen)); - if (isChosen) - SelectedUser = user; - if (SelectedUserChanged != null) - SelectedUserChanged(this, new SelectedUserEventArgs(user, isChosen)); - } + base.OnTextChanged(e); + _selectedUser = string.IsNullOrEmpty(Text) ? null : new GABUser(Text); + SelectedUserChanged?.Invoke(this, new SelectedUserEventArgs(_selectedUser, false)); + } + + protected override void OnSelectedItemChanged() + { + _selectedUser = (GABUser)SelectedItem?.Item; + SelectedUserChanged?.Invoke(this, new SelectedUserEventArgs(_selectedUser, true)); } #endregion @@ -117,7 +183,7 @@ namespace Acacia.UI if (_gab != value) { _gab = value; - LookupUsers(false); + DataSource = _gab == null ? null : new GABDataSource(_gab); } } } @@ -126,147 +192,20 @@ namespace Acacia.UI #endregion - protected override void OnTextChanged(EventArgs e) - { - LookupUsers(true); - SelectCurrentUser(false); - } - - private void SelectCurrentUser(bool isChosen) - { - /*GABUser user = null; - // Select whatever is currently in the text box as a user - if (DataSource != null) - { - // Find if there's a user matching - user = ((List)DataSource).FirstOrDefault((u) => u.DisplayName == Text); - } - if (user == null && Text.Length > 0) - { - // Make a new one - user = new GABUser(Text, Text); - } - SetSelectedUser(user, isChosen);*/ - } - - /*private bool _needUpdate; - - protected override void OnTextUpdate(EventArgs e) - { - _needUpdate = true; - } - - protected override void OnSelectedIndexChanged(EventArgs e) - { - base.OnSelectedIndexChanged(e); - SetSelectedUser((GABUser)SelectedItem, true); - } - - protected override void OnKeyDown(KeyEventArgs e) - { - base.OnKeyDown(e); - if (e.KeyCode == Keys.Enter) - { - SelectCurrentUser(true); - } - else - { - SetSelectedUser(null, false); - } - } - - protected override void OnDataSourceChanged(EventArgs e) - { - // Suppress to prevent automatic selection - }*/ - - private string _lastText; - private List _allUsers; - - private void LookupUsers(bool dropDown) - { - // Cannot lookup if there is no GAB - if (_gab == null) - return; - - string text = this.Text; - // Only search if the text actually changed - if (_lastText != text) - { - // Limit search results if there is a filter, otherwise show everything - List users; - if (text.Length == 0) - { - // Cache the list of all users - if (_allUsers == null) - { - _allUsers = Lookup("", int.MaxValue); - } - users = _allUsers; - } - else - { - users = Lookup(text, 8); - } - - // Sort the users if we have them - users.Sort(); - - _lastText = text; - - // Setting the datasource will trigger a select if there is a match - BeginUpdate(); - DataSource = users; - //SetItemsCore(users); - if (dropDown) - DroppedDown = true; - //Cursor.Current = Cursors.Default; - //Text = _lastText; - //SelectionLength = 0; - //SelectionStart = _lastText.Length; - EndUpdate(); - } - } - - #region Lookup helpers - // TODO: these probably belong in GAB - - public List Lookup(string text, int max) - { - // Begin GAB lookup, search on full name or username - using (ISearch search = _gab.Contacts.Search()) - { - ISearchOperator oper = search.AddOperator(SearchOperator.Or); - oper.AddField("urn:schemas:contacts:cn").SetOperation(SearchOperation.Like, text + "%"); - oper.AddField("urn:schemas:contacts:customerid").SetOperation(SearchOperation.Like, text + "%"); - - // Fetch the results up to the limit. - // TODO: make limit a property? - List users = new List(); - foreach (IContactItem result in search.Search(max)) - { - users.Add(new GABUser(result.FullName, result.CustomerID)); - } - - return users; - } - } - public GABUser LookupExact(string username) { - if (_gab?.Contacts != null) + string s = username.ToLower(); + if (DataSource != null) { - // Begin GAB lookup, search on full name or username - using (ISearch search = _gab.Contacts.Search()) + foreach(GABUser user in DataSource.Items) { - search.AddField("urn:schemas:contacts:customerid").SetOperation(SearchOperation.Equal, username); - - // Fetch the result, if any. - List users = new List(); - using (IContactItem result = search.SearchOne()) + if ( + user.FullName?.ToLower().Equals(s) == true || + user.UserName?.ToLower().Equals(s) == true || + user.EmailAddress?.ToLower().Equals(s) == true + ) { - if (result != null) - return new GABUser(result.FullName, result.CustomerID); + return user; } } } @@ -274,6 +213,74 @@ namespace Acacia.UI return new GABUser(username); } + #region Rendering + + private static readonly Size NameSpacing = new Size(12, 4); + private static readonly Padding ItemPadding = new Padding(5); + private static readonly Padding BorderPadding = new Padding(2); + private const int BorderThickness = 1; + + protected override void OnMeasureItem(MeasureItemEventArgs e) + { + GABUser item = (GABUser)e.Item; + + Size nameSize = TextRenderer.MeasureText(e.Graphics, item.FullName, Font); + Size loginSize = TextRenderer.MeasureText(e.Graphics, item.UserName, Font); + Size emailSize = TextRenderer.MeasureText(e.Graphics, GetSecondLine(item), Font); + + e.ItemWidth = Math.Max(emailSize.Width, nameSize.Width + loginSize.Width + NameSpacing.Width) + + ItemPadding.Horizontal; + e.ItemHeight = emailSize.Height + Math.Max(nameSize.Height, loginSize.Height) + + ItemPadding.Vertical + + NameSpacing.Height + + BorderThickness + BorderPadding.Vertical; + } + + private string GetSecondLine(GABUser item) + { + if (item is NotFoundGABUser) + return NotFoundText; + else + return item.EmailAddress; + } + + protected override void OnDrawItem(DrawItemEventArgs e) + { + GABUser item = (GABUser)e.Item; + + // Draw the background + e.DrawBackground(); + + // Get the sizes + Size nameSize = TextRenderer.MeasureText(e.Graphics, item.FullName, Font); + Size loginSize = TextRenderer.MeasureText(e.Graphics, item.UserName, Font); + Size emailSize = TextRenderer.MeasureText(e.Graphics, item.EmailAddress, Font); + + // Draw the full name top-left + Point pt = e.Bounds.TopLeft(); + pt.Y += ItemPadding.Top; + pt.X += ItemPadding.Left; + TextRenderer.DrawText(e.Graphics, item.FullName, Font, pt, e.ForeColor); + + // Draw the username top-right + pt.X = e.Bounds.Right - loginSize.Width - ItemPadding.Right; + TextRenderer.DrawText(e.Graphics, item.UserName, Font, pt, e.ForeColor); + + // Draw the email below + pt.Y += Math.Max(nameSize.Height, loginSize.Height) + NameSpacing.Height; + pt.X = e.Bounds.X + ItemPadding.Left; + + TextRenderer.DrawText(e.Graphics, GetSecondLine(item), Font, pt, e.ForeColor); + + // Draw a separator line + if (e.Index < DisplayItemCount - 1) + { + int lineY = e.Bounds.Bottom - 1 - BorderThickness - BorderPadding.Bottom; + e.Graphics.DrawLine(Pens.LightGray, BorderPadding.Left, lineY, e.Bounds.Width - BorderPadding.Right, lineY); + } + + } + #endregion } } diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/Util.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/Util.cs index 27bb50c..921da7a 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/Util.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Utils/Util.cs @@ -179,5 +179,15 @@ namespace Acacia.Utils int additional = (align - (size % align)) % align; return size + additional; } + + public static NumType Bound(NumType value, NumType min, NumType max) + where NumType : IComparable + { + if (value.CompareTo(min) < 0) + return min; + if (value.CompareTo(max) > 0) + return max; + return value; + } } } diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/GABUser.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/GABUser.cs index f269e45..fd3ff98 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/GABUser.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/ZPush/GABUser.cs @@ -1,4 +1,6 @@ -/// Copyright 2016 Kopano b.v. + +using Acacia.Stubs; +/// Copyright 2016 Kopano b.v. /// /// This program is free software: you can redistribute it and/or modify /// it under the terms of the GNU Affero General Public License, version 3, @@ -13,7 +15,6 @@ /// along with this program.If not, see. /// /// Consult LICENSE file for details - using System; using System.Collections.Generic; using System.Linq; @@ -36,8 +37,9 @@ namespace Acacia.ZPush public readonly string FullName; public readonly string UserName; + public readonly string EmailAddress; - public GABUser(string displayName, string userName) + private GABUser(string displayName, string userName) { this.FullName = displayName; this.UserName = userName; @@ -49,6 +51,13 @@ namespace Acacia.ZPush this.UserName = userName; } + public GABUser(IContactItem item) + { + this.FullName = item.FullName; + this.EmailAddress = item.Email1Address; + this.UserName = item.CustomerID; + } + public int CompareTo(GABUser other) { return FullName.CompareTo(other.FullName); diff --git a/translations/KOE.pot b/translations/KOE.pot index 414a6ca..24eae6b 100644 --- a/translations/KOE.pot +++ b/translations/KOE.pot @@ -172,6 +172,18 @@ msgctxt "AcaciaZPushPlugin\\Features\\SharedFolders\\SharedFoldersDialog\\labelS msgid "Open folders for user" msgstr "" +#: AcaciaZPushPlugin\Features\SharedFolders\SharedFoldersDialog\gabLookup.NotFoundText +#, csharp-format +msgctxt "AcaciaZPushPlugin\\Features\\SharedFolders\\SharedFoldersDialog\\gabLookup.NotFoundText" +msgid "The user was not found" +msgstr "" + +#: AcaciaZPushPlugin\Features\SharedFolders\SharedFoldersDialog\gabLookup.Placeholder +#, csharp-format +msgctxt "AcaciaZPushPlugin\\Features\\SharedFolders\\SharedFoldersDialog\\gabLookup.Placeholder" +msgid "Start typing name" +msgstr "" + #: AcaciaZPushPlugin\Features\SharedFolders\SharedFoldersDialog\buttonOpenUser.Text #, csharp-format msgctxt "AcaciaZPushPlugin\\Features\\SharedFolders\\SharedFoldersDialog\\buttonOpenUser.Text" @@ -190,6 +202,12 @@ msgctxt "AcaciaZPushPlugin\\Features\\SharedFolders\\SharedFoldersDialog\\_label msgid "Send as owner" msgstr "" +#: AcaciaZPushPlugin\Features\SharedFolders\SharedFoldersDialog\_labelReminders.Text +#, csharp-format +msgctxt "AcaciaZPushPlugin\\Features\\SharedFolders\\SharedFoldersDialog\\_labelReminders.Text" +msgid "Show reminders" +msgstr "" + #: AcaciaZPushPlugin\Features\SharedFolders\SharedFoldersDialog\_labelPermissions.Text #, csharp-format msgctxt "AcaciaZPushPlugin\\Features\\SharedFolders\\SharedFoldersDialog\\_labelPermissions.Text" @@ -202,12 +220,6 @@ msgctxt "AcaciaZPushPlugin\\Features\\SharedFolders\\SharedFoldersDialog\\labelP msgid "Permissions" msgstr "" -#: AcaciaZPushPlugin\Features\SharedFolders\SharedFoldersDialog\_labelReminders.Text -#, csharp-format -msgctxt "AcaciaZPushPlugin\\Features\\SharedFolders\\SharedFoldersDialog\\_labelReminders.Text" -msgid "Show reminders" -msgstr "" - #: AcaciaZPushPlugin\Features\SharedFolders\SharedFoldersDialog\$this.Text #, csharp-format msgctxt "AcaciaZPushPlugin\\Features\\SharedFolders\\SharedFoldersDialog\\$this.Text" diff --git a/translations/de.po b/translations/de.po index dd491ab..cc6647e 100644 --- a/translations/de.po +++ b/translations/de.po @@ -1211,3 +1211,15 @@ msgctxt "AcaciaZPushPlugin\\Properties\\Resources\\SharedFolders_PrivateEvent_Ti msgid "Private event" msgstr "Privater Termin" +#: AcaciaZPushPlugin\Features\SharedFolders\SharedFoldersDialog\gabLookup.NotFoundText +#, csharp-format +msgctxt "AcaciaZPushPlugin\\Features\\SharedFolders\\SharedFoldersDialog\\gabLookup.NotFoundText" +msgid "The user was not found" +msgstr "" + +#: AcaciaZPushPlugin\Features\SharedFolders\SharedFoldersDialog\gabLookup.Placeholder +#, csharp-format +msgctxt "AcaciaZPushPlugin\\Features\\SharedFolders\\SharedFoldersDialog\\gabLookup.Placeholder" +msgid "Start typing name" +msgstr "" + diff --git a/translations/en.po b/translations/en.po index 51b6004..68b4eda 100644 --- a/translations/en.po +++ b/translations/en.po @@ -172,6 +172,18 @@ msgctxt "AcaciaZPushPlugin\\Features\\SharedFolders\\SharedFoldersDialog\\labelS msgid "Open folders for user" msgstr "Open folders for user" +#: AcaciaZPushPlugin\Features\SharedFolders\SharedFoldersDialog\gabLookup.NotFoundText +#, csharp-format +msgctxt "AcaciaZPushPlugin\\Features\\SharedFolders\\SharedFoldersDialog\\gabLookup.NotFoundText" +msgid "The user was not found" +msgstr "The user was not found" + +#: AcaciaZPushPlugin\Features\SharedFolders\SharedFoldersDialog\gabLookup.Placeholder +#, csharp-format +msgctxt "AcaciaZPushPlugin\\Features\\SharedFolders\\SharedFoldersDialog\\gabLookup.Placeholder" +msgid "Start typing name" +msgstr "Start typing name" + #: AcaciaZPushPlugin\Features\SharedFolders\SharedFoldersDialog\buttonOpenUser.Text #, csharp-format msgctxt "AcaciaZPushPlugin\\Features\\SharedFolders\\SharedFoldersDialog\\buttonOpenUser.Text" @@ -190,6 +202,12 @@ msgctxt "AcaciaZPushPlugin\\Features\\SharedFolders\\SharedFoldersDialog\\_label msgid "Send as owner" msgstr "Send as owner" +#: AcaciaZPushPlugin\Features\SharedFolders\SharedFoldersDialog\_labelReminders.Text +#, csharp-format +msgctxt "AcaciaZPushPlugin\\Features\\SharedFolders\\SharedFoldersDialog\\_labelReminders.Text" +msgid "Show reminders" +msgstr "Show reminders" + #: AcaciaZPushPlugin\Features\SharedFolders\SharedFoldersDialog\_labelPermissions.Text #, csharp-format msgctxt "AcaciaZPushPlugin\\Features\\SharedFolders\\SharedFoldersDialog\\_labelPermissions.Text" @@ -202,12 +220,6 @@ msgctxt "AcaciaZPushPlugin\\Features\\SharedFolders\\SharedFoldersDialog\\labelP msgid "Permissions" msgstr "Permissions" -#: AcaciaZPushPlugin\Features\SharedFolders\SharedFoldersDialog\_labelReminders.Text -#, csharp-format -msgctxt "AcaciaZPushPlugin\\Features\\SharedFolders\\SharedFoldersDialog\\_labelReminders.Text" -msgid "Show reminders" -msgstr "Show reminders" - #: AcaciaZPushPlugin\Features\SharedFolders\SharedFoldersDialog\$this.Text #, csharp-format msgctxt "AcaciaZPushPlugin\\Features\\SharedFolders\\SharedFoldersDialog\\$this.Text" diff --git a/translations/fr.po b/translations/fr.po index 939527c..4a4b509 100644 --- a/translations/fr.po +++ b/translations/fr.po @@ -1209,3 +1209,15 @@ msgctxt "AcaciaZPushPlugin\\Properties\\Resources\\SharedFolders_PrivateEvent_Ti msgid "Private event" msgstr "" +#: AcaciaZPushPlugin\Features\SharedFolders\SharedFoldersDialog\gabLookup.NotFoundText +#, csharp-format +msgctxt "AcaciaZPushPlugin\\Features\\SharedFolders\\SharedFoldersDialog\\gabLookup.NotFoundText" +msgid "The user was not found" +msgstr "" + +#: AcaciaZPushPlugin\Features\SharedFolders\SharedFoldersDialog\gabLookup.Placeholder +#, csharp-format +msgctxt "AcaciaZPushPlugin\\Features\\SharedFolders\\SharedFoldersDialog\\gabLookup.Placeholder" +msgid "Start typing name" +msgstr "" + diff --git a/translations/hu.po b/translations/hu.po index 5bae25c..d2c373a 100644 --- a/translations/hu.po +++ b/translations/hu.po @@ -1224,3 +1224,15 @@ msgstr "" msgctxt "AcaciaZPushPlugin\\Properties\\Resources\\SharedFolders_PrivateEvent_Title" msgid "Private event" msgstr "" + +#: AcaciaZPushPlugin\Features\SharedFolders\SharedFoldersDialog\gabLookup.NotFoundText +#, csharp-format +msgctxt "AcaciaZPushPlugin\\Features\\SharedFolders\\SharedFoldersDialog\\gabLookup.NotFoundText" +msgid "The user was not found" +msgstr "" + +#: AcaciaZPushPlugin\Features\SharedFolders\SharedFoldersDialog\gabLookup.Placeholder +#, csharp-format +msgctxt "AcaciaZPushPlugin\\Features\\SharedFolders\\SharedFoldersDialog\\gabLookup.Placeholder" +msgid "Start typing name" +msgstr "" diff --git a/translations/it.po b/translations/it.po index 95ae03b..2b6769c 100644 --- a/translations/it.po +++ b/translations/it.po @@ -1176,3 +1176,15 @@ msgctxt "AcaciaZPushPlugin\\Properties\\Resources\\SharedFolders_PrivateEvent_Ti msgid "Private event" msgstr "" +#: AcaciaZPushPlugin\Features\SharedFolders\SharedFoldersDialog\gabLookup.NotFoundText +#, csharp-format +msgctxt "AcaciaZPushPlugin\\Features\\SharedFolders\\SharedFoldersDialog\\gabLookup.NotFoundText" +msgid "The user was not found" +msgstr "" + +#: AcaciaZPushPlugin\Features\SharedFolders\SharedFoldersDialog\gabLookup.Placeholder +#, csharp-format +msgctxt "AcaciaZPushPlugin\\Features\\SharedFolders\\SharedFoldersDialog\\gabLookup.Placeholder" +msgid "Start typing name" +msgstr "" + diff --git a/translations/nb.po b/translations/nb.po index 092e0fd..00a458e 100644 --- a/translations/nb.po +++ b/translations/nb.po @@ -964,3 +964,15 @@ msgctxt "AcaciaZPushPlugin\\Properties\\Resources\\SharedFolders_PrivateEvent_Ti msgid "Private event" msgstr "" +#: AcaciaZPushPlugin\Features\SharedFolders\SharedFoldersDialog\gabLookup.NotFoundText +#, csharp-format +msgctxt "AcaciaZPushPlugin\\Features\\SharedFolders\\SharedFoldersDialog\\gabLookup.NotFoundText" +msgid "The user was not found" +msgstr "" + +#: AcaciaZPushPlugin\Features\SharedFolders\SharedFoldersDialog\gabLookup.Placeholder +#, csharp-format +msgctxt "AcaciaZPushPlugin\\Features\\SharedFolders\\SharedFoldersDialog\\gabLookup.Placeholder" +msgid "Start typing name" +msgstr "" + diff --git a/translations/nl.po b/translations/nl.po index 4389be7..c08d08f 100644 --- a/translations/nl.po +++ b/translations/nl.po @@ -1211,3 +1211,15 @@ msgctxt "AcaciaZPushPlugin\\Properties\\Resources\\SharedFolders_PrivateEvent_Ti msgid "Private event" msgstr "Privé afspraak" +#: AcaciaZPushPlugin\Features\SharedFolders\SharedFoldersDialog\gabLookup.NotFoundText +#, csharp-format +msgctxt "AcaciaZPushPlugin\\Features\\SharedFolders\\SharedFoldersDialog\\gabLookup.NotFoundText" +msgid "The user was not found" +msgstr "" + +#: AcaciaZPushPlugin\Features\SharedFolders\SharedFoldersDialog\gabLookup.Placeholder +#, csharp-format +msgctxt "AcaciaZPushPlugin\\Features\\SharedFolders\\SharedFoldersDialog\\gabLookup.Placeholder" +msgid "Start typing name" +msgstr "" + diff --git a/translations/pt_br.po b/translations/pt_br.po index 99461b7..6c42c38 100644 --- a/translations/pt_br.po +++ b/translations/pt_br.po @@ -966,3 +966,15 @@ msgctxt "AcaciaZPushPlugin\\Properties\\Resources\\SharedFolders_PrivateEvent_Ti msgid "Private event" msgstr "Evento particular" +#: AcaciaZPushPlugin\Features\SharedFolders\SharedFoldersDialog\gabLookup.NotFoundText +#, csharp-format +msgctxt "AcaciaZPushPlugin\\Features\\SharedFolders\\SharedFoldersDialog\\gabLookup.NotFoundText" +msgid "The user was not found" +msgstr "" + +#: AcaciaZPushPlugin\Features\SharedFolders\SharedFoldersDialog\gabLookup.Placeholder +#, csharp-format +msgctxt "AcaciaZPushPlugin\\Features\\SharedFolders\\SharedFoldersDialog\\gabLookup.Placeholder" +msgid "Start typing name" +msgstr "" +