File: MS\Internal\AutomationProxies\WindowsListViewGroupHelper.cs
Project: wpf\src\UIAutomation\Win32Providers\UIAutomationClientsideProviders.csproj (UIAutomationClientsideProviders)

//---------------------------------------------------------------------------
//
// <copyright file="WindowsListViewGroupHelper.cs" company="Microsoft">
//    Copyright (C) Microsoft Corporation.  All rights reserved.
// </copyright>
//
//
// Description: Windows ListView Group helper classes
//
// History:
//                     alexsn - Created (in DotNet)
//        2003/08/08 - alexsn Updated for WCP
//
//---------------------------------------------------------------------------
using System;
using System.Windows.Automation;
using System.Windows.Automation.Provider;
using System.Windows;
using System.Collections;
using System.Runtime.InteropServices;
using System.ComponentModel;
using MS.Win32;
 
namespace MS.Internal.AutomationProxies
{
    // Class representing collection of ListView GroupManagers
    class GroupManagerCollection
    {
 
        //------------------------------------------------------
        //
        //  Internal Methods
        //
        //------------------------------------------------------
 
        #region Internal Methods
 
        // Ensures GroupManager creation for the specified listview
        // This method will be called only from certain methods on the LV
        // Called from LV: FirstChild, LastChild, ElementFromPoint, GetFocus
        internal void EnsureCreation(IntPtr hwnd)
        {
            if (!Contains(hwnd))
            {
                _groupManagers[hwnd] = GroupManager.CreateGroupManager(hwnd);
            }
        }
 
        internal void Remove(IntPtr hwnd)
        {
            _groupManagers.Remove(hwnd);
        }
 
        // O(1)
        internal bool Contains(IntPtr hwnd)
        {
            return _groupManagers.ContainsKey(hwnd);
        }
 
        internal GroupManager this[IntPtr hwnd]
        {
            get
            {
                if (!WindowsListView.IsGroupViewEnabled(hwnd))
                {
                    // Group was disabled but we did not get the needed event
                    // since for some things events are not being sent
                    // (e.g: Going from some LV modes to - List mode, List mode does not have Groups)
 
                    // alexsn @
 
 
                    throw new InvalidOperationException(SR.Get(SRID.OperationCannotBePerformed));
                }
 
                // The group may have been discarded by the reorder winevent.
                EnsureCreation (hwnd);
 
                GroupManager manager = _groupManagers[hwnd] as GroupManager;
                if (manager == null)
                {
                    // E.G. Going from the List mode to the something that has Group will cause this
 
                    // alexsn @
 
 
                    throw new InvalidOperationException(SR.Get(SRID.OperationCannotBePerformed));
                }
                return manager;
            }
        }
 
        #endregion Internal Methods
 
        //------------------------------------------------------
        //
        //  Private Fields
        //
        //------------------------------------------------------
 
        #region Private Fields
 
        static private Hashtable _groupManagers = new Hashtable(10);
 
        #endregion Private Fields
    }
 
    // Class responsible for managing listview groups
    class GroupManager
    {
 
        // ------------------------------------------------------
        //
        //  Constructors
        //
        //------------------------------------------------------
 
        #region Constructor
 
        private GroupManager(int groups, IntPtr hwnd, bool isComctrlV6OnOsVerV6orHigher)
        {
            _groups = new ArrayList(groups);
            _hwnd = hwnd;
            _isComctrlV6OnOsVerV6orHigher = isComctrlV6OnOsVerV6orHigher;
        }
 
        #endregion Constructor
 
        //------------------------------------------------------
        //
        //  Internal Methods
        //
        //------------------------------------------------------
 
