using System; using System.Collections.Generic; using System.Text; using System.Diagnostics; using Beefy.gfx; namespace Beefy.widgets { public abstract class ListViewItem : Widget { public ListView mListView; public String mLabel ~ delete _; public IDrawable mIconImage; public int32 mColumnIdx; public float mSelfHeight; // Designed height only for ourselves, not including children public float mChildAreaHeight; public float mBottomPadding; public int32 mDepth; public float mShowChildPct; public uint32 mIconImageColor; public ListViewItem mParentItem; public List mSubItems; // de DeleteContainerAndItems!(_); public List mChildItems ~ DeleteContainerAndItems!(_); // Tree view //int mIdx = sIdx++; //static int sIdx = 0; internal this() { } public ~this() { if (mColumnIdx == 0) delete mSubItems; } virtual public StringView Label { get { return (mLabel != null) ? mLabel : default; } set { String.NewOrSet!(mLabel, value);} } virtual public IDrawable IconImage { get { return mIconImage; } set { mIconImage = value; } } virtual public uint32 IconImageColor { get { return mIconImageColor; } set { mIconImageColor = value; } } protected bool mIsSelected; protected bool mIsFocused; virtual public bool Selected { get { return mIsSelected; } set { if (mIsSelected != value) { mIsSelected = value; MarkDirty(); } if ((!mIsSelected) && (mIsFocused)) { mIsFocused = false; MarkDirty(); } } } virtual public bool Focused { get { return mIsFocused; } set { if (mIsFocused != value) { Selected = true; mIsFocused = value; if (mListView.mOnFocusChanged.HasListeners) mListView.mOnFocusChanged(this); MarkDirty(); } } } virtual public bool IsParent { get { return mChildItems != null; } set { if (value) MakeParent(); else if (IsParent) Runtime.FatalError("Cannot undo parent status"); } } public virtual float LabelX { get { return 0; } } public virtual float LabelWidth { get { return mWidth; } } public virtual void Init(ListView listView) { } protected override void RemovedFromWindow() { base.RemovedFromWindow(); if (mChildItems != null) { for (var child in mChildItems) child.RemovedFromWindow(); } if ((mColumnIdx == 0) && (mSubItems != null)) { for (var subItem in mSubItems) { if (subItem != this) subItem.RemovedFromWindow(); } } } public void WithItems(Action func) { if (mChildItems != null) { for (ListViewItem child in mChildItems) { func(child); child.WithItems(func); } } } public void WithSelectedItems(Action func, bool skipSelectedChildrenOnSelectedItems = false) { bool selfSelected = Selected; if (selfSelected) func(this); if ((mChildItems != null) && ((!skipSelectedChildrenOnSelectedItems) || (!selfSelected))) { for (ListViewItem child in mChildItems) { child.WithSelectedItems(func, skipSelectedChildrenOnSelectedItems); } } } public ListViewItem FindFirstSelectedItem() { if (Selected) return this; if (mChildItems != null) { for (ListViewItem child in mChildItems) { ListViewItem selected = child.FindFirstSelectedItem(); if (selected != null) return selected; } } return null; } public ListViewItem FindLastSelectedItem(bool filterToVisible = false) { ListViewItem selected = null; if (mIsSelected && (!filterToVisible || mVisible)) selected = this; if (mChildItems != null) { for (ListViewItem child in mChildItems) { ListViewItem checkSelected = child.FindLastSelectedItem(filterToVisible); if (checkSelected != null) selected = checkSelected; } } return selected; } public ListViewItem FindFocusedItem() { ListViewItem selectedListViewItem = null; WithSelectedItems(scope [&] (listViewItem) => { if ((listViewItem.mIsFocused) || (selectedListViewItem == null)) selectedListViewItem = listViewItem; }); return selectedListViewItem; } public void SelectItemExclusively(ListViewItem item) { WithSelectedItems(scope (listViewItem) => { if (listViewItem != item) listViewItem.Selected = false; }); if (item != null) { item.Selected = true; item.Focused = true; } } public void SelectItem(ListViewItem item, bool checkKeyStates = false) { if (item == null) { SelectItemExclusively(null); return; } if (item != null) { if ((mListView.mAllowMultiSelect) && (checkKeyStates) && (mWidgetWindow.IsKeyDown(KeyCode.Shift))) { var focusedItem = FindFocusedItem(); if ((focusedItem != item) && (focusedItem != null)) { ListViewItem spanStart = null; ListViewItem selectEndElement = null; ListViewItem prevItem = null; WithItems(scope [&] (checkItem) => { defer { prevItem = checkItem; } if (selectEndElement != null) return; if (checkItem == focusedItem) { if (spanStart != null) { // Done. Select from end. selectEndElement = spanStart; return; } spanStart = checkItem; return; } if (spanStart != null) { if (!checkItem.Selected) { if (spanStart == focusedItem) selectEndElement = prevItem; spanStart = null; } } else if (spanStart == null) { if (checkItem.Selected) spanStart = checkItem; } }); focusedItem.Focused = false; bool foundOldEnd = false; bool foundNewHead = false; WithItems(scope [&] (checkItem) => { checkItem.Selected = foundNewHead ^ foundOldEnd; if (checkItem == item) { foundNewHead = true; checkItem.Selected = true; } if (checkItem == selectEndElement) { foundOldEnd = true; checkItem.Selected = true; } }); item.Focused = true; return; } } if ((mListView.mAllowMultiSelect) && (checkKeyStates) && (mWidgetWindow.IsKeyDown(KeyCode.Control))) { if (!item.Focused) item.Selected = !item.Selected; } else { SelectItemExclusively(item); if (item.Selected) item.Focused = true; } } } public virtual void Clear() { if (mChildItems == null) return; for (var childItem in mChildItems) { childItem.DoRemove(false); delete childItem; } mChildItems.Clear(); } void DoRemove(bool removeFromList) { if ((mSubItems != null) && (mColumnIdx == 0)) { for (int32 i = 1; i < mSubItems.Count; i++) { var subItem = mSubItems[i]; subItem.Remove(); delete subItem; } mSubItems.Clear(); } if (mColumnIdx == 0) { if (removeFromList) { mListView.mListSizeDirty = true; mParentItem.mChildItems.Remove(this); } } else { base.RemoveSelf(); } mParentItem = null; RemovedFromWindow(); } public virtual void Remove() { DoRemove(true); } public int GetSubItemCount() { if (mSubItems == null) return 0; return mSubItems.Count; } public void InitSubItem(int columnIdx, ListViewItem subItem) { Debug.Assert(columnIdx > 0); if (mSubItems == null) { mSubItems = new List(); mSubItems.Add(this); // Add ourselves to column zero } while (columnIdx >= mSubItems.Count) mSubItems.Add(null); subItem.mDepth = mDepth; subItem.mListView = mListView; subItem.mSubItems = mSubItems; // Share with column zero subItem.mColumnIdx = (.)columnIdx; subItem.mParentItem = mParentItem; mSubItems[columnIdx] = subItem; /*mParentItem.*/AddWidget(subItem); mListView.mListSizeDirty = true; } public ListViewItem CreateSubItem(int columnIdx) { ListViewItem subItem = mListView.CreateListViewItem_Internal(); InitSubItem(columnIdx, subItem); return subItem; } public ListViewItem GetMainItem() { if (mColumnIdx == 0) return this; return mSubItems[0]; } public ListViewItem GetSubItem(int columnIdx) { if (mColumnIdx == columnIdx) return this; return mSubItems[columnIdx]; } public virtual void MakeParent() { if (mChildItems == null) mChildItems = new List(); } public virtual void TryUnmakeParent() { if ((mChildItems != null) && (mChildItems.Count == 0)) { DeleteAndNullify!(mChildItems); } } public virtual bool Open(bool open, bool immediate = false) { return false; } public override void InitChildren() { base.InitChildren(); if (mChildItems != null) { for (var item in mChildItems) { item.mWidgetWindow = mWidgetWindow; item.InitChildren(); } } } public virtual ListViewItem CreateChildItemAtIndex(int idx) { MakeParent(); ListViewItem child = mListView.CreateListViewItem_Internal(); child.mListView = mListView; child.mParentItem = this; child.mDepth = mDepth + 1; child.mVisible = mShowChildPct > 0.0f; mChildItems.Insert(idx, child); AddWidgetUntracked(child); mListView.mListSizeDirty = true; return child; } public virtual ListViewItem CreateChildItem() { return CreateChildItemAtIndex((mChildItems != null) ? mChildItems.Count : 0); } public int32 GetIndexOfChild(ListViewItem item) { return mChildItems.IndexOf(item); } public virtual void RemoveChildItemAt(int idx, bool deleteItem = true) { var item = mChildItems[idx]; mChildItems.RemoveAt(idx); RemoveWidget(item); item.mParentItem = null; item.mListView = null; mListView.mListSizeDirty = true; if (deleteItem) delete item; } public virtual void RemoveChildItem(ListViewItem item, bool deleteItem = true) { int32 idx = mChildItems.IndexOf(item); RemoveChildItemAt(idx, deleteItem); } protected void SetDepth(int32 depth) { if (mDepth == depth) return; mDepth = depth; if (mSubItems != null) for (ListViewItem anItem in mSubItems) anItem.SetDepth(depth); if (mChildItems != null) for (ListViewItem anItem in mChildItems) anItem.SetDepth(depth + 1); } public virtual void AddChildAtIndex(int index, ListViewItem item) { mChildItems.Insert(index, item); AddWidgetUntracked(item); item.mParentItem = this; item.mListView = mListView; item.SetDepth(mDepth + 1); mListView.mListSizeDirty = true; } public virtual void InsertChild(ListViewItem item, ListViewItem insertBefore) { int pos = (insertBefore != null) ? mChildItems.IndexOf(insertBefore) : mChildItems.Count; AddChildAtIndex(pos, item); } public virtual void AddChild(ListViewItem item, ListViewItem addAfter = null) { int pos = (addAfter != null) ? (mChildItems.IndexOf(addAfter) + 1) : mChildItems.Count; AddChildAtIndex(pos, item); } public int GetChildCount() { if (mChildItems == null) return 0; return mChildItems.Count; } public ListViewItem GetChildAtIndex(int idx) { return mChildItems[idx]; } public virtual float CalculatedDesiredHeight() { mChildAreaHeight = 0; if (mChildItems != null) { for (ListViewItem listViewItem in mChildItems) mChildAreaHeight += listViewItem.CalculatedDesiredHeight(); } mChildAreaHeight *= mShowChildPct; return mChildAreaHeight + mSelfHeight + mBottomPadding; } public virtual float ResizeComponents(float xOffset) { float curY = 0; if ((mSubItems != null) && (mColumnIdx == 0)) { float curX = -mX - xOffset; int columnCount = Math.Min(mSubItems.Count, mListView.mColumns.Count); for (int32 aColumnIdx = 0; aColumnIdx < columnCount; aColumnIdx++) { ListViewColumn aColumn = mListView.mColumns[aColumnIdx]; if (aColumnIdx > 0) { ListViewItem subItem = mSubItems[aColumnIdx]; if (subItem != null) { subItem.mVisible = mVisible; if (aColumnIdx == columnCount - 1) subItem.Resize(curX, /*mY*/0, Math.Max(aColumn.mWidth, mWidth - curX), mHeight); else { float aWidth = aColumn.mWidth; subItem.Resize(curX, /*mY*/0, aWidth, mHeight); } if (aColumnIdx > 0) subItem.ResizeComponents(xOffset); } } curX += aColumn.mWidth; } } curY += mSelfHeight; if (mChildItems != null) { for (ListViewItem child in mChildItems) { child.mVisible = (mShowChildPct > 0.0f); child.ResizeClamped(child.mX, curY, mWidth - child.mX, child.mSelfHeight); float resizeXOfs = xOffset; if (mParentItem != null) resizeXOfs += mX; float childSize = (int)child.ResizeComponents(resizeXOfs); curY += childSize; } } mChildAreaHeight = CalculatedDesiredHeight() - mSelfHeight - mBottomPadding; return mChildAreaHeight + mSelfHeight + mBottomPadding; } public override void Resize(float x, float y, float width, float height) { base.Resize(x, y, width, height); } public override void MouseClicked(float x, float y, int32 btn) { base.MouseClicked(x, y, btn); if (mParentItem != null) // Don't notify for root mListView.mOnItemMouseClicked(this, x, y, btn); } public override void MouseDown(float x, float y, int32 btn, int32 btnCount) { base.MouseDown(x, y, btn, btnCount); if (mParentItem != null) // Don't notify for root mListView.mOnItemMouseDown(this, x, y, btn, btnCount); } } public class ListViewColumn { public String mLabel ~ delete _; public float mWidth; public float mMinWidth; public float mMaxWidth; public String Label { get { return mLabel; } set { String.NewOrSet!(mLabel, value); } } } public class ListViewDragData { List mDragItems; } public abstract class ListView : ScrollableWidget { public List mColumns = new List() ~ DeleteContainerAndItems!(_); protected ListViewItem mRoot; public bool mListSizeDirty; public float mHeaderHeight; public Event> mOnFocusChanged ~ _.Dispose(); public float mBottomInset = 8; public bool mAllowMultiSelect = true; public Event mOnItemMouseDown ~ _.Dispose(); public Event mOnItemMouseClicked ~ _.Dispose(); public this() { mRoot = CreateListViewItem(); mRoot.mSelfHeight = 0; mRoot.mListView = this; mListSizeDirty = true; mScrollContent = mRoot; mScrollContentContainer.AddWidget(mRoot); } public virtual ListViewColumn AddColumn(float width, String label) { ListViewColumn aColumn = new ListViewColumn(); aColumn.mWidth = width; aColumn.Label = label; aColumn.mMinWidth = 64; mColumns.Add(aColumn); return aColumn; } protected virtual ListViewItem CreateListViewItem() { return null; } internal virtual ListViewItem CreateListViewItem_Internal() { var listViewItem = CreateListViewItem(); listViewItem.Init(this); return listViewItem; } public virtual ListViewItem GetRoot() { return mRoot; } protected virtual void ColumnResized(ListViewColumn column) { mListSizeDirty = true; } public override void Update() { base.Update(); } public override void UpdateAll() { base.UpdateAll(); UpdateListSize(); } public virtual float GetListWidth() { float columnWidths = 0; for (ListViewColumn aColumn in mColumns) columnWidths += aColumn.mWidth; return columnWidths; } public void UpdateListSize() { // Do this in UpdateAll to give children a change to resize items if (mListSizeDirty) { float listWidth = GetListWidth(); mRoot.mWidth = Math.Max(listWidth, mScrollContentContainer.mWidth); mRoot.mHeight = mRoot.CalculatedDesiredHeight() + mBottomInset; mRoot.ResizeComponents(0); UpdateScrollbarData(); mListSizeDirty = false; } } public override void Resize(float x, float y, float width, float height) { if ((x == mX) && (y == mY) && (width == mWidth) && (height == mHeight)) return; base.Resize(x, y, width, height); mListSizeDirty = true; } public void Resized() { mListSizeDirty = true; } public void EnsureItemVisible(ListViewItem item, bool centerView) { if (mVertScrollbar == null) return; if (mListSizeDirty) UpdateListSize(); if (mScrollContentContainer.mHeight <= 0) return; float aX; float aY; item.SelfToOtherTranslate(mScrollContent, 0, 0, out aX, out aY); float topInsets = 0; float lineHeight = item.mSelfHeight; if (aY < mVertPos.mDest + topInsets) { float scrollPos = aY - topInsets; if (centerView) { scrollPos -= mScrollContentContainer.mHeight * 0.50f; scrollPos = (float)Math.Round(scrollPos / lineHeight) * lineHeight; } VertScrollTo(scrollPos); } else if (aY + lineHeight + mBottomInset >= mVertPos.mDest + mScrollContentContainer.mHeight) { float scrollPos = aY + lineHeight + mBottomInset - mScrollContentContainer.mHeight; if (centerView) { // Show slightly more content on bottom scrollPos += mScrollContentContainer.mHeight * 0.50f; scrollPos = (float)Math.Round(scrollPos / lineHeight) * lineHeight; } VertScrollTo(scrollPos); } } ListViewItem FindClosestItemAtYPosition(ListViewItem parentItem, float y, bool addHeight) { if (parentItem.mChildItems != null) { for (var item in parentItem.mChildItems) { var closestItem = FindClosestItemAtYPosition(item, y + item.mY, addHeight); if (closestItem != null) return closestItem; } } if ((y >= 0) && (y < parentItem.mSelfHeight)) return parentItem; return null; } public override void KeyDown(KeyCode keyCode, bool isRepeat) { base.KeyDown(keyCode, isRepeat); switch (keyCode) { case (KeyCode)'A': if ((mAllowMultiSelect) && (mWidgetWindow.GetKeyFlags() == KeyFlags.Ctrl)) { mRoot.WithItems(scope (listViewItem) => { listViewItem.Selected = true; }); } break; default: } bool isDoingSpanSelection = mAllowMultiSelect && mWidgetWindow.IsKeyDown(KeyCode.Shift); //ListViewItem firstSelectedItem = isDoingSpanSelection ? mRoot.FindFirstSelectedItem() : mRoot.FindFocusedItem(); ListViewItem firstSelectedItem = mRoot.FindFocusedItem(); if (firstSelectedItem != null) { ListViewItem selectedItem = firstSelectedItem; bool triedMove = false; ListViewItem newSelection = null; switch (keyCode) { case KeyCode.Home: newSelection = GetRoot().mChildItems[0]; case KeyCode.End: newSelection = GetRoot(); while (newSelection.mChildAreaHeight > 0) { newSelection = newSelection.mChildItems[newSelection.mChildItems.Count - 1]; } case KeyCode.PageUp: int32 numIterations = (int32)(mScrollContentContainer.mHeight / selectedItem.mSelfHeight); for (int32 i = 0; i < numIterations; i++) KeyDown(KeyCode.Up, false); case KeyCode.PageDown: int32 numIterations = (int32)(mScrollContentContainer.mHeight / selectedItem.mSelfHeight); for (int32 i = 0; i < numIterations; i++) KeyDown(KeyCode.Down, false); case KeyCode.Up: int32 idx = selectedItem.mParentItem.mChildItems.IndexOf(selectedItem); if (idx > 0) { newSelection = selectedItem.mParentItem.mChildItems[idx - 1]; while (newSelection.mChildAreaHeight > 0) newSelection = newSelection.mChildItems[newSelection.mChildItems.Count - 1]; } else if (selectedItem.mParentItem != mRoot) { newSelection = selectedItem.mParentItem; } triedMove = true; case KeyCode.Down: if ((selectedItem.mChildItems != null) && (selectedItem.mChildItems.Count > 0) && (selectedItem.mChildAreaHeight > 0)) newSelection = selectedItem.mChildItems[0]; else { while (selectedItem != mRoot) { var childItems = selectedItem.mParentItem.mChildItems; int32 idx = childItems.IndexOf(selectedItem); if (idx < childItems.Count - 1) { newSelection = childItems[idx + 1]; break; } selectedItem = selectedItem.mParentItem; } } triedMove = true; case KeyCode.Left: if (!selectedItem.Open(false)) { if (selectedItem.mParentItem != GetRoot()) newSelection = selectedItem.mParentItem; } case KeyCode.Right: if (!selectedItem.Open(true)) { if ((selectedItem.mChildItems != null) && (selectedItem.mChildItems.Count > 0)) newSelection = selectedItem.mChildItems[0]; } case KeyCode.Space: if (!selectedItem.Open(false)) selectedItem.Open(true); case KeyCode.Return: var mouseEvent = scope Beefy.events.MouseEvent(); mouseEvent.mSender = selectedItem; mouseEvent.mBtnCount = 2; selectedItem.mOnMouseDown(mouseEvent); default: } if (newSelection != null) { mRoot.SelectItem(newSelection, isDoingSpanSelection); EnsureItemVisible(newSelection, false); } else if ((triedMove) && (!isDoingSpanSelection) && (firstSelectedItem != null)) mRoot.SelectItemExclusively(firstSelectedItem); } } } }