From 833cb6e4d330027d4bb53c1a62deb82a66e5013a Mon Sep 17 00:00:00 2001 From: Patrick Simpson Date: Mon, 19 Jun 2017 15:51:23 +0200 Subject: [PATCH] Added edit and button portion of custom combo box. --- .../AcaciaZPushPlugin.csproj | 8 +- .../AcaciaZPushPlugin/Controls/KComboBox.cs | 168 ++++++++++ .../AcaciaZPushPlugin/Controls/KUIUtil.cs | 51 +++ .../Controls/KVisualStateTracker.cs | 299 ++++++++++++++++++ .../Controls/KVisualStyle.cs | 141 +++++++++ .../SharedFoldersDialog.Designer.cs | 1 - .../AcaciaZPushPlugin/Native/UXTheme.cs | 24 ++ 7 files changed, 690 insertions(+), 2 deletions(-) create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KComboBox.cs create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KVisualStateTracker.cs create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KVisualStyle.cs create mode 100644 src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/UXTheme.cs diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj index 51e22ce..eee1c65 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/AcaciaZPushPlugin.csproj @@ -220,6 +220,9 @@ KBusyIndicator.cs + + UserControl + Component @@ -251,6 +254,8 @@ + + @@ -319,6 +324,7 @@ + @@ -403,7 +409,7 @@ UserControl - Component + UserControl GABLookupControl.cs diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KComboBox.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KComboBox.cs new file mode 100644 index 0000000..7b9bc8b --- /dev/null +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KComboBox.cs @@ -0,0 +1,168 @@ +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 +{ + public class KComboBox : UserControl + { + #region Properties + + public ComboBoxStyle DropDownStyle + { + get; + set; + } + + public string DisplayMember + { + get; + set; + } + + #endregion + + #region Components + + private TextBox _edit; + + #endregion + + #region Init + + public KComboBox() + { + SetupRenderer(); + + _edit = new TextBox(); + _edit.BorderStyle = BorderStyle.None; + Controls.Add(_edit); + _state.AddControl(_edit); + } + + #endregion + + #region Drop down + + private void Button_Clicked() + { + + } + + #endregion + + #region Rendering + + private enum State + { + // Values match those defined in vsstyles.h so no conversion is needed. + Normal = 1, Hot = 2, Pressed = 3, Disabled = 4 + } + + private KVisualStyle _style = new KVisualStyle("COMBOBOX"); + + // Enum from vsstyles.h + enum COMBOBOXPARTS + { + CP_DROPDOWNBUTTON = 1, + CP_BACKGROUND = 2, + CP_TRANSPARENTBACKGROUND = 3, + CP_BORDER = 4, + CP_READONLY = 5, + CP_DROPDOWNBUTTONRIGHT = 6, + CP_DROPDOWNBUTTONLEFT = 7, + CP_CUEBANNER = 8, + }; + + private KVisualStateTracker _state; + private KVisualStateTracker.Part _stateButton; + + public void SetupRenderer(bool enableVisualStyles = true) + { + SetStyle(ControlStyles.AllPaintingInWmPaint, true); + SetStyle(ControlStyles.ContainerControl, true); + SetStyle(ControlStyles.OptimizedDoubleBuffer, true); + SetStyle(ControlStyles.ResizeRedraw, true); + SetStyle(ControlStyles.Selectable, true); + SetStyle(ControlStyles.SupportsTransparentBackColor, true); + SetStyle(ControlStyles.UserMouse, true); + SetStyle(ControlStyles.UserPaint, true); + SetStyle(ControlStyles.Selectable, true); + + _style[COMBOBOXPARTS.CP_DROPDOWNBUTTON].SetPadding(COMBOBOXPARTS.CP_DROPDOWNBUTTONLEFT, + COMBOBOXPARTS.CP_DROPDOWNBUTTONRIGHT); + + _state = new KVisualStateTracker(this, State.Normal, State.Disabled); + _state.Root.WithHot(State.Hot); + _state.Root.WithFocus(State.Hot); + + _stateButton = _state.Root.AddPart().WithPressed(State.Pressed); + _stateButton.Clicked += Button_Clicked; + + // TODO if (enableVisualStyles && Application.RenderWithVisualStyles) + } + + protected override void OnPaint(PaintEventArgs e) + { + _style[COMBOBOXPARTS.CP_BORDER]?.DrawBackground(e.Graphics, _state.Root.State, ClientRectangle); + _style[COMBOBOXPARTS.CP_DROPDOWNBUTTON]?.DrawBackground(e.Graphics, _stateButton.State, _stateButton.Rectangle); + } + + #endregion + + #region Layout + + protected override void OnLayout(LayoutEventArgs e) + { + base.OnLayout(e); + + using (Graphics graphics = CreateGraphics()) + { + // Determine the border insets + Padding insets = _style[COMBOBOXPARTS.CP_BORDER]?.GetMargins(graphics, _state.Root.State) ?? new Padding(); + + // Determine the button size + Size? buttonSize = _style[COMBOBOXPARTS.CP_DROPDOWNBUTTON]?.GetPartSize(graphics, _state.Root.State); + if (!buttonSize.HasValue) + buttonSize = new Size(ClientRectangle.Height, ClientRectangle.Height); + + Rectangle buttonRect = new Rectangle(); + buttonRect.X = ClientRectangle.Width - buttonSize.Value.Width; + buttonRect.Width = buttonSize.Value.Width; + buttonRect.Y = 0; + buttonRect.Height = ClientRectangle.Height; + _stateButton.Rectangle = buttonRect; + + // Set the edit control + Rectangle editRect = new Rectangle(insets.Left, insets.Top, buttonRect.X - insets.Left, + ClientRectangle.Height - insets.Vertical); + editRect = editRect.CenterVertically(new Size(editRect.Width, _edit.PreferredHeight)); + _edit.SetBounds(editRect.X, editRect.Y, editRect.Width, editRect.Height); + } + } + + public override Size GetPreferredSize(Size proposedSize) + { + // TODO: cache sizes? + using (Graphics graphics = CreateGraphics()) + { + Size editSize = _edit.GetPreferredSize(proposedSize); + Padding insets = _style[COMBOBOXPARTS.CP_BORDER]?.GetMargins(graphics, _state.Root.State) ?? new Padding(); + + Size prefSize = editSize.Expand(insets); + + Size? buttonSize = _style[COMBOBOXPARTS.CP_DROPDOWNBUTTON]?.GetPartSize(graphics, _state.Root.State); + if (!buttonSize.HasValue) + buttonSize = new Size(prefSize.Height, prefSize.Height); + + return new Size(prefSize.Width + buttonSize.Value.Width, Math.Max(prefSize.Height, buttonSize.Value.Height)); + } + } + + #endregion + } +} diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KUIUtil.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KUIUtil.cs index 9e9eb86..2e4d15f 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KUIUtil.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KUIUtil.cs @@ -35,6 +35,12 @@ namespace Acacia.Controls return new Rectangle(x, y, size.Width, size.Height); } + public static Rectangle CenterVertically(this Rectangle _this, Size size) + { + int y = _this.Y + (_this.Height - size.Height) / 2; + return new Rectangle(_this.X, y, size.Width, size.Height); + } + public static Rectangle Expand(this Rectangle _this, Padding padding) { Rectangle r = _this; @@ -75,6 +81,51 @@ namespace Acacia.Controls return new Size((int)(_this.Width * graphics.DpiX / 96), (int)(_this.Height * graphics.DpiY / 96)); } + /// + /// Adds the sizes horizontally. The height is the maximum of any of the elements' height. + /// Any of the heights may be null. + /// + /// + /// The added sizes. Null is returned only if all sizes are null. + /// + public static Size? AddHorizontally(this Size? _this, params Size?[] add) + { + Size? s = _this; + + foreach(Size? s2 in add) + { + if (s2.HasValue) + { + if (!s.HasValue) + s = s2; + else + s = new Size(s.Value.Width + s2.Value.Width, Math.Max(s.Value.Height, s2.Value.Height)); + } + } + + return s; + } + public static Size? AddHorizontally(this Size _this, params Size?[] add) + { + return AddHorizontally((Size?)_this, add); + } + + public static Size Expand(this Size _this, Padding padding) + { + Size r = _this; + r.Width += padding.Horizontal; + r.Height += padding.Vertical; + return r; + } + + public static Size Shrink(this Size _this, Padding padding) + { + Size r = _this; + r.Width -= padding.Horizontal; + r.Height -= padding.Vertical; + return r; + } + #endregion } } diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KVisualStateTracker.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KVisualStateTracker.cs new file mode 100644 index 0000000..a281619 --- /dev/null +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KVisualStateTracker.cs @@ -0,0 +1,299 @@ +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 +{ + internal class KVisualStateTracker + where StateTypeId: struct, IConvertible + { + public class Part + { + private KVisualStateTracker _tracker; + private readonly StateTypeId? _normalState; + private readonly StateTypeId? _disabledState; + private StateTypeId? _hotState; + private StateTypeId? _pressedState; + private StateTypeId? _focusState; + private bool _mouseOver; + private bool _mousePressed; + private bool _focused; + private readonly Part _parent; + private readonly List _children = new List(); + private Rectangle? _rectangle; + + public Action Clicked { get; set; } + + public Part(KVisualStateTracker tracker, StateTypeId? normalState, StateTypeId? disabledState) + { + this._tracker = tracker; + this._normalState = normalState; + this._disabledState = disabledState; + } + + public Part(KVisualStateTracker tracker, Part parent) + { + this._tracker = parent._tracker; + this._parent = parent; + } + + public StateTypeId State + { + get + { + if (_parent != null && !_mouseOver) + { + return _parent.State; + } + + if (!_tracker._control.Enabled) + { + return DisabledState; + } + + if (_focused && FocusedState.HasValue) + return FocusedState.Value; + + if (_mouseOver && _mousePressed) + return _pressedState.Value; + + if (_mouseOver && HotState.HasValue) + return HotState.Value; + + return NormalState; + } + } + + public Rectangle Rectangle + { + get + { + if (_rectangle.HasValue) + return _rectangle.Value; + + return _tracker._control.ClientRectangle; + } + + set + { + if (_parent == null) + throw new InvalidOperationException("Cannot set rectangle on root element"); + _rectangle = value; + } + + } + + private StateTypeId DisabledState + { + get + { + if (_disabledState.HasValue) + return _disabledState.Value; + return _parent.DisabledState; + } + } + + private StateTypeId NormalState + { + get + { + if (_normalState.HasValue) + return _normalState.Value; + return _parent.NormalState; + } + } + + private StateTypeId? HotState + { + get + { + if (_hotState.HasValue) + return _hotState.Value; + return _parent.HotState; + } + } + + private StateTypeId? FocusedState + { + get + { + if (_focusState.HasValue) + return _focusState.Value; + return _parent.FocusedState; + } + } + + private bool MouseOver + { + get { return _mouseOver; } + set + { + if (_mouseOver != value) + { + _mouseOver = value; + Invalidate(); + } + } + } + + private bool MousePressed + { + get { return _mousePressed; } + set + { + if (_mousePressed != value) + { + _mousePressed = value; + Invalidate(); + } + } + } + + private bool Focused + { + get { return _focused; } + set + { + if (_focused != value) + { + _focused = value; + Invalidate(); + } + } + } + + private void Invalidate() + { + _tracker.Invalidate(); + } + + internal void GotFocus(object sender, EventArgs e) + { + Focused = true; + } + + internal void LostFocus(object sender, EventArgs e) + { + Focused = false; + } + + internal void MouseDown(object sender, MouseEventArgs e) + { + if (_pressedState != null && e.Button.HasFlag(MouseButtons.Left)) + { + MousePressed = Rectangle.Contains(e.Location); + } + + foreach (Part child in _children) + { + child.MouseDown(sender, e); + } + } + + internal void MouseUp(object sender, MouseEventArgs e) + { + foreach (Part child in _children) + { + child.MouseUp(sender, e); + } + + if (e.Button.HasFlag(MouseButtons.Left)) + { + if (_pressedState != null) + { + MousePressed = false; + } + + if (MouseOver && Clicked != null) + { + Clicked(); + } + } + } + + internal void MouseMove(object sender, MouseEventArgs e) + { + MouseOver = Rectangle.Contains(e.Location); + + foreach(Part child in _children) + { + child.MouseMove(sender, e); + } + } + + internal void MouseLeave(object sender, EventArgs e) + { + MouseOver = false; + foreach (Part child in _children) + { + child.MouseLeave(sender, e); + } + } + + public Part AddPart() + { + Part child = new Part(_tracker, this); + _children.Add(child); + return child; + } + + public Part WithPressed(StateTypeId? pressedState) + { + this._pressedState = pressedState; + return this; + } + + public Part WithHot(StateTypeId? hotState) + { + this._hotState = hotState; + return this; + } + + public Part WithFocus(StateTypeId? focusState) + { + this._focusState = focusState; + return this; + } + } + + private readonly Control _control; + private readonly List _additionalControls = new List(); + public readonly Part Root; + + public KVisualStateTracker(Control control, StateTypeId normalState, StateTypeId disabledState) + { + this._control = control; + Root = new Part(this, normalState, disabledState); + AddControl(_control); + _control.EnabledChanged += Control_EnabledChanged; + } + + public void AddControl(Control child) + { + if (child != _control) + { + _additionalControls.Add(child); + } + child.MouseLeave += Root.MouseLeave; + child.MouseMove += Root.MouseMove; + child.MouseDown += Root.MouseDown; + child.MouseUp += Root.MouseUp; + child.GotFocus += Root.GotFocus; + child.LostFocus += Root.LostFocus; + } + + private void Control_EnabledChanged(object sender, EventArgs e) + { + Invalidate(); + } + + private void Invalidate() + { + _control.Invalidate(); + } + } +} diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KVisualStyle.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KVisualStyle.cs new file mode 100644 index 0000000..6137b42 --- /dev/null +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Controls/KVisualStyle.cs @@ -0,0 +1,141 @@ +using Acacia.Native; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; +using System.Windows.Forms.VisualStyles; + +namespace Acacia.Controls +{ + internal class KVisualStyle + where PartTypeId : struct, IConvertible + where StateTypeId : struct, IConvertible + { + public class Part + { + private readonly KVisualStyle _style; + private readonly PartTypeId _partId; + private Dictionary _renderers; + private Part _paddingLeft; + private Part _paddingRight; + + public Part(KVisualStyle style, PartTypeId id) + { + this._style = style; + this._partId = id; + } + + public void DrawBackground(Graphics graphics, StateTypeId state, Rectangle rect) + { + VisualStyleRenderer r = GetRenderer(state); + if (r != null) + { + r.DrawBackground(graphics, rect); + } + } + + public void DrawText(Graphics graphics, StateTypeId state, Rectangle rect, string text) + { + VisualStyleRenderer r = GetRenderer(state); + if (r != null) + { + r.DrawText(graphics, rect, text); // TODO: disabled + } + } + + private VisualStyleRenderer GetRenderer(StateTypeId state) + { + InitRenderers(); + VisualStyleRenderer renderer; + _renderers.TryGetValue(state, out renderer); + return renderer; + } + + private void InitRenderers() + { + if (_renderers == null) + { + _renderers = new Dictionary(); + foreach (StateTypeId entry in Enum.GetValues(typeof(StateTypeId))) + { + try + { + int id = _partId.ToInt32(null); + int entryId = entry.ToInt32(null); + _renderers.Add(entry, new VisualStyleRenderer(_style.ClassName, id, entryId)); + } + catch (Exception e) { Logger.Instance.Trace(this, "Renderer not supported: {0}", e); } + } + } + } + + public Size? GetPartSize(Graphics graphics, StateTypeId state) + { + VisualStyleRenderer renderer = GetRenderer(state); + if (renderer == null) + return null; + + Size size = renderer.GetPartSize(graphics, ThemeSizeType.True); + return size.AddHorizontally(_paddingLeft?.GetPartSize(graphics, state), + _paddingRight?.GetPartSize(graphics, state)); + } + + public Padding? GetMargins(Graphics graphics, StateTypeId state) + { + VisualStyleRenderer renderer = GetRenderer(state); + if (renderer == null) + return null; + + + // VisualStyleRenderer.GetMargins always throws an exception, make an explicit API call + int stateId = state.ToInt32(null); + UXTheme.MARGINS margins; + IntPtr hdc = graphics.GetHdc(); + try + { + UXTheme.GetThemeMargins(renderer.Handle, hdc, this._partId.ToInt32(null), stateId, + (int)MarginProperty.SizingMargins, IntPtr.Zero, out margins); + // TODO: include padding + return new Padding(margins.cxLeftWidth, margins.cyTopHeight, margins.cxRightWidth, margins.cyBottomHeight); + } + finally + { + graphics.ReleaseHdc(hdc); + } + } + + + public void SetPadding(PartTypeId paddingLeft, PartTypeId paddingRight) + { + this._paddingLeft = _style[paddingLeft]; + this._paddingRight = _style[paddingRight]; + } + + } + + private readonly string ClassName; + private readonly Dictionary _parts = new Dictionary(); + + public KVisualStyle(string name) + { + this.ClassName = name; + } + + public Part this[PartTypeId index] + { + get + { + Part part; + if (!_parts.TryGetValue(index, out part)) + { + part = new Part(this, index); + _parts.Add(index, part); + } + return part; + } + } + } +} diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/SharedFoldersDialog.Designer.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/SharedFoldersDialog.Designer.cs index f93a21e..4268118 100644 --- a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/SharedFoldersDialog.Designer.cs +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Features/SharedFolders/SharedFoldersDialog.Designer.cs @@ -111,7 +111,6 @@ // this.gabLookup.DisplayMember = "DisplayName"; resources.ApplyResources(this.gabLookup, "gabLookup"); - this.gabLookup.FormattingEnabled = true; this.gabLookup.GAB = null; this.gabLookup.Name = "gabLookup"; this.gabLookup.SelectedUserChanged += new Acacia.UI.GABLookupControl.SelectedUserEventHandler(this.gabLookup_SelectedUserChanged); diff --git a/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/UXTheme.cs b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/UXTheme.cs new file mode 100644 index 0000000..34887e0 --- /dev/null +++ b/src/AcaciaZPushPlugin/AcaciaZPushPlugin/Native/UXTheme.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace Acacia.Native +{ + public static class UXTheme + { + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public struct MARGINS + { + public int cxLeftWidth; + public int cxRightWidth; + public int cyTopHeight; + public int cyBottomHeight; + } + + [DllImport("uxtheme.dll")] + public static extern int GetThemeMargins(IntPtr hTheme, IntPtr hdc, int iPartId, int iStateId, int iPropId, IntPtr prc, out MARGINS pMargins); + } +}