frontend/windows/WBControls/FlatTabControl.cs (1,659 lines of code) (raw):

/* * Copyright (c) 2009, 2019, Oracle and/or its affiliates. All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License, version 2.0, * as published by the Free Software Foundation. * * This program is designed to work with certain software (including * but not limited to OpenSSL) that is licensed under separate terms, as * designated in a particular file or component or in included license * documentation. The authors of MySQL hereby grant you an additional * permission to link the program and your derivative works with the * separately licensed software that they have either included with * the program or referenced in the documentation. * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See * the GNU General Public License, version 2.0, for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA */ using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Drawing.Drawing2D; using System.Runtime.InteropServices; using System.Windows.Forms; using MySQL.Utilities; using MySQL.Utilities.SysUtils; namespace MySQL.Controls { /// <summary> /// Summary description for FlatTabControl. /// </summary> [ToolboxBitmap(typeof(System.Windows.Forms.TabControl))] public class FlatTabControl : System.Windows.Forms.TabControl { // Summary: determines position and look of the tabs. public enum TabStyleType { NoTabs, // Don't show any tabs. TopNormal, // Tabs standing on the content above the top line. BottomNormal, // Tabs hanging down below the content. TopTransparent, // Tabs standing on the content, like TopNormal. Different colors, though. // Background of space not covered by a tab is transparent. Different outline too. } public enum CloseButtonVisiblity { ShowButton, HideButton, InheritVisibility, } private const String tabRelayoutMessageName = "WindowsForms12_TabBaseReLayout"; private uint tabRelayoutMessageID; // TabInfo keeps all necessary layout information. private class TabInfo { internal TabPage page; internal bool isValid; internal CloseButtonVisiblity closeButtonVisibility; // For individual close buttons. internal bool isBusy; // Show a busy indicator is to be shown for that tab (in place of the close button). internal Rectangle tabArea; internal Rectangle buttonArea; internal TabInfo() { this.page = null; isValid = false; closeButtonVisibility = CloseButtonVisiblity.InheritVisibility; } } /// <summary> /// Required designer variable. /// </summary> private System.ComponentModel.Container components = null; private ImageList leftRightImages = null; private Bitmap darkCloseButton = null; private Bitmap lightCloseButton = null; private Dictionary<string, Bitmap> busyIndicators = new Dictionary<string, Bitmap>(); private SubClass scroller = null; private Form activationTracker = null; internal Control auxView = null; // For top-transparent there is no tab background color. private Color topTransparentTabColor = Color.White; private Color topTransparentTabTextColor = Color.Black; private Color topNormalTabTextColor = Color.Black; private Color topNormalTabSelectedFocusedColor = Color.Blue; private Color topNormalTabSelectedFocusedTextColor = Color.White; private Color topNormalTabSelectedUnfocusedColor = Color.Gray; private Color topNormalTabSelectedUnfocusedTextColor = Color.White; private Color unselectedTabColor = Color.Gray; // Normal top + bottom style. Also for uncovered areas. private Color bottomTabTextColor = Color.Black; private Color bottomTabSelectedColor = Color.White; private Color bottomTabSelectedTextColor = Color.Black; private int glowSize = 12; // The size of the glow behind text on glass tabs. private bool showCloseButton = true; // Central flag for close buttons. Can be overridden by each page. private bool showFocusState = true; private bool hideWhenEmpty = false; // Painting/Layouting private TabStyleType tabStyle = TabStyleType.TopNormal; private List<TabInfo> layoutInfo = new List<TabInfo>(); private int scrollOffset; private int pendingScrollIntoViewIndex = -1; // Set when scrollIntoView was called while we were minimized. // Some layout constants. private const int buttonSpacing = 5; // Number of pixels between text and close button. private const int scrollerSpacing = 5; // Distance of last tab and the scroller when scrolled to the end. private const int scrollScaleFactor = 5; // Number of pixel we scroll on a single scroll click. // The padding within the tabs. private Padding itemPadding = new Padding(6, 0, 6, 0); // The padding around the tab pages in the tab control. private Padding contentPadding = new Padding(); // Maximum tab size. private int maxTabSize = 200; // Mouse handling. private int lastTabHit = -1; private Point lastClickPosition; private bool buttonHit = false; // Tab Switch Shortcut private bool useDefaultTabSwitchKey = true; #region Construction and destruction public FlatTabControl() { // This call is required by the Windows.Forms Form Designer. InitializeComponent(); // Some special styles for the control. SetStyle(ControlStyles.UserPaint, true); SetStyle(ControlStyles.AllPaintingInWmPaint, true); SetStyle(ControlStyles.DoubleBuffer, true); //SetStyle(ControlStyles.ResizeRedraw, true); // Doesn't really work. Need Invalidate() call in OnResize too. SetStyle(ControlStyles.SupportsTransparentBackColor, true); SetStyle(ControlStyles.OptimizedDoubleBuffer, true); UpdateStyles(); BackgroundColor = Color.FromKnownColor(KnownColor.Control); leftRightImages = new ImageList(); System.Resources.ResourceManager resources = new System.Resources.ResourceManager(typeof(FlatTabControl)); Bitmap icons = ((System.Drawing.Bitmap)(resources.GetObject("TabIcons"))); if (icons != null) { icons.MakeTransparent(Color.White); leftRightImages.Images.AddStrip(icons); } // Both buttons really should have the same size. darkCloseButton = ((System.Drawing.Bitmap)(resources.GetObject("tab_close_dark"))); lightCloseButton = ((System.Drawing.Bitmap)(resources.GetObject("tab_close_light"))); CanReorderTabs = false; AllowDrop = false; } protected override void OnCreateControl() { base.OnCreateControl(); FindScroller(); // Hacker alarm: in order to allow proper scroller customization/setup we have to listen to // a message registered by the tab control, which is obviously doing the scroller // (up/down) setup. We need this to do our own setup. Regular Windows messages // like WM_WINDOWPOSCHANGED are not sufficient. tabRelayoutMessageID = Win32.RegisterWindowMessage(tabRelayoutMessageName); } /// <summary> /// Clean up any resources being used. /// </summary> protected override void Dispose(bool disposing) { if (disposing) { if (components != null) { components.Dispose(); } leftRightImages.Dispose(); darkCloseButton.Dispose(); lightCloseButton.Dispose(); foreach (Bitmap image in busyIndicators.Values) { // Must be the same delegate we used to start the animation with! ImageAnimator.StopAnimate(image, new EventHandler(BusyAnimationStep)); } } base.Dispose(disposing); } #endregion #region Native Windows code needed for some adjustments protected override void WndProc(ref System.Windows.Forms.Message m) { switch ((WM) m.Msg) { case (WM)TCM.ADJUSTRECT: { // We need to adjust the display rectangle, which directly determines where the content is shown. Win32.RECT rectangle = (Win32.RECT) m.GetLParam(typeof(Win32.RECT)); // The "larger" value indicates the direction of the computation. // A 1 means from display rectangle to window rectangle. A 0 the other way around. int topOffset = 0; if (tabStyle == TabStyleType.TopNormal || tabStyle == TabStyleType.TopTransparent) topOffset = ItemSize.Height; int bottomOffset = 0; if (tabStyle == TabStyleType.BottomNormal) bottomOffset = ItemSize.Height; int larger = m.WParam.ToInt32(); if (larger == 0) { rectangle.Left += Margin.Left + contentPadding.Left; rectangle.Right -= Margin.Right + contentPadding.Right; rectangle.Top += Margin.Top + ContentPadding.Top + topOffset; rectangle.Bottom -= Margin.Bottom + ContentPadding.Bottom + bottomOffset; } else { rectangle.Left -= Margin.Left + contentPadding.Left; rectangle.Right += Margin.Right + contentPadding.Right; rectangle.Top -= Margin.Top + contentPadding.Top + topOffset; rectangle.Bottom += Margin.Bottom + contentPadding.Bottom + bottomOffset; } Marshal.StructureToPtr(rectangle, m.LParam, true); break; } case WM.HSCROLL: switch ((SB) Win32.LoWord(m.WParam.ToInt32())) { case SB.THUMBPOSITION: scrollOffset = -scrollScaleFactor * Win32.HiWord(m.WParam.ToInt32()); Invalidate(); break; } m.Result = new IntPtr(1); break; case WM.WINDOWPOSCHANGED: base.WndProc(ref m); UpdateScroller(); if (tabStyle == TabStyleType.BottomNormal) AdjustLayoutInfo(null); break; case WM.SIZE: base.WndProc(ref m); if (m.WParam.ToInt32() == Win32.SIZE_RESTORED && m.LParam.ToInt32() != 0) { // Window has been resized. if (pendingScrollIntoViewIndex > -1) ScrollIntoView(pendingScrollIntoViewIndex); pendingScrollIntoViewIndex = -1; } break; case WM.NCHITTEST: { // Declare everything as being part of the client area, so we get mouse events for that. m.Result = new IntPtr(1); // HT_CLIENT break; } case WM.LBUTTONDOWN: { // Handle this message directly to keep the base class from doing unwanted things. uint lparam = (uint)m.LParam.ToInt32(); int x = Win32.LoWord(lparam); int y = Win32.HiWord(lparam); MouseEventArgs args = new MouseEventArgs(MouseButtons.Left, 1, x, y, 0); OnMouseDown(args); m.Result = IntPtr.Zero; break; } default: base.WndProc(ref m); // The tab relayout message is a message registered by the original tab control // and obviously the one where the tab control also sets the position of the scroller. // Since we have our own view of how that should be done we listen to this message to // make our own adjustments. if (tabRelayoutMessageID != 0 && m.Msg == tabRelayoutMessageID) UpdateScroller(); break; } } private int ScrollerWndProc(ref Message m) { switch ((WM) m.Msg) { case WM.ERASEBKGND: m.Result = new IntPtr(1); break; case WM.PAINT: { IntPtr hDC = Win32.GetDC(m.HWnd); Graphics g = Graphics.FromHdc(hDC); DrawScroller(g); g.Dispose(); Win32.ReleaseDC(m.HWnd, hDC); Win32.RECT clientArea = new Win32.RECT(); Win32.GetClientRect(m.HWnd, ref clientArea); Win32.ValidateRect(m.HWnd, ref clientArea); // return 0 (processed) m.Result = IntPtr.Zero; } return 1; case (WM) UDM.SETRANGE: // The systab control sends this message when it wants to update the scroller, however // that works diametrically opposed to our own intents, so it is simply switched off here. // Our implementation uses UDM_SETRANGE32. m.Result = new IntPtr(1); return 1; } return 0; } protected void RecalculateFrame() { // Trigger a resize event which will make the base control send the recalculate message. Width += 1; Width -= 1; } #endregion #region Drawing the control and its parts. /// <summary> /// Load drawing colors once application has finished setup or the user changed the color scheme. /// </summary> public void UpdateColors() { topTransparentTabColor = Conversions.GetApplicationColor(ApplicationColor.AppColorMainTab, false); topTransparentTabTextColor = Conversions.GetApplicationColor(ApplicationColor.AppColorMainTab, true); topNormalTabTextColor = Conversions.GetApplicationColor(ApplicationColor.AppColorTabUnselected, true); topNormalTabSelectedFocusedColor = Conversions.GetApplicationColor(ApplicationColor.AppColorTopTabSelectedFocused, false); topNormalTabSelectedFocusedTextColor = Conversions.GetApplicationColor(ApplicationColor.AppColorTopTabSelectedFocused, true); topNormalTabSelectedUnfocusedColor = Conversions.GetApplicationColor(ApplicationColor.AppColorTopTabSelectedUnfocused, false); topNormalTabSelectedUnfocusedTextColor = Conversions.GetApplicationColor(ApplicationColor.AppColorTopTabSelectedUnfocused, true); unselectedTabColor = Conversions.GetApplicationColor(ApplicationColor.AppColorTabUnselected, false); bottomTabTextColor = Conversions.GetApplicationColor(ApplicationColor.AppColorTabUnselected, true); bottomTabSelectedColor = Conversions.GetApplicationColor(ApplicationColor.AppColorBottomTabSelected, false); bottomTabSelectedTextColor = Conversions.GetApplicationColor(ApplicationColor.AppColorBottomTabSelected, true); Invalidate(); } protected override void OnPaintBackground(PaintEventArgs e) { // Do not erase the background. We either use a transparent background or completely // draw it in the DrawControl function. } protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); DrawControl(e.Graphics); } internal void DrawControl(Graphics g) { if (!Visible) return; g.SmoothingMode = SmoothingMode.AntiAlias; bool drawFocused = showFocusState && ControlUtilities.IsHierarchyFocused(this); RectangleF clientArea = ClientRectangle; clientArea.Offset(-0.5f, -0.5f); // Fill client area with the control's background color // (everything outside the content rectangle, e.g. including margin). // Exclude the transparent part if TopTransparent is set as tab style (though only if // we are running on Aero). float topSpace = 0; if (tabStyle == TabStyleType.TopTransparent && ControlUtilities.IsCompositionEnabled()) { topSpace = ItemSize.Height + Margin.Top; clientArea.Y += topSpace; clientArea.Height -= topSpace; } if (BackgroundColor != Color.Transparent) { using (Brush brush = new SolidBrush(BackgroundColor)) g.FillRectangle(brush, clientArea); } // Fill the tab control area within margins. We might have already excluded the top part. clientArea.X += Margin.Left; clientArea.Width -= Margin.Horizontal; if (topSpace == 0) { clientArea.Y += Margin.Top; clientArea.Height -= Margin.Vertical; } else clientArea.Height -= Margin.Bottom; using (SolidBrush brush = new SolidBrush(unselectedTabColor)) g.FillRectangle(brush, clientArea); // Some additional handling for certain tab styles. switch (tabStyle) { case TabStyleType.TopNormal: { // Draw the separator line. if (SelectedTab != null) { clientArea.Y += ItemSize.Height; clientArea.Height = ContentPadding.Top; Color color; if (drawFocused) color = topNormalTabSelectedFocusedColor; else color = topNormalTabSelectedUnfocusedColor; using (SolidBrush brush = new SolidBrush(color)) g.FillRectangle(brush, clientArea); } break; } } if (TabPages.Count == 0 || tabStyle == TabStyleType.NoTabs) return; // Draw only tabs which lie at least partially in the visible area. Rectangle clipRect = new Rectangle(Margin.Left, Margin.Top, ClientSize.Width - Margin.Horizontal, ItemSize.Height); if (tabStyle == TabStyleType.BottomNormal) clipRect.Y = ClientSize.Height - Margin.Bottom - ItemSize.Height; g.SetClip(clipRect); int leftBorder = Margin.Left - scrollOffset; float rightBorder = clientArea.Width - Margin.Horizontal; if (SelectedIndex >= 0) { // Draw in two rounds. The selected tab first and then all others. // Each tab drawing excludes its drawn area from the clip region so it can't be overdrawn. ValidateTab(SelectedIndex); if (layoutInfo[SelectedIndex].tabArea.Right > leftBorder || layoutInfo[SelectedIndex].tabArea.Left < rightBorder) DrawTab(g, SelectedIndex, drawFocused); } for (int i = 0; i < TabCount; i++) { if (i != SelectedIndex) { ValidateTab(i); if (layoutInfo[i].tabArea.Right > leftBorder || layoutInfo[i].tabArea.Left < rightBorder) DrawTab(g, i, drawFocused); } } } internal void DrawTab(Graphics g, int index, bool drawFocused) { RectangleF bounds = GetTabRect(index); TabPage page = TabPages[index]; bool isSelected = (SelectedIndex == index); // For high quality drawing with antialiased lines we need to specify line and gradient coordinates // which lie between two pixels (not exactly *on* one, so adjust by a half pixel offset for these cases. bounds.X -= 0.5f; bounds.Y -= 0.5f; GraphicsPath outline = null; if (isSelected || tabStyle == TabStyleType.TopTransparent) { outline = GetTabOutline(bounds, isSelected); Color tabColor; switch (tabStyle) { case TabStyleType.BottomNormal: tabColor = bottomTabSelectedColor; break; case TabStyleType.TopTransparent: if (isSelected) tabColor = topTransparentTabColor; else tabColor = Color.FromArgb(128, topTransparentTabColor); break; default: // Top normal. if (isSelected) { if (drawFocused) tabColor = topNormalTabSelectedFocusedColor; else tabColor = topNormalTabSelectedUnfocusedColor; } else tabColor = topNormalTabTextColor; break; } using (SolidBrush brush = new SolidBrush(tabColor)) g.FillPath(brush, outline); } bool drawingOnGlass = (tabStyle == TabStyleType.TopTransparent && !isSelected && ControlUtilities.IsCompositionEnabled()); // Tab icon. Compute position here but draw after the text (to draw it over the text glow // if necessary). bool drawImage = false; Point imagePosition = new Point(0, 0); if ((page.ImageIndex >= 0) && (ImageList != null) && (ImageList.Images.Count > page.ImageIndex) && (ImageList.Images[page.ImageIndex] != null)) { drawImage = true; int iconSpacing = 4; imagePosition = new Point((int) bounds.X + itemPadding.Left, (int) bounds.Y); int adjustment = ImageList.ImageSize.Width + iconSpacing; imagePosition.Y += ((int)bounds.Height - ImageList.ImageSize.Height) / 2; bounds.X += adjustment; bounds.Width -= adjustment; } bool buttonSpaceNeeded = IsCloseButtonVisible(index) || (layoutInfo[index].isBusy); Rectangle buttonRect = GetTabButtonRect(index); // Tab text. if (page.Text.Length > 0) { using (StringFormat stringFormat = new StringFormat()) { stringFormat.Alignment = StringAlignment.Near; stringFormat.Trimming = StringTrimming.EllipsisCharacter; stringFormat.FormatFlags = StringFormatFlags.NoWrap; stringFormat.LineAlignment = StringAlignment.Center; // Create format flags variant of the StringFormat for Aero rendering. TextFormatFlags formatFlags = TextFormatFlags.EndEllipsis | TextFormatFlags.VerticalCenter | TextFormatFlags.SingleLine; Brush brush; switch (tabStyle) { case TabStyleType.TopTransparent: brush = new SolidBrush(topTransparentTabTextColor); break; case TabStyleType.BottomNormal: if (isSelected) brush = new SolidBrush(bottomTabSelectedTextColor); else brush = new SolidBrush(bottomTabTextColor); break; default: // Top tab normal. if (isSelected) { if (drawFocused) brush = new SolidBrush(topNormalTabSelectedFocusedTextColor); else brush = new SolidBrush(topNormalTabSelectedUnfocusedTextColor); } else brush = new SolidBrush(topNormalTabTextColor); break; } int buttonPart = 0; if (buttonSpaceNeeded) buttonPart = buttonSpacing + buttonRect.Width; // When computing the left padding take the necessary glow around text into account, so we // don't add too much extra space. if (drawingOnGlass) { int leftSpace = itemPadding.Left - (renderWithGlow ? glowSize : 0) + 1; int rightSpace = itemPadding.Right > glowSize ? itemPadding.Right - glowSize : 0; bounds.X += leftSpace; bounds.Width -= buttonPart + leftSpace + rightSpace; } else { bounds.X += itemPadding.Left; bounds.Width -= itemPadding.Horizontal + buttonPart; } bounds.Height -= itemPadding.Vertical; Font font = this.Font; if (Conversions.InHighContrastMode()) font = new Font(font, font.Style | FontStyle.Bold); // For writing text on a (semi) transparent surface (e.g. when using Aero) // normal text output routines will mess up the ClearType/anti aliasing extra pixels // or the text color (sometimes both together). Hence we use a path to draw the text. if (tabStyle != TabStyleType.TopTransparent || isSelected) { // Since GraphicsPath.AddString renders text significantly different we use it only // if we really need to. g.DrawString(page.Text, font, brush, bounds, stringFormat); } else { if (ControlUtilities.IsCompositionEnabled()) { Rectangle intBounds = Rectangle.Ceiling(bounds); Win32.DrawTextOnGlass(g, page.Text, font, intBounds, Color.Black, formatFlags, renderWithGlow); } else using (GraphicsPath textPath = new GraphicsPath()) { float emSize = g.DpiY * font.SizeInPoints / 72; if (tabStyle == TabStyleType.TopTransparent) bounds.Y--; // Account for outline border. textPath.AddString(page.Text, font.FontFamily, (int)font.Style, emSize, bounds, stringFormat); g.FillPath(brush, textPath); } } brush.Dispose(); } } if (drawImage) g.DrawImageUnscaled(ImageList.Images[page.ImageIndex], imagePosition); // Finally the close button if this tab is active (or we use the TopTransparent style or // the tab is marked as busy). if ((isSelected || tabStyle == TabStyleType.TopTransparent || layoutInfo[index].isBusy) && buttonSpaceNeeded) { if (layoutInfo[index].isBusy) { Bitmap indicator; switch (tabStyle) { case TabStyleType.TopNormal: if (isSelected) { if (drawFocused) indicator = GetBusyIndicatorForStyle("blue"); else indicator = GetBusyIndicatorForStyle("white"); } else indicator = GetBusyIndicatorForStyle("white"); break; default: indicator = GetBusyIndicatorForStyle("white"); break; } ImageAnimator.UpdateFrames(indicator); g.DrawImageUnscaled(indicator, buttonRect); } else { switch (tabStyle) { case TabStyleType.TopTransparent: g.DrawImageUnscaled(darkCloseButton, buttonRect); break; case TabStyleType.TopNormal: { bool drawDark = isSelected && drawFocused; if (Conversions.UseWin8Drawing()) drawDark = false; else if (Conversions.InHighContrastMode()) drawDark = !drawDark; if (drawDark) g.DrawImageUnscaled(darkCloseButton, buttonRect); else g.DrawImageUnscaled(lightCloseButton, buttonRect); break; } case TabStyleType.BottomNormal: g.DrawImageUnscaled(darkCloseButton, buttonRect); break; } } } if (outline != null) { // The Widen operation discards the inner part, so we need to compose the result. GraphicsPath inner = (GraphicsPath)outline.Clone(); Pen pen = new Pen(Color.White, 1.5f); outline.Widen(pen); outline.AddPath(inner, true); g.ExcludeClip(new Region(outline)); inner.Dispose(); outline.Dispose(); pen.Dispose(); } } /// <summary> /// Creates the outline of a tab depending on its style. /// </summary> /// <param name="bounds">The bounding rectangle for the tab.</param> /// <returns>The outline of the tab as path, ready to be filled.</returns> private GraphicsPath GetTabOutline(RectangleF recBounds, bool isSelected) { GraphicsPath path = new GraphicsPath(); float cornerSize = 6.5f; switch (tabStyle) { case TabStyleType.TopNormal: path.AddLine(recBounds.Left, recBounds.Bottom, recBounds.Left, recBounds.Top); path.AddLine(recBounds.Left, recBounds.Top, recBounds.Right, recBounds.Top); path.AddLine(recBounds.Right, recBounds.Top, recBounds.Right, recBounds.Bottom); path.AddLine(recBounds.Right, recBounds.Bottom, recBounds.Left, recBounds.Bottom); break; case TabStyleType.BottomNormal: path.AddLine(recBounds.Left, recBounds.Top, recBounds.Right, recBounds.Top); path.AddLine(recBounds.Right, recBounds.Top, recBounds.Right, recBounds.Bottom); path.AddLine(recBounds.Right, recBounds.Bottom, recBounds.Left, recBounds.Bottom); path.AddLine(recBounds.Left, recBounds.Bottom, recBounds.Left, recBounds.Top); break; case TabStyleType.TopTransparent: float distance = isSelected ? 0 : 1; path.AddLine(recBounds.Left, recBounds.Bottom - distance, recBounds.Left, recBounds.Top + cornerSize); path.AddArc(recBounds.Left, recBounds.Top, cornerSize, cornerSize, 180, 90); path.AddArc(recBounds.Right - cornerSize - 8, recBounds.Top, 1.5f * cornerSize, cornerSize, -90, 60); path.AddLine(recBounds.Right + 14, recBounds.Bottom - distance, recBounds.Left, recBounds.Bottom - distance); break; } return path; } /// <summary> /// Returns the rectangle of the given tab for drawing. /// </summary> /// <param name="index"></param> /// <returns></returns> new public Rectangle GetTabRect(int index) { ValidateTab(index); Rectangle area = layoutInfo[index].tabArea; area.Offset(scrollOffset, 0); return area; } /// <summary> /// Returns the rectangle of the close button for drawing. /// </summary> /// <param name="index"></param> /// <returns></returns> public Rectangle GetTabButtonRect(int index) { ValidateTab(index); Rectangle area = layoutInfo[index].buttonArea; area.Offset(scrollOffset, 0); return area; } /// <summary> /// Checks if the busy animation for the given style has been loaded already. If not /// the image is loaded and the animation for it started. /// </summary> /// <returns>The image registered as busy animation.</returns> internal Bitmap GetBusyIndicatorForStyle(string type) { if (!busyIndicators.ContainsKey(type)) { System.Resources.ResourceManager resources = new System.Resources.ResourceManager(typeof(FlatTabControl)); switch (type) { case "blue": busyIndicators[type] = ((System.Drawing.Bitmap)(resources.GetObject("busy-indicator-blue"))); break; case "darkblue": busyIndicators[type] = ((System.Drawing.Bitmap)(resources.GetObject("busy-indicator-darkblue"))); break; case "yellow": busyIndicators[type] = ((System.Drawing.Bitmap)(resources.GetObject("busy-indicator-yellow"))); break; default: busyIndicators[type] = ((System.Drawing.Bitmap)(resources.GetObject("busy-indicator-white"))); break; } ImageAnimator.Animate(busyIndicators[type], new EventHandler(BusyAnimationStep)); } return busyIndicators[type]; } internal void DrawScroller(Graphics g) { if ((leftRightImages == null) || (leftRightImages.Images.Count != 4)) return; Rectangle clientArea = this.ClientRectangle; Win32.RECT r0 = new Win32.RECT(); Win32.GetClientRect(scroller.Handle, ref r0); // Fill background of the scroller. Decrease width and height by 1 to // account for right and bottom borders which should not be used in drawing. r0.Right--; r0.Bottom--; Rectangle scrollerRect = new Rectangle(r0.Location, r0.Size); using (Brush br = new SolidBrush(SystemColors.Control)) g.FillRectangle(br, scrollerRect); // Make a small outer border. using (Pen border = new Pen(SystemColors.ControlDark)) { Rectangle rborder = scrollerRect; rborder.Inflate(-1, -1); g.DrawRectangle(border, rborder); } int nMiddle = (r0.Width / 2); int nTop = (r0.Height - leftRightImages.ImageSize.Height) / 2 + 1; int nLeft = (nMiddle - leftRightImages.ImageSize.Width) / 2; Rectangle r1 = new Rectangle(new Point(nLeft, nTop), leftRightImages.ImageSize); Rectangle r2 = new Rectangle(new Point(nMiddle + nLeft, nTop), leftRightImages.ImageSize); // Finally draw the buttons. Image img = leftRightImages.Images[1]; if (img != null) { if (TabCount > 0) { Rectangle r3 = this.GetTabRect(0); if (r3.Left < clientArea.Left) g.DrawImage(img, r1); else { img = leftRightImages.Images[3]; if (img != null) g.DrawImage(img, r1); } } } img = leftRightImages.Images[0]; if (img != null) { if (this.TabCount > 0) { Rectangle r3 = this.GetTabRect(this.TabCount - 1); if (r3.Right > (clientArea.Width - r0.Width)) g.DrawImage(img, r2); else { img = leftRightImages.Images[2]; if (img != null) g.DrawImage(img, r2); } } } } /// <summary> /// Searches for a child control of type UpDown32, which the standard tab control /// maintains and we want to customize. /// </summary> private void FindScroller() { // Find the UpDown control. IntPtr pWnd = Win32.GetWindow(this.Handle, Win32.GW_CHILD); while (pWnd != IntPtr.Zero) { // Get the window class name char[] className = new char[33]; int length = Win32.GetClassName(pWnd, className, 32); string s = new string(className, 0, length); if (s == "msctls_updown32") { scroller = new SubClass(pWnd, true); scroller.SubClassedWndProc += new SubClass.SubClassWndProcEventHandler(ScrollerWndProc); break; } pWnd = Win32.GetWindow(pWnd, Win32.GW_HWNDNEXT); } } private void UpdateScroller() { // Seems the scroll is sometimes re-created, so also check for a 0 handle. if (scroller == null || scroller.Handle == IntPtr.Zero) FindScroller(); if (scroller != null) { if (tabStyle == TabStyleType.NoTabs) { scrollOffset = 0; Win32.ShowWindow(scroller.Handle, (int)SW.HIDE); return; } Win32.RECT rect = new Win32.RECT(); Win32.GetClientRect(scroller.Handle, ref rect); int totalTabWidth = GetTotalTabWidth(); int availableWidth = ClientSize.Width - Margin.Horizontal; if (totalTabWidth > availableWidth) { // Decrease available space also by that used of the scroller now. availableWidth -= rect.Width; Win32.ShowWindow(scroller.Handle, (int)SW.SHOWNOACTIVATE); // The scroll range is the number of steps (scaled pixels) we need to scroll over the full tab space. int scrollRange = (int) Math.Ceiling((totalTabWidth - availableWidth) / (float) scrollScaleFactor) + (int) ((scrollerSpacing / (float) scrollScaleFactor)); Win32.SendMessage(scroller.Handle, (int) UDM.SETRANGE32, IntPtr.Zero, new IntPtr(scrollRange)); // Make sure the scroll position is still ok before setting it in the scroller. // Always scroll so that the right border stays close to the scroller. Rectangle bounds = GetTabRect(TabCount - 1); if (bounds.Right < ClientSize.Width - Margin.Right - scrollerSpacing - rect.Width) scrollOffset += ClientSize.Width - Margin.Right - scrollerSpacing - rect.Width - bounds.Right; Win32.SendMessage(scroller.Handle, (int)UDM.SETPOS32, IntPtr.Zero, new IntPtr(-scrollOffset / scrollScaleFactor)); // Disable scroll acceleration by setting a single scroll increment. Win32.UDACCEL acceleration = new Win32.UDACCEL(); acceleration.nInc = 5; acceleration.nSec = 0; IntPtr parameter = Marshal.AllocHGlobal(Marshal.SizeOf(acceleration)); Marshal.StructureToPtr(acceleration, parameter, true); Win32.SendMessage(scroller.Handle, (int)UDM.SETACCEL, new IntPtr(1), parameter); Marshal.FreeHGlobal(parameter); } else { scrollOffset = 0; Win32.ShowWindow(scroller.Handle, (int)SW.HIDE); } Win32.InvalidateRect(scroller.Handle, ref rect, true); int scrollerWidth = rect.Width; rect.Left = Width - scrollerWidth - Margin.Right; rect.Right = rect.Left + scrollerWidth; if (tabStyle == TabStyleType.BottomNormal) rect.Top = ClientSize.Height - ItemSize.Height - Margin.Bottom; else rect.Top = Margin.Top; rect.Bottom = rect.Top + ItemSize.Height; Win32.MoveWindow(scroller.Handle, rect.Left, rect.Top, rect.Width, rect.Height, true); } } /// <summary> /// Computes the overall size of all tabs. /// </summary> /// <returns></returns> private int GetTotalTabWidth() { int result = 0; for (int i = 0; i < TabCount; i++) result += GetTabRect(i).Width; return result; } #endregion #region Event handling protected override void OnResize(EventArgs e) { base.OnResize(e); if (auxView != null) { auxView.Top = Top + Height - auxView.Height; auxView.Left = Left + Width - auxView.Width; } } protected override void OnHandleCreated(EventArgs e) { base.OnHandleCreated(e); RegisterForActivation(this, new EventArgs()); } /// <summary> /// Triggered when the form we use to track activation changes is disposed. If we get this notification /// it means we are still alive, which can only be true if we have been re-parented in the meantime. /// Hence query the current top level control and use this as new tracker. /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void RegisterForActivation(object sender, EventArgs e) { activationTracker = TopLevelControl as Form; if (activationTracker != null) { activationTracker.Deactivate += new EventHandler(ActivationChanged); activationTracker.Activated += new EventHandler(ActivationChanged); activationTracker.Disposed += new EventHandler(RegisterForActivation); } } protected override void OnHandleDestroyed(EventArgs e) { base.OnHandleDestroyed(e); // Unregister our activation listener from our host form. if (activationTracker != null) { activationTracker.Deactivate -= ActivationChanged; activationTracker.Activated -= ActivationChanged; } } private void ActivationChanged(object sender, EventArgs e) { Invalidate(); } override protected void OnControlAdded(ControlEventArgs e) { base.OnControlAdded(e); if (e.Control is TabPage) { TabPage page = e.Control as TabPage; page.TextChanged += new EventHandler(PageTextChanged); if (tabStyle == TabStyleType.BottomNormal) page.BackColor = Color.White; layoutInfo.Insert(TabPages.IndexOf(page), new TabInfo()); AdjustLayoutInfo(page); UpdateScroller(); Show(); // If this is the first page then it is automatically selected, // but doesn't send a selection change event. Hence we do it manually. if (TabCount == 1) OnSelectedIndexChanged(null); } } override protected void OnControlRemoved(ControlEventArgs e) { base.OnControlRemoved(e); if (e.Control is TabPage) { TabPage page = e.Control as TabPage; page.TextChanged -= PageTextChanged; // The page is still in the tab view and removed when execution returns to the caller. // So we cannot use our normal handling and have to invalidate following pages here. int index = TabPages.IndexOf(page); layoutInfo.RemoveAt(index); while (index < layoutInfo.Count) layoutInfo[index++].isValid = false; UpdateScroller(); if (!DesignMode && hideWhenEmpty && TabCount == 1) Hide(); } } override protected void OnSelectedIndexChanged(EventArgs e) { base.OnSelectedIndexChanged(e); ScrollIntoView(SelectedIndex); if (scroller != null) { Win32.RECT clientArea = new Win32.RECT(); Win32.GetClientRect(scroller.Handle, ref clientArea); Win32.InvalidateRect(scroller.Handle, ref clientArea, false); } Invalidate(); // We need to update border and background colors. } override protected void OnMouseDown(MouseEventArgs e) { switch (e.Button) { case MouseButtons.Left: Focus(); lastTabHit = TabIndexFromPosition(e.Location); if (lastTabHit > -1) { lastClickPosition = e.Location; if (SelectedIndex != lastTabHit) SelectedIndex = lastTabHit; else buttonHit = GetTabButtonRect(lastTabHit).Contains(e.Location); } break; default: base.OnMouseDown(e); break; } } protected override void OnMouseUp(MouseEventArgs e) { switch (e.Button) { case MouseButtons.Left: if (lastTabHit > -1) { if (buttonHit && GetTabButtonRect(lastTabHit).Contains(e.Location) && (TabCount > 1 || CanCloseLastTab)) { // Close tab if the application agrees. TabPage page = TabPages[lastTabHit]; CloseTabPage(page); } } break; case MouseButtons.Right: { int tab = TabIndexFromPosition(e.Location); if (tab > -1) { OnTabShowMenu(new TabMenuEventArgs(TabPages[tab], tab, PointToScreen(e.Location))); } } break; default: base.OnMouseUp(e); break; } } protected override void OnMouseMove(MouseEventArgs e) { switch (e.Button) { case MouseButtons.Left: if (lastTabHit > -1 && !buttonHit && CanReorderTabs && (Math.Abs(lastClickPosition.X - e.Location.X) > 5 || Math.Abs(lastClickPosition.Y - e.Location.Y) > 5)) { TabPage page = TabPages[lastTabHit]; if (DoDragDrop(page, DragDropEffects.Move) == DragDropEffects.Move) { int newIndex = TabPages.IndexOf(page); OnTabMoved(new TabMovedEventArgs(page, lastTabHit, newIndex)); } } else base.OnMouseMove(e); break; default: base.OnMouseMove(e); break; } } protected override void OnDragOver(DragEventArgs e) { // See if a tab page is being dragged. if (e.Data.GetData(typeof(TabPage)) == null) { base.OnDragOver(e); return; } TabPage dragTab = (TabPage) e.Data.GetData(typeof(TabPage)); int dragTabIndex = TabPages.IndexOf(dragTab); if (dragTabIndex < 0) { // Tab page doesn't belong to this view. Ignore it. e.Effect = DragDropEffects.None; return; } // Hover over a tab? int hoverIndex = TabIndexFromPosition(PointToClient(new Point(e.X, e.Y))); if (hoverIndex < 0) { e.Effect = DragDropEffects.None; return; } e.Effect = DragDropEffects.Move; // Do this check after we set the drop effect so we don't get a block cursor // when we already moved a page and still hover with the mouse over that. if (dragTabIndex == hoverIndex) return; Rectangle dragTabRect = GetTabRect(dragTabIndex); Rectangle hoverTabRect = GetTabRect(hoverIndex); if (dragTabRect.Width < hoverTabRect.Width) { Point tcLocation = PointToScreen(Location); if (dragTabIndex < hoverIndex) { if ((e.X - tcLocation.X) > ((hoverTabRect.X + hoverTabRect.Width) - dragTabRect.Width)) MovePage(dragTab, hoverIndex); } else if (dragTabIndex > hoverIndex) { if ((e.X - tcLocation.X) < (hoverTabRect.X + dragTabRect.Width)) MovePage(dragTab, hoverIndex); } } else MovePage(dragTab, hoverIndex); } protected override void OnEnter(EventArgs e) { base.OnEnter(e); Invalidate(); } protected override void OnLeave(EventArgs e) { base.OnLeave(e); Invalidate(); } protected override void OnSizeChanged(EventArgs e) { if (IsHandleCreated) { this.BeginInvoke((MethodInvoker) delegate { base.OnSizeChanged(e); }); } } void PageTextChanged(object sender, EventArgs e) { AdjustLayoutInfo(sender as TabPage); } public event EventHandler<TabClosingEventArgs> TabClosing; protected internal virtual void OnTabClosing(TabClosingEventArgs args) { if (TabClosing != null) TabClosing(this, args); } public event EventHandler<TabClosedEventArgs> TabClosed; protected internal virtual void OnTabClosed(TabClosedEventArgs args) { if (TabClosed != null) TabClosed(this, args); } public event EventHandler<TabMovingEventArgs> TabMoving; protected internal virtual void OnTabMoving(TabMovingEventArgs args) { if (TabMoving != null) TabMoving(this, args); } public event EventHandler<TabMovedEventArgs> TabMoved; protected internal virtual void OnTabMoved(TabMovedEventArgs args) { if (TabMoved != null) TabMoved(this, args); } public event EventHandler<TabMenuEventArgs> TabShowMenu; protected internal virtual void OnTabShowMenu(TabMenuEventArgs args) { if (TabShowMenu != null) TabShowMenu(this, args); } protected override void OnKeyDown(KeyEventArgs ke) { if (ke.KeyData == (Keys.Control | Keys.Tab)) if (!useDefaultTabSwitchKey) return; base.OnKeyDown(ke); } #endregion #region Component Designer generated code /// <summary> /// Required method for Designer support - do not modify /// the contents of this method with the code editor. /// </summary> private void InitializeComponent() { components = new System.ComponentModel.Container(); } #endregion #region Other implementation /// <summary> /// Determines if the the close button for a given tab should be shown. /// </summary> private bool IsCloseButtonVisible(int index) { return (layoutInfo[index].closeButtonVisibility == CloseButtonVisiblity.ShowButton) || (layoutInfo[index].closeButtonVisibility == CloseButtonVisiblity.InheritVisibility && showCloseButton); } /// <summary> /// Adjusts the size of the layout info list and marks all entries that follow the given page /// as invalid, so that they re-compute their layout next time it is required. /// </summary> private void AdjustLayoutInfo(TabPage page) { // Since add and remove of entries in the layoutInfo list does not consider where changes // happened it's better to adjust the list in advance where possible. if (layoutInfo.Count > TabCount) layoutInfo.RemoveRange(TabCount, layoutInfo.Count - TabCount); for (int i = 0; i < TabCount; i++) { if (i >= layoutInfo.Count) layoutInfo.Add(new TabInfo()); layoutInfo[i].page = TabPages[i]; } bool invalidate = (page == null) ? true : false; foreach (TabInfo entry in layoutInfo) { // Invalidate this page and all following. if (entry.page == page) invalidate = true; if (invalidate) entry.isValid = false; } } /// <summary> /// Recomputes layout information for this tab if not yet done. /// </summary> private void ValidateTab(int index) { if (index >= layoutInfo.Count) AdjustLayoutInfo(TabPages[index]); if (!layoutInfo[index].isValid) { Graphics g = CreateGraphics(); // Find first valid entry before the given one. // We have to validate all entries after that until this one. int currentIndex = index; while (currentIndex >= 0 && !layoutInfo[currentIndex].isValid) currentIndex--; int offsetX = Margin.Left; if (currentIndex > -1) offsetX = layoutInfo[currentIndex].tabArea.Right; int offsetY = Margin.Top; if (tabStyle == TabStyleType.BottomNormal) offsetY = ClientSize.Height - ItemSize.Height - Margin.Bottom; while (++currentIndex <= index) { TabInfo info = layoutInfo[currentIndex]; int textWidth = 0; if (info.page.Text.Length > 0) { Font font = this.Font; if (Conversions.InHighContrastMode()) font = new Font(font, font.Style | FontStyle.Bold); SizeF measuredTextSize = g.MeasureString(info.page.Text, font); textWidth = (int)Math.Ceiling(measuredTextSize.Width); } int buttonPart = 0; bool buttonSpaceNeeded = IsCloseButtonVisible(currentIndex) || info.isBusy; Size buttonSize; if (info.isBusy) { // Assuming here all busy indicator images are of the same size. // Not a strong limitation actually, since all tabs are of same height too. System.Resources.ResourceManager resources = new System.Resources.ResourceManager(typeof(FlatTabControl)); Bitmap image = ((System.Drawing.Bitmap)(resources.GetObject("busy-indicator-blue"))); buttonSize = image.Size; } else buttonSize = new Size(darkCloseButton.Width, darkCloseButton.Height); if (buttonSpaceNeeded) buttonPart = buttonSpacing + buttonSize.Width; int tabWidth = itemPadding.Horizontal + textWidth + buttonPart; if ((info.page.ImageIndex >= 0) && (ImageList != null) && (ImageList.Images.Count > info.page.ImageIndex) && (ImageList.Images[info.page.ImageIndex] != null)) { tabWidth += ImageList.ImageSize.Width; // Add 4 px spacing if there is something after the icon. if (buttonSpaceNeeded || textWidth > 0) tabWidth += 4; } if (ItemSize.Width > tabWidth) tabWidth = ItemSize.Width; if (tabWidth > maxTabSize) tabWidth = maxTabSize; info.tabArea = new Rectangle(offsetX, offsetY, tabWidth, ItemSize.Height); offsetX = info.tabArea.Right; if (buttonSpaceNeeded) { int buttonOffset = offsetY + (info.tabArea.Height - buttonSize.Height) / 2 + 1; info.buttonArea = new Rectangle(offsetX - itemPadding.Right - buttonSize.Width, buttonOffset, buttonSize.Width, buttonSize.Height); } else info.buttonArea = Rectangle.Empty; info.isValid = true; } g.Dispose(); } } /// <summary> /// Allows to set individual close buttons for a tab. /// </summary> public void SetCloseButtonVisibility(int index, CloseButtonVisiblity visibility) { AdjustLayoutInfo(TabPages[index]); layoutInfo[index].closeButtonVisibility = visibility; } /// <summary> /// Mark individual tabs as busy causing them to show a busy indicator instead of the close button. /// </summary> public void SetBusy(int index, bool busy) { if (layoutInfo[index].isBusy != busy) { if (!busy) { layoutInfo[index].isBusy = false; AdjustLayoutInfo(TabPages[index]); } else { AdjustLayoutInfo(TabPages[index]); layoutInfo[index].isBusy = true; } Invalidate(); } } private void BusyAnimationStep(object sender, EventArgs args) { foreach (TabInfo info in layoutInfo) if (info.isBusy) { Rectangle area = info.tabArea; area.Offset(scrollOffset, 0); Invalidate(area, false); } } /// <summary> /// Does the actual work to close a tab page, sending out appropriate events and updating /// internal structures. Returns true if the page was actually closed. /// </summary> public bool CloseTabPage(TabPage page) { if (page == null) return false; if (TabCount > 1 || CanCloseLastTab) { TabClosingEventArgs args = new TabClosingEventArgs(page, true, TabPages.IndexOf(page)); OnTabClosing(args); if (args.canClose) { // The page could be removed in the OnTabClosing event, so prepare for that. int tabIndex = TabPages.IndexOf(page); if (tabIndex > -1) { SetBusy(tabIndex, false); // Remove this tab and select its predecessor if possible. if (SelectedIndex > 0) SelectedIndex = SelectedIndex - 1; lastTabHit = -1; TabPages.Remove(page); } AdjustLayoutInfo(null); Update(); OnTabClosed(new TabClosedEventArgs(page)); // In the case OnTabClosed did not dispose of the page do it now. if (!page.IsDisposed) page.Dispose(); return true; } } return false; } /// <summary> /// Determines if the given position lies within the bounds of a tab and, if so, returns the tab's /// index to the caller. /// </summary> /// <param name="point">The position to check for in local coordinates.</param> /// <returns>The index of the tab found or -1 if none.</returns> public int TabIndexFromPosition(Point point) { for (int i = 0; i < TabCount; i++) if (GetTabRect(i).Contains(point)) return i; return -1; } /// <summary> /// Adjusts the scroll offset so that the given tab is in the visible area. /// </summary> /// <param name="index">The index of the tab to make visible.</param> /// <returns>True if an adjustment was necessary, otherwise False.</returns> public bool ScrollIntoView(int index) { bool result = false; if (index < 0 || index >= TabCount) return result; // If our bounds are empty then the application is likely minimized and we delay setting // the horizontal offset until we are restored. if (Width == 0 && Height == 0) { pendingScrollIntoViewIndex = index; return result; } Rectangle area = GetTabRect(index); if (area.Left < 0) { scrollOffset -= area.Left; result = true; } else { Win32.RECT rect = new Win32.RECT(); if (scroller != null) Win32.GetClientRect(scroller.Handle, ref rect); int rightBorder = ClientSize.Width - Margin.Right - rect.Width - scrollerSpacing; if (area.Right > rightBorder) { scrollOffset -= area.Right - rightBorder; result = true; } } if (result && scroller != null) Win32.SendMessage(scroller.Handle, (int)UDM.SETPOS32, IntPtr.Zero, new IntPtr(-scrollOffset / scrollScaleFactor)); return result; } /// <summary> /// Moves the given page to the new index. /// </summary> private void MovePage(TabPage page, int newIndex) { int oldIndex = TabPages.IndexOf(page); TabMovingEventArgs args = new TabMovingEventArgs(page, oldIndex, newIndex); OnTabMoving(args); if (args.Cancel) return; bool selectTab = (oldIndex == SelectedIndex); // The simple approach here would be to remove the page and insert it at the new location. // However this will produce heavy flickering, so we move the pages one by one instead. // While moving the pages also move their corresponding tab info to keep current states. TabInfo oldInfo = layoutInfo[oldIndex]; if (oldIndex < newIndex) { for (int index = oldIndex; index < newIndex; index++) { TabPages[index] = TabPages[index + 1]; layoutInfo[index] = layoutInfo[index + 1]; } TabPages[newIndex] = page; layoutInfo[newIndex] = oldInfo; AdjustLayoutInfo(TabPages[oldIndex]); } else { for (int index = oldIndex; index > newIndex; index--) { TabPages[index] = TabPages[index - 1]; layoutInfo[index] = layoutInfo[index - 1]; } TabPages[newIndex] = page; layoutInfo[newIndex] = oldInfo; AdjustLayoutInfo(page); } // Keep the tab selected which was selected before. if (selectTab) SelectedIndex = newIndex; Invalidate(); } // In order to simplify general handling of page content (which is very often just a container // with the actual content) we introduce here the concept of documents. // A document is the content of a tab. When using this concept you should only have one control on each // tab to make this work. Otherwise use the tab pages directly and handle search etc. yourself. // See also ITabDocument and TabDocument and the Documents property below. /// <summary> /// Scans the given control and all its children to find a docked ITabDocument. /// Due to various docking structures the document can be anywhere in the hierarchy. /// </summary> /// <param name="control"></param> /// <returns></returns> private ITabDocument DocumentFromHierarchy(Control control) { if (control is ITabDocument) return control as ITabDocument; foreach (Control child in control.Controls) { ITabDocument result = DocumentFromHierarchy(child); if (result != null) return result; } return null; } /// <summary> /// Returns the document (i.e. the first control on a tab page) whose page has the given title. /// </summary> /// <param name="text"></param> /// <returns></returns> public ITabDocument FindDocument(string text) { foreach (TabPage page in TabPages) if (page.Text == text) { ITabDocument result = DocumentFromHierarchy(page); if (result != null) return result; } return null; } /// <summary> /// Returns the document stored on the tab page with the given index or null if there is none. /// </summary> public ITabDocument DocumentFromIndex(int index) { return DocumentFromPage(TabPages[index]); } /// <summary> /// Returns the index for the given document or -1 if not found. /// </summary> public int IndexFromDocument(ITabDocument document) { int i = 0; foreach (TabPage page in TabPages) { if (DocumentFromHierarchy(page) == document) return i; i++; } return -1; } /// <summary> /// Returns the document stored on the given tab page or null if there is none. /// </summary> public ITabDocument DocumentFromPage(TabPage page) { return DocumentFromHierarchy(page); } /// <summary> /// Checks if the document is the first control on any of our pages. /// </summary> /// <returns>True if the document was found, otherwise false.</returns> public bool HasDocument(ITabDocument document) { foreach (TabPage page in TabPages) if (DocumentFromHierarchy(page) == document) return true; return false; } /// <summary> /// Adds the given document to a new tab page and returns the index of that new page. /// </summary> /// <param name="document"></param> /// <returns></returns> public int AddDocument(ITabDocument document) { TabPage page = new TabPage(); TabDocument control = document as TabDocument; control.SetHost(page); page.Controls.Add(control); control.Dock = DockStyle.Fill; control.Margin = new Padding(0); control.Padding = new Padding(0); control.Show(); TabPages.Add(page); return TabPages.Count - 1; } /// <summary> /// Removes a previously added document from this control. This method removes the hosting tab, /// regardless of other content on it. The document itself is not freed. /// </summary> /// <param name="document"></param> public void RemoveDocument(ITabDocument document) { foreach (TabPage page in TabPages) if (DocumentFromHierarchy(page) == document) { page.Controls.Clear(); TabPages.Remove(page); return; } } /// <summary> /// Tries to close the given document by sending out TabClosing. If that returns true /// the document is closed and its hosting tab removed. /// </summary> /// <returns>True if the document was closed, otherwise false.</returns> public bool CloseDocument(ITabDocument document) { foreach (TabPage page in TabPages) if (DocumentFromHierarchy(page) == document) return CloseTabPage(page); return false; } /// <summary> /// Returns the list of documents in an array, e.g. to allow manipulation of this list in a loop. /// </summary> /// <returns></returns> public ITabDocument[] DocumentsToArray() { ITabDocument[] result = new ITabDocument[TabCount]; int i = 0; foreach (ITabDocument content in Documents) result[i++] = content; return result; } #endregion #region Properties /// <summary> /// Returns the currently active document if there is one. /// </summary> public ITabDocument ActiveDocument { get { if (SelectedTab != null) return DocumentFromHierarchy(SelectedTab); return null; } } public Control AuxControl { get { return auxView; } set { if (auxView != value) { auxView = value; //auxView.BackColor = Color.Transparent; Transparent doesn't work as expected on Win. auxView.Dock = DockStyle.None; Parent.Controls.Add(auxView); Parent.Controls.SetChildIndex(auxView, 0); } } } /// <summary> /// Helper enumeration that returns the control which represents an ITabDocument on each page /// or null if there is none on the specific page. /// </summary> /// <returns></returns> public IEnumerable<ITabDocument> Documents { get { foreach (TabPage page in TabPages) yield return DocumentFromHierarchy(page); } } public TabStyleType TabStyle { get { return tabStyle; } set { if (value != tabStyle) { tabStyle = value; RecalculateFrame(); AdjustLayoutInfo(null); UpdateScroller(); } } } public Padding ItemPadding { get { return itemPadding; } set { itemPadding = value; AdjustLayoutInfo(null); Invalidate(); } } public Padding ContentPadding { get { return contentPadding; } set { contentPadding = value; AdjustLayoutInfo(null); Invalidate(); } } [Browsable(true)] public bool ShowCloseButton { get { return showCloseButton; } set { if (value != showCloseButton) { showCloseButton = value; RecalculateFrame(); AdjustLayoutInfo(null); Invalidate(); } } } [Browsable(true)] public bool ShowFocusState { get { return showFocusState; } set { if (value != showFocusState) { showFocusState = value; Invalidate(); } } } [Browsable(true)] public bool CanCloseLastTab { get; set; } [Browsable(true)] public bool HideWhenEmpty { get { return hideWhenEmpty; } set { if (value != hideWhenEmpty) { hideWhenEmpty = value; if (!DesignMode && hideWhenEmpty && TabCount == 0) Hide(); else Show(); } } } [Browsable(true)] public int MaxTabSize { get { return maxTabSize; } set { if (value < ItemSize.Width) value = ItemSize.Width; if (value != maxTabSize) { maxTabSize = value; RecalculateFrame(); AdjustLayoutInfo(null); Invalidate(); } } } [Browsable(true)] public bool CanReorderTabs { get; set; } bool renderWithGlow = true; [Browsable(true)] public bool RenderWithGlow { get { return renderWithGlow; } set { if (value != renderWithGlow) { renderWithGlow = value; Invalidate(); } } } private Color backColor; [Browsable(true)] public Color BackgroundColor { get { return backColor; } set { if (value != backColor) { backColor = value; Invalidate(); } } } [Browsable(true)] public bool DefaultTabSwitch { get { return useDefaultTabSwitchKey; } set { useDefaultTabSwitchKey = value; } } // With the switch to .NET 4.0 client profile we have to separate runtime // and design time code. [Editor("MySQL.Controls.FlatTabControlCollectionEditor", "UITypeEditor")] public new TabPageCollection TabPages { get { return base.TabPages; } } #endregion } //------------------------------------------------------------------------------------------------ // In order to aid managing documents on tab pages (instead of plain controls) we define an interface and // a base class which implements it, to be used by the application, if it wants to maintain // the "document metaphor". public interface ITabDocument { void Activate(); void Close(); String TabText { get; set; } Control Content { get; } int ToolbarHeight { get; } } public class TabDocument : Form, ITabDocument { private TabPage host = null; private String tabText = ""; private String toolTipText = ""; public TabDocument() { FormBorderStyle = FormBorderStyle.None; TopLevel = false; } new virtual public void Show() { base.Show(); } new virtual public void Activate() { Show(); base.Activate(); (host.Parent as TabControl).SelectedTab = host; } public void SetHost(TabPage host) { if (this.host != host) { if (this.host != null) { FlatTabControl tabControl = this.host.Parent as FlatTabControl; if (tabControl != null) { // Remove the tab page this document was docked to if it gets moved to another one. this.host.Controls.Clear(); tabControl.TabPages.Remove(this.host); tabControl.OnTabClosed(new TabClosedEventArgs(this.host)); } } this.host = host; if (host != null) { host.Text = tabText; host.ToolTipText = toolTipText; } } } protected override void Dispose(bool disposing) { host = null; base.Dispose(disposing); } /// <summary> /// Called when the document is closed already. Just remove its tab page. /// No need to trigger tab closing and closed events. /// </summary> override protected void OnFormClosed(FormClosedEventArgs e) { if (host != null && host.Parent != null) { host.Controls.Clear(); (host.Parent as FlatTabControl).TabPages.Remove(host); } base.OnFormClosed(e); } public virtual String TabText { get { return tabText; } set { tabText = value; if (host != null) host.Text = value; } } public virtual Control Content { get { return this; } } public int ToolbarHeight { get { if (Controls.Count > 0 && Controls[0] is Panel) { // If this tab document has a menu and/or toolbar then it is placed on a panel // in the root of this document. Panel container = Controls[0] as Panel; if (container.Controls.Count > 0 && (container.Controls[0] is ToolStrip)) return container.Height; } return 0; } } public virtual String ToolTipText { get { return toolTipText; } set { toolTipText = value; if (host != null) host.ToolTipText = value; } } } #region Event argument classes public class TabClosingEventArgs : EventArgs { public int index; public TabPage page; public bool canClose; public TabClosingEventArgs(TabPage page, bool canClose, int index) { this.page = page; this.canClose = canClose; this.index = index; } } public class TabClosedEventArgs : EventArgs { public TabPage page; public TabClosedEventArgs(TabPage page) { this.page = page; } } public class TabMenuEventArgs : EventArgs { public TabPage page; public int pageIndex; public Point location; public TabMenuEventArgs(TabPage page, int index, Point pos) { this.page = page; this.pageIndex = index; this.location = pos; } }; public class TabMovedEventArgs : EventArgs { public int FromIndex, ToIndex; public TabPage MovedPage; public TabMovedEventArgs(TabPage page, int from, int to) { MovedPage = page; FromIndex = from; ToIndex = to; } } public class TabMovingEventArgs : EventArgs { public int FromIndex, ToIndex; public TabPage MovedPage; public bool Cancel; public TabMovingEventArgs(TabPage page, int from, int to) { MovedPage = page; FromIndex = from; ToIndex = to; Cancel = false; } } #endregion }