
Imports System
Imports System.Collections.Generic
Imports System.Text
Imports System.Windows.Forms
Imports System.ComponentModel
Imports System.Drawing
Imports System.Security.Permissions

''' <summary>
''' <c>MultiPurposeComboBox</c> is an extension of <c>ComboBox</c> which provides drop-down customization.
''' </summary>
''' 
<SecurityPermissionAttribute(SecurityAction.LinkDemand, Flags:=SecurityPermissionFlag.UnmanagedCode)> _
Public Class MultiPurposeComboBox
    Inherits ComboBox
#Region "Construction and destruction"

    Public Sub New()
        MyBase.New()
        m_sizeCombo = New Size(MyBase.DropDownWidth, MyBase.DropDownHeight)
        AddHandler m_dropDown.Closing, AddressOf m_dropDown_Closing
    End Sub

    Private Sub m_dropDown_Closing(ByVal sender As Object, ByVal e As ToolStripDropDownClosingEventArgs)
        m_lastHideTime = DateTime.Now
    End Sub

    Public Sub New(ByVal dropControl As Control)
        Me.New()
        DropDownControl = dropControl
    End Sub

#End Region

#Region "ComboBox overrides"

    Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
        If disposing Then
            If m_dropDown IsNot Nothing Then
                m_dropDown.Dispose()
                m_dropDown = Nothing
            End If
            If m_dropDownHost IsNot Nothing Then
                m_dropDownHost.Dispose()
                m_dropDownHost = Nothing
            End If
            If m_timerAutoFocus IsNot Nothing Then
                m_timerAutoFocus.Dispose()
                m_timerAutoFocus = Nothing
            End If
        End If
        MyBase.Dispose(disposing)
    End Sub

#End Region

#Region "Event handlers"

    Private Sub timerAutoFocus_Tick(ByVal sender As Object, ByVal e As EventArgs)
        If m_dropDownHost.IsOnDropDown AndAlso Not DropDownControl.Focused Then
            DropDownControl.Focus()
            m_timerAutoFocus.Enabled = False
        End If
    End Sub

    Private Sub m_dropDown_LostFocus(ByVal sender As Object, ByVal e As EventArgs)
        m_lastHideTime = DateTime.Now
    End Sub

#End Region

