// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Data; using System.Text; using System.Windows.Forms; using System.Runtime.InteropServices; namespace UnrealControls { //begining is at (0,0) //pt1 is at (15, 15) //pt2 is at (20, 17) //this creates a box that is 20x17 /// /// VS.NET style tab control. /// public partial class DynamicTabControl : UserControl { /// /// Collection of tab pages. /// public class DynamicTabPageCollection : IList, ICollection, IEnumerable { DynamicTabControl m_owner; List m_pages = new List(); /// /// Constructor. /// /// The owner of the collection. public DynamicTabPageCollection(DynamicTabControl owner) { if(owner == null) { throw new ArgumentNullException("owner", "Owner can not be null!"); } m_owner = owner; } #region IList Members /// /// Returns the index of the specified item. /// /// The item to retrieve an index for. /// -1 if the item is not a part of the collection. public int IndexOf(DynamicTabPage item) { return m_pages.IndexOf(item); } /// /// Inserts a new item into the collection. /// /// The index to insert the item. /// The item to be inserted. public void Insert(int index, DynamicTabPage item) { int curIndex = -1; if((curIndex = m_pages.IndexOf(item)) == -1) { m_pages.Insert(index, item); m_owner.OnTabPageAdded(index); } else { m_owner.SelectedIndex = curIndex; } } /// /// Removes the item at the specified index. /// /// The index of the item to be removed. public void RemoveAt(int index) { if(index >= 0 && index < m_pages.Count) { DynamicTabPage page = m_pages[index]; m_pages.RemoveAt(index); m_owner.OnTabPageRemoved(index, page); } } /// /// Overloaded [] operator. /// /// The index into the collection. /// The tab page at the specified index. public DynamicTabPage this[int index] { get { return m_pages[index]; } set { m_pages[index] = value; } } #endregion #region ICollection Members /// /// Adds a tab page to the collection. /// /// The tab page to be added. public void Add(DynamicTabPage item) { //m_pages.Add(item); Insert(0, item); } /// /// Clears the collection. /// public void Clear() { m_pages.Clear(); } /// /// Returns true if the specified item exists in the collection. /// /// The item to be checked. /// True if the specified item exists in the collection. public bool Contains(DynamicTabPage item) { return m_pages.Contains(item); } /// /// Copies the collection into an array. /// /// The array to receive the collection. /// The index to start copying at. public void CopyTo(DynamicTabPage[] array, int arrayIndex) { m_pages.CopyTo(array, arrayIndex); } /// /// The number of items in the collection. /// public int Count { get { return m_pages.Count; } } /// /// Returns false. /// public bool IsReadOnly { get { return false; } } /// /// Removes the specified item from the collection. /// /// The item to be removed. /// True if the item was successfully removed. public bool Remove(DynamicTabPage item) { for(int i = 0; i < m_pages.Count; ++i) { if(m_pages[i] == item) { RemoveAt(i); return true; } } return false; } #endregion #region IEnumerable Members /// /// Gets an enumerator. /// /// An enumerator. public IEnumerator GetEnumerator() { return m_pages.GetEnumerator(); } #endregion #region IEnumerable Members /// /// Gets an enumerator. /// /// An enumerator. System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return m_pages.GetEnumerator(); } #endregion } const int SLIDEWIDTH = 24; const int TABHEIGHT = 17; const int CURVEOFFSETY = 15; const int CURVEOFFSETX = 15; const int BOXHEIGHT = 15; const int BOXWIDTH = 16; const int ARROWLEFTOFFSET = 40; DynamicTabPageCollection mTabPages; XButton mXButton = new XButton(); MenuArrow mMnuArrow = new MenuArrow(); ContextMenuStrip mCtxMenu = new ContextMenuStrip(); Font mBoldFont; int mSelectedTab; EventHandler mSelectedIndexChanged; /// /// Event for when the selected tab has changed. /// public event EventHandler SelectedIndexChanged { add { mSelectedIndexChanged += value; } remove { mSelectedIndexChanged -= value; } } /// /// Gets the collection of tab pages. /// public DynamicTabPageCollection TabPages { get { return mTabPages; } } /// /// Gets/Sets the index of the currently selected tab. /// public int SelectedIndex { get { return mSelectedTab; } set { SelectTab(value); } } /// /// Gets/Sets the currently selected tab. /// public DynamicTabPage SelectedTab { get { if(mSelectedTab >= 0 && mSelectedTab < mTabPages.Count) { return mTabPages[mSelectedTab]; } return null; } set { SelectTab(mTabPages.IndexOf(value)); } } /// /// Constructor. /// public DynamicTabControl() { InitializeComponent(); this.Controls.Add(mXButton); //m_xButton.Click += new XButtonClickedDelegate(m_xButton_Click); mXButton.Click +=new EventHandler(XButton_Click); this.Controls.Add(mMnuArrow); //m_mnuArrow.Click += new MenuArrowClickedDelegate(m_mnuArrow_Click); mMnuArrow.Click += new EventHandler(MnuArrow_Click); this.UpdateMenuItemPositions(); mTabPages = new DynamicTabPageCollection(this); mBoldFont = new Font(this.Font, FontStyle.Bold); this.DoubleBuffered = true; } /// /// Called when the tab selection arrow has been clicked. /// /// The tab selection arrow. /// Event args. void MnuArrow_Click(object sender, EventArgs e) { foreach(DynamicTabControlMenuItem item in mCtxMenu.Items) { item.Click -= new EventHandler(CtxMenuHandler); } mCtxMenu.Items.Clear(); if(mTabPages.Count > 0) { for(int i = 0; i < mTabPages.Count; ++i) { mCtxMenu.Items.Add(new DynamicTabControlMenuItem(mTabPages[i].Text, i, new EventHandler(CtxMenuHandler))); } mCtxMenu.Show(this, new Point(mMnuArrow.Bounds.Left, mMnuArrow.Bounds.Bottom)); } } /// /// The context menu for the tab selection arrow. /// /// The context menu /// Event args. void CtxMenuHandler(object sender, EventArgs e) { DynamicTabControlMenuItem item = sender as DynamicTabControlMenuItem; if(item != null) { SelectTab(item.TabPageIndex); } } /// /// Called when the X button has been clicked to close a tab. /// /// The X button. /// Event args. void XButton_Click(object sender, EventArgs e) { mTabPages.RemoveAt(mSelectedTab); } /// /// Called to paint the tab control. /// /// Event args. protected override void OnPaint(PaintEventArgs e) { StringFormat fmt = new StringFormat(); fmt.Alignment = StringAlignment.Center; fmt.LineAlignment = StringAlignment.Center; //e.Graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.ClearTypeGridFit; SizeF size; bool clip = true; int offX = 0; int halfSlide = SLIDEWIDTH / 2; bool setArrowLine = false; for(int i = 0; i < mTabPages.Count; ++i, offX += (int)size.Width + halfSlide) { if(i == 0 || i == mSelectedTab) { clip = false; } else { clip = true; } size = e.Graphics.MeasureString(mTabPages[i].Text, mBoldFont); if(offX + size.Width + SLIDEWIDTH >= this.Width - ARROWLEFTOFFSET && i > 0) { setArrowLine = true; break; } DrawTab(offX, size, e.Graphics, mTabPages[i].Text, fmt, clip, i == mSelectedTab, mTabPages[i]); } mMnuArrow.DrawLine = setArrowLine; e.Graphics.Clip = new Region(); e.Graphics.DrawLine(SystemPens.ControlDark, offX + halfSlide, 3 + TABHEIGHT, this.Width, 3 + TABHEIGHT); base.OnPaint(e); } /// /// Called to draw a tab. /// /// The X offset of the tab. /// The size of the tab in pixels. /// The GDI+ graphics object. /// The text to be displayed on the tab. /// The format of the text. /// The clipping region. /// The currently selected tab. private void DrawTab(int offX, SizeF size, Graphics graphics, string text, StringFormat fmt, bool clip, bool selectedTab, DynamicTabPage tabPage) { Rectangle tabRect = new Rectangle(offX, 3, (int)size.Width + SLIDEWIDTH, TABHEIGHT); Point[] points = { new Point(tabRect.Left, tabRect.Bottom), new Point(tabRect.Left + CURVEOFFSETX , tabRect.Bottom - CURVEOFFSETY), new Point(tabRect.Left + SLIDEWIDTH, tabRect.Top), new Point(tabRect.Right - 2, tabRect.Top), new Point(tabRect.Right, tabRect.Top + 2), new Point(tabRect.Right, tabRect.Bottom), }; if(clip) { graphics.Clip = new Region(new Rectangle(tabRect.Left + SLIDEWIDTH / 2 + 1, tabRect.Top, tabRect.Width, tabRect.Height + 1)); } else { graphics.Clip = new Region(); } graphics.FillPolygon(tabPage.TabBackgroundColor, points); graphics.DrawPolygon(SystemPens.ControlDark, points); RectangleF rect = new RectangleF((float)(tabRect.Left + CURVEOFFSETX), (float)tabRect.Top, (float)(tabRect.Right - tabRect.Left - CURVEOFFSETX), (float)tabRect.Bottom - (float)tabRect.Top); graphics.DrawString(text, selectedTab ? mBoldFont : this.Font, tabPage.TabForegroundColor, rect, fmt); if(selectedTab) { graphics.DrawLine(SystemPens.Window, tabRect.Left + 1, tabRect.Bottom, tabRect.Right - 1, tabRect.Bottom); } } /// /// Called when a tab page has been added. /// /// The index of the new tab page. void OnTabPageAdded(int index) { mTabPages[index].TextChanged += new EventHandler(DynamicTabControl_TextChanged); mTabPages[index].Bounds = new Rectangle(0, TABHEIGHT + 4, this.Width, this.Height - TABHEIGHT - 4); this.Controls.Add(mTabPages[index]); if(mTabPages.Count == 1) { mSelectedTab = 0; //this.Controls.Add(mTabPages[0]); OnSelectedIndexChanged(this, new EventArgs()); mTabPages[0].SelectTab(); } else if(index <= mSelectedTab) { ++mSelectedTab; //OnSelectedIndexChanged(this, new EventArgs()); SelectTab(index); } else { SelectTab(index); } this.Invalidate(); } /// /// Called when the text of a tab page has changed. /// /// The tab page. /// Event args. void DynamicTabControl_TextChanged(object sender, EventArgs e) { Invalidate(); } /// /// Called when a mouse button has been pressed. /// /// Event args. protected override void OnMouseDown(MouseEventArgs e) { if(e.Button == MouseButtons.Left && e.Y < 3 + TABHEIGHT) { int offX = 0; int halfSlide = SLIDEWIDTH / 2; SizeF size; using(Graphics gfx = this.CreateGraphics()) { for(int i = 0; i < mTabPages.Count; ++i, offX += (int)size.Width + halfSlide) { size = gfx.MeasureString(mTabPages[i].Text, mBoldFont); if(offX + size.Width + SLIDEWIDTH >= this.Width - ARROWLEFTOFFSET && i > 0) { break; } Rectangle rect; if(i == mSelectedTab || i == 0) { rect = new Rectangle(offX, 3, (int)size.Width + SLIDEWIDTH, TABHEIGHT); } else { rect = new Rectangle(offX + halfSlide, 3, (int)size.Width + halfSlide, TABHEIGHT); } if(rect.Contains(e.X, e.Y)) { if(e.X >= offX + CURVEOFFSETX) { if(i + 1 != mSelectedTab) { SelectTab(i); break; } else { float x = offX + size.Width + halfSlide; if(PointInTriangle(new PointF(e.X, e.Y), new PointF(x + CURVEOFFSETX, 3.0f), new PointF(x, 3 + TABHEIGHT), new PointF(x + CURVEOFFSETX, 3 + TABHEIGHT))) { SelectTab(i + 1); break; } else { SelectTab(i); break; } } } else { if(i == 0 || i == mSelectedTab) { if(PointInTriangle(new PointF(e.X, e.Y), new PointF(offX + CURVEOFFSETX, 3.0f), new PointF(offX, 3 + TABHEIGHT), new PointF(offX + CURVEOFFSETX, 3 + TABHEIGHT))) { SelectTab(i); break; } } else { if(PointInTriangle(new PointF(e.X, e.Y), new PointF(offX + CURVEOFFSETX, 3.0f), new PointF(offX, 3 + TABHEIGHT), new PointF(offX + CURVEOFFSETX, 3 + TABHEIGHT)) && e.X > offX + halfSlide) { SelectTab(i); break; } } } //end else } //end if pt is in tab rect } //end for } //end using } //end if base.OnMouseDown(e); } /// /// Returns true if the specified point is within the specified triangle. /// /// The point in question. /// Triangle p1. /// Triangle p2. /// Triangle p3. /// True if i is within the triangle defined by p1, p2, and p3. private bool PointInTriangle(PointF i, PointF p0, PointF p1, PointF p2) { PointF e0 = new PointF(i.X - p0.X, i.Y - p0.Y); PointF e1 = new PointF(p1.X - p0.X, p1.Y - p0.Y); PointF e2 = new PointF(p2.X - p0.X, p2.Y - p0.Y); float d = e2.Y * e1.X - e2.X * e1.Y; if(d == 0.0f) { return false; } float u = (e0.Y * e1.X - e0.X * e1.Y) / d; if(u < 0.0f || u > 1.0f) { return false; } float v = (e0.X - e2.X * u) / e1.X; if(v < 0.0f) { return false; } if(v + u > 1.0f) { return false; } return true; } /// /// Selects the tab at the specified index. /// /// The index of the tab to be selected. private void SelectTab(int i) { if(mSelectedTab != i && i >= 0 && i < mTabPages.Count) { if(mSelectedTab >= 0 && mSelectedTab < mTabPages.Count) { mTabPages[mSelectedTab].DeselectTab(); //this.Controls.Remove(mTabPages[mSelectedTab]); mTabPages[mSelectedTab].Visible = false; } if(TabInView(i)) { mSelectedTab = i; //this.Controls.Add(mTabPages[i]); mTabPages[i].Visible = true; mTabPages[i].SelectTab(); } else { DynamicTabPage tab = mTabPages[i]; mTabPages.RemoveAt(i); mTabPages.Insert(0, tab); mSelectedTab = 0; //this.Controls.Add(mTabPages[0]); mTabPages[0].Visible = true; mTabPages[0].SelectTab(); } OnSelectedIndexChanged(this, new EventArgs()); Invalidate(); } } /// /// Returns true if the tab at the specified index is in view. /// /// The index of the tab to be checked. /// True if the tab with the specified index is in view. public bool TabInView(int index) { int offX = 0; int halfSlide = SLIDEWIDTH / 2; SizeF size; using(Graphics gfx = this.CreateGraphics()) { for(int i = 0; i < mTabPages.Count; ++i, offX += (int)size.Width + halfSlide) { size = gfx.MeasureString(mTabPages[i].Text, mBoldFont); if(offX + size.Width + SLIDEWIDTH >= this.Width - ARROWLEFTOFFSET && i > 0) { break; } if(i == index) { return true; } } } return false; } /// /// Called when a control has been added to the tab page. /// /// protected override void OnControlAdded(ControlEventArgs e) { if(!(e.Control is DynamicTabPage || e.Control is XButton || e.Control is MenuArrow)) { this.Controls.Remove(e.Control); } } /// /// Called when the tab control has been resized. /// /// Event args. protected override void OnResize(EventArgs e) { base.OnResize(e); UpdateMenuItemPositions(); if(mTabPages != null) { foreach(DynamicTabPage page in mTabPages) { page.Bounds = new Rectangle(0, TABHEIGHT + 4, this.Width, this.Height - TABHEIGHT - 4); } } Invalidate(false); } /// /// Updates the arrow button and X button positions. /// void UpdateMenuItemPositions() { mMnuArrow.Bounds = new Rectangle(this.Width - ARROWLEFTOFFSET, 3, BOXWIDTH, BOXHEIGHT); mXButton.Bounds = new Rectangle((this.Width - ARROWLEFTOFFSET) + BOXWIDTH + 3, 3, BOXWIDTH, BOXHEIGHT); } /// /// Called when a tab page has been removed. /// /// The index of the removed tab page. /// The tab page being removed. internal void OnTabPageRemoved(int index, DynamicTabPage page) { page.TextChanged -= new EventHandler(this.DynamicTabControl_TextChanged); if(index == mSelectedTab) { page.DeselectTab(); this.Controls.Remove(page); if(mSelectedTab < mTabPages.Count) { //this.Controls.Add(mTabPages[mSelectedTab]); mTabPages[mSelectedTab].Visible = true; mTabPages[mSelectedTab].SelectTab(); } else { mSelectedTab = mTabPages.Count - 1; if(mSelectedTab >= 0) { //this.Controls.Add(mTabPages[mSelectedTab]); mTabPages[mSelectedTab].Visible = true; mTabPages[mSelectedTab].SelectTab(); } } OnSelectedIndexChanged(this, new EventArgs()); } else if(index < mSelectedTab) { --mSelectedTab; OnSelectedIndexChanged(this, new EventArgs()); } Invalidate(); } /// /// Called when the index of the selected tab page has changed. /// /// The tab. /// Event args. protected virtual void OnSelectedIndexChanged(object sender, EventArgs e) { if(mSelectedIndexChanged != null) { mSelectedIndexChanged(sender, e); } } } }