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
}