#Region "Methods"

    ''' <summary>
    ''' Prepare custom container to hold drop-down control.
    ''' </summary>
    ''' <param name="control">Control to contain.</param>
    ''' <returns>Specialized container.</returns>
    Protected Overridable Function PrepareDropDown(ByVal control As Control) As Control
        If AllowResizeDropDown Then
            ' Encapsulate contorl within a resizable container.
            Return New ResizableDropDownContainer(control)
        Else
            ' By default simply return the drop down control.
            Return control
        End If
    End Function

    ''' <summary>
    ''' Displays drop-down area of combo box, if not already shown.
    ''' </summary>
    Public Overridable Sub ShowDropDown()
        If DropDownContainer IsNot Nothing AndAlso m_dropDownHost IsNot Nothing AndAlso Not IsDroppedDown Then
            ' Restore original container size.
            AutoSizeDropDown()

            ' Display drop-down control.
            m_dropDown.Show(Me, 0, Me.Height)
            m_bDroppedDown = True

            ' Initialize automatic focus timer?
            If m_timerAutoFocus Is Nothing Then
                m_timerAutoFocus = New Timer()
                m_timerAutoFocus.Interval = 10
                AddHandler m_timerAutoFocus.Tick, AddressOf timerAutoFocus_Tick
            End If
            ' Enable the timer!
            m_timerAutoFocus.Enabled = True
            m_sShowTime = DateTime.Now
        End If
    End Sub

    ''' <summary>
    ''' Hides drop-down area of combo box, if shown.
    ''' </summary>
    Public Overridable Sub HideDropDown()
        If DropDownContainer IsNot Nothing AndAlso m_dropDownHost IsNot Nothing AndAlso IsDroppedDown Then
            ' Hide drop-down control.
            m_dropDown.Hide()
            m_bDroppedDown = False

            ' Disable automatic focus timer.
            If m_timerAutoFocus IsNot Nothing AndAlso m_timerAutoFocus.Enabled Then
                m_timerAutoFocus.Enabled = False
            End If
        End If
    End Sub

    ''' <summary>
    ''' Automatically resize drop-down from properties.
    ''' </summary>
    Protected Sub AutoSizeDropDown()
        If m_dropDownHost IsNot Nothing Then

            m_dropDownHost.Margin = m_dropDownHost.Padding

            m_dropDownHost.Padding = New Padding(0)
            Select Case DropDownSizeMode
                Case SizeMode.UseComboSize
                    DropDownContainer.Size = New Size(Width, m_sizeCombo.Height)
                    Exit Select
                Case SizeMode.UseControlSize

                    DropDownContainer.Size = New Size(m_sizeOriginal.Width, m_sizeOriginal.Height)
                    Exit Select
                Case SizeMode.UseDropDownSize

                    DropDownContainer.Size = m_sizeCombo
                    Exit Select
            End Select
        End If
    End Sub

    ''' <summary>
    ''' Assigns control to custom drop-down area of combo box.
    ''' </summary>
    ''' <param name="control">Control to be used as drop-down. Please note that this control must not be contained elsewhere.</param>
    Protected Overridable Sub AssignControl(ByVal control As Control)
        ' If specified control is different then...
        If control IsNot DropDownControl Then
            ' Preserve original container size.
            m_sizeOriginal = control.Size

            ' Reference the user-specified drop down control.
            m_dropDownCtrl = control
            UpdateDropDownHost(control)
        End If
    End Sub

    ''' <summary>
    ''' Updates drop-down control host.
    ''' </summary>
    ''' <param name="control">Control being used as drop-down.</param>
    Protected Sub UpdateDropDownHost(ByVal control As Control)
        ' Clear previous control.
        m_dropDown.Items.Clear()

        Dim prevControlSize As Size = control.Size

        ' A new drop-down container will be required to store the newly specified
        ' control. If no container is specified, this will be equal to the newly
        ' specified control!
        If DropDownContainer IsNot Nothing AndAlso DropDownContainer IsNot DropDownControl Then
            m_dropDownCnt.Dispose()
            m_dropDownCnt = Nothing
        End If
        m_dropDownCnt = PrepareDropDown(control)

        ' A new drop-down host is required to accomodate the newly specified control.
        If m_dropDownHost IsNot Nothing Then
            m_dropDownHost.Dispose()
            m_dropDownHost = Nothing
        End If
        m_dropDownHost = New ToolStripControlHost(DropDownContainer)
        AddHandler m_dropDownHost.LostFocus, AddressOf m_dropDown_LostFocus
        m_dropDownHost.AutoSize = False
        m_dropDown.Items.Add(m_dropDownHost)

        ' Calculate required drop-down size.
        Dim dropDownCnt As IDropDownContainers = TryCast(DropDownContainer, IDropDownContainers)
        If dropDownCnt IsNot Nothing Then
            m_sizeOriginal = dropDownCnt.CalculateContainerSize(prevControlSize)
        Else
            m_sizeOriginal = prevControlSize
        End If

        AutoSizeDropDown()
    End Sub

#End Region