        #region Internal Methods
        internal NativeMethods.Win32Rect GetGroupRc(int id)
        {
            Group group = GetGroup(id);
 
            if (group == null)
            {
                return NativeMethods.Win32Rect.Empty;
            }
 
            return GetGroupRcInternal(group);
        }
        internal NativeMethods.Win32Rect GetGroupRcByIndex(int index)
        {
            if (index >= _groups.Count)
            {
                return NativeMethods.Win32Rect.Empty;
            }
 
            return GetGroupRcInternal((Group)_groups[index]);
        }
        internal int[] GetGroupIds()
        {
            int count = _groups.Count;
            int[] groupIds = new int[count];
 
            for (int i = 0; i < count; i++)
            {
                groupIds[i] = ((Group)_groups[i])._groupID;
            }
 
            return groupIds;
        }
        internal bool IsGroupIdValid(int groupID)
        {
            int count = _groups.Count;
 
            for (int i = 0; i < count; i++)
            {
                Group group = (Group)_groups[i];
 
                if (group._groupID == groupID)
                {
                    return true;
                }
            }
 
            return false;
        }
 
        internal int GetGroupIdByIndex(int index)
        {
            if (index >= _groups.Count)
            {
                return -1;
            }
 
            return ((Group)_groups[index])._groupID;
        }
        
        internal int GroupCount()
        {
            return _groups.Count;
        }
 
        internal bool AreGroupsValid()
        {
            int count = _groups.Count;
 
            for (int i = 0; i < count; i++)
            {
                Group group = (Group)_groups[i];
 
                if (!ListViewHasGroup(_hwnd, group._groupID))
                {
                    return false;
                }
            }
 
            // Make sure that no new group have been added, try to match all the GroupId to an 
            // existing one.
            int itemCount = WindowsListView.GetItemCount (_hwnd);
            NativeMethods.LVITEM_V6 item = new NativeMethods.LVITEM_V6 ();
 
            item.mask = NativeMethods.LVIF_GROUPID;
 
            for (item.iItem = 0; item.iItem < itemCount; item.iItem++)
            {
                if (!XSendMessage.GetItem(_hwnd, ref item) || GetGroup(item.iGroupID) == null)
                {
                    return false;
                }
            }
 
            return true;
        }
 
        internal GroupInfo GetGroupInfo(int groupID)
        {
            Group group = GetGroup(groupID);
 
            if (group != null)
            {
                return new GroupInfo(group.Items, group.Count);
            }
 
            // empty group info
            return GroupInfo.Null;
        }
        internal static GroupManager CreateGroupManager(IntPtr hwnd)
        {
            return InitializeManager(hwnd);
        }
 
        // detect whether the lv has a specified group
        internal static bool ListViewHasGroup(IntPtr hwnd, int groupID)
        {
            return Misc.ProxySendMessageInt(hwnd, NativeMethods.LVM_HASGROUP, new IntPtr(groupID), IntPtr.Zero) != 0;
        }
        #endregion Internal Methods
 
        //------------------------------------------------------
        //
        //  Internal Fields
        //
        //------------------------------------------------------
 
        #region Internal Fields
        // structure containing group information
        internal struct GroupInfo
        {
            //------------------------------------------------------
            //
            //  Constructor
            //
            //------------------------------------------------------
 
            #region Constructor
            internal GroupInfo(int[] items, int count)
            {
                _items = items; // OK to do an assignment here, instead of deep copy
                _count = count;
            }
            #endregion Constructor
 
            //------------------------------------------------------
            //
            //  Public Methods
            //
            //------------------------------------------------------
 
            #region Public Methods
            static public bool operator true(GroupInfo info)
            {
                return info._items != null;
            }
            static public bool operator false(GroupInfo info)
            {
                return info._items == null;
            }
            static public bool operator !(GroupInfo info)
            {
                if (info)
                {
                    return false;
                }
 
                return true;
            }
            #endregion Public Methods
 
            //------------------------------------------------------
            //
            //  Internal Methods
            //
            //------------------------------------------------------
 
            #region Internal Methods
            internal int IndexOf(int item)
            {
                int index = 0;
 
                while (index < _count)
                {
                    if (_items[index] == item)
                    {
                        return index;
                    }
 
                    index++;
                }
 
                return -1;
            }
            #endregion Internal Methods
 
