From 61e87e84b5205675b82275cd2b295c1fccb1a127 Mon Sep 17 00:00:00 2001 From: BDisp Date: Mon, 11 Jan 2021 15:08:07 +0000 Subject: [PATCH] Fixes #1073, #1058, #480 #1049. Provides more automation to the ScrollBarView, allowing easily implement on any view. --- Terminal.Gui/Core/View.cs | 3 +- Terminal.Gui/Views/ListView.cs | 41 +- Terminal.Gui/Views/ScrollBarView.cs | 538 +++++++++++++++++++ Terminal.Gui/Views/ScrollView.cs | 376 +------------ Terminal.Gui/Views/TextView.cs | 26 +- UICatalog/Scenarios/Editor.cs | 51 ++ UICatalog/Scenarios/ListViewWithSelection.cs | 31 +- 7 files changed, 690 insertions(+), 376 deletions(-) create mode 100644 Terminal.Gui/Views/ScrollBarView.cs diff --git a/Terminal.Gui/Core/View.cs b/Terminal.Gui/Core/View.cs index 0e7bd5bb6..b4ccd5f0d 100644 --- a/Terminal.Gui/Core/View.cs +++ b/Terminal.Gui/Core/View.cs @@ -741,7 +741,7 @@ namespace Terminal.Gui { public void SetChildNeedsDisplay () { ChildNeedsDisplay = true; - if (container != null) + if (container != null && !container.ChildNeedsDisplay) container.SetChildNeedsDisplay (); } @@ -1342,6 +1342,7 @@ namespace Terminal.Gui { // Draw the subview // Use the view's bounds (view-relative; Location will always be (0,0) if (view.Visible && view.Frame.Width > 0 && view.Frame.Height > 0) { + view.OnDrawContent (view.Bounds); view.Redraw (view.Bounds); } } diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index 84404d71e..41b1ae856 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -298,11 +298,6 @@ namespace Terminal.Gui { Driver.SetAttribute (current); Move (0, 0); var f = Frame; - if (selected < top) { - top = selected; - } else if (selected >= top + f.Height) { - top = selected; - } var item = top; bool focused = HasFocus; int col = allowsMarking ? 2 : 0; @@ -477,8 +472,11 @@ namespace Terminal.Gui { } else if (selected + 1 < source.Count) { //can move by down by one. selected++; - if (selected >= top + Frame.Height) + if (selected >= top + Frame.Height) { top++; + } else if (selected < top) { + top = selected; + } OnSelectedChanged (); SetNeedsDisplay (); } else if (selected == 0) { @@ -511,8 +509,11 @@ namespace Terminal.Gui { if (selected > Source.Count) { selected = Source.Count - 1; } - if (selected < top) + if (selected < top) { top = selected; + } else if (selected > top + Frame.Height) { + top = Math.Max (selected - Frame.Height + 1, 0); + } OnSelectedChanged (); SetNeedsDisplay (); } @@ -551,6 +552,26 @@ namespace Terminal.Gui { return true; } + /// + /// Scrolls the view down. + /// + /// Number of lines to scroll down. + public virtual void ScrollDown (int lines) + { + top = Math.Min (top + lines, source.Count - 1); + SetNeedsDisplay (); + } + + /// + /// Scrolls the view up. + /// + /// Number of lines to scroll up. + public virtual void ScrollUp (int lines) + { + top = Math.Max (top - lines, 0); + SetNeedsDisplay (); + } + int lastSelectedItem = -1; private bool allowsMultipleSelection = true; @@ -630,10 +651,10 @@ namespace Terminal.Gui { } if (me.Flags == MouseFlags.WheeledDown) { - MoveDown (); + ScrollDown (1); return true; } else if (me.Flags == MouseFlags.WheeledUp) { - MoveUp (); + ScrollUp (1); return true; } @@ -655,6 +676,8 @@ namespace Terminal.Gui { return true; } + + } /// diff --git a/Terminal.Gui/Views/ScrollBarView.cs b/Terminal.Gui/Views/ScrollBarView.cs new file mode 100644 index 000000000..985d31ef4 --- /dev/null +++ b/Terminal.Gui/Views/ScrollBarView.cs @@ -0,0 +1,538 @@ +// +// ScrollBarView.cs: ScrollBarView view. +// +// Authors: +// Miguel de Icaza (miguel@gnome.org) +// + +using System; + +namespace Terminal.Gui { + /// + /// ScrollBarViews are views that display a 1-character scrollbar, either horizontal or vertical + /// + /// + /// + /// The scrollbar is drawn to be a representation of the Size, assuming that the + /// scroll position is set at Position. + /// + /// + /// If the region to display the scrollbar is larger than three characters, + /// arrow indicators are drawn. + /// + /// + public class ScrollBarView : View { + bool vertical; + int size, position, contentOffset; + bool showScrollIndicator; + bool keepContentAlwaysInViewport = true; + bool autoHideScrollBars = true; + Dim originalHostWidth, originalHostHeight; + bool hosted; + bool showBothScrollIndicator => OtherScrollBarView != null && OtherScrollBarView.showScrollIndicator; + + /// + /// Initializes a new instance of the class using layout. + /// + /// Frame for the scrollbar. + public ScrollBarView (Rect rect) : this (rect, 0, 0, false) { } + + /// + /// Initializes a new instance of the class using layout. + /// + /// Frame for the scrollbar. + /// The size that this scrollbar represents. Sets the property. + /// The position within this scrollbar. Sets the property. + /// If set to true this is a vertical scrollbar, otherwise, the scrollbar is horizontal. Sets the property. + public ScrollBarView (Rect rect, int size, int position, bool isVertical) : base (rect) + { + Init (size, position, isVertical); + } + + /// + /// Initializes a new instance of the class using layout. + /// + public ScrollBarView () : this (0, 0, false) { } + + /// + /// Initializes a new instance of the class using layout. + /// + /// The size that this scrollbar represents. + /// The position within this scrollbar. + /// If set to true this is a vertical scrollbar, otherwise, the scrollbar is horizontal. + public ScrollBarView (int size, int position, bool isVertical) : base () + { + Init (size, position, isVertical); + } + + /// + /// Initializes a new instance of the class using layout. + /// + public ScrollBarView (View host, bool isVertical) : this (0, 0, isVertical) + { + hosted = true; + originalHostWidth = host.Width; + originalHostHeight = host.Height; + X = isVertical ? Pos.Right(host) : Pos.Left (host); + Y = isVertical ? Pos.Top (host) : Pos.Bottom (host); + Host = host; + Host.SuperView.Add (this); + ShowScrollIndicator = true; + AutoHideScrollBars = true; + } + + void Init (int size, int position, bool isVertical) + { + vertical = isVertical; + this.position = position; + this.size = size; + WantContinuousButtonPressed = true; + } + + /// + /// If set to true this is a vertical scrollbar, otherwise, the scrollbar is horizontal. + /// + public bool IsVertical { + get => vertical; + set { + vertical = value; + SetNeedsDisplay (); + } + } + + /// + /// The size of content the scrollbar represents. + /// + /// The size. + /// The is typically the size of the virtual content. E.g. when a Scrollbar is + /// part of a the Size is set to the appropriate dimension of . + public int Size { + get => size; + set { + size = value; + ShowHideScrollBars (); + SetNeedsDisplay (); + } + } + + /// + /// Represents the top left/top corner coordinate that is displayed by the scrollbar. + /// + /// The content offset. + public int ContentOffset { + get { + return contentOffset; + } + set { + var co = -Math.Abs (value); + if (contentOffset != co) { + if (CanScroll (contentOffset - co, out int max, vertical)) { + if (max == contentOffset - co) { + contentOffset = co; + } else { + contentOffset = co + max; + } + } + Position = Math.Max (0, -contentOffset); + OnChangedPosition (); + SetNeedsDisplay (); + } + } + } + + /// + /// This event is raised when the position on the scrollbar has changed. + /// + public event Action ChangedPosition; + + /// + /// The position, relative to , to set the scrollbar at. + /// + /// The position. + public int Position { + get => position; + set { + position = value; + SetNeedsDisplay (); + } + } + + /// + /// Get or sets the view that host this + /// + public View Host { get; internal set; } + + /// + /// Represent a vertical or horizontal ScrollBarView other than this. + /// + public ScrollBarView OtherScrollBarView { get; set; } + + /// + /// Gets or sets the visibility for the vertical or horizontal scroll indicator. + /// + /// true if show vertical or horizontal scroll indicator; otherwise, false. + public bool ShowScrollIndicator { + get => showScrollIndicator; + set { + if (value == showScrollIndicator) { + return; + } + + showScrollIndicator = value; + SetNeedsLayout (); + if (value) { + Visible = true; + } else { + Visible = false; + } + Width = vertical ? 1 : Dim.Width (Host); + Height = vertical ? Dim.Height (Host) : 1; + if (vertical) { + Host.Width = showScrollIndicator ? originalHostWidth - 1 : originalHostWidth; + } else { + Host.Height = showScrollIndicator ? originalHostHeight - 1 : originalHostHeight; + } + } + } + + /// + /// Get or sets if the view-port is kept always visible in the area of this + /// + public bool KeepContentAlwaysInViewport { + get { return keepContentAlwaysInViewport; } + set { + if (keepContentAlwaysInViewport != value) { + keepContentAlwaysInViewport = value; + int co = 0; + if (value && !vertical && -contentOffset + Host.Bounds.Width > size) { + co = size - Host.Bounds.Width + (showBothScrollIndicator ? 1 : 0); + } + if (value && vertical && -contentOffset + Host.Bounds.Height > size) { + co = size - Host.Bounds.Height + (showBothScrollIndicator ? 1 : 0); + } + if (co != 0) { + ContentOffset = co; + } + if (OtherScrollBarView != null && OtherScrollBarView.keepContentAlwaysInViewport != value) { + OtherScrollBarView.KeepContentAlwaysInViewport = value; + } + } + } + } + + /// + /// If true the vertical/horizontal scroll bars won't be showed if it's not needed. + /// + public bool AutoHideScrollBars { + get => autoHideScrollBars; + set { + if (autoHideScrollBars != value) { + autoHideScrollBars = value; + SetNeedsDisplay (); + } + } + } + + void SetPosition (int newPos) + { + Position = newPos; + ContentOffset = Position; + OnChangedPosition (); + } + + /// + /// Virtual method to invoke the action event. + /// + public virtual void OnChangedPosition () + { + ChangedPosition?.Invoke (); + } + + internal bool pending; + + void ShowHideScrollBars () + { + if (!hosted || !autoHideScrollBars) { + return; + } + + if (vertical) { + if (Host.Bounds.Height == 0 || Host.Bounds.Height > size) { + if (showScrollIndicator) { + ShowScrollIndicator = false; + } + } else if (Host.Bounds.Height > 0 && Host.Bounds.Height == size) { + pending = true; + } else if (!showScrollIndicator) { + ShowScrollIndicator = true; + } + } else { + if (Host.Bounds.Width == 0 || Host.Bounds.Width > size) { + if (showScrollIndicator) { + ShowScrollIndicator = false; + } + } else if (Host.Bounds.Width > 0 && Host.Bounds.Width == size && OtherScrollBarView.pending) { + if (showScrollIndicator) { + ShowScrollIndicator = false; + } + if (showBothScrollIndicator) { + OtherScrollBarView.showScrollIndicator = false; + } + } else { + if (OtherScrollBarView.pending) { + if (!showBothScrollIndicator) { + OtherScrollBarView.showScrollIndicator = true; + } + } + if (!showScrollIndicator) { + ShowScrollIndicator = true; + } + } + } + } + + int posTopTee; + int posLeftTee; + int posBottomTee; + int posRightTee; + + /// + public override void Redraw (Rect region) + { + if (ColorScheme == null || Size == 0) { + return; + } + + Driver.SetAttribute (ColorScheme.Normal); + + if ((vertical && Bounds.Height == 0) || (!vertical && Bounds.Width == 0)) { + return; + } + + if (vertical) { + if (region.Right < Bounds.Width - 1) { + return; + } + + var col = Bounds.Width - 1; + var bh = Bounds.Height; + Rune special; + + if (bh < 4) { + var by1 = position * bh / Size; + var by2 = (position + bh) * bh / Size; + + Move (col, 0); + if (Bounds.Height == 1) { + Driver.AddRune (Driver.Diamond); + } else { + Driver.AddRune (Driver.UpArrow); + } + if (Bounds.Height == 3) { + Move (col, 1); + Driver.AddRune (Driver.Diamond); + } + if (Bounds.Height > 1) { + Move (col, Bounds.Height - 1); + Driver.AddRune (Driver.DownArrow); + } + } else { + bh -= 2; + var by1 = position * bh / Size; + var by2 = KeepContentAlwaysInViewport ? Math.Min (((position + bh) * bh / Size) + 1, bh - 1) : (position + bh) * bh / Size; + if (KeepContentAlwaysInViewport && by1 == by2) { + by1 = Math.Max (by1 - 1, 0); + } + + Move (col, 0); + Driver.AddRune (Driver.UpArrow); + Move (col, Bounds.Height - 1); + Driver.AddRune (Driver.DownArrow); + + bool hasTopTee = false; + bool hasDiamond = false; + bool hasBottomTee = false; + for (int y = 0; y < bh; y++) { + Move (col, y + 1); + if ((y < by1 || y > by2) && ((position > 0 && !hasTopTee) || (hasTopTee && hasBottomTee))) { + special = Driver.Stipple; + } else { + if (y != by2 && y > 1 && by2 - by1 == 0 && by1 < bh - 1 && hasTopTee && !hasDiamond) { + hasDiamond = true; + special = Driver.Diamond; + } else { + if (y == by1 && !hasTopTee) { + hasTopTee = true; + posTopTee = y; + special = Driver.TopTee; + } else if ((position == 0 && y == bh - 1 || y >= by2 || by2 == 0) && !hasBottomTee) { + hasBottomTee = true; + posBottomTee = y; + special = Driver.BottomTee; + } else { + special = Driver.VLine; + } + } + } + Driver.AddRune (special); + } + if (!hasTopTee) { + Move (col, Bounds.Height - 2); + Driver.AddRune (Driver.TopTee); + } + } + } else { + if (region.Bottom < Bounds.Height - 1) { + return; + } + + var row = Bounds.Height - 1; + var bw = Bounds.Width; + Rune special; + + if (bw < 4) { + var bx1 = position * bw / Size; + var bx2 = (position + bw) * bw / Size; + + Move (0, row); + Driver.AddRune (Driver.LeftArrow); + Driver.AddRune (Driver.RightArrow); + } else { + bw -= 2; + var bx1 = position * bw / Size; + var bx2 = KeepContentAlwaysInViewport ? Math.Min (((position + bw) * bw / Size) + 1, bw - 1) : (position + bw) * bw / Size; + if (KeepContentAlwaysInViewport && bx1 == bx2) { + bx1 = Math.Max (bx1 - 1, 0); + } + + Move (0, row); + Driver.AddRune (Driver.LeftArrow); + + bool hasLeftTee = false; + bool hasDiamond = false; + bool hasRightTee = false; + for (int x = 0; x < bw; x++) { + if ((x < bx1 || x >= bx2 + 1) && ((position > 0 && !hasLeftTee) || (hasLeftTee && hasRightTee))) { + special = Driver.Stipple; + } else { + if (x != bx2 && x > 1 && bx2 - bx1 == 0 && bx1 < bw - 1 && hasLeftTee && !hasDiamond) { + hasDiamond = true; + special = Driver.Diamond; + } else { + if (x == bx1 && !hasLeftTee) { + hasLeftTee = true; + posLeftTee = x; + special = Driver.LeftTee; + } else if ((position == 0 && x == bw - 1 || x >= bx2 || bx2 == 0) && !hasRightTee) { + hasRightTee = true; + posRightTee = x; + special = Driver.RightTee; + } else { + special = Driver.HLine; + } + } + } + Driver.AddRune (special); + } + if (!hasLeftTee) { + Move (Bounds.Width -2, row); + Driver.AddRune (Driver.LeftTee); + } + + Driver.AddRune (Driver.RightArrow); + } + } + } + + int lastLocation = -1; + + /// + public override bool MouseEvent (MouseEvent me) + { + if (me.Flags != MouseFlags.Button1Pressed && me.Flags != MouseFlags.Button1Clicked && + !me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) { + return false; + } + + if (!me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) { + lastLocation = -1; + } + + int location = vertical ? me.Y : me.X; + int barsize = vertical ? Bounds.Height : Bounds.Width; + int posTopLeftTee = vertical ? posTopTee : posLeftTee; + int posBottomRightTee = vertical ? posBottomTee : posRightTee; + + barsize -= 2; + var pos = Position; + if (location == 0) { + if (pos > 0) { + SetPosition (pos - 1); + } + } else if (location == barsize + 1) { + if (CanScroll (1, out _, vertical)) { + SetPosition (pos + 1); + } + } else if (location > 0 && location < barsize + 1) { + var b1 = pos * barsize / Size; + var b2 = KeepContentAlwaysInViewport ? Math.Min (((pos + barsize) * barsize / Size) + 1, barsize - 1) : (pos + barsize) * barsize / Size; + if (KeepContentAlwaysInViewport && b1 == b2) { + b1 = Math.Max (b1 - 1, 0); + } + + if (location > b1 && location <= b2 + 1) { + if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1Clicked) { + if (location == 1) { + SetPosition (0); + } else if (location == barsize) { + CanScroll (Size - pos, out int nv, vertical); + if (nv > 0) { + SetPosition (Math.Min (pos + nv, Size)); + } + } + } else if (me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) { + var mb = (b2 - b1) / 2; + var ml = mb + b1 + (mb == 0 ? 1 : 0); + if ((location >= b1 && location <= ml) || (location < lastLocation && lastLocation > -1)) { + lastLocation = location; + var np = b1 * Size / barsize; + SetPosition (np); + } else if (location > lastLocation) { + var np = location * Size / barsize; + CanScroll (np - pos, out int nv, vertical); + if (nv > 0) { + SetPosition (pos + nv); + } + } + } + } else { + if (location >= b2 + 1 && location > posTopLeftTee && location > b1 && location > posBottomRightTee && posBottomRightTee > 0) { + CanScroll (location, out int nv, vertical); + if (nv > 0) { + SetPosition (Math.Min (pos + nv, Size)); + } + } else if (location <= b1) { + SetPosition (Math.Max (pos - barsize - location, 0)); + } + } + } + + return true; + } + + internal bool CanScroll (int n, out int max, bool isVertical = false) + { + var size = isVertical ? + (KeepContentAlwaysInViewport ? Host.Bounds.Height + (showBothScrollIndicator ? -2 : -1) : 0) : + (KeepContentAlwaysInViewport ? Host.Bounds.Width + (showBothScrollIndicator ? -2 : -1) : 0); + var cSize = -Size; + var cOffSet = contentOffset; + var newSize = Math.Max (cSize, cOffSet - n); + max = cSize < newSize - size ? n : -cSize + (cOffSet - size) - 1; + if (cSize < newSize - size) { + return true; + } + return false; + } + } +} diff --git a/Terminal.Gui/Views/ScrollView.cs b/Terminal.Gui/Views/ScrollView.cs index 6e441b16c..0a5b139bc 100644 --- a/Terminal.Gui/Views/ScrollView.cs +++ b/Terminal.Gui/Views/ScrollView.cs @@ -1,5 +1,5 @@ // -// ScrollView.cs: ScrollView and ScrollBarView views. +// ScrollView.cs: ScrollView view. // // Authors: // Miguel de Icaza (miguel@gnome.org) @@ -15,345 +15,6 @@ using System; using System.Reflection; namespace Terminal.Gui { - /// - /// ScrollBarViews are views that display a 1-character scrollbar, either horizontal or vertical - /// - /// - /// - /// The scrollbar is drawn to be a representation of the Size, assuming that the - /// scroll position is set at Position. - /// - /// - /// If the region to display the scrollbar is larger than three characters, - /// arrow indicators are drawn. - /// - /// - public class ScrollBarView : View { - bool vertical = false; - int size = 0, position = 0; - - /// - /// If set to true this is a vertical scrollbar, otherwise, the scrollbar is horizontal. - /// - public bool IsVertical { - get => vertical; - set { - vertical = value; - SetNeedsDisplay (); - } - } - - /// - /// The size of content the scrollbar represents. - /// - /// The size. - /// The is typically the size of the virtual content. E.g. when a Scrollbar is - /// part of a the Size is set to the appropriate dimension of . - public int Size { - get => size; - set { - size = value; - SetNeedsDisplay (); - } - } - - /// - /// This event is raised when the position on the scrollbar has changed. - /// - public event Action ChangedPosition; - - /// - /// The position, relative to , to set the scrollbar at. - /// - /// The position. - public int Position { - get => position; - set { - position = value; - SetNeedsDisplay (); - } - } - - /// - /// Get or sets the view that host this - /// - public ScrollView Host { get; internal set; } - - void SetPosition (int newPos) - { - Position = newPos; - ChangedPosition?.Invoke (); - } - - /// - /// Initializes a new instance of the class using layout. - /// - /// Frame for the scrollbar. - public ScrollBarView (Rect rect) : this (rect, 0, 0, false) { } - - /// - /// Initializes a new instance of the class using layout. - /// - /// Frame for the scrollbar. - /// The size that this scrollbar represents. Sets the property. - /// The position within this scrollbar. Sets the property. - /// If set to true this is a vertical scrollbar, otherwise, the scrollbar is horizontal. Sets the property. - public ScrollBarView (Rect rect, int size, int position, bool isVertical) : base (rect) - { - Init (size, position, isVertical); - } - - /// - /// Initializes a new instance of the class using layout. - /// - public ScrollBarView () : this (0, 0, false) { } - - /// - /// Initializes a new instance of the class using layout. - /// - /// The size that this scrollbar represents. - /// The position within this scrollbar. - /// If set to true this is a vertical scrollbar, otherwise, the scrollbar is horizontal. - public ScrollBarView (int size, int position, bool isVertical) : base () - { - Init (size, position, isVertical); - } - - void Init (int size, int position, bool isVertical) - { - vertical = isVertical; - this.position = position; - this.size = size; - WantContinuousButtonPressed = true; - } - - int posTopTee; - int posLeftTee; - int posBottomTee; - int posRightTee; - - /// - public override void Redraw (Rect region) - { - if (ColorScheme == null || Size == 0) - return; - - Driver.SetAttribute (ColorScheme.Normal); - - if (Bounds.Height == 0) { - return; - } - - if (vertical) { - if (region.Right < Bounds.Width - 1) - return; - - var col = Bounds.Width - 1; - var bh = Bounds.Height; - Rune special; - - if (bh < 4) { - var by1 = position * bh / Size; - var by2 = (position + bh) * bh / Size; - - Move (col, 0); - if (Bounds.Height == 1) { - Driver.AddRune (Driver.Diamond); - } else { - Driver.AddRune (Driver.UpArrow); - } - if (Bounds.Height == 3) { - Move (col, 1); - Driver.AddRune (Driver.Diamond); - } - if (Bounds.Height > 1) { - Move (col, Bounds.Height - 1); - Driver.AddRune (Driver.DownArrow); - } - } else { - bh -= 2; - var by1 = position * bh / Size; - var by2 = Host.KeepContentAlwaysInViewport ? Math.Min (((position + bh) * bh / Size) + 1, bh - 1) : (position + bh) * bh / Size; - if (Host.KeepContentAlwaysInViewport && by1 == by2) { - by1 = Math.Max (by1 - 1, 0); - } - - Move (col, 0); - Driver.AddRune (Driver.UpArrow); - Move (col, Bounds.Height - 1); - Driver.AddRune (Driver.DownArrow); - - bool hasTopTee = false; - bool hasDiamond = false; - bool hasBottomTee = false; - for (int y = 0; y < bh; y++) { - Move (col, y + 1); - if ((y < by1 || y > by2) && ((position > 0 && !hasTopTee) || (hasTopTee && hasBottomTee))) { - special = Driver.Stipple; - } else { - if (y != by2 && y > 1 && by2 - by1 == 0 && by1 < bh - 1 && hasTopTee && !hasDiamond) { - hasDiamond = true; - special = Driver.Diamond; - } else { - if (y == by1 && !hasTopTee) { - hasTopTee = true; - posTopTee = y; - special = Driver.TopTee; - } else if ((position == 0 && y == bh - 1 || y >= by2 || by2 == 0) && !hasBottomTee) { - hasBottomTee = true; - posBottomTee = y; - special = Driver.BottomTee; - } else { - special = Driver.VLine; - } - } - } - Driver.AddRune (special); - } - if (!hasTopTee) { - Move (col, Bounds.Height - 2); - Driver.AddRune (Driver.TopTee); - } - } - } else { - if (region.Bottom < Bounds.Height - 1) - return; - - var row = Bounds.Height - 1; - var bw = Bounds.Width; - Rune special; - - if (bw < 4) { - var bx1 = position * bw / Size; - var bx2 = (position + bw) * bw / Size; - - Move (0, row); - Driver.AddRune (Driver.LeftArrow); - Driver.AddRune (Driver.RightArrow); - } else { - bw -= 2; - var bx1 = position * bw / Size; - var bx2 = Host.KeepContentAlwaysInViewport ? Math.Min (((position + bw) * bw / Size) + 1, bw - 1) : (position + bw) * bw / Size; - if (Host.KeepContentAlwaysInViewport && bx1 == bx2) { - bx1 = Math.Max (bx1 - 1, 0); - } - - Move (0, row); - Driver.AddRune (Driver.LeftArrow); - - bool hasLeftTee = false; - bool hasDiamond = false; - bool hasRightTee = false; - for (int x = 0; x < bw; x++) { - if ((x < bx1 || x >= bx2 + 1) && ((position > 0 && !hasLeftTee) || (hasLeftTee && hasRightTee))) { - special = Driver.Stipple; - } else { - if (x != bx2 && x > 1 && bx2 - bx1 == 0 && bx1 < bw - 1 && hasLeftTee && !hasDiamond) { - hasDiamond = true; - special = Driver.Diamond; - } else { - if (x == bx1 && !hasLeftTee) { - hasLeftTee = true; - posLeftTee = x; - special = Driver.LeftTee; - } else if ((position == 0 && x == bw - 1 || x >= bx2 || bx2 == 0) && !hasRightTee) { - hasRightTee = true; - posRightTee = x; - special = Driver.RightTee; - } else { - special = Driver.HLine; - } - } - } - Driver.AddRune (special); - } - if (!hasLeftTee) { - Move (Bounds.Width -2, row); - Driver.AddRune (Driver.LeftTee); - } - - Driver.AddRune (Driver.RightArrow); - } - } - } - - int lastLocation = -1; - - /// - public override bool MouseEvent (MouseEvent me) - { - if (me.Flags != MouseFlags.Button1Pressed && me.Flags != MouseFlags.Button1Clicked && - !me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) { - return false; - } - - if (!me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) { - lastLocation = -1; - } - - int location = vertical ? me.Y : me.X; - int barsize = vertical ? Bounds.Height : Bounds.Width; - int posTopLeftTee = vertical ? posTopTee : posLeftTee; - int posBottomRightTee = vertical ? posBottomTee : posRightTee; - - barsize -= 2; - var pos = Position; - if (location == 0) { - if (pos > 0) { - SetPosition (pos - 1); - } - } else if (location == barsize + 1) { - if (Host.CanScroll (1, out _, vertical)) { - SetPosition (pos + 1); - } - } else if (location > 0 && location < barsize + 1) { - var b1 = pos * barsize / Size; - var b2 = Host.KeepContentAlwaysInViewport ? Math.Min (((pos + barsize) * barsize / Size) + 1, barsize - 1) : (pos + barsize) * barsize / Size; - if (Host.KeepContentAlwaysInViewport && b1 == b2) { - b1 = Math.Max (b1 - 1, 0); - } - - if (location > b1 && location <= b2 + 1) { - if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1Clicked) { - if (location == 1) { - SetPosition (0); - } else if (location == barsize) { - Host.CanScroll (Size - pos, out int nv, vertical); - if (nv > 0) { - SetPosition (Math.Min (pos + nv, Size)); - } - } - } else if (me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) { - var mb = (b2 - b1) / 2; - var ml = mb + b1 + (mb == 0 ? 1 : 0); - if ((location >= b1 && location <= ml) || (location < lastLocation && lastLocation > -1)) { - lastLocation = location; - var np = b1 * Size / barsize; - SetPosition (np); - } else if (location > lastLocation) { - var np = location * Size / barsize; - Host.CanScroll (np - pos, out int nv, vertical); - if (nv > 0) { - SetPosition (pos + nv); - } - } - } - } else { - if (location >= b2 + 1 && location > posTopLeftTee && location > b1 && location > posBottomRightTee && posBottomRightTee > 0) { - Host.CanScroll (location, out int nv, vertical); - if (nv > 0) { - SetPosition (Math.Min (pos + nv, Size)); - } - } else if (location <= b1) { - SetPosition (Math.Max (pos - barsize - location, 0)); - } - } - } - - return true; - } - } - /// /// Scrollviews are views that present a window into a virtual space where subviews are added. Similar to the iOS UIScrollView. /// @@ -483,6 +144,8 @@ namespace Terminal.Gui { set { if (keepContentAlwaysInViewport != value) { keepContentAlwaysInViewport = value; + vertical.OtherScrollBarView.KeepContentAlwaysInViewport = value; + horizontal.OtherScrollBarView.KeepContentAlwaysInViewport = value; Point p = default; if (value && -contentOffset.X + Bounds.Width > contentSize.Width) { p = new Point (contentSize.Width - Bounds.Width + (showVerticalScrollIndicator ? 1 : 0), -contentOffset.Y); @@ -540,17 +203,21 @@ namespace Terminal.Gui { public bool ShowHorizontalScrollIndicator { get => showHorizontalScrollIndicator; set { - if (value == showHorizontalScrollIndicator) + if (value == showHorizontalScrollIndicator) { return; + } showHorizontalScrollIndicator = value; SetNeedsLayout (); if (value) { base.Add (horizontal); + horizontal.OtherScrollBarView = vertical; + horizontal.OtherScrollBarView.ShowScrollIndicator = value; horizontal.MouseEnter += View_MouseEnter; horizontal.MouseLeave += View_MouseLeave; } else { - Remove (horizontal); + base.Remove (horizontal); + horizontal.OtherScrollBarView = null; horizontal.MouseEnter -= View_MouseEnter; horizontal.MouseLeave -= View_MouseLeave; } @@ -575,17 +242,21 @@ namespace Terminal.Gui { public bool ShowVerticalScrollIndicator { get => showVerticalScrollIndicator; set { - if (value == showVerticalScrollIndicator) + if (value == showVerticalScrollIndicator) { return; + } showVerticalScrollIndicator = value; SetNeedsLayout (); if (value) { base.Add (vertical); + vertical.OtherScrollBarView = horizontal; + vertical.OtherScrollBarView.ShowScrollIndicator = value; vertical.MouseEnter += View_MouseEnter; vertical.MouseLeave += View_MouseLeave; } else { Remove (vertical); + vertical.OtherScrollBarView = null; vertical.MouseEnter -= View_MouseEnter; vertical.MouseLeave -= View_MouseLeave; } @@ -739,7 +410,7 @@ namespace Terminal.Gui { /// Number of lines to scroll. public bool ScrollDown (int lines) { - if (CanScroll (lines, out _, true)) { + if (vertical.CanScroll (lines, out _, true)) { ContentOffset = new Point (contentOffset.X, contentOffset.Y - lines); return true; } @@ -753,28 +424,13 @@ namespace Terminal.Gui { /// Number of columns to scroll by. public bool ScrollRight (int cols) { - if (CanScroll (cols, out _)) { + if (horizontal.CanScroll (cols, out _)) { ContentOffset = new Point (contentOffset.X - cols, contentOffset.Y); return true; } return false; } - internal bool CanScroll (int n, out int max, bool isVertical = false) - { - var size = isVertical ? - (KeepContentAlwaysInViewport ? Bounds.Height + (showHorizontalScrollIndicator ? -2 : -1) : 0) : - (KeepContentAlwaysInViewport ? Bounds.Width + (showVerticalScrollIndicator ? -2 : -1) : 0); - var cSize = isVertical ? -contentSize.Height : -contentSize.Width; - var cOffSet = isVertical ? contentOffset.Y : contentOffset.X; - var newSize = Math.Max (cSize, cOffSet - n); - max = cSize < newSize - size ? n : -cSize + (cOffSet - size) - 1; - if (cSize < newSize - size) { - return true; - } - return false; - } - /// public override bool ProcessKey (KeyEvent kb) { diff --git a/Terminal.Gui/Views/TextView.cs b/Terminal.Gui/Views/TextView.cs index 3284843f4..50e9dc36f 100644 --- a/Terminal.Gui/Views/TextView.cs +++ b/Terminal.Gui/Views/TextView.cs @@ -460,6 +460,26 @@ namespace Terminal.Gui { } } + /// + /// Gets or sets the top row. + /// + public int TopRow { get => topRow; set => topRow = Math.Max (Math.Min (value, Lines - 1), 0); } + + /// + /// Gets or sets the left column. + /// + public int LeftColumn { get => leftColumn; set => leftColumn = Math.Max (Math.Min (value, Maxlength - 1), 0); } + + /// + /// Gets the maximum visible length line. + /// + public int Maxlength => model.GetMaxVisibleLine (topRow, topRow + Frame.Height); + + /// + /// Gets the number of lines. + /// + public int Lines => model.Count; + /// /// Loads the contents of the file into the . /// @@ -875,10 +895,10 @@ namespace Terminal.Gui { idx = 0; } if (isRow) { - topRow = idx > model.Count - 1 ? model.Count - 1 : idx; + topRow = Math.Max (idx > model.Count - 1 ? model.Count - 1 : idx, 0); } else { var maxlength = model.GetMaxVisibleLine (topRow, topRow + Frame.Height); - leftColumn = idx > maxlength - 1 ? maxlength - 1 : idx; + leftColumn = Math.Max (idx > maxlength - 1 ? maxlength - 1 : idx, 0); } SetNeedsDisplay (); } @@ -1374,7 +1394,7 @@ namespace Terminal.Gui { if (ev.Flags == MouseFlags.Button1Clicked) { if (model.Count > 0) { - var maxCursorPositionableLine = (model.Count - 1) - topRow; + var maxCursorPositionableLine = Math.Max ((model.Count - 1) - topRow, 0); if (ev.Y > maxCursorPositionableLine) { currentRow = maxCursorPositionableLine; } else { diff --git a/UICatalog/Scenarios/Editor.cs b/UICatalog/Scenarios/Editor.cs index 8c273f1b0..c76695d4d 100644 --- a/UICatalog/Scenarios/Editor.cs +++ b/UICatalog/Scenarios/Editor.cs @@ -13,6 +13,7 @@ namespace UICatalog { private string _fileName = "demo.txt"; private TextView _textView; private bool _saved = true; + private ScrollBarView _vertical; public override void Init (Toplevel top, ColorScheme colorScheme) { @@ -35,6 +36,7 @@ namespace UICatalog { new MenuItem ("C_ut", "", () => Cut()), new MenuItem ("_Paste", "", () => Paste()) }), + new MenuBarItem ("_ScrollBarView", CreateKeepChecked ()) }); Top.Add (menu); @@ -67,6 +69,41 @@ namespace UICatalog { LoadFile (); Win.Add (_textView); + + _vertical = new ScrollBarView (_textView, true); + var horizontal = new ScrollBarView (_textView, false); + _vertical.OtherScrollBarView = horizontal; + horizontal.OtherScrollBarView = _vertical; + + _vertical.ChangedPosition += () => { + _textView.TopRow = _vertical.Position; + if (_textView.TopRow != _vertical.Position) { + _vertical.Position = _textView.TopRow; + } + _textView.SetNeedsDisplay (); + }; + + horizontal.ChangedPosition += () => { + _textView.LeftColumn = horizontal.Position; + if (_textView.LeftColumn != horizontal.Position) { + horizontal.Position = _textView.LeftColumn; + } + _textView.SetNeedsDisplay (); + }; + + _textView.DrawContent += (e) => { + _vertical.Size = _textView.Lines - 1; + _vertical.ContentOffset = _textView.TopRow; + horizontal.Size = _textView.Maxlength - 1; + horizontal.ContentOffset = _textView.LeftColumn; + _vertical.ColorScheme = horizontal.ColorScheme = _textView.ColorScheme; + if (_vertical.ShowScrollIndicator) { + _vertical.Redraw (e); + } + if (horizontal.ShowScrollIndicator) { + horizontal.Redraw (e); + } + }; } public override void Setup () @@ -145,11 +182,25 @@ namespace UICatalog { sb.Append ("Hello world.\n"); sb.Append ("This is a test of the Emergency Broadcast System.\n"); + for (int i = 0; i < 40; i++) { + sb.Append ("This is a test with a very long line and many lines to test the ScrollViewBar against the TextView.\n"); + } var sw = System.IO.File.CreateText (fileName); sw.Write (sb.ToString ()); sw.Close (); } + private MenuItem [] CreateKeepChecked () + { + var item = new MenuItem (); + item.Title = "Keep Content Always In Viewport"; + item.CheckType |= MenuItemCheckStyle.Checked; + item.Checked = true; + item.Action += () => _vertical.KeepContentAlwaysInViewport = item.Checked = !item.Checked; + + return new MenuItem [] { item }; + } + public override void Run () { base.Run (); diff --git a/UICatalog/Scenarios/ListViewWithSelection.cs b/UICatalog/Scenarios/ListViewWithSelection.cs index 5a0e03daa..7ad3f9579 100644 --- a/UICatalog/Scenarios/ListViewWithSelection.cs +++ b/UICatalog/Scenarios/ListViewWithSelection.cs @@ -6,7 +6,7 @@ using System.Linq; using Terminal.Gui; namespace UICatalog { - [ScenarioMetadata (Name: "List View With Selection", Description: "ListView with colunns and selection")] + [ScenarioMetadata (Name: "List View With Selection", Description: "ListView with columns and selection")] [ScenarioCategory ("Controls")] class ListViewWithSelection : Scenario { @@ -55,9 +55,34 @@ namespace UICatalog { }; Win.Add (_listView); - + var vertical = new ScrollBarView (_listView, true); + + vertical.ChangedPosition += () => { + _listView.TopItem = vertical.Position; + if (_listView.TopItem != vertical.Position) { + vertical.Position = _listView.TopItem; + } + _listView.SetNeedsDisplay (); + }; + + _listView.DrawContent += (e) => { + vertical.Size = _listView.Source.Count; + vertical.ContentOffset = _listView.TopItem; + vertical.ColorScheme = _listView.ColorScheme; + if (vertical.ShowScrollIndicator) { + vertical.Redraw (e); + } + }; + _listView.SetSource (_scenarios); + var k = "Keep Content Always In Viewport"; + var keepCheckBox = new CheckBox (k, vertical.AutoHideScrollBars) { + X = Pos.AnchorEnd (k.Length + 3), + Y = 0, + }; + keepCheckBox.Toggled += (_) => vertical.KeepContentAlwaysInViewport = keepCheckBox.Checked; + Win.Add (keepCheckBox); } private void _customRenderCB_Toggled (bool prev) @@ -84,7 +109,7 @@ namespace UICatalog { Win.SetNeedsDisplay (); } - // This is basicaly the same implementation used by the UICatalog main window + // This is basically the same implementation used by the UICatalog main window internal class ScenarioListDataSource : IListDataSource { int _nameColumnWidth = 30; private List scenarios;