using FastReport.Utils; using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Windows.Forms; namespace FastReport.Controls { internal class BreadCrumbControl : UserControl { private Image image; private bool pathEditMode; private TextBox pathEditBox; private List visibleItems; private List overflowItems; private int XOffset => this.LogicalToDevice(3); public List Items { get; } internal List OverflowItems => overflowItems; public string PathSeparator { get; set; } [Browsable(false)] public string Path => pathEditMode ? pathEditBox.Text : GetPath(); public bool AllowPathEdit { get; set; } [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public Image Image { get => image; set { image = value; UpdateLayout(); } } [Browsable(false), DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] public ToolStripRenderer OverflowMenuRenderer { get; set; } public event EventHandler ItemClick; public event EventHandler PathEdited; private string GetPath() { var result = ""; for (int i = 0; i < Items.Count; i++) { result += Items[i].Text; if (i < Items.Count - 1) result += PathSeparator; } return result; } private void CreatePathEdit() { pathEditMode = true; if (pathEditBox == null) { pathEditBox = new TextBox(); pathEditBox.BorderStyle = BorderStyle.None; pathEditBox.Parent = this; pathEditBox.PreviewKeyDown += (s, e) => { if (e.KeyCode == Keys.Escape) e.IsInputKey = true; }; pathEditBox.KeyPress += (s, e) => { if (e.KeyChar == (char)13) { e.Handled = true; PathEdited?.Invoke(this, EventArgs.Empty); HidePathEdit(); } else if (e.KeyChar == (char)27) { e.Handled = true; HidePathEdit(); } }; pathEditBox.LostFocus += (s, e) => { HidePathEdit(); }; } pathEditBox.Location = new Point(XOffset, (Height - pathEditBox.PreferredHeight) / 2); pathEditBox.Size = new Size(Width - XOffset * 2, pathEditBox.PreferredHeight); pathEditBox.Text = GetPath(); pathEditBox.Visible = true; pathEditBox.SelectAll(); pathEditBox.Focus(); } private void HidePathEdit() { pathEditBox.Visible = false; pathEditMode = false; Refresh(); } private int LayoutItem(BreadCrumbItemBase item, int offset) { item.SetOwner(this); visibleItems.Add(item); item.Measure(); return item.Layout(offset); } private bool LayoutItems(int startIndex) { visibleItems.Clear(); bool rtl = RightToLeft == RightToLeft.Yes; int x = rtl ? Width - XOffset : XOffset; x = LayoutItem(new BreadCrumbImageItem(), x); if (startIndex > 0) x = LayoutItem(new BreadCrumbOverflowItem(), x); for (int i = startIndex; i < Items.Count; i++) { var item = Items[i]; x = LayoutItem(item, x); if (i < Items.Count - 1) x = LayoutItem(new BreadCrumbSeparatorItem(), x); } return Width < 20 || // startup bug workaround (rtl ? x > 20 : x < Width - 20); } private void DrawItems(Graphics g) { for (int i = 0; i < visibleItems.Count; i++) { var item = visibleItems[i]; item.Paint(g); } } protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); Graphics g = e.Graphics; g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality; if (!pathEditMode) DrawItems(g); g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.None; this.DrawVisualStyleBorder(g, new Rectangle(0, 0, Width - 1, Height - 1)); } protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); foreach (var item in visibleItems) { item.IsMouseOver = item.Bounds.Contains(e.Location); } Refresh(); } protected override void OnMouseUp(MouseEventArgs e) { base.OnMouseUp(e); if (pathEditMode) return; foreach (var item in visibleItems) { if (item.IsMouseOver) { item.Click(); return; } } if (AllowPathEdit) { CreatePathEdit(); Refresh(); } } protected override void OnMouseLeave(EventArgs e) { base.OnMouseLeave(e); foreach (var item in visibleItems) { item.IsMouseOver = false; } Refresh(); } internal void OnItemClick(BreadCrumbItem item) => ItemClick?.Invoke(item, EventArgs.Empty); public void UpdateLayout() { overflowItems.Clear(); int startIndex = 0; while (!LayoutItems(startIndex)) { overflowItems.Add(Items[startIndex]); startIndex++; } Refresh(); } public BreadCrumbControl() { Items = new List(); visibleItems = new List(); overflowItems = new List(); PathSeparator = System.IO.Path.DirectorySeparatorChar.ToString(); BackColor = SystemColors.Window; SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer, true); } } internal class BreadCrumbItemBase { private BreadCrumbControl owner; internal BreadCrumbControl Owner => owner; internal int Left { get; set; } internal int Top { get; set; } internal int Width { get; set; } internal int Height { get; set; } internal Rectangle Bounds => new Rectangle(Left, Top, Width, Height); internal bool IsMouseOver { get; set; } internal void SetOwner(BreadCrumbControl owner) => this.owner = owner; internal virtual void Measure() { Height = Owner.Height; } internal virtual int Layout(int xOffset) { bool rtl = Owner.RightToLeft == RightToLeft.Yes; Left = rtl ? xOffset - Width : xOffset; Top = 0; return rtl ? xOffset - Width : xOffset + Width; } internal virtual void Paint(Graphics g) { if (IsMouseOver) g.FillRectangle(SystemBrushes.ControlLight, Bounds); } internal virtual void Click() { } } internal class BreadCrumbItem : BreadCrumbItemBase { private int textHeight; public string Text { get; set; } public object Tag { get; set; } internal override void Measure() { #if AVALONIA TextRenderer.FontScale = (float)Owner.DpiScale; #endif var size = TextRenderer.MeasureText(Text, Owner.Font); Width = size.Width; textHeight = size.Height; Height = Owner.Height; } internal override void Paint(Graphics g) { base.Paint(g); TextRenderer.DrawText(g, Text, Owner.Font, new Point(Left, Top + (Height - textHeight) / 2), Owner.ForeColor, Owner.RightToLeft == RightToLeft.Yes ? TextFormatFlags.RightToLeft : TextFormatFlags.Default); } internal override void Click() { Owner.OnItemClick(this); } } internal class BreadCrumbImageItem : BreadCrumbItemBase { internal override void Measure() { Width = Owner.Image?.Width ?? 0; Height = Owner.Height; } internal override void Paint(Graphics g) { if (Owner.Image != null) { g.DrawImage(Owner.Image, Left, (Height - Owner.Image.Height) / 2, Owner.Image.Width, Owner.Image.Height); } } } internal class BreadCrumbOverflowItem : BreadCrumbItemBase { private ContextMenuStrip menu; private void CreateContextMenu() { if (menu == null) menu = new ContextMenuStrip(); menu.Font = Owner.Font; menu.Renderer = Owner.OverflowMenuRenderer; menu.Items.Clear(); foreach (var item in Owner.OverflowItems) { var menuItem = new ToolStripMenuItem() { Text = item.Text, Tag = item, Image = Owner.Image }; menuItem.Click += (s, e) => Owner.OnItemClick((s as ToolStripMenuItem).Tag as BreadCrumbItem); menu.Items.Add(menuItem); } } internal override void Measure() { Width = Owner.LogicalToDevice(15); Height = Owner.Height; } internal override void Paint(Graphics g) { base.Paint(g); using (var pen = new Pen(Owner.ForeColor, Owner.LogicalToDevice(1f))) { bool rtl = Owner.RightToLeft == RightToLeft.Yes; int _3 = Owner.LogicalToDevice(3); int _6 = Owner.LogicalToDevice(6); int x = Left + _3; int y = Height / 2; g.DrawLines(pen, new Point[] { new Point(x + (rtl ? _3 : _6), y - _3), new Point(x + (rtl ? _6 : _3), y), new Point(x + (rtl ? _3 : _6), y + _3) }); } } internal override void Click() { CreateContextMenu(); menu.Show(Owner, new Point(Owner.RightToLeft == RightToLeft.Yes ? Left + Width : Left, Top + Height)); } } internal class BreadCrumbSeparatorItem : BreadCrumbItemBase { internal override void Measure() { Width = Owner.LogicalToDevice(12); Height = Owner.Height; } internal override void Paint(Graphics g) { base.Paint(g); using (var pen = new Pen(Owner.ForeColor, Owner.LogicalToDevice(1f))) { bool rtl = Owner.RightToLeft == RightToLeft.Yes; int _3 = Owner.LogicalToDevice(3); int _6 = Owner.LogicalToDevice(6); int x = Left; int y = Height / 2; g.DrawLines(pen, new Point[] { new Point(x + (rtl ? _6 : _3), y - _3), new Point(x + (rtl ? _3 : _6), y), new Point(x + (rtl ? _6 : _3), y + _3) }); } } } }