#Region "Win32 message handlers"

    Public Const WM_COMMAND As UInteger = 273
    Public Const WM_USER As UInteger = 1024
    Public Const WM_REFLECT As UInteger = WM_USER + 7168
    Public Const WM_LBUTTONDOWN As UInteger = 513

    Public Const CBN_DROPDOWN As UInteger = 7
    Public Const CBN_CLOSEUP As UInteger = 8

    Public Shared Function HIWORD(ByVal n As Integer) As UInteger
        Return CInt((n >> 16)) And 65535
    End Function

    Public Overloads Overrides Function PreProcessMessage(ByRef m As Message) As Boolean
        If m.Msg = (WM_REFLECT + WM_COMMAND) Then
            If HIWORD(CInt(m.WParam)) = CBN_DROPDOWN Then
                Return False
            End If
        End If
        Return MyBase.PreProcessMessage(m)
    End Function

    Private Shared m_sShowTime As DateTime = DateTime.Now

    Private Sub AutoDropDown()
        If m_dropDown IsNot Nothing AndAlso m_dropDown.Visible Then
            HideDropDown()
        ElseIf (DateTime.Now - m_lastHideTime).Milliseconds > 50 Then
            ShowDropDown()
        End If
    End Sub

    Protected Overloads Overrides Sub WndProc(ByRef m As Message)
        If m.Msg = WM_LBUTTONDOWN Then
            AutoDropDown()
            Return
        End If

        If m.Msg = (WM_REFLECT + WM_COMMAND) Then
            Select Case HIWORD(CInt(m.WParam))
                Case CBN_DROPDOWN
                    AutoDropDown()
                    Return
                Case CBN_CLOSEUP

                    If (DateTime.Now - m_sShowTime).Seconds > 1 Then
                        HideDropDown()
                    End If
                    Return
            End Select
        End If

        MyBase.WndProc(m)
    End Sub

#End Region

#Region "Enumerations"

    Public Enum SizeMode
        UseComboSize
        UseControlSize
        UseDropDownSize
    End Enum

#End Region