            //------------------------------------------------------
            //
            //  Internal Fields
            //
            //------------------------------------------------------
 
            #region Internal Fields
 
            internal int[] _items;
            internal int _count;
            static readonly internal GroupInfo Null = new GroupInfo(null, -1);
 
            #endregion Internal Fields
 
        }
        // collection of groups
        internal ArrayList _groups;
        // list hwnd
        internal IntPtr _hwnd;
 
        #endregion Internal Fields
 
        //------------------------------------------------------
        //
        //  Private Methods
        //
        //------------------------------------------------------
 
        #region Private Methods
 
        private Group GetGroup(int id)
        {
            int count = _groups.Count;
 
            Group current = null;
            for (int i = 0; i < count; i++)
            {
                current = (Group)(_groups[i]);
                if (current._groupID == id)
                {
                    return current;
                }
            }
            return null;
        }
 
        private static unsafe GroupManager InitializeManager(IntPtr hwnd)
        {
            bool isComctrlV6OnOsVerV6orHigher = Misc.IsComctrlV6OnOsVerV6orHigher(hwnd);
           
            int itemCount = WindowsListView.GetItemCount(hwnd);
            NativeMethods.LVITEM_V6 item = new NativeMethods.LVITEM_V6();
            item.mask = NativeMethods.LVIF_GROUPID;
 
            // The only place where the GroupManager gets constructed
            GroupManager manager = new GroupManager(itemCount, hwnd, isComctrlV6OnOsVerV6orHigher);
            
            if (isComctrlV6OnOsVerV6orHigher)
            {
                NativeMethods.LVITEMINDEX ii = new NativeMethods.LVITEMINDEX(-1, -1);
 
                int flags = NativeMethods.LVNI_VISIBLEONLY | NativeMethods.LVNI_VISIBLEORDER;
 
                // When a listview is being "grouped by" an item may be in more than one group.  The itemCount
                // is the number of unique items.  This loop may iterate for more than the unique items in the group.
                // We are taking advantage of that fact the the array list will expand if there are alot of duplicates.
                while (XSendMessage.XSend (hwnd, NativeMethods.LVM_GETNEXTITEMINDEX, new IntPtr(&ii), flags, Marshal.SizeOf(ii.GetType())))
                {
                    // need to convert the item id to a group id
                    NativeMethods.LVGROUP_V6 groupInfo = new NativeMethods.LVGROUP_V6();
                    groupInfo.Init(Marshal.SizeOf(typeof(NativeMethods.LVGROUP_V6)));
                    groupInfo.mask = NativeMethods.LVGF_GROUPID;
                    
                    bool lresult  = XSendMessage.XSend(hwnd, NativeMethods.LVM_GETGROUPINFOBYINDEX, new IntPtr(ii.iGroup), new IntPtr(&groupInfo), Marshal.SizeOf(typeof(NativeMethods.LVGROUP_V6)));
                    if (!lresult)
                    {
                        if (groupInfo.iGroupID == -1)
                        {
                            // A -1 here means that there are no duplicates in this grouped listview so
                            // we have to get the group the old way.  This is done for performance reasons.
                            break;
                        }
                        // no group for this item should never happen.  
                        // If it ever does the other items might ok so just keep going.
                        continue;
                    }
                    
                    if (!manager.Add(groupInfo.iGroupID, ii.iItem))
                    {
                        // we had problem adding item to the needed group at this point it makes no
                        // sense to continue
                        System.Diagnostics.Debug.Assert(false, "Cannot add item to the needed group");
                        return null;
                    }
                }
            }
 
            bool sortNeeded = false;
            // If the code above did not yield anything try this way.  This will work for 
            // listviews pre Vista and grouped listviews in vista that don't have duplicate items.
            if (manager.GroupCount() == 0)
            {
                // if we get the groups this way they need to be sorted.  The code above brings them in sorted.
                sortNeeded = true;
                int current = 0;
                while (current < itemCount)
                {
                    item.iItem = current;
                    if (XSendMessage.GetItem(hwnd, ref item) && manager.Add(item.iGroupID, item.iItem))
                    {
                        current++;
                    }
                    else
                    {
                        // we had problem adding item to the needed group at this point it makes no
                        // sense to continue
                        System.Diagnostics.Debug.Assert(false, "Cannot add item to the needed group");
                        return null;
                    }
                }
            }
 
            // Sort items within the group
            int groupsCount = manager.GroupCount();
            for (int i = 0; i < groupsCount; i++)
            {
                Group group = (Group)manager._groups[i];
                Array.Sort(group.Items, 0, group.Count, new SortGroupItems(hwnd));
            }
 
 
            // Depending on how we got the group info we may need to sort it.
            // In vista the the listview can put the list items in the correct order.
            // Pre vista or old ui (v5) will always need to be sorted.
            if (sortNeeded)
            {
                // Sort groups
                manager._groups.Sort(new SortGroups(hwnd));
            }
            
            return manager;
        }
 
