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);
+ }
+}