#Region "Properties"

    ''' <summary>
    ''' Drop-down container. Equals drop-down control when no container is specified.
    ''' </summary>
    <Browsable(False)> _
    Public ReadOnly Property DropDownContainer() As Control
        Get
            Return Me.m_dropDownCnt
        End Get
    End Property

    ''' <summary>
    ''' Actual drop-down control itself.
    ''' </summary>
    <Browsable(False)> _
    Public Property DropDownControl() As Control
        Get
            Return m_dropDownCtrl
        End Get
        Set(ByVal value As Control)
            AssignControl(value)
        End Set
    End Property

    ''' <summary>
    ''' Indicates if drop-down is currently shown.
    ''' </summary>
    <Browsable(False)> _
    Public ReadOnly Property IsDroppedDown() As Boolean
        Get
            Return Me.m_bDroppedDown AndAlso DropDownContainer.Visible
        End Get
    End Property

    ''' <summary>
    ''' Indicates if drop-down is resizable.
    ''' </summary>
    <Category("Custom Drop-Down"), Description("Indicates if drop-down is resizable.")> _
    Public Property AllowResizeDropDown() As Boolean
        Get
            Return Me.m_bIsResizable
        End Get
        Set(ByVal value As Boolean)
            If value <> Me.m_bIsResizable Then
                Me.m_bIsResizable = value
                If m_dropDownHost IsNot Nothing Then
                    UpdateDropDownHost(DropDownControl)
                End If
            End If
        End Set
    End Property

    ''' <summary>
    ''' Indicates current sizing mode.
    ''' </summary>
    <Category("Custom Drop-Down"), Description("Indicates current sizing mode."), DefaultValue(SizeMode.UseComboSize)> _
    Public Property DropDownSizeMode() As SizeMode
        Get
            Return Me.m_sizeMode
        End Get
        Set(ByVal value As SizeMode)
            If value <> Me.m_sizeMode Then
                Me.m_sizeMode = value
                AutoSizeDropDown()
            End If
        End Set
    End Property

    <Category("Custom Drop-Down")> _
    Public Property DropSize() As Size
        Get
            Return m_sizeCombo
        End Get
        Set(ByVal value As Size)
            m_sizeCombo = value
            If DropDownSizeMode = SizeMode.UseDropDownSize Then
                AutoSizeDropDown()
            End If
        End Set
    End Property

    <Category("Custom Drop-Down"), Browsable(False)> _
    Public Property ControlSize() As Size
        Get
            Return m_sizeOriginal
        End Get
        Set(ByVal value As Size)
            m_sizeOriginal = value
            If DropDownSizeMode = SizeMode.UseControlSize Then
                AutoSizeDropDown()
            End If
        End Set
    End Property

#End Region

#Region "Hide some unwanted properties"

    <Browsable(False)> _
    Public Shadows ReadOnly Property Items() As ObjectCollection
        Get
            Return MyBase.Items
        End Get
    End Property

    <Browsable(False)> _
    Public Shadows Property ItemHeight() As Integer
        Get
            Return MyBase.ItemHeight
        End Get
        Set(ByVal value As Integer)
            MyBase.ItemHeight = value
        End Set
    End Property

#End Region

#Region "Attributes"

    ''' <summary>
    ''' Drop-down control host.
    ''' </summary>
    Private m_dropDownHost As ToolStripControlHost
    ''' <summary>
    ''' Toolstrip drop-down container.
    ''' </summary>
    Private m_dropDown As New ToolStripDropDown()
    ''' <summary>
    ''' Drop-down container. Equals drop-down control when no container is specified.
    ''' </summary>
    Private m_dropDownCnt As Control
    ''' <summary>
    ''' Actual drop-down control itself.
    ''' </summary>
    Private m_dropDownCtrl As Control
    ''' <summary>
    ''' Indicates if drop-down is currently shown.
    ''' </summary>
    Private m_bDroppedDown As Boolean = False
    ''' <summary>
    ''' Indicates current sizing mode.
    ''' </summary>
    Private m_sizeMode As SizeMode = SizeMode.UseComboSize
    ''' <summary>
    ''' Time drop-down was last hidden.
    ''' </summary>
    Private m_lastHideTime As DateTime = DateTime.Now

    ''' <summary>
    ''' Automatic focus timer helps make sure drop-down control is focused for user
    ''' input upon drop-down.
    ''' </summary>
    Private m_timerAutoFocus As Timer
    ''' <summary>
    ''' Original size of control dimensions when first assigned.
    ''' </summary>
    Private m_sizeOriginal As New Size(1, 1)
    ''' <summary>
    ''' Original size of combo box dropdown when first assigned.
    ''' </summary>
    Private m_sizeCombo As Size
    ''' <summary>
    ''' Indicates if drop-down is resizable.
    ''' </summary>
    Private m_bIsResizable As Boolean = True

#End Region

End Class

''' <summary>
''' Interface of a drop down container.
''' </summary>
''' 
Public Interface IDropDownContainers
    Function CalculateContainerSize(ByVal controlSize As Size) As Size
End Interface

''' <summary>
''' Provides resizable drop-down container for use in <c>CustomComboBox</c>.
''' </summary>
''' 
Public Class ResizableDropDownContainer
    Inherits Control
    Implements IDropDownContainers

#Region "Construction and destruction"

    Public Sub New(ByVal childControl As Control)
        If childControl Is Nothing Then
            Throw New NullReferenceException()
        End If
        Me.Controls.Add(childControl)
        m_ctrlChild = childControl

        ' The minimum size of this container must allow for resizable corner.
        Me.MinimumSize = New Size(childControl.MinimumSize.Width, childControl.MinimumSize.Height + 18)
    End Sub

#End Region

#Region "IDropDownContainer Members"

    Public Function CalculateContainerSize(ByVal controlSize As Size) As Size
        controlSize.Height += 18
        Return controlSize
    End Function

#End Region

#Region "Control overrides"

    Protected Overloads Overrides Sub OnPaint(ByVal e As PaintEventArgs)
        MyBase.OnPaint(e)

        ' Draw resize grip.
        Dim rectGrip As New Rectangle(Right - 16, Bottom - 16, 16, 16)
        ControlPaint.DrawSizeGrip(e.Graphics, BackColor, rectGrip)
    End Sub

    Protected Overloads Overrides Sub OnSizeChanged(ByVal e As EventArgs)
        MyBase.OnSizeChanged(e)

        ' Position child control.
        ChildControl.Left = ChildControl.Top = 0
        ChildControl.Width = DisplayRectangle.Width
        ChildControl.Height = DisplayRectangle.Height - 18
    End Sub

    Protected Overloads Overrides Sub OnMouseDown(ByVal e As MouseEventArgs)
        MyBase.OnMouseDown(e)

        If e.Button = MouseButtons.Left Then
            Dim rectGrip As Rectangle = CalculateGripRectangle()

            If rectGrip.Contains(e.Location) Then
                m_bDragGrip = Capture = True
                UpdateCursor(e.Location)

                ' Store anchor position.
                m_ptAnchor = e.Location
            End If
        End If
    End Sub

    Protected Overloads Overrides Sub OnMouseMove(ByVal e As MouseEventArgs)
        MyBase.OnMouseMove(e)

        If e.Button = MouseButtons.Left Then
            If IsDraggingGrip Then
                ' Adjust container dimensions.
                Size = New Size(Width + e.X - m_ptAnchor.X, Height + e.Y - m_ptAnchor.Y)
                m_ptAnchor = e.Location

                ' Invalidate container!
                Invalidate()
            End If
        End If
        If Not IsDraggingGrip Then
            UpdateCursor(e.Location)
        End If
    End Sub

    Protected Overloads Overrides Sub OnMouseUp(ByVal e As MouseEventArgs)
        MyBase.OnMouseUp(e)

        m_bDragGrip = Capture = False
        UpdateCursor(e.Location)
    End Sub

    Protected Overloads Overrides Sub OnMouseLeave(ByVal e As EventArgs)
        MyBase.OnMouseLeave(e)

        ' Use default cursor by default.
        Cursor = Cursors.[Default]
    End Sub

#End Region

#Region "Methods"

    ''' <summary>
    ''' Update control cursor according to the location specified.
    ''' </summary>
    ''' <param name="location">Cursor location.</param>
    Protected Sub UpdateCursor(ByVal location As Point)
        ' Use cursor for bottom-right grip.
        Dim rectGrip As Rectangle = CalculateGripRectangle()
        If rectGrip.Contains(location) Then
            Cursor = Cursors.SizeNWSE
        Else
            Cursor = Cursors.[Default]
        End If
    End Sub

    ''' <summary>
    ''' Calculate display rectangle of control grip handle.
    ''' </summary>
    ''' <returns>Grip handle rectangle.</returns>
    Protected Function CalculateGripRectangle() As Rectangle
        Return New Rectangle(Right - 16, Bottom - 16, 16, 16)
    End Function

#End Region

#Region "Properties"

    ''' <summary>
    ''' Child control.
    ''' </summary>
    <Browsable(False)> _
    Public ReadOnly Property ChildControl() As Control
        Get
            Return Me.m_ctrlChild
        End Get
    End Property

    ''' <summary>
    ''' Indicates if resizer grip is currently being dragged.
    ''' </summary>
    Public ReadOnly Property IsDraggingGrip() As Boolean
        Get
            Return Me.m_bDragGrip
        End Get
    End Property

#End Region

#Region "Attributes"

    ''' <summary>
    ''' Child control.
    ''' </summary>
    Private m_ctrlChild As Control

    ''' <summary>
    ''' Anchor position used to determine distance whilst dragging resize corner.
    ''' </summary>
    Private m_ptAnchor As Point

    ''' <summary>
    ''' Indicates if resizer grip is currently being dragged.
    ''' </summary>
    Private m_bDragGrip As Boolean = False

#End Region

    Public Function CalculateContainerSize1(ByVal controlSize As System.Drawing.Size) As System.Drawing.Size Implements IDropDownContainers.CalculateContainerSize

    End Function

End Class