        private unsafe int GetGroupHeaderHeight()
        {
            NativeMethods.LVGROUPMETRICS metric = new NativeMethods.LVGROUPMETRICS (sizeof(NativeMethods.LVGROUPMETRICS), NativeMethods.LVGMF_BORDERSIZE);
            XSendMessage.XSend(_hwnd, NativeMethods.LVM_GETGROUPMETRICS, IntPtr.Zero, new IntPtr(&(metric.cbSize)), metric.cbSize, XSendMessage.ErrorValue.NoCheck);
 
            return metric.Top + padding;
        }
 
        private bool Add(int id, int item)
        {
            Group group = GetGroup(id);
            if (group == null)
            {
                group = new Group(id, _hwnd, _isComctrlV6OnOsVerV6orHigher);
                _groups.Add(group);
            }
            // group already exist, simply add an item to it
            return group.Add(item);
        }
 
        // Retrieve the rect of the group.
        private NativeMethods.Win32Rect GetGroupRcInternal(Group group)
        {
            NativeMethods.Win32Rect rcGroup = group.GetGroupRect();
            if (rcGroup.IsEmpty)
            {
                // LVM_GETGROUPRECT failed.
                rcGroup = group.CalculateRectNoHeader();
                // add the header's height hence changing the rcGroup.top coordinate
                // increase top by subtracting header from it
                rcGroup.top -= GetGroupHeaderHeight();
            }
            return rcGroup;
        }
 
        #endregion Private Methods
 
        //------------------------------------------------------
        //
        //  Private Fields
        //
        //------------------------------------------------------
 
        #region Private Fields
 
        // Group header padding
        private const int padding = 12;
        
        private bool _isComctrlV6OnOsVerV6orHigher;
            
        #endregion Private Fields
 
        //------------------------------------------------------
        //
        //  Nested classes
        //
        //------------------------------------------------------
 
        #region Nested classes
 
        // Implementation of IComparer used to
        // sort groups.
        // Note: we want to have groups sorted in the way they displayed to the user
        // Hence we cannot sort by group id, since if group has the lower id it does not mean
        // that it displayed before the group with the higher id.
        // Hence we will sort groups by the rect of the first item within the group
        private class SortGroups : IComparer
        {
            //------------------------------------------------------
            //
            //  Constructors
            //
            //------------------------------------------------------
 
            #region Constructors
 
            internal  SortGroups(IntPtr hwnd)
            {
                _hwnd = hwnd;
            }
 
            #endregion Constructor
 
            //------------------------------------------------------
            //
            //  IComparer
            //
            //------------------------------------------------------
 
            #region IComparer
 
