|
@@ -1,737 +1,929 @@
|
|
|
-using System.Text;
|
|
|
using System;
|
|
|
using System.Collections.Generic;
|
|
|
-using System.Data;
|
|
|
using System.Linq;
|
|
|
|
|
|
-namespace Terminal.Gui {
|
|
|
+namespace Terminal.Gui;
|
|
|
+
|
|
|
+/// <summary>
|
|
|
+/// Control that hosts multiple sub views, presenting a single one at once.
|
|
|
+/// </summary>
|
|
|
+public class TabView : View {
|
|
|
+ private Tab _selectedTab;
|
|
|
|
|
|
/// <summary>
|
|
|
- /// Control that hosts multiple sub views, presenting a single one at once
|
|
|
+ /// The default <see cref="MaxTabTextWidth"/> to set on new <see cref="TabView"/> controls.
|
|
|
/// </summary>
|
|
|
- public class TabView : View {
|
|
|
- private Tab selectedTab;
|
|
|
-
|
|
|
- /// <summary>
|
|
|
- /// The default <see cref="MaxTabTextWidth"/> to set on new <see cref="TabView"/> controls
|
|
|
- /// </summary>
|
|
|
- public const uint DefaultMaxTabTextWidth = 30;
|
|
|
-
|
|
|
- /// <summary>
|
|
|
- /// This sub view is the 2 or 3 line control that represents the actual tabs themselves
|
|
|
- /// </summary>
|
|
|
- TabRowView tabsBar;
|
|
|
-
|
|
|
- /// <summary>
|
|
|
- /// This sub view is the main client area of the current tab. It hosts the <see cref="Tab.View"/>
|
|
|
- /// of the tab, the <see cref="SelectedTab"/>
|
|
|
- /// </summary>
|
|
|
- View contentView;
|
|
|
- private List<Tab> tabs = new List<Tab> ();
|
|
|
+ public const uint DefaultMaxTabTextWidth = 30;
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// All tabs currently hosted by the control
|
|
|
- /// </summary>
|
|
|
- /// <value></value>
|
|
|
- public IReadOnlyCollection<Tab> Tabs { get => tabs.AsReadOnly (); }
|
|
|
-
|
|
|
- /// <summary>
|
|
|
- /// When there are too many tabs to render, this indicates the first
|
|
|
- /// tab to render on the screen.
|
|
|
- /// </summary>
|
|
|
- /// <value></value>
|
|
|
- public int TabScrollOffset { get; set; }
|
|
|
+ /// <summary>
|
|
|
+ /// This sub view is the 2 or 3 line control that represents the actual tabs themselves.
|
|
|
+ /// </summary>
|
|
|
+ TabRowView _tabsBar;
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// The maximum number of characters to render in a Tab header. This prevents one long tab
|
|
|
- /// from pushing out all the others.
|
|
|
- /// </summary>
|
|
|
- public uint MaxTabTextWidth { get; set; } = DefaultMaxTabTextWidth;
|
|
|
+ /// <summary>
|
|
|
+ /// This sub view is the main client area of the current tab. It hosts the <see cref="Tab.View"/>
|
|
|
+ /// of the tab, the <see cref="SelectedTab"/>.
|
|
|
+ /// </summary>
|
|
|
+ View _contentView;
|
|
|
+ private List<Tab> _tabs = new List<Tab> ();
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Event for when <see cref="SelectedTab"/> changes
|
|
|
- /// </summary>
|
|
|
- public event EventHandler<TabChangedEventArgs> SelectedTabChanged;
|
|
|
+ /// <summary>
|
|
|
+ /// All tabs currently hosted by the control.
|
|
|
+ /// </summary>
|
|
|
+ /// <value></value>
|
|
|
+ public IReadOnlyCollection<Tab> Tabs { get => _tabs.AsReadOnly (); }
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Event fired when a <see cref="Tab"/> is clicked. Can be used to cancel navigation,
|
|
|
- /// show context menu (e.g. on right click) etc.
|
|
|
- /// </summary>
|
|
|
- public event EventHandler<TabMouseEventArgs> TabClicked;
|
|
|
+ /// <summary>
|
|
|
+ /// When there are too many tabs to render, this indicates the first
|
|
|
+ /// tab to render on the screen.
|
|
|
+ /// </summary>
|
|
|
+ /// <value></value>
|
|
|
+ public int TabScrollOffset {
|
|
|
+ get => _tabScrollOffset;
|
|
|
+ set {
|
|
|
+ _tabScrollOffset = EnsureValidScrollOffsets (value);
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// The currently selected member of <see cref="Tabs"/> chosen by the user
|
|
|
- /// </summary>
|
|
|
- /// <value></value>
|
|
|
- public Tab SelectedTab {
|
|
|
- get => selectedTab;
|
|
|
- set {
|
|
|
+ /// <summary>
|
|
|
+ /// The maximum number of characters to render in a Tab header. This prevents one long tab
|
|
|
+ /// from pushing out all the others.
|
|
|
+ /// </summary>
|
|
|
+ public uint MaxTabTextWidth { get; set; } = DefaultMaxTabTextWidth;
|
|
|
|
|
|
- var old = selectedTab;
|
|
|
+ /// <summary>
|
|
|
+ /// Event for when <see cref="SelectedTab"/> changes.
|
|
|
+ /// </summary>
|
|
|
+ public event EventHandler<TabChangedEventArgs> SelectedTabChanged;
|
|
|
|
|
|
- if (selectedTab != null) {
|
|
|
+ /// <summary>
|
|
|
+ /// Event fired when a <see cref="Tab"/> is clicked. Can be used to cancel navigation,
|
|
|
+ /// show context menu (e.g. on right click) etc.
|
|
|
+ /// </summary>
|
|
|
+ public event EventHandler<TabMouseEventArgs> TabClicked;
|
|
|
|
|
|
- if (selectedTab.View != null) {
|
|
|
- // remove old content
|
|
|
- contentView.Remove (selectedTab.View);
|
|
|
- }
|
|
|
+ /// <summary>
|
|
|
+ /// The currently selected member of <see cref="Tabs"/> chosen by the user.
|
|
|
+ /// </summary>
|
|
|
+ /// <value></value>
|
|
|
+ public Tab SelectedTab {
|
|
|
+ get => _selectedTab;
|
|
|
+ set {
|
|
|
+ UnSetCurrentTabs ();
|
|
|
+
|
|
|
+ var old = _selectedTab;
|
|
|
+
|
|
|
+ if (_selectedTab != null) {
|
|
|
+ if (_selectedTab.View != null) {
|
|
|
+ // remove old content
|
|
|
+ _contentView.Remove (_selectedTab.View);
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- selectedTab = value;
|
|
|
-
|
|
|
- if (value != null) {
|
|
|
+ _selectedTab = value;
|
|
|
|
|
|
- // add new content
|
|
|
- if (selectedTab.View != null) {
|
|
|
- contentView.Add (selectedTab.View);
|
|
|
- }
|
|
|
+ if (value != null) {
|
|
|
+ // add new content
|
|
|
+ if (_selectedTab.View != null) {
|
|
|
+ _contentView.Add (_selectedTab.View);
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- EnsureSelectedTabIsVisible ();
|
|
|
+ EnsureSelectedTabIsVisible ();
|
|
|
|
|
|
- if (old != value) {
|
|
|
- OnSelectedTabChanged (old, value);
|
|
|
+ if (old != value) {
|
|
|
+ if (old?.HasFocus == true) {
|
|
|
+ SelectedTab.SetFocus ();
|
|
|
}
|
|
|
-
|
|
|
+ OnSelectedTabChanged (old, value);
|
|
|
}
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Render choices for how to display tabs. After making changes, call <see cref="ApplyStyleChanges()"/>
|
|
|
- /// </summary>
|
|
|
- /// <value></value>
|
|
|
- public TabStyle Style { get; set; } = new TabStyle ();
|
|
|
-
|
|
|
- /// <summary>
|
|
|
- /// Initializes a <see cref="TabView"/> class using <see cref="LayoutStyle.Computed"/> layout.
|
|
|
- /// </summary>
|
|
|
- public TabView () : base ()
|
|
|
- {
|
|
|
- CanFocus = true;
|
|
|
- contentView = new View ();
|
|
|
- tabsBar = new TabRowView (this);
|
|
|
+ /// <summary>
|
|
|
+ /// Render choices for how to display tabs. After making changes, call <see cref="ApplyStyleChanges()"/>.
|
|
|
+ /// </summary>
|
|
|
+ /// <value></value>
|
|
|
+ public TabStyle Style { get; set; } = new TabStyle ();
|
|
|
|
|
|
- ApplyStyleChanges ();
|
|
|
+ /// <summary>
|
|
|
+ /// Initializes a <see cref="TabView"/> class using <see cref="LayoutStyle.Computed"/> layout.
|
|
|
+ /// </summary>
|
|
|
+ public TabView () : base ()
|
|
|
+ {
|
|
|
+ CanFocus = true;
|
|
|
+ _tabsBar = new TabRowView (this);
|
|
|
+ _contentView = new View ();
|
|
|
+
|
|
|
+ ApplyStyleChanges ();
|
|
|
+
|
|
|
+ base.Add (_tabsBar);
|
|
|
+ base.Add (_contentView);
|
|
|
+
|
|
|
+ // Things this view knows how to do
|
|
|
+ AddCommand (Command.Left, () => { SwitchTabBy (-1); return true; });
|
|
|
+ AddCommand (Command.Right, () => { SwitchTabBy (1); return true; });
|
|
|
+ AddCommand (Command.LeftHome, () => { TabScrollOffset = 0; SelectedTab = Tabs.FirstOrDefault (); return true; });
|
|
|
+ AddCommand (Command.RightEnd, () => { TabScrollOffset = Tabs.Count - 1; SelectedTab = Tabs.LastOrDefault (); return true; });
|
|
|
+ AddCommand (Command.NextView, () => { _contentView.SetFocus (); return true; });
|
|
|
+ AddCommand (Command.PreviousView, () => { SuperView?.FocusPrev (); return true; });
|
|
|
+ AddCommand (Command.PageDown, () => { TabScrollOffset += _tabLocations.Length; SelectedTab = Tabs.ElementAt (TabScrollOffset); return true; });
|
|
|
+ AddCommand (Command.PageUp, () => { TabScrollOffset -= _tabLocations.Length; SelectedTab = Tabs.ElementAt (TabScrollOffset); return true; });
|
|
|
+
|
|
|
+ // Default keybindings for this view
|
|
|
+ KeyBindings.Add (KeyCode.CursorLeft, Command.Left);
|
|
|
+ KeyBindings.Add (KeyCode.CursorRight, Command.Right);
|
|
|
+ KeyBindings.Add (KeyCode.Home, Command.LeftHome);
|
|
|
+ KeyBindings.Add (KeyCode.End, Command.RightEnd);
|
|
|
+ KeyBindings.Add (KeyCode.CursorDown, Command.NextView);
|
|
|
+ KeyBindings.Add (KeyCode.CursorUp, Command.PreviousView);
|
|
|
+ KeyBindings.Add (KeyCode.PageDown, Command.PageDown);
|
|
|
+ KeyBindings.Add (KeyCode.PageUp, Command.PageUp);
|
|
|
+ }
|
|
|
|
|
|
- base.Add (tabsBar);
|
|
|
- base.Add (contentView);
|
|
|
+ /// <summary>
|
|
|
+ /// Updates the control to use the latest state settings in <see cref="Style"/>.
|
|
|
+ /// This can change the size of the client area of the tab (for rendering the
|
|
|
+ /// selected tab's content). This method includes a call
|
|
|
+ /// to <see cref="View.SetNeedsDisplay()"/>.
|
|
|
+ /// </summary>
|
|
|
+ public void ApplyStyleChanges ()
|
|
|
+ {
|
|
|
+ _contentView.BorderStyle = Style.ShowBorder ? LineStyle.Single : LineStyle.None;
|
|
|
+ _contentView.Width = Dim.Fill ();
|
|
|
|
|
|
- // Things this view knows how to do
|
|
|
- AddCommand (Command.Left, () => { SwitchTabBy (-1); return true; });
|
|
|
- AddCommand (Command.Right, () => { SwitchTabBy (1); return true; });
|
|
|
- AddCommand (Command.LeftHome, () => { SelectedTab = Tabs.FirstOrDefault (); return true; });
|
|
|
- AddCommand (Command.RightEnd, () => { SelectedTab = Tabs.LastOrDefault (); return true; });
|
|
|
+ if (Style.TabsOnBottom) {
|
|
|
+ // Tabs are along the bottom so just dodge the border
|
|
|
+ if (Style.ShowBorder) {
|
|
|
+ _contentView.Border.Thickness = new Thickness (1, 1, 1, 0);
|
|
|
+ }
|
|
|
|
|
|
- // Default keybindings for this view
|
|
|
- KeyBindings.Add (KeyCode.CursorLeft, Command.Left);
|
|
|
- KeyBindings.Add (KeyCode.CursorRight, Command.Right);
|
|
|
- KeyBindings.Add (KeyCode.Home, Command.LeftHome);
|
|
|
- KeyBindings.Add (KeyCode.End, Command.RightEnd);
|
|
|
- }
|
|
|
+ _contentView.Y = 0;
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Updates the control to use the latest state settings in <see cref="Style"/>.
|
|
|
- /// This can change the size of the client area of the tab (for rendering the
|
|
|
- /// selected tab's content). This method includes a call
|
|
|
- /// to <see cref="View.SetNeedsDisplay()"/>
|
|
|
- /// </summary>
|
|
|
- public void ApplyStyleChanges ()
|
|
|
- {
|
|
|
- contentView.X = Style.ShowBorder ? 1 : 0;
|
|
|
- contentView.Width = Dim.Fill (Style.ShowBorder ? 1 : 0);
|
|
|
+ var tabHeight = GetTabHeight (false);
|
|
|
|
|
|
- if (Style.TabsOnBottom) {
|
|
|
- // Tabs are along the bottom so just dodge the border
|
|
|
- contentView.Y = Style.ShowBorder ? 1 : 0;
|
|
|
+ // Fill client area leaving space at bottom for tabs
|
|
|
+ _contentView.Height = Dim.Fill (tabHeight);
|
|
|
|
|
|
- // Fill client area leaving space at bottom for tabs
|
|
|
- contentView.Height = Dim.Fill (GetTabHeight (false));
|
|
|
+ _tabsBar.Height = tabHeight;
|
|
|
|
|
|
- var tabHeight = GetTabHeight (false);
|
|
|
- tabsBar.Height = tabHeight;
|
|
|
+ _tabsBar.Y = Pos.Bottom (_contentView);
|
|
|
|
|
|
- tabsBar.Y = Pos.Percent (100) - tabHeight;
|
|
|
+ } else {
|
|
|
|
|
|
- } else {
|
|
|
+ // Tabs are along the top
|
|
|
+ if (Style.ShowBorder) {
|
|
|
+ _contentView.Border.Thickness = new Thickness (1, 0, 1, 1);
|
|
|
+ }
|
|
|
|
|
|
- // Tabs are along the top
|
|
|
+ _tabsBar.Y = 0;
|
|
|
|
|
|
- var tabHeight = GetTabHeight (true);
|
|
|
+ var tabHeight = GetTabHeight (true);
|
|
|
|
|
|
- //move content down to make space for tabs
|
|
|
- contentView.Y = tabHeight;
|
|
|
+ //move content down to make space for tabs
|
|
|
+ _contentView.Y = Pos.Bottom (_tabsBar);
|
|
|
|
|
|
- // Fill client area leaving space at bottom for border
|
|
|
- contentView.Height = Dim.Fill (Style.ShowBorder ? 1 : 0);
|
|
|
+ // Fill client area leaving space at bottom for border
|
|
|
+ _contentView.Height = Dim.Fill ();
|
|
|
|
|
|
- // The top tab should be 2 or 3 rows high and on the top
|
|
|
+ // The top tab should be 2 or 3 rows high and on the top
|
|
|
|
|
|
- tabsBar.Height = tabHeight;
|
|
|
+ _tabsBar.Height = tabHeight;
|
|
|
|
|
|
- // Should be able to just use 0 but switching between top/bottom tabs repeatedly breaks in ValidatePosDim if just using the absolute value 0
|
|
|
- tabsBar.Y = Pos.Percent (0);
|
|
|
- }
|
|
|
- if (IsInitialized) {
|
|
|
- LayoutSubviews ();
|
|
|
- }
|
|
|
- SetNeedsDisplay ();
|
|
|
+ // Should be able to just use 0 but switching between top/bottom tabs repeatedly breaks in ValidatePosDim if just using the absolute value 0
|
|
|
}
|
|
|
+ if (IsInitialized) {
|
|
|
+ LayoutSubviews ();
|
|
|
+ }
|
|
|
+ SetNeedsDisplay ();
|
|
|
+ }
|
|
|
|
|
|
+ ///<inheritdoc/>
|
|
|
+ public override void OnDrawContent (Rect contentArea)
|
|
|
+ {
|
|
|
+ Driver.SetAttribute (GetNormalColor ());
|
|
|
+
|
|
|
+ if (Tabs.Any ()) {
|
|
|
+ var savedClip = ClipToBounds ();
|
|
|
+ _tabsBar.OnDrawContent (contentArea);
|
|
|
+ _contentView.SetNeedsDisplay ();
|
|
|
+ _contentView.Draw ();
|
|
|
+ Driver.Clip = savedClip;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- ///<inheritdoc/>
|
|
|
- public override void OnDrawContent (Rect contentArea)
|
|
|
- {
|
|
|
- Move (0, 0);
|
|
|
- Driver.SetAttribute (GetNormalColor ());
|
|
|
-
|
|
|
- if (Style.ShowBorder) {
|
|
|
+ ///<inheritdoc/>
|
|
|
+ public override void OnDrawContentComplete (Rect contentArea)
|
|
|
+ {
|
|
|
+ _tabsBar.OnDrawContentComplete (contentArea);
|
|
|
+ }
|
|
|
|
|
|
- // How much space do we need to leave at the bottom to show the tabs
|
|
|
- int spaceAtBottom = Math.Max (0, GetTabHeight (false) - 1);
|
|
|
- int startAtY = Math.Max (0, GetTabHeight (true) - 1);
|
|
|
+ /// <summary>
|
|
|
+ /// Disposes the control and all <see cref="Tabs"/>.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="disposing"></param>
|
|
|
+ protected override void Dispose (bool disposing)
|
|
|
+ {
|
|
|
+ base.Dispose (disposing);
|
|
|
|
|
|
- Border.DrawFrame (new Rect (0, startAtY, Bounds.Width,
|
|
|
- Math.Max (Bounds.Height - spaceAtBottom - startAtY, 0)), false);
|
|
|
- }
|
|
|
+ // The selected tab will automatically be disposed but
|
|
|
+ // any tabs not visible will need to be manually disposed
|
|
|
|
|
|
- if (Tabs.Any ()) {
|
|
|
- tabsBar.OnDrawContent (contentArea);
|
|
|
- contentView.SetNeedsDisplay ();
|
|
|
- var savedClip = contentView.ClipToBounds ();
|
|
|
- contentView.Draw ();
|
|
|
- Driver.Clip = savedClip;
|
|
|
+ foreach (var tab in Tabs) {
|
|
|
+ if (!Equals (SelectedTab, tab)) {
|
|
|
+ tab.View?.Dispose ();
|
|
|
}
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Disposes the control and all <see cref="Tabs"/>
|
|
|
- /// </summary>
|
|
|
- /// <param name="disposing"></param>
|
|
|
- protected override void Dispose (bool disposing)
|
|
|
- {
|
|
|
- base.Dispose (disposing);
|
|
|
+ /// <summary>
|
|
|
+ /// Raises the <see cref="SelectedTabChanged"/> event.
|
|
|
+ /// </summary>
|
|
|
+ protected virtual void OnSelectedTabChanged (Tab oldTab, Tab newTab)
|
|
|
+ {
|
|
|
|
|
|
- // The selected tab will automatically be disposed but
|
|
|
- // any tabs not visible will need to be manually disposed
|
|
|
+ SelectedTabChanged?.Invoke (this, new TabChangedEventArgs (oldTab, newTab));
|
|
|
+ }
|
|
|
|
|
|
- foreach (var tab in Tabs) {
|
|
|
- if (!Equals (SelectedTab, tab)) {
|
|
|
- tab.View?.Dispose ();
|
|
|
- }
|
|
|
+ /// <summary>
|
|
|
+ /// Changes the <see cref="SelectedTab"/> by the given <paramref name="amount"/>.
|
|
|
+ /// Positive for right, negative for left. If no tab is currently selected then
|
|
|
+ /// the first tab will become selected.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="amount"></param>
|
|
|
+ public void SwitchTabBy (int amount)
|
|
|
+ {
|
|
|
+ if (Tabs.Count == 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- }
|
|
|
+ // if there is only one tab anyway or nothing is selected
|
|
|
+ if (Tabs.Count == 1 || SelectedTab == null) {
|
|
|
+ SelectedTab = Tabs.ElementAt (0);
|
|
|
+ SetNeedsDisplay ();
|
|
|
+ return;
|
|
|
}
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Raises the <see cref="SelectedTabChanged"/> event
|
|
|
- /// </summary>
|
|
|
- protected virtual void OnSelectedTabChanged (Tab oldTab, Tab newTab)
|
|
|
- {
|
|
|
+ var currentIdx = Tabs.IndexOf (SelectedTab);
|
|
|
|
|
|
- SelectedTabChanged?.Invoke (this, new TabChangedEventArgs (oldTab, newTab));
|
|
|
+ // Currently selected tab has vanished!
|
|
|
+ if (currentIdx == -1) {
|
|
|
+ SelectedTab = Tabs.ElementAt (0);
|
|
|
+ SetNeedsDisplay ();
|
|
|
+ return;
|
|
|
}
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Changes the <see cref="SelectedTab"/> by the given <paramref name="amount"/>.
|
|
|
- /// Positive for right, negative for left. If no tab is currently selected then
|
|
|
- /// the first tab will become selected
|
|
|
- /// </summary>
|
|
|
- /// <param name="amount"></param>
|
|
|
- public void SwitchTabBy (int amount)
|
|
|
- {
|
|
|
- if (Tabs.Count == 0) {
|
|
|
- return;
|
|
|
- }
|
|
|
+ var newIdx = Math.Max (0, Math.Min (currentIdx + amount, Tabs.Count - 1));
|
|
|
|
|
|
- // if there is only one tab anyway or nothing is selected
|
|
|
- if (Tabs.Count == 1 || SelectedTab == null) {
|
|
|
- SelectedTab = Tabs.ElementAt (0);
|
|
|
- SetNeedsDisplay ();
|
|
|
- return;
|
|
|
- }
|
|
|
+ SelectedTab = _tabs [newIdx];
|
|
|
+ SetNeedsDisplay ();
|
|
|
|
|
|
- var currentIdx = Tabs.IndexOf (SelectedTab);
|
|
|
+ EnsureSelectedTabIsVisible ();
|
|
|
+ }
|
|
|
|
|
|
- // Currently selected tab has vanished!
|
|
|
- if (currentIdx == -1) {
|
|
|
- SelectedTab = Tabs.ElementAt (0);
|
|
|
- SetNeedsDisplay ();
|
|
|
- return;
|
|
|
- }
|
|
|
+ /// <summary>
|
|
|
+ /// Updates <see cref="TabScrollOffset"/> to be a valid index of <see cref="Tabs"/>.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="value">The value to validate.</param>
|
|
|
+ /// <remarks>Changes will not be immediately visible in the display until you call <see cref="View.SetNeedsDisplay()"/>.</remarks>
|
|
|
+ /// <returns>The valid <see cref="TabScrollOffset"/> for the given value.</returns>
|
|
|
+ public int EnsureValidScrollOffsets (int value)
|
|
|
+ {
|
|
|
+ return Math.Max (Math.Min (value, Tabs.Count - 1), 0);
|
|
|
+ }
|
|
|
|
|
|
- var newIdx = Math.Max (0, Math.Min (currentIdx + amount, Tabs.Count - 1));
|
|
|
+ /// <summary>
|
|
|
+ /// Updates <see cref="TabScrollOffset"/> to ensure that <see cref="SelectedTab"/> is visible.
|
|
|
+ /// </summary>
|
|
|
+ public void EnsureSelectedTabIsVisible ()
|
|
|
+ {
|
|
|
+ if (!IsInitialized || SelectedTab == null) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- SelectedTab = tabs [newIdx];
|
|
|
- SetNeedsDisplay ();
|
|
|
+ // if current viewport does not include the selected tab
|
|
|
+ if (!CalculateViewport (Bounds).Any (r => Equals (SelectedTab, r.Tab))) {
|
|
|
|
|
|
- EnsureSelectedTabIsVisible ();
|
|
|
+ // Set scroll offset so the first tab rendered is the
|
|
|
+ TabScrollOffset = Math.Max (0, Tabs.IndexOf (SelectedTab));
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Updates <see cref="TabScrollOffset"/> to be a valid index of <see cref="Tabs"/>
|
|
|
- /// </summary>
|
|
|
- /// <remarks>Changes will not be immediately visible in the display until you call <see cref="View.SetNeedsDisplay()"/></remarks>
|
|
|
- public void EnsureValidScrollOffsets ()
|
|
|
- {
|
|
|
- TabScrollOffset = Math.Max (Math.Min (TabScrollOffset, Tabs.Count - 1), 0);
|
|
|
+ /// <summary>
|
|
|
+ /// Returns the number of rows occupied by rendering the tabs, this depends
|
|
|
+ /// on <see cref="TabStyle.ShowTopLine"/> and can be 0 (e.g. if
|
|
|
+ /// <see cref="TabStyle.TabsOnBottom"/> and you ask for <paramref name="top"/>).
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="top">True to measure the space required at the top of the control,
|
|
|
+ /// false to measure space at the bottom.</param>.
|
|
|
+ /// <returns></returns>
|
|
|
+ private int GetTabHeight (bool top)
|
|
|
+ {
|
|
|
+ if (top && Style.TabsOnBottom) {
|
|
|
+ return 0;
|
|
|
}
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Updates <see cref="TabScrollOffset"/> to ensure that <see cref="SelectedTab"/> is visible
|
|
|
- /// </summary>
|
|
|
- public void EnsureSelectedTabIsVisible ()
|
|
|
- {
|
|
|
- if (!IsInitialized || SelectedTab == null) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // if current viewport does not include the selected tab
|
|
|
- if (!CalculateViewport (Bounds).Any (r => Equals (SelectedTab, r.Tab))) {
|
|
|
-
|
|
|
- // Set scroll offset so the first tab rendered is the
|
|
|
- TabScrollOffset = Math.Max (0, Tabs.IndexOf (SelectedTab));
|
|
|
- }
|
|
|
+ if (!top && !Style.TabsOnBottom) {
|
|
|
+ return 0;
|
|
|
}
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Returns the number of rows occupied by rendering the tabs, this depends
|
|
|
- /// on <see cref="TabStyle.ShowTopLine"/> and can be 0 (e.g. if
|
|
|
- /// <see cref="TabStyle.TabsOnBottom"/> and you ask for <paramref name="top"/>).
|
|
|
- /// </summary>
|
|
|
- /// <param name="top">True to measure the space required at the top of the control,
|
|
|
- /// false to measure space at the bottom</param>
|
|
|
- /// <returns></returns>
|
|
|
- private int GetTabHeight (bool top)
|
|
|
- {
|
|
|
- if (top && Style.TabsOnBottom) {
|
|
|
- return 0;
|
|
|
- }
|
|
|
+ return Style.ShowTopLine ? 3 : 2;
|
|
|
+ }
|
|
|
|
|
|
- if (!top && !Style.TabsOnBottom) {
|
|
|
- return 0;
|
|
|
- }
|
|
|
+ private TabToRender [] _tabLocations;
|
|
|
+ private int _tabScrollOffset;
|
|
|
|
|
|
- return Style.ShowTopLine ? 3 : 2;
|
|
|
- }
|
|
|
+ /// <summary>
|
|
|
+ /// Returns which tabs to render at each x location.
|
|
|
+ /// </summary>
|
|
|
+ /// <returns></returns>
|
|
|
+ private IEnumerable<TabToRender> CalculateViewport (Rect bounds)
|
|
|
+ {
|
|
|
+ UnSetCurrentTabs ();
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Returns which tabs to render at each x location
|
|
|
- /// </summary>
|
|
|
- /// <returns></returns>
|
|
|
- private IEnumerable<TabToRender> CalculateViewport (Rect bounds)
|
|
|
- {
|
|
|
- int i = 1;
|
|
|
+ int i = 1;
|
|
|
+ View prevTab = null;
|
|
|
|
|
|
- // Starting at the first or scrolled to tab
|
|
|
- foreach (var tab in Tabs.Skip (TabScrollOffset)) {
|
|
|
+ // Starting at the first or scrolled to tab
|
|
|
+ foreach (var tab in Tabs.Skip (TabScrollOffset)) {
|
|
|
|
|
|
- // while there is space for the tab
|
|
|
- var tabTextWidth = tab.Text.EnumerateRunes ().Sum (c => c.GetColumns ());
|
|
|
+ if (prevTab != null) {
|
|
|
+ tab.X = Pos.Right (prevTab);
|
|
|
+ } else {
|
|
|
+ tab.X = 0;
|
|
|
+ }
|
|
|
+ tab.Y = 0;
|
|
|
|
|
|
- string text = tab.Text;
|
|
|
+ // while there is space for the tab
|
|
|
+ var tabTextWidth = tab.DisplayText.EnumerateRunes ().Sum (c => c.GetColumns ());
|
|
|
|
|
|
- // The maximum number of characters to use for the tab name as specified
|
|
|
- // by the user (MaxTabTextWidth). But not more than the width of the view
|
|
|
- // or we won't even be able to render a single tab!
|
|
|
- var maxWidth = Math.Max (0, Math.Min (bounds.Width - 3, MaxTabTextWidth));
|
|
|
+ string text = tab.DisplayText;
|
|
|
|
|
|
- // if tab view is width <= 3 don't render any tabs
|
|
|
- if (maxWidth == 0) {
|
|
|
- yield return new TabToRender (i, tab, string.Empty, Equals (SelectedTab, tab), 0);
|
|
|
- break;
|
|
|
- }
|
|
|
+ // The maximum number of characters to use for the tab name as specified
|
|
|
+ // by the user (MaxTabTextWidth). But not more than the width of the view
|
|
|
+ // or we won't even be able to render a single tab!
|
|
|
+ var maxWidth = Math.Max (0, Math.Min (bounds.Width - 3, MaxTabTextWidth));
|
|
|
|
|
|
- if (tabTextWidth > maxWidth) {
|
|
|
- text = tab.Text.Substring (0, (int)maxWidth);
|
|
|
- tabTextWidth = (int)maxWidth;
|
|
|
- }
|
|
|
+ prevTab = tab;
|
|
|
|
|
|
- // if there is not enough space for this tab
|
|
|
- if (i + tabTextWidth >= bounds.Width) {
|
|
|
- break;
|
|
|
- }
|
|
|
+ tab.Width = 2;
|
|
|
+ tab.Height = Style.ShowTopLine ? 3 : 2;
|
|
|
|
|
|
- // there is enough space!
|
|
|
- yield return new TabToRender (i, tab, text, Equals (SelectedTab, tab), tabTextWidth);
|
|
|
- i += tabTextWidth + 1;
|
|
|
+ // if tab view is width <= 3 don't render any tabs
|
|
|
+ if (maxWidth == 0) {
|
|
|
+ tab.Visible = true;
|
|
|
+ tab.MouseClick += Tab_MouseClick;
|
|
|
+ yield return new TabToRender (i, tab, string.Empty, Equals (SelectedTab, tab), 0);
|
|
|
+ break;
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Adds the given <paramref name="tab"/> to <see cref="Tabs"/>
|
|
|
- /// </summary>
|
|
|
- /// <param name="tab"></param>
|
|
|
- /// <param name="andSelect">True to make the newly added Tab the <see cref="SelectedTab"/></param>
|
|
|
- public void AddTab (Tab tab, bool andSelect)
|
|
|
- {
|
|
|
- if (tabs.Contains (tab)) {
|
|
|
- return;
|
|
|
+ if (tabTextWidth > maxWidth) {
|
|
|
+ text = tab.DisplayText.Substring (0, (int)maxWidth);
|
|
|
+ tabTextWidth = (int)maxWidth;
|
|
|
}
|
|
|
|
|
|
- tabs.Add (tab);
|
|
|
-
|
|
|
- if (SelectedTab == null || andSelect) {
|
|
|
- SelectedTab = tab;
|
|
|
+ tab.Width = Math.Max (tabTextWidth + 2, 1);
|
|
|
+ tab.Height = Style.ShowTopLine ? 3 : 2;
|
|
|
|
|
|
- EnsureSelectedTabIsVisible ();
|
|
|
-
|
|
|
- tab.View?.SetFocus ();
|
|
|
+ // if there is not enough space for this tab
|
|
|
+ if (i + tabTextWidth >= bounds.Width) {
|
|
|
+ tab.Visible = false;
|
|
|
+ break;
|
|
|
}
|
|
|
|
|
|
- SetNeedsDisplay ();
|
|
|
+ // there is enough space!
|
|
|
+ tab.Visible = true;
|
|
|
+ tab.MouseClick += Tab_MouseClick;
|
|
|
+ yield return new TabToRender (i, tab, text, Equals (SelectedTab, tab), tabTextWidth);
|
|
|
+ i += tabTextWidth + 1;
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Removes the given <paramref name="tab"/> from <see cref="Tabs"/>.
|
|
|
- /// Caller is responsible for disposing the tab's hosted <see cref="Tab.View"/>
|
|
|
- /// if appropriate.
|
|
|
- /// </summary>
|
|
|
- /// <param name="tab"></param>
|
|
|
- public void RemoveTab (Tab tab)
|
|
|
- {
|
|
|
- if (tab == null || !tabs.Contains (tab)) {
|
|
|
- return;
|
|
|
+ private void UnSetCurrentTabs ()
|
|
|
+ {
|
|
|
+ if (_tabLocations != null) {
|
|
|
+ foreach (var tabToRender in _tabLocations) {
|
|
|
+ tabToRender.Tab.MouseClick -= Tab_MouseClick;
|
|
|
+ tabToRender.Tab.Visible = false;
|
|
|
}
|
|
|
+ _tabLocations = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- // what tab was selected before closing
|
|
|
- var idx = tabs.IndexOf (tab);
|
|
|
+ private void Tab_MouseClick (object sender, MouseEventEventArgs e)
|
|
|
+ {
|
|
|
+ e.Handled = _tabsBar.MouseEvent (e.MouseEvent);
|
|
|
+ }
|
|
|
|
|
|
- tabs.Remove (tab);
|
|
|
+ /// <summary>
|
|
|
+ /// Adds the given <paramref name="tab"/> to <see cref="Tabs"/>.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="tab"></param>
|
|
|
+ /// <param name="andSelect">True to make the newly added Tab the <see cref="SelectedTab"/>.</param>
|
|
|
+ public void AddTab (Tab tab, bool andSelect)
|
|
|
+ {
|
|
|
+ if (_tabs.Contains (tab)) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- // if the currently selected tab is no longer a member of Tabs
|
|
|
- if (SelectedTab == null || !Tabs.Contains (SelectedTab)) {
|
|
|
- // select the tab closest to the one that disappeared
|
|
|
- var toSelect = Math.Max (idx - 1, 0);
|
|
|
+ _tabs.Add (tab);
|
|
|
+ _tabsBar.Add (tab);
|
|
|
|
|
|
- if (toSelect < Tabs.Count) {
|
|
|
- SelectedTab = Tabs.ElementAt (toSelect);
|
|
|
- } else {
|
|
|
- SelectedTab = Tabs.LastOrDefault ();
|
|
|
- }
|
|
|
-
|
|
|
- }
|
|
|
+ if (SelectedTab == null || andSelect) {
|
|
|
+ SelectedTab = tab;
|
|
|
|
|
|
EnsureSelectedTabIsVisible ();
|
|
|
- SetNeedsDisplay ();
|
|
|
+
|
|
|
+ tab.View?.SetFocus ();
|
|
|
}
|
|
|
|
|
|
- private class TabToRender {
|
|
|
- public int X { get; set; }
|
|
|
- public Tab Tab { get; set; }
|
|
|
-
|
|
|
- /// <summary>
|
|
|
- /// True if the tab that is being rendered is the selected one
|
|
|
- /// </summary>
|
|
|
- /// <value></value>
|
|
|
- public bool IsSelected { get; set; }
|
|
|
- public int Width { get; }
|
|
|
- public string TextToRender { get; }
|
|
|
-
|
|
|
- public TabToRender (int x, Tab tab, string textToRender, bool isSelected, int width)
|
|
|
- {
|
|
|
- X = x;
|
|
|
- Tab = tab;
|
|
|
- IsSelected = isSelected;
|
|
|
- Width = width;
|
|
|
- TextToRender = textToRender;
|
|
|
- }
|
|
|
+ SetNeedsDisplay ();
|
|
|
+ }
|
|
|
+
|
|
|
+ /// <summary>
|
|
|
+ /// Removes the given <paramref name="tab"/> from <see cref="Tabs"/>.
|
|
|
+ /// Caller is responsible for disposing the tab's hosted <see cref="Tab.View"/>
|
|
|
+ /// if appropriate.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="tab"></param>
|
|
|
+ public void RemoveTab (Tab tab)
|
|
|
+ {
|
|
|
+ if (tab == null || !_tabs.Contains (tab)) {
|
|
|
+ return;
|
|
|
}
|
|
|
|
|
|
- private class TabRowView : View {
|
|
|
+ // what tab was selected before closing
|
|
|
+ var idx = _tabs.IndexOf (tab);
|
|
|
|
|
|
- readonly TabView host;
|
|
|
+ _tabs.Remove (tab);
|
|
|
|
|
|
- public TabRowView (TabView host)
|
|
|
- {
|
|
|
- this.host = host;
|
|
|
+ // if the currently selected tab is no longer a member of Tabs
|
|
|
+ if (SelectedTab == null || !Tabs.Contains (SelectedTab)) {
|
|
|
+ // select the tab closest to the one that disappeared
|
|
|
+ var toSelect = Math.Max (idx - 1, 0);
|
|
|
|
|
|
- CanFocus = true;
|
|
|
- Height = 1;
|
|
|
- Width = Dim.Fill ();
|
|
|
+ if (toSelect < Tabs.Count) {
|
|
|
+ SelectedTab = Tabs.ElementAt (toSelect);
|
|
|
+ } else {
|
|
|
+ SelectedTab = Tabs.LastOrDefault ();
|
|
|
}
|
|
|
|
|
|
- public override bool OnEnter (View view)
|
|
|
- {
|
|
|
- Driver.SetCursorVisibility (CursorVisibility.Invisible);
|
|
|
- return base.OnEnter (view);
|
|
|
- }
|
|
|
+ }
|
|
|
|
|
|
- public override void OnDrawContent (Rect contentArea)
|
|
|
- {
|
|
|
- var tabLocations = host.CalculateViewport (Bounds).ToArray ();
|
|
|
- var width = Bounds.Width;
|
|
|
- Driver.SetAttribute (GetNormalColor ());
|
|
|
+ EnsureSelectedTabIsVisible ();
|
|
|
+ SetNeedsDisplay ();
|
|
|
+ }
|
|
|
|
|
|
- if (host.Style.ShowTopLine) {
|
|
|
- RenderOverline (tabLocations, width);
|
|
|
- }
|
|
|
+ private class TabToRender {
|
|
|
+ public int X { get; set; }
|
|
|
+ public Tab Tab { get; set; }
|
|
|
|
|
|
- RenderTabLine (tabLocations, width);
|
|
|
+ /// <summary>
|
|
|
+ /// True if the tab that is being rendered is the selected one.
|
|
|
+ /// </summary>
|
|
|
+ /// <value></value>
|
|
|
+ public bool IsSelected { get; set; }
|
|
|
+ public int Width { get; }
|
|
|
+ public string TextToRender { get; }
|
|
|
|
|
|
- RenderUnderline (tabLocations, width);
|
|
|
- Driver.SetAttribute (GetNormalColor ());
|
|
|
- }
|
|
|
+ public TabToRender (int x, Tab tab, string textToRender, bool isSelected, int width)
|
|
|
+ {
|
|
|
+ X = x;
|
|
|
+ Tab = tab;
|
|
|
+ IsSelected = isSelected;
|
|
|
+ Width = width;
|
|
|
+ TextToRender = textToRender;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Renders the line of the tabs that does not adjoin the content
|
|
|
- /// </summary>
|
|
|
- /// <param name="tabLocations"></param>
|
|
|
- /// <param name="width"></param>
|
|
|
- private void RenderOverline (TabToRender [] tabLocations, int width)
|
|
|
- {
|
|
|
- // if tabs are on the bottom draw the side of the tab that doesn't border the content area at the bottom otherwise the top
|
|
|
- int y = host.Style.TabsOnBottom ? 2 : 0;
|
|
|
+ private class TabRowView : View {
|
|
|
+ readonly TabView _host;
|
|
|
+ View _rightScrollIndicator;
|
|
|
+ View _leftScrollIndicator;
|
|
|
|
|
|
- Move (0, y);
|
|
|
+ public TabRowView (TabView host)
|
|
|
+ {
|
|
|
+ this._host = host;
|
|
|
|
|
|
- var selected = tabLocations.FirstOrDefault (t => t.IsSelected);
|
|
|
+ CanFocus = true;
|
|
|
+ Height = 1;
|
|
|
+ Width = Dim.Fill ();
|
|
|
|
|
|
- // Clear out everything
|
|
|
- Driver.AddStr (new string (' ', width));
|
|
|
+ _rightScrollIndicator = new View () { Id = "rightScrollIndicator", Width = 1, Height = 1, Visible = false, Text = CM.Glyphs.RightArrow.ToString () };
|
|
|
+ _rightScrollIndicator.MouseClick += _host.Tab_MouseClick;
|
|
|
+ _leftScrollIndicator = new View () { Id = "leftScrollIndicator", Width = 1, Height = 1, Visible = false, Text = CM.Glyphs.LeftArrow.ToString () };
|
|
|
+ _leftScrollIndicator.MouseClick += _host.Tab_MouseClick;
|
|
|
|
|
|
- // Nothing is selected... odd but we are done
|
|
|
- if (selected == null) {
|
|
|
- return;
|
|
|
- }
|
|
|
+ Add (_rightScrollIndicator, _leftScrollIndicator);
|
|
|
+ }
|
|
|
|
|
|
- Move (selected.X - 1, y);
|
|
|
- Driver.AddRune (host.Style.TabsOnBottom ? CM.Glyphs.LLCorner : CM.Glyphs.ULCorner);
|
|
|
+ public override bool OnEnter (View view)
|
|
|
+ {
|
|
|
+ Driver.SetCursorVisibility (CursorVisibility.Invisible);
|
|
|
+ return base.OnEnter (view);
|
|
|
+ }
|
|
|
|
|
|
- for (int i = 0; i < selected.Width; i++) {
|
|
|
+ public override void OnDrawContent (Rect contentArea)
|
|
|
+ {
|
|
|
+ _host._tabLocations = _host.CalculateViewport (Bounds).ToArray ();
|
|
|
|
|
|
- if (selected.X + i > width) {
|
|
|
- // we ran out of space horizontally
|
|
|
- return;
|
|
|
- }
|
|
|
+ // clear any old text
|
|
|
+ Clear ();
|
|
|
|
|
|
- Driver.AddRune (CM.Glyphs.HLine);
|
|
|
- }
|
|
|
+ RenderTabLine ();
|
|
|
|
|
|
- // Add the end of the selected tab
|
|
|
- Driver.AddRune (host.Style.TabsOnBottom ? CM.Glyphs.LRCorner : CM.Glyphs.URCorner);
|
|
|
+ RenderUnderline ();
|
|
|
+ Driver.SetAttribute (GetNormalColor ());
|
|
|
+ }
|
|
|
|
|
|
+ public override void OnDrawContentComplete (Rect contentArea)
|
|
|
+ {
|
|
|
+ if (_host._tabLocations == null) {
|
|
|
+ return;
|
|
|
}
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Renders the line with the tab names in it
|
|
|
- /// </summary>
|
|
|
- /// <param name="tabLocations"></param>
|
|
|
- /// <param name="width"></param>
|
|
|
- private void RenderTabLine (TabToRender [] tabLocations, int width)
|
|
|
- {
|
|
|
- int y;
|
|
|
+ var tabLocations = _host._tabLocations;
|
|
|
+ var selectedTab = -1;
|
|
|
|
|
|
- if (host.Style.TabsOnBottom) {
|
|
|
-
|
|
|
- y = 1;
|
|
|
- } else {
|
|
|
- y = host.Style.ShowTopLine ? 1 : 0;
|
|
|
- }
|
|
|
+ for (int i = 0; i < tabLocations.Length; i++) {
|
|
|
+ View tab = tabLocations [i].Tab;
|
|
|
+ var vts = tab.BoundsToScreen (tab.Bounds);
|
|
|
+ LineCanvas lc = new LineCanvas ();
|
|
|
+ var selectedOffset = _host.Style.ShowTopLine && tabLocations [i].IsSelected ? 0 : 1;
|
|
|
|
|
|
+ if (tabLocations [i].IsSelected) {
|
|
|
+ selectedTab = i;
|
|
|
|
|
|
- // clear any old text
|
|
|
- Move (0, y);
|
|
|
- Driver.AddStr (new string (' ', width));
|
|
|
+ if (i == 0 && _host.TabScrollOffset == 0) {
|
|
|
+ if (_host.Style.TabsOnBottom) {
|
|
|
+ // Upper left vertical line
|
|
|
+ lc.AddLine (new Point (vts.X - 1, vts.Y - 1), -1, Orientation.Vertical, tab.BorderStyle);
|
|
|
+ } else {
|
|
|
+ // Lower left vertical line
|
|
|
+ lc.AddLine (new Point (vts.X - 1, vts.Bottom - selectedOffset), -1, Orientation.Vertical, tab.BorderStyle);
|
|
|
+ }
|
|
|
+ } else if (i > 0 && i <= tabLocations.Length - 1) {
|
|
|
+ if (_host.Style.TabsOnBottom) {
|
|
|
+ // URCorner
|
|
|
+ lc.AddLine (new Point (vts.X - 1, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle);
|
|
|
+ lc.AddLine (new Point (vts.X - 1, vts.Y - 1), -1, Orientation.Horizontal, tab.BorderStyle);
|
|
|
+ } else {
|
|
|
+ // LRCorner
|
|
|
+ lc.AddLine (new Point (vts.X - 1, vts.Bottom - selectedOffset), -1, Orientation.Vertical, tab.BorderStyle);
|
|
|
+ lc.AddLine (new Point (vts.X - 1, vts.Bottom - selectedOffset), -1, Orientation.Horizontal, tab.BorderStyle);
|
|
|
+ }
|
|
|
|
|
|
- foreach (var toRender in tabLocations) {
|
|
|
+ if (_host.Style.ShowTopLine) {
|
|
|
+ if (_host.Style.TabsOnBottom) {
|
|
|
+ // Lower left tee
|
|
|
+ lc.AddLine (new Point (vts.X - 1, vts.Bottom), -1, Orientation.Vertical, tab.BorderStyle);
|
|
|
+ lc.AddLine (new Point (vts.X - 1, vts.Bottom), 0, Orientation.Horizontal, tab.BorderStyle);
|
|
|
+ } else {
|
|
|
+ // Upper left tee
|
|
|
+ lc.AddLine (new Point (vts.X - 1, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle);
|
|
|
+ lc.AddLine (new Point (vts.X - 1, vts.Y - 1), 0, Orientation.Horizontal, tab.BorderStyle);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- if (toRender.IsSelected) {
|
|
|
- Move (toRender.X - 1, y);
|
|
|
- Driver.AddRune (CM.Glyphs.VLine);
|
|
|
+ if (i < tabLocations.Length - 1) {
|
|
|
+ if (_host.Style.ShowTopLine) {
|
|
|
+ if (_host.Style.TabsOnBottom) {
|
|
|
+ // Lower right tee
|
|
|
+ lc.AddLine (new Point (vts.Right, vts.Bottom), -1, Orientation.Vertical, tab.BorderStyle);
|
|
|
+ lc.AddLine (new Point (vts.Right, vts.Bottom), 0, Orientation.Horizontal, tab.BorderStyle);
|
|
|
+ } else {
|
|
|
+ // Upper right tee
|
|
|
+ lc.AddLine (new Point (vts.Right, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle);
|
|
|
+ lc.AddLine (new Point (vts.Right, vts.Y - 1), 0, Orientation.Horizontal, tab.BorderStyle);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- Move (toRender.X, y);
|
|
|
+ if (_host.Style.TabsOnBottom) {
|
|
|
+ //URCorner
|
|
|
+ lc.AddLine (new Point (vts.Right, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle);
|
|
|
+ lc.AddLine (new Point (vts.Right, vts.Y - 1), 1, Orientation.Horizontal, tab.BorderStyle);
|
|
|
+ } else {
|
|
|
+ //LLCorner
|
|
|
+ lc.AddLine (new Point (vts.Right, vts.Bottom - selectedOffset), -1, Orientation.Vertical, tab.BorderStyle);
|
|
|
+ lc.AddLine (new Point (vts.Right, vts.Bottom - selectedOffset), 1, Orientation.Horizontal, tab.BorderStyle);
|
|
|
+ }
|
|
|
|
|
|
- // if tab is the selected one and focus is inside this control
|
|
|
- if (toRender.IsSelected && host.HasFocus) {
|
|
|
+ } else if (selectedTab == -1) {
|
|
|
+ if (i == 0 && string.IsNullOrEmpty (tab.Text)) {
|
|
|
+ if (_host.Style.TabsOnBottom) {
|
|
|
+ if (_host.Style.ShowTopLine) {
|
|
|
+ // LLCorner
|
|
|
+ lc.AddLine (new Point (vts.X - 1, vts.Bottom), -1, Orientation.Vertical, tab.BorderStyle);
|
|
|
+ lc.AddLine (new Point (vts.X - 1, vts.Bottom), 1, Orientation.Horizontal, tab.BorderStyle);
|
|
|
+ }
|
|
|
+ // ULCorner
|
|
|
+ lc.AddLine (new Point (vts.X - 1, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle);
|
|
|
+ lc.AddLine (new Point (vts.X - 1, vts.Y - 1), 1, Orientation.Horizontal, tab.BorderStyle);
|
|
|
+ } else {
|
|
|
+ if (_host.Style.ShowTopLine) {
|
|
|
+ // ULCorner
|
|
|
+ lc.AddLine (new Point (vts.X - 1, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle);
|
|
|
+ lc.AddLine (new Point (vts.X - 1, vts.Y - 1), 1, Orientation.Horizontal, tab.BorderStyle);
|
|
|
+ }
|
|
|
+ // LLCorner
|
|
|
+ lc.AddLine (new Point (vts.X - 1, vts.Bottom), -1, Orientation.Vertical, tab.BorderStyle);
|
|
|
+ lc.AddLine (new Point (vts.X - 1, vts.Bottom), 1, Orientation.Horizontal, tab.BorderStyle);
|
|
|
+ }
|
|
|
+ } else if (i > 0) {
|
|
|
+ if (_host.Style.ShowTopLine || _host.Style.TabsOnBottom) {
|
|
|
+ // Upper left tee
|
|
|
+ lc.AddLine (new Point (vts.X - 1, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle);
|
|
|
+ lc.AddLine (new Point (vts.X - 1, vts.Y - 1), 0, Orientation.Horizontal, tab.BorderStyle);
|
|
|
+ }
|
|
|
|
|
|
- if (host.Focused == this) {
|
|
|
+ // Lower left tee
|
|
|
+ lc.AddLine (new Point (vts.X - 1, vts.Bottom), -1, Orientation.Vertical, tab.BorderStyle);
|
|
|
+ lc.AddLine (new Point (vts.X - 1, vts.Bottom), 0, Orientation.Horizontal, tab.BorderStyle);
|
|
|
|
|
|
- // if focus is the tab bar ourself then show that they can switch tabs
|
|
|
- Driver.SetAttribute (ColorScheme.HotFocus);
|
|
|
+ }
|
|
|
+ } else if (i < tabLocations.Length - 1) {
|
|
|
+ if (_host.Style.ShowTopLine) {
|
|
|
+ // Upper right tee
|
|
|
+ lc.AddLine (new Point (vts.Right, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle);
|
|
|
+ lc.AddLine (new Point (vts.Right, vts.Y - 1), 0, Orientation.Horizontal, tab.BorderStyle);
|
|
|
+ }
|
|
|
|
|
|
- } else {
|
|
|
+ if (_host.Style.ShowTopLine || !_host.Style.TabsOnBottom) {
|
|
|
+ // Lower right tee
|
|
|
+ lc.AddLine (new Point (vts.Right, vts.Bottom), -1, Orientation.Vertical, tab.BorderStyle);
|
|
|
+ lc.AddLine (new Point (vts.Right, vts.Bottom), 0, Orientation.Horizontal, tab.BorderStyle);
|
|
|
+ } else {
|
|
|
+ // Upper right tee
|
|
|
+ lc.AddLine (new Point (vts.Right, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle);
|
|
|
+ lc.AddLine (new Point (vts.Right, vts.Y - 1), 0, Orientation.Horizontal, tab.BorderStyle);
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- // Focus is inside the tab
|
|
|
- Driver.SetAttribute (ColorScheme.HotNormal);
|
|
|
- }
|
|
|
+ if (i == 0 && i != selectedTab && _host.TabScrollOffset == 0 && _host.Style.ShowBorder) {
|
|
|
+ if (_host.Style.TabsOnBottom) {
|
|
|
+ // Upper left vertical line
|
|
|
+ lc.AddLine (new Point (vts.X - 1, vts.Y - 1), 0, Orientation.Vertical, tab.BorderStyle);
|
|
|
+ lc.AddLine (new Point (vts.X - 1, vts.Y - 1), 1, Orientation.Horizontal, tab.BorderStyle);
|
|
|
+ } else {
|
|
|
+ // Lower left vertical line
|
|
|
+ lc.AddLine (new Point (vts.X - 1, vts.Bottom), 0, Orientation.Vertical, tab.BorderStyle);
|
|
|
+ lc.AddLine (new Point (vts.X - 1, vts.Bottom), 1, Orientation.Horizontal, tab.BorderStyle);
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- Driver.AddStr (toRender.TextToRender);
|
|
|
- Driver.SetAttribute (GetNormalColor ());
|
|
|
+ if (i == tabLocations.Length - 1 && i != selectedTab) {
|
|
|
+ if (_host.Style.TabsOnBottom) {
|
|
|
+ // Upper right tee
|
|
|
+ lc.AddLine (new Point (vts.Right, vts.Y - 1), 1, Orientation.Vertical, tab.BorderStyle);
|
|
|
+ lc.AddLine (new Point (vts.Right, vts.Y - 1), 0, Orientation.Horizontal, tab.BorderStyle);
|
|
|
+ } else {
|
|
|
+ // Lower right tee
|
|
|
+ lc.AddLine (new Point (vts.Right, vts.Bottom), -1, Orientation.Vertical, tab.BorderStyle);
|
|
|
+ lc.AddLine (new Point (vts.Right, vts.Bottom), 0, Orientation.Horizontal, tab.BorderStyle);
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- if (toRender.IsSelected) {
|
|
|
- Driver.AddRune (CM.Glyphs.VLine);
|
|
|
+ if (i == tabLocations.Length - 1) {
|
|
|
+ var arrowOffset = 1;
|
|
|
+ var lastSelectedTab = !_host.Style.ShowTopLine && i == selectedTab ? 1 : _host.Style.TabsOnBottom ? 1 : 0;
|
|
|
+ var tabsBarVts = BoundsToScreen (Bounds);
|
|
|
+ var lineLength = tabsBarVts.Right - vts.Right;
|
|
|
+ // Right horizontal line
|
|
|
+ if (ShouldDrawRightScrollIndicator ()) {
|
|
|
+ if (lineLength - arrowOffset > 0) {
|
|
|
+ if (_host.Style.TabsOnBottom) {
|
|
|
+ lc.AddLine (new Point (vts.Right, vts.Y - lastSelectedTab), lineLength - arrowOffset, Orientation.Horizontal, tab.BorderStyle);
|
|
|
+ } else {
|
|
|
+ lc.AddLine (new Point (vts.Right, vts.Bottom - lastSelectedTab), lineLength - arrowOffset, Orientation.Horizontal, tab.BorderStyle);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if (_host.Style.TabsOnBottom) {
|
|
|
+ lc.AddLine (new Point (vts.Right, vts.Y - lastSelectedTab), lineLength, Orientation.Horizontal, tab.BorderStyle);
|
|
|
+ } else {
|
|
|
+ lc.AddLine (new Point (vts.Right, vts.Bottom - lastSelectedTab), lineLength, Orientation.Horizontal, tab.BorderStyle);
|
|
|
+ }
|
|
|
+ if (_host.Style.ShowBorder) {
|
|
|
+ if (_host.Style.TabsOnBottom) {
|
|
|
+ // More LRCorner
|
|
|
+ lc.AddLine (new Point (tabsBarVts.Right - 1, vts.Y - lastSelectedTab), -1, Orientation.Vertical, tab.BorderStyle);
|
|
|
+ } else {
|
|
|
+ // More URCorner
|
|
|
+ lc.AddLine (new Point (tabsBarVts.Right - 1, vts.Bottom - lastSelectedTab), 1, Orientation.Vertical, tab.BorderStyle);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ tab.LineCanvas.Merge (lc);
|
|
|
+ tab.OnRenderLineCanvas ();
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Renders the line of the tab that adjoins the content of the tab
|
|
|
- /// </summary>
|
|
|
- /// <param name="tabLocations"></param>
|
|
|
- /// <param name="width"></param>
|
|
|
- private void RenderUnderline (TabToRender [] tabLocations, int width)
|
|
|
- {
|
|
|
- int y = GetUnderlineYPosition ();
|
|
|
+ /// <summary>
|
|
|
+ /// Renders the line with the tab names in it.
|
|
|
+ /// </summary>
|
|
|
+ private void RenderTabLine ()
|
|
|
+ {
|
|
|
+ var tabLocations = _host._tabLocations;
|
|
|
+ int y;
|
|
|
|
|
|
- Move (0, y);
|
|
|
+ if (_host.Style.TabsOnBottom) {
|
|
|
|
|
|
- // If host has no border then we need to draw the solid line first (then we draw gaps over the top)
|
|
|
- if (!host.Style.ShowBorder) {
|
|
|
+ y = 1;
|
|
|
+ } else {
|
|
|
+ y = _host.Style.ShowTopLine ? 1 : 0;
|
|
|
+ }
|
|
|
|
|
|
- for (int x = 0; x < width; x++) {
|
|
|
- Driver.AddRune (CM.Glyphs.HLine);
|
|
|
+ View selected = null;
|
|
|
+ var topLine = _host.Style.ShowTopLine ? 1 : 0;
|
|
|
+ var width = Bounds.Width;
|
|
|
+
|
|
|
+ foreach (var toRender in tabLocations) {
|
|
|
+ var tab = toRender.Tab;
|
|
|
+
|
|
|
+ if (toRender.IsSelected) {
|
|
|
+ selected = tab;
|
|
|
+ if (_host.Style.TabsOnBottom) {
|
|
|
+ tab.Border.Thickness = new Thickness (1, 0, 1, topLine);
|
|
|
+ tab.Margin.Thickness = new Thickness (0, 1, 0, 0);
|
|
|
+ } else {
|
|
|
+ tab.Border.Thickness = new Thickness (1, topLine, 1, 0);
|
|
|
+ tab.Margin.Thickness = new Thickness (0, 0, 0, topLine);
|
|
|
}
|
|
|
-
|
|
|
+ } else if (selected == null) {
|
|
|
+ if (_host.Style.TabsOnBottom) {
|
|
|
+ tab.Border.Thickness = new Thickness (1, 1, 0, topLine);
|
|
|
+ tab.Margin.Thickness = new Thickness (0, 0, 0, 0);
|
|
|
+ } else {
|
|
|
+ tab.Border.Thickness = new Thickness (1, topLine, 0, 1);
|
|
|
+ tab.Margin.Thickness = new Thickness (0, 0, 0, 0);
|
|
|
+ }
|
|
|
+ tab.Width = Math.Max (tab.Width.Anchor (0) - 1, 1);
|
|
|
+ } else {
|
|
|
+ if (_host.Style.TabsOnBottom) {
|
|
|
+ tab.Border.Thickness = new Thickness (0, 1, 1, topLine);
|
|
|
+ tab.Margin.Thickness = new Thickness (0, 0, 0, 0);
|
|
|
+ } else {
|
|
|
+ tab.Border.Thickness = new Thickness (0, topLine, 1, 1);
|
|
|
+ tab.Margin.Thickness = new Thickness (0, 0, 0, 0);
|
|
|
+ }
|
|
|
+ tab.Width = Math.Max (tab.Width.Anchor (0) - 1, 1);
|
|
|
}
|
|
|
- var selected = tabLocations.FirstOrDefault (t => t.IsSelected);
|
|
|
|
|
|
- if (selected == null) {
|
|
|
- return;
|
|
|
- }
|
|
|
+ tab.Text = toRender.TextToRender;
|
|
|
|
|
|
- Move (selected.X - 1, y);
|
|
|
+ LayoutSubviews ();
|
|
|
|
|
|
- Driver.AddRune (selected.X == 1 ? CM.Glyphs.VLine :
|
|
|
- (host.Style.TabsOnBottom ? CM.Glyphs.URCorner : CM.Glyphs.LRCorner));
|
|
|
+ tab.OnDrawAdornments ();
|
|
|
|
|
|
- Driver.AddStr (new string (' ', selected.Width));
|
|
|
+ var prevAttr = Driver.GetAttribute ();
|
|
|
|
|
|
- Driver.AddRune (selected.X + selected.Width == width - 1 ?
|
|
|
- CM.Glyphs.VLine :
|
|
|
- (host.Style.TabsOnBottom ? CM.Glyphs.ULCorner : CM.Glyphs.LLCorner));
|
|
|
+ // if tab is the selected one and focus is inside this control
|
|
|
+ if (toRender.IsSelected && _host.HasFocus) {
|
|
|
|
|
|
- // draw scroll indicators
|
|
|
+ if (_host.Focused == this) {
|
|
|
|
|
|
- // if there are more tabs to the left not visible
|
|
|
- if (host.TabScrollOffset > 0) {
|
|
|
- Move (0, y);
|
|
|
+ // if focus is the tab bar ourself then show that they can switch tabs
|
|
|
+ prevAttr = ColorScheme.HotFocus;
|
|
|
+ } else {
|
|
|
|
|
|
- // indicate that
|
|
|
- Driver.AddRune (CM.Glyphs.LeftArrow);
|
|
|
+ // Focus is inside the tab
|
|
|
+ prevAttr = ColorScheme.HotNormal;
|
|
|
+ }
|
|
|
}
|
|
|
+ tab.TextFormatter.Draw (tab.BoundsToScreen (tab.Bounds), prevAttr, ColorScheme.HotNormal);
|
|
|
|
|
|
- // if there are more tabs to the right not visible
|
|
|
- if (ShouldDrawRightScrollIndicator (tabLocations)) {
|
|
|
- Move (width - 1, y);
|
|
|
+ tab.OnRenderLineCanvas ();
|
|
|
|
|
|
- // indicate that
|
|
|
- Driver.AddRune (CM.Glyphs.RightArrow);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private bool ShouldDrawRightScrollIndicator (TabToRender [] tabLocations)
|
|
|
- {
|
|
|
- return tabLocations.LastOrDefault ()?.Tab != host.Tabs.LastOrDefault ();
|
|
|
+ Driver.SetAttribute (GetNormalColor ());
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- private int GetUnderlineYPosition ()
|
|
|
- {
|
|
|
- if (host.Style.TabsOnBottom) {
|
|
|
+ /// <summary>
|
|
|
+ /// Renders the line of the tab that adjoins the content of the tab.
|
|
|
+ /// </summary>
|
|
|
+ private void RenderUnderline ()
|
|
|
+ {
|
|
|
+ int y = GetUnderlineYPosition ();
|
|
|
|
|
|
- return 0;
|
|
|
- } else {
|
|
|
+ var selected = _host._tabLocations.FirstOrDefault (t => t.IsSelected);
|
|
|
|
|
|
- return host.Style.ShowTopLine ? 2 : 1;
|
|
|
- }
|
|
|
+ if (selected == null) {
|
|
|
+ return;
|
|
|
}
|
|
|
|
|
|
- public override bool MouseEvent (MouseEvent me)
|
|
|
- {
|
|
|
- var hit = ScreenToTab (me.X, me.Y);
|
|
|
+ // draw scroll indicators
|
|
|
|
|
|
- bool isClick = me.Flags.HasFlag (MouseFlags.Button1Clicked) ||
|
|
|
- me.Flags.HasFlag (MouseFlags.Button2Clicked) ||
|
|
|
- me.Flags.HasFlag (MouseFlags.Button3Clicked);
|
|
|
+ // if there are more tabs to the left not visible
|
|
|
+ if (_host.TabScrollOffset > 0) {
|
|
|
+ _leftScrollIndicator.X = 0;
|
|
|
+ _leftScrollIndicator.Y = y;
|
|
|
|
|
|
- if (isClick) {
|
|
|
- host.OnTabClicked (new TabMouseEventArgs (hit, me));
|
|
|
+ // indicate that
|
|
|
+ _leftScrollIndicator.Visible = true;
|
|
|
+ // Ensures this is clicked instead of the first tab
|
|
|
+ BringSubviewToFront (_leftScrollIndicator);
|
|
|
+ _leftScrollIndicator.Draw ();
|
|
|
+ } else {
|
|
|
+ _leftScrollIndicator.Visible = false;
|
|
|
+ }
|
|
|
|
|
|
- // user canceled click
|
|
|
- if (me.Handled) {
|
|
|
- return true;
|
|
|
- }
|
|
|
- }
|
|
|
+ // if there are more tabs to the right not visible
|
|
|
+ if (ShouldDrawRightScrollIndicator ()) {
|
|
|
+ _rightScrollIndicator.X = Bounds.Width - 1;
|
|
|
+ _rightScrollIndicator.Y = y;
|
|
|
|
|
|
- if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) &&
|
|
|
- !me.Flags.HasFlag (MouseFlags.Button1DoubleClicked) &&
|
|
|
- !me.Flags.HasFlag (MouseFlags.Button1TripleClicked))
|
|
|
- return false;
|
|
|
+ // indicate that
|
|
|
+ _rightScrollIndicator.Visible = true;
|
|
|
+ // Ensures this is clicked instead of the last tab if under this
|
|
|
+ BringSubviewToFront (_rightScrollIndicator);
|
|
|
+ _rightScrollIndicator.Draw ();
|
|
|
+ } else {
|
|
|
+ _rightScrollIndicator.Visible = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- if (!HasFocus && CanFocus) {
|
|
|
- SetFocus ();
|
|
|
- }
|
|
|
+ private bool ShouldDrawRightScrollIndicator ()
|
|
|
+ {
|
|
|
+ return _host._tabLocations.LastOrDefault ()?.Tab != _host.Tabs.LastOrDefault ();
|
|
|
+ }
|
|
|
|
|
|
- if (me.Flags.HasFlag (MouseFlags.Button1Clicked) ||
|
|
|
- me.Flags.HasFlag (MouseFlags.Button1DoubleClicked) ||
|
|
|
- me.Flags.HasFlag (MouseFlags.Button1TripleClicked)) {
|
|
|
+ private int GetUnderlineYPosition ()
|
|
|
+ {
|
|
|
+ if (_host.Style.TabsOnBottom) {
|
|
|
+
|
|
|
+ return 0;
|
|
|
+ } else {
|
|
|
|
|
|
- var scrollIndicatorHit = ScreenToScrollIndicator (me.X, me.Y);
|
|
|
+ return _host.Style.ShowTopLine ? 2 : 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
|
|
|
- if (scrollIndicatorHit != 0) {
|
|
|
+ public override bool MouseEvent (MouseEvent me)
|
|
|
+ {
|
|
|
+ var hit = me.View is Tab ? (Tab)me.View : null;
|
|
|
|
|
|
- host.SwitchTabBy (scrollIndicatorHit);
|
|
|
+ bool isClick = me.Flags.HasFlag (MouseFlags.Button1Clicked) ||
|
|
|
+ me.Flags.HasFlag (MouseFlags.Button2Clicked) ||
|
|
|
+ me.Flags.HasFlag (MouseFlags.Button3Clicked);
|
|
|
|
|
|
- SetNeedsDisplay ();
|
|
|
- return true;
|
|
|
- }
|
|
|
+ if (isClick) {
|
|
|
+ _host.OnTabClicked (new TabMouseEventArgs (hit, me));
|
|
|
|
|
|
- if (hit != null) {
|
|
|
- host.SelectedTab = hit;
|
|
|
- SetNeedsDisplay ();
|
|
|
- return true;
|
|
|
- }
|
|
|
+ // user canceled click
|
|
|
+ if (me.Handled) {
|
|
|
+ return true;
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
+ if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) &&
|
|
|
+ !me.Flags.HasFlag (MouseFlags.Button1DoubleClicked) &&
|
|
|
+ !me.Flags.HasFlag (MouseFlags.Button1TripleClicked))
|
|
|
return false;
|
|
|
+
|
|
|
+ if (!HasFocus && CanFocus) {
|
|
|
+ SetFocus ();
|
|
|
}
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Calculates whether scroll indicators are visible and if so whether the click
|
|
|
- /// was on one of them.
|
|
|
- /// </summary>
|
|
|
- /// <param name="x"></param>
|
|
|
- /// <param name="y"></param>
|
|
|
- /// <returns>-1 for click in scroll left, 1 for scroll right or 0 for no hit</returns>
|
|
|
- private int ScreenToScrollIndicator (int x, int y)
|
|
|
- {
|
|
|
- // scroll indicator is showing
|
|
|
- if (host.TabScrollOffset > 0 && x == 0) {
|
|
|
-
|
|
|
- return y == GetUnderlineYPosition () ? -1 : 0;
|
|
|
+ if (me.Flags.HasFlag (MouseFlags.Button1Clicked) ||
|
|
|
+ me.Flags.HasFlag (MouseFlags.Button1DoubleClicked) ||
|
|
|
+ me.Flags.HasFlag (MouseFlags.Button1TripleClicked)) {
|
|
|
+
|
|
|
+ int scrollIndicatorHit = 0;
|
|
|
+ if (me.View != null && me.View.Id == "rightScrollIndicator") {
|
|
|
+ scrollIndicatorHit = 1;
|
|
|
+ } else if (me.View != null && me.View.Id == "leftScrollIndicator") {
|
|
|
+ scrollIndicatorHit = -1;
|
|
|
}
|
|
|
|
|
|
- // scroll indicator is showing
|
|
|
- if (x == Bounds.Width - 1 && ShouldDrawRightScrollIndicator (host.CalculateViewport (Bounds).ToArray ())) {
|
|
|
+ if (scrollIndicatorHit != 0) {
|
|
|
|
|
|
- return y == GetUnderlineYPosition () ? 1 : 0;
|
|
|
+ _host.SwitchTabBy (scrollIndicatorHit);
|
|
|
+
|
|
|
+ SetNeedsDisplay ();
|
|
|
+ return true;
|
|
|
}
|
|
|
|
|
|
- return 0;
|
|
|
+ if (hit != null) {
|
|
|
+ _host.SelectedTab = hit;
|
|
|
+ SetNeedsDisplay ();
|
|
|
+ return true;
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Translates the client coordinates of a click into a tab when the click is on top of a tab
|
|
|
- /// </summary>
|
|
|
- /// <param name="x"></param>
|
|
|
- /// <param name="y"></param>
|
|
|
- /// <returns></returns>
|
|
|
- public Tab ScreenToTab (int x, int y)
|
|
|
- {
|
|
|
- var tabs = host.CalculateViewport (Bounds);
|
|
|
-
|
|
|
- return tabs.LastOrDefault (t => x >= t.X && x < t.X + t.Width)?.Tab;
|
|
|
- }
|
|
|
+ return false;
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- /// <summary>
|
|
|
- /// Raises the <see cref="TabClicked"/> event.
|
|
|
- /// </summary>
|
|
|
- /// <param name="tabMouseEventArgs"></param>
|
|
|
- protected virtual private void OnTabClicked (TabMouseEventArgs tabMouseEventArgs)
|
|
|
- {
|
|
|
- TabClicked?.Invoke (this, tabMouseEventArgs);
|
|
|
- }
|
|
|
+ /// <summary>
|
|
|
+ /// Raises the <see cref="TabClicked"/> event.
|
|
|
+ /// </summary>
|
|
|
+ /// <param name="tabMouseEventArgs"></param>
|
|
|
+ protected virtual private void OnTabClicked (TabMouseEventArgs tabMouseEventArgs)
|
|
|
+ {
|
|
|
+ TabClicked?.Invoke (this, tabMouseEventArgs);
|
|
|
}
|
|
|
}
|