            int IComparer.Compare(object x, object y)
            {
                Group g1 = (Group)x;
                Group g2 = (Group)y;
 
                // piggy back on the SortGroupItem.Compare
                SortGroupItems helper = new SortGroupItems(_hwnd);
                return ((IComparer)helper).Compare(g1.Items[0], g2.Items[0]);
            }
 
            #endregion IComparer
 
            //------------------------------------------------------
            //
            //  Private Fields
            //
            //------------------------------------------------------
 
            #region Private Fields
 
            private IntPtr _hwnd;
 
            #endregion Private Fields
        }
 
        // Implementation of IComparer used to
        // sort items within group based on their rectangle
 
        // NOTE: After the sort the listviewitems DIRECTION in array will be the following (except when in Detail mode)
        //  0   1   2
        //  3   4   5
        //  6   7
        private class SortGroupItems : IComparer
        {
 
            //------------------------------------------------------
            //
            //  Constructors
            //
            //------------------------------------------------------
 
            #region  Constructors
 
            internal SortGroupItems(IntPtr hwnd)
            {
                _hwnd = hwnd;
            }
 
            #endregion Constructor
 
            //------------------------------------------------------
            //
            //  IComparer
            //
            //------------------------------------------------------
 
            #region IComparer
 
            int IComparer.Compare(object x, object y)
            {
                int item1 = (int)x;
                int item2 = (int)y;
 
                // get the rect of 2 items
                NativeMethods.Win32Rect rc1;
                WindowsListView.GetItemRect(_hwnd, item1, NativeMethods.LVIR_BOUNDS, out rc1);
 
                NativeMethods.Win32Rect rc2;
                WindowsListView.GetItemRect(_hwnd, item2, NativeMethods.LVIR_BOUNDS, out rc2);
 
 
                // compare rectangles
                if (rc1.left < rc2.left || rc1.top < rc2.top)
                {
                    return -1;
                }
                else if (rc1.left != rc2.left || rc1.top != rc2.top)
                {
                    return 1;
                }
                return 0;
            }
 
            #endregion IComparer
 
            //------------------------------------------------------
            //
            //  Private Fields
            //
            //------------------------------------------------------
 
            #region Private Fields
 
            private IntPtr _hwnd;
 
            #endregion Private Fields
        }
 
 
        // Class describing single ListView Group
        private class Group
        {            
 
            //------------------------------------------------------
            //
            //  Constructors
            //
            //------------------------------------------------------
 
            #region Constructors
 
            internal Group(int id, IntPtr hwnd, bool isComctrlV6OnOsVerV6orHigher)
            {
                _items = new int[_size];
                _groupID = id;
                _hwnd = hwnd;
                _index = -1;
                _isComctrlV6OnOsVerV6orHigher = isComctrlV6OnOsVerV6orHigher;
            }
 
            #endregion Constructor
 
            //------------------------------------------------------
            //
            //  Internal Methods
            //
            //------------------------------------------------------
 
            #region Internal Methods
 
            internal int Count
            {
                get
                {
                    return _index + 1;
                }
            }
            internal int[] Items
            {
                get
                {
                    return _items;
                }
            }
 
            internal unsafe NativeMethods.Win32Rect GetGroupRect()
            {
                NativeMethods.Win32Rect rect = new NativeMethods.Win32Rect();
                bool isCollapsed = WindowsListViewGroup.IsCollapsed(_hwnd, _groupID);
                rect.top = isCollapsed ? NativeMethods.LVGGR_HEADER : NativeMethods.LVGGR_GROUP;
                XSendMessage.XSend(_hwnd, NativeMethods.LVM_GETGROUPRECT,
                          new IntPtr(_groupID), new IntPtr(&rect), Marshal.SizeOf(rect.GetType()));
                
                Misc.MapWindowPoints(_hwnd, IntPtr.Zero, ref rect, 2);
                
                return rect;
            }
 
            internal NativeMethods.Win32Rect CalculateRectNoHeader()
            {
                NativeMethods.Win32Rect rcLv = NativeMethods.Win32Rect.Empty;
 
                if (!Misc.GetWindowRect(_hwnd, ref rcLv))
                {
                    return NativeMethods.Win32Rect.Empty;
                }
 
                // set top to the top coordinate of the first item
                NativeMethods.Win32Rect item;
                WindowsListView.GetItemRect(_hwnd, _items[0], NativeMethods.LVIR_BOUNDS, out item);
 
                NativeMethods.Win32Rect groupRc;
                groupRc.top = item.top;
 
                // left coordinate defined by the left coordinate of the listview
                groupRc.left = rcLv.left;
 
                int count = Count;
                // bottom defined by the bottom coordinate of the last item
                if (count > 1)
                {
                    // get the rect of the last item in the group
                    WindowsListView.GetItemRect(_hwnd, _items[count - 1], NativeMethods.LVIR_BOUNDS, out item);
                }
 
                groupRc.bottom = item.bottom;
 
                // right coordinate defined by lv.right
                groupRc.right = rcLv.right;
 
                // when vertical scrollbar is present take it into account
                if (WindowScroll.Scrollable(_hwnd, NativeMethods.SB_VERT))
                {
                    NativeMethods.Win32Rect rc = GetScrollbarRect();
                    int width = rc.right - rc.left;
 
                    if (Misc.IsControlRTL(_hwnd))
                    {
                        // Right to left mirroring style
                        groupRc.left += width;
                    }
                    else
                    {
                        groupRc.right -= width;
                    }
                }
 
                return groupRc;
            }
 
            // Add lvitem to the collection
            internal bool Add(int item)
            {
                // Check if we have an empty place in our array
                _index++;
                EnsureCapacity(_index);
                _items[_index] = item;
                return true;
            }
 
 
            #endregion Internal Methods
 
 
            //------------------------------------------------------
            //
            //  Internal Fields
            //
            //------------------------------------------------------
 
            #region Internal Fields
 
            // id of the group
            internal int _groupID;
 
            #endregion Internal Fields
 
            //------------------------------------------------------
            //
            //  Private Methods
            //
            //------------------------------------------------------
 
            #region Private Methods
 
            // grow the size of _items if needed
            private void EnsureCapacity(int min)
            {
                System.Diagnostics.Debug.Assert(min <= _items.Length, "EnsureCapacity: min is > _items.Length");
                if (min == _items.Length)
                {
                    // grow _items by factor of 2
                    int[] temp = _items;
                    _items = new int[temp.Length * 2];
                    Array.Copy(temp, _items, temp.Length);
                }
            }
 
            // get rect of the v-scrollbar
            private NativeMethods.Win32Rect GetScrollbarRect()
            {
                NativeMethods.ScrollBarInfo sbi = new NativeMethods.ScrollBarInfo ();
                sbi.cbSize = Marshal.SizeOf(sbi.GetType());
 
                if (Misc.GetScrollBarInfo(_hwnd, NativeMethods.OBJID_VSCROLL, ref sbi))
                {
                    return new NativeMethods.Win32Rect(sbi.rcScrollBar.left, sbi.rcScrollBar.top, sbi.rcScrollBar.right, sbi.rcScrollBar.bottom);
                }
 
                return NativeMethods.Win32Rect.Empty;
            }
 
 
            #endregion Private Methods
 
            //------------------------------------------------------
            //
            //  Private Fields
            //
            //------------------------------------------------------
 
            #region Private Fields
 
            // Collection of items in the group
            // Store lvitem indexies. Use array to prevent boxing/unboxing
            // alexsn @
 
 
 
 
 
            private int[] _items;
            private int _index; // current location in the array of items
            private IntPtr _hwnd; // lv hwnd
            private const int _size = 16;
            private bool _isComctrlV6OnOsVerV6orHigher;
 
            #endregion Private Fields
        }
 
        #endregion Nested classes
 
    }
}