namespace Terminal.Gui; /// Slider control. public class Slider : Slider { /// Initializes a new instance of the class. public Slider () { } /// Initializes a new instance of the class. /// Initial slider options. /// Initial slider options. public Slider (List options, Orientation orientation = Orientation.Horizontal) : base (options, orientation) { } } /// /// Provides a slider control letting the user navigate from a set of typed options in a linear manner using the /// keyboard or mouse. /// /// public class Slider : View { private readonly SliderConfiguration _config = new (); // List of the current set options. private readonly List _setOptions = new (); // Options private List> _options; #region Initialize private void SetInitialProperties ( List> options, Orientation orientation = Orientation.Horizontal ) { Width = Dim.Auto (DimAutoStyle.Content); Height = Dim.Auto (DimAutoStyle.Content); CanFocus = true; CursorVisibility = CursorVisibility.Default; _options = options ?? new List> (); _config._sliderOrientation = orientation; SetDefaultStyle (); SetCommands (); SetContentSize (); // BUGBUG: This should not be needed - Need to ensure SetRelativeLayout gets called during EndInit Initialized += (s, e) => { SetContentSize (); }; LayoutStarted += (s, e) => { SetContentSize (); }; } // TODO: Make configurable via ConfigurationManager private void SetDefaultStyle () { _config._showLegends = true; switch (_config._sliderOrientation) { case Orientation.Horizontal: Style.SpaceChar = new () { Rune = Glyphs.HLine }; // '─' Style.OptionChar = new () { Rune = Glyphs.BlackCircle }; // '┼●🗹□⏹' break; case Orientation.Vertical: Style.SpaceChar = new () { Rune = Glyphs.VLine }; Style.OptionChar = new () { Rune = Glyphs.BlackCircle }; break; } // TODO(jmperricone) Wide Vertical ??? /* │ │ ┼─ 40 │ │ ███ 30 ▒▒▒ ▒▒▒ ▒▒▒ 20 ▒▒▒ ▒▒▒ ███ 10 │ │ ─●─ 0 */ _config._legendsOrientation = _config._sliderOrientation; Style.EmptyChar = new () { Rune = new (' ') }; Style.SetChar = new () { Rune = Glyphs.ContinuousMeterSegment }; // ■ Style.RangeChar = new () { Rune = Glyphs.Stipple }; // ░ ▒ ▓ // Medium shade not blinking on curses. Style.StartRangeChar = new () { Rune = Glyphs.ContinuousMeterSegment }; Style.EndRangeChar = new () { Rune = Glyphs.ContinuousMeterSegment }; Style.DragChar = new () { Rune = Glyphs.Diamond }; // TODO: Support left & right (top/bottom) // First = '├', // Last = '┤', } #endregion #region Constructors /// Initializes a new instance of the class. public Slider () : this (new ()) { } /// Initializes a new instance of the class. /// Initial slider options. /// Initial slider orientation. public Slider (List options, Orientation orientation = Orientation.Horizontal) { if (options is null) { SetInitialProperties (null, orientation); } else { SetInitialProperties ( options.Select ( e => { var legend = e.ToString (); return new SliderOption { Data = e, Legend = legend, LegendAbbr = (Rune)(legend?.Length > 0 ? legend [0] : ' ') }; } ) .ToList (), orientation ); } } #endregion #region Properties /// /// Setting the Text of a slider is a shortcut to setting options. The text is a CSV string of the options. /// public override string Text { get { if (_options.Count == 0) { return string.Empty; } // Return labels as a CSV string return string.Join (",", _options); } set { if (string.IsNullOrEmpty (value)) { Options = []; } else { IEnumerable list = value.Split (',').Select (x => x.Trim ()); Options = list.Select (x => new SliderOption { Legend = x }).ToList (); } } } /// Allow no selection. public bool AllowEmpty { get => _config._allowEmpty; set { _config._allowEmpty = value; if (!value && _options.Count > 0 && _setOptions.Count == 0) { SetOption (0); } } } /// Gets or sets the minimum number of rows/columns between . The default is 1. public int MinimumInnerSpacing { get => _config._minInnerSpacing; set { _config._minInnerSpacing = value; SetContentSize (); } } /// Slider Type. public SliderType Type { get => _config._type; set { _config._type = value; // Todo: Custom logic to preserve options. _setOptions.Clear (); SetNeedsDisplay (); } } /// Slider Orientation. public Orientation Orientation { get => _config._sliderOrientation; set => OnOrientationChanged (value); } /// Legends Orientation. public Orientation LegendsOrientation { get => _config._legendsOrientation; set { _config._legendsOrientation = value; SetContentSize (); } } /// Slider styles. public SliderStyle Style { get; set; } = new (); /// Set the slider options. public List> Options { get => _options; set { // _options should never be null _options = value ?? throw new ArgumentNullException (nameof (value)); if (_options.Count == 0) { return; } SetContentSize (); } } /// Allow range start and end be in the same option, as a single option. public bool RangeAllowSingle { get => _config._rangeAllowSingle; set => _config._rangeAllowSingle = value; } /// Show/Hide spacing before and after the first and last option. public bool ShowEndSpacing { get => _config._showEndSpacing; set { _config._showEndSpacing = value; SetContentSize (); } } /// Show/Hide the options legends. public bool ShowLegends { get => _config._showLegends; set { _config._showLegends = value; SetContentSize (); } } /// /// Gets or sets whether the minimum or ideal size will be used when calculating the size of the slider. /// public bool UseMinimumSize { get => _config._useMinimumSize; set { _config._useMinimumSize = value; SetContentSize (); } } #endregion #region Events /// /// Fired when the slider orientation has changed. Can be cancelled by setting /// to true. /// public event EventHandler OrientationChanged; /// Called when the slider orientation has changed. Invokes the event. /// /// True of the event was cancelled. public virtual bool OnOrientationChanged (Orientation newOrientation) { var args = new OrientationEventArgs (newOrientation); OrientationChanged?.Invoke (this, args); if (!args.Cancel) { _config._sliderOrientation = newOrientation; switch (_config._sliderOrientation) { case Orientation.Horizontal: Style.SpaceChar = new () { Rune = Glyphs.HLine }; // '─' break; case Orientation.Vertical: Style.SpaceChar = new () { Rune = Glyphs.VLine }; break; } SetKeyBindings (); SetContentSize (); } return args.Cancel; } /// Event raised when the slider option/s changed. The dictionary contains: key = option index, value = T public event EventHandler> OptionsChanged; /// Overridable method called when the slider options have changed. Raises the event. public virtual void OnOptionsChanged () { OptionsChanged?.Invoke (this, new (GetSetOptionDictionary ())); SetNeedsDisplay (); } /// Event raised When the option is hovered with the keys or the mouse. public event EventHandler> OptionFocused; private int _lastFocusedOption; // for Range type; the most recently focused option. Used to determine shrink direction /// Overridable function that fires the event. /// /// if the focus change was cancelled. /// public virtual bool OnOptionFocused (int newFocusedOption, SliderEventArgs args) { if (newFocusedOption > _options.Count - 1 || newFocusedOption < 0) { return true; } OptionFocused?.Invoke (this, args); if (!args.Cancel) { _lastFocusedOption = FocusedOption; FocusedOption = newFocusedOption; //PositionCursor (); } return args.Cancel; } #endregion Events #region Public Methods /// The focused option (has the cursor). public int FocusedOption { get; set; } /// Causes the specified option to be set and be focused. public bool SetOption (int optionIndex) { // TODO: Handle range type. // Note: Maybe return false only when optionIndex doesn't exist, otherwise true. if (!_setOptions.Contains (optionIndex) && optionIndex >= 0 && optionIndex < _options.Count) { FocusedOption = optionIndex; SetFocusedOption (); return true; } return false; } /// Causes the specified option to be un-set and be focused. public bool UnSetOption (int optionIndex) { if (!AllowEmpty && _setOptions.Count > 2 && _setOptions.Contains (optionIndex)) { FocusedOption = optionIndex; SetFocusedOption (); return true; } return false; } /// Get the indexes of the set options. public List GetSetOptions () { // Copy return _setOptions.OrderBy (e => e).ToList (); } #endregion Public Methods #region Helpers private void MoveAndAdd (int x, int y, Rune rune) { Move (x, y); Driver?.AddRune (rune); } private void MoveAndAdd (int x, int y, string str) { Move (x, y); Driver?.AddStr (str); } /// Sets the dimensions of the Slider to the ideal values. private void SetContentSize () { if (_options.Count == 0) { return; } bool horizontal = _config._sliderOrientation == Orientation.Horizontal; if (UseMinimumSize) { CalcSpacingConfig (CalcMinLength ()); } else { CalcSpacingConfig (horizontal ? Viewport.Width : Viewport.Height); } SetContentSize (new (GetIdealWidth (), GetIdealHeight ())); return; void CalcSpacingConfig (int size) { _config._cachedInnerSpacing = 0; _config._startSpacing = 0; _config._endSpacing = 0; int maxLegend; // Because the legends are centered, the longest one determines inner spacing if (_config._sliderOrientation == _config._legendsOrientation) { maxLegend = int.Max (_options.Max (s => s.Legend?.GetColumns () ?? 1), 1); } else { maxLegend = 1; } int minSizeThatFitsLegends = _options.Count == 1 ? maxLegend : _options.Sum (o => o.Legend.GetColumns ()); string first; string last; if (minSizeThatFitsLegends > size) { _config._showLegendsAbbr = false; if (_config._sliderOrientation == _config._legendsOrientation) { _config._showLegendsAbbr = true; foreach (SliderOption o in _options.Where (op => op.LegendAbbr == default (Rune))) { o.LegendAbbr = (Rune)(o.Legend?.GetColumns () > 0 ? o.Legend [0] : ' '); } } first = "x"; last = "x"; } else { _config._showLegendsAbbr = false; first = _options.First ().Legend; last = _options.Last ().Legend; } // --o-- // Hello // Left = He // Right = lo int firstLeft = (first.Length - 1) / 2; // Chars count of the first option to the left. int lastRight = last.Length / 2; // Chars count of the last option to the right. if (_config._sliderOrientation != _config._legendsOrientation) { firstLeft = 0; lastRight = 0; } // -1 because it's better to have an extra space at right than to clip int width = size - firstLeft - lastRight - 1; _config._startSpacing = firstLeft; if (_options.Count == 1) { _config._cachedInnerSpacing = maxLegend; } else { _config._cachedInnerSpacing = Math.Max (0, (int)Math.Floor ((double)width / (_options.Count - 1)) - 1); } _config._cachedInnerSpacing = Math.Max (_config._minInnerSpacing, _config._cachedInnerSpacing); _config._endSpacing = lastRight; } } /// Calculates the min dimension required for all options and inner spacing with abbreviated legends /// private int CalcMinLength () { if (_options.Count == 0) { return 0; } var length = 0; length += _config._startSpacing + _config._endSpacing; length += _options.Count; length += (_options.Count - 1) * _config._minInnerSpacing; return length; } /// /// Gets the ideal width of the slider. The ideal width is the minimum width required to display all options and inner /// spacing. /// /// public int GetIdealWidth () { if (UseMinimumSize) { return Orientation == Orientation.Horizontal ? CalcMinLength () : CalcIdealThickness (); } return Orientation == Orientation.Horizontal ? CalcIdealLength () : CalcIdealThickness (); } /// /// Gets the ideal height of the slider. The ideal height is the minimum height required to display all options and /// inner spacing. /// /// public int GetIdealHeight () { if (UseMinimumSize) { return Orientation == Orientation.Horizontal ? CalcIdealThickness () : CalcMinLength (); } return Orientation == Orientation.Horizontal ? CalcIdealThickness () : CalcIdealLength (); } /// /// Calculates the ideal dimension required for all options, inner spacing, and legends (non-abbreviated, with one /// space between). /// /// private int CalcIdealLength () { if (_options.Count == 0) { return 0; } bool isVertical = Orientation == Orientation.Vertical; var length = 0; if (_config._showLegends) { if (_config._legendsOrientation == _config._sliderOrientation && _options.Count > 0) { // Each legend should be centered in a space the width of the longest legend, with one space between. // Calculate the total length required for all legends. //if (!isVertical) { int maxLegend = int.Max (_options.Max (s => s.Legend?.GetColumns () ?? 1), 1); length = maxLegend * _options.Count + (_options.Count - 1); } // //{ // length = CalcMinLength (); //} } else { length = CalcMinLength (); } } return Math.Max (length, CalcMinLength ()); } /// /// Calculates the minimum dimension required for the slider and legends. /// /// private int CalcIdealThickness () { var thickness = 1; // Always show the slider. if (_config._showLegends) { if (_config._legendsOrientation != _config._sliderOrientation && _options.Count > 0) { thickness += _options.Max (s => s.Legend?.GetColumns () ?? 0); } else { thickness += 1; } } return thickness; } #endregion Helpers #region Cursor and Position internal bool TryGetPositionByOption (int option, out (int x, int y) position) { position = (-1, -1); if (option < 0 || option >= _options.Count ()) { return false; } var offset = 0; offset += _config._startSpacing; offset += option * (_config._cachedInnerSpacing + 1); if (_config._sliderOrientation == Orientation.Vertical) { position = (0, offset); } else { position = (offset, 0); } return true; } /// Tries to get the option index by the position. /// /// /// /// /// internal bool TryGetOptionByPosition (int x, int y, int threshold, out int optionIdx) { // Fix(jmperricone): Not working. optionIdx = -1; if (Orientation == Orientation.Horizontal) { if (y != 0) { return false; } for (int xx = x - threshold; xx < x + threshold + 1; xx++) { int cx = xx; cx -= _config._startSpacing; int option = cx / (_config._cachedInnerSpacing + 1); bool valid = cx % (_config._cachedInnerSpacing + 1) == 0; if (!valid || option < 0 || option > _options.Count - 1) { continue; } optionIdx = option; return true; } } else { if (x != 0) { return false; } for (int yy = y - threshold; yy < y + threshold + 1; yy++) { int cy = yy; cy -= _config._startSpacing; int option = cy / (_config._cachedInnerSpacing + 1); bool valid = cy % (_config._cachedInnerSpacing + 1) == 0; if (!valid || option < 0 || option > _options.Count - 1) { continue; } optionIdx = option; return true; } } return false; } /// public override Point? PositionCursor () { if (TryGetPositionByOption (FocusedOption, out (int x, int y) position)) { if (IsInitialized && Viewport.Contains (position.x, position.y)) { Move (position.x, position.y); return new (position.x, position.y); } } return base.PositionCursor (); } #endregion Cursor and Position #region Drawing /// public override void OnDrawContent (Rectangle viewport) { // TODO: make this more surgical to reduce repaint if (_options is null || _options.Count == 0) { return; } // Draw Slider DrawSlider (); // Draw Legends. if (_config._showLegends) { DrawLegends (); } if (_dragPosition.HasValue && _moveRenderPosition.HasValue) { AddRune (_moveRenderPosition.Value.X, _moveRenderPosition.Value.Y, Style.DragChar.Rune); } } private string AlignText (string text, int width, Alignment alignment) { if (text is null) { return ""; } if (text.Length > width) { text = text [..width]; } int w = width - text.Length; string s1 = new (' ', w / 2); string s2 = new (' ', w % 2); // Note: The formatter doesn't handle all of this ??? switch (alignment) { case Alignment.Fill: return TextFormatter.Justify (text, width); case Alignment.Start: return text + s1 + s1 + s2; case Alignment.Center: if (text.Length % 2 != 0) { return s1 + text + s1 + s2; } return s1 + s2 + text + s1; case Alignment.End: return s1 + s1 + s2 + text; default: return text; } } private void DrawSlider () { // TODO: be more surgical on clear Clear (); // Attributes var normalAttr = new Attribute (Color.White, Color.Black); var setAttr = new Attribute (Color.Black, Color.White); if (IsInitialized) { normalAttr = ColorScheme?.Normal ?? Application.Current.ColorScheme.Normal; setAttr = Style.SetChar.Attribute ?? ColorScheme!.HotNormal; } bool isVertical = _config._sliderOrientation == Orientation.Vertical; bool isLegendsVertical = _config._legendsOrientation == Orientation.Vertical; bool isReverse = _config._sliderOrientation != _config._legendsOrientation; var x = 0; var y = 0; bool isSet = _setOptions.Count > 0; // Left Spacing if (_config._showEndSpacing && _config._startSpacing > 0) { Driver?.SetAttribute ( isSet && _config._type == SliderType.LeftRange ? Style.RangeChar.Attribute ?? normalAttr : Style.SpaceChar.Attribute ?? normalAttr ); Rune rune = isSet && _config._type == SliderType.LeftRange ? Style.RangeChar.Rune : Style.SpaceChar.Rune; for (var i = 0; i < _config._startSpacing; i++) { MoveAndAdd (x, y, rune); if (isVertical) { y++; } else { x++; } } } else { Driver?.SetAttribute (Style.EmptyChar.Attribute ?? normalAttr); for (var i = 0; i < _config._startSpacing; i++) { MoveAndAdd (x, y, Style.EmptyChar.Rune); if (isVertical) { y++; } else { x++; } } } // Slider if (_options.Count > 0) { for (var i = 0; i < _options.Count; i++) { var drawRange = false; if (isSet) { switch (_config._type) { case SliderType.LeftRange when i <= _setOptions [0]: drawRange = i < _setOptions [0]; break; case SliderType.RightRange when i >= _setOptions [0]: drawRange = i >= _setOptions [0]; break; case SliderType.Range when _setOptions.Count == 1: drawRange = false; break; case SliderType.Range when _setOptions.Count == 2: if ((i >= _setOptions [0] && i <= _setOptions [1]) || (i >= _setOptions [1] && i <= _setOptions [0])) { drawRange = (i >= _setOptions [0] && i < _setOptions [1]) || (i >= _setOptions [1] && i < _setOptions [0]); } break; } } // Draw Option Driver?.SetAttribute ( isSet && _setOptions.Contains (i) ? Style.SetChar.Attribute ?? setAttr : drawRange ? Style.RangeChar.Attribute ?? setAttr : Style.OptionChar.Attribute ?? normalAttr ); Rune rune = drawRange ? Style.RangeChar.Rune : Style.OptionChar.Rune; if (isSet) { if (_setOptions [0] == i) { rune = Style.StartRangeChar.Rune; } else if (_setOptions.Count > 1 && _setOptions [1] == i) { rune = Style.EndRangeChar.Rune; } else if (_setOptions.Contains (i)) { rune = Style.SetChar.Rune; } } MoveAndAdd (x, y, rune); if (isVertical) { y++; } else { x++; } // Draw Spacing if (_config._showEndSpacing || i < _options.Count - 1) { // Skip if is the Last Spacing. Driver?.SetAttribute ( drawRange && isSet ? Style.RangeChar.Attribute ?? setAttr : Style.SpaceChar.Attribute ?? normalAttr ); for (var s = 0; s < _config._cachedInnerSpacing; s++) { MoveAndAdd (x, y, drawRange && isSet ? Style.RangeChar.Rune : Style.SpaceChar.Rune); if (isVertical) { y++; } else { x++; } } } } } int remaining = isVertical ? Viewport.Height - y : Viewport.Width - x; // Right Spacing if (_config._showEndSpacing) { Driver?.SetAttribute ( isSet && _config._type == SliderType.RightRange ? Style.RangeChar.Attribute ?? normalAttr : Style.SpaceChar.Attribute ?? normalAttr ); Rune rune = isSet && _config._type == SliderType.RightRange ? Style.RangeChar.Rune : Style.SpaceChar.Rune; for (var i = 0; i < remaining; i++) { MoveAndAdd (x, y, rune); if (isVertical) { y++; } else { x++; } } } else { Driver?.SetAttribute (Style.EmptyChar.Attribute ?? normalAttr); for (var i = 0; i < remaining; i++) { MoveAndAdd (x, y, Style.EmptyChar.Rune); if (isVertical) { y++; } else { x++; } } } } private void DrawLegends () { // Attributes var normalAttr = new Attribute (Color.White, Color.Black); var setAttr = new Attribute (Color.Black, Color.White); Attribute spaceAttr = normalAttr; if (IsInitialized) { normalAttr = Style.LegendAttributes.NormalAttribute ?? ColorScheme?.Normal ?? ColorScheme.Disabled; setAttr = Style.LegendAttributes.SetAttribute ?? ColorScheme?.HotNormal ?? ColorScheme.Normal; spaceAttr = Style.LegendAttributes.EmptyAttribute ?? normalAttr; } bool isTextVertical = _config._legendsOrientation == Orientation.Vertical; bool isSet = _setOptions.Count > 0; var x = 0; var y = 0; Move (x, y); if (_config._sliderOrientation == Orientation.Horizontal && _config._legendsOrientation == Orientation.Vertical) { x += _config._startSpacing; } if (_config._sliderOrientation == Orientation.Vertical && _config._legendsOrientation == Orientation.Horizontal) { y += _config._startSpacing; } if (_config._sliderOrientation == Orientation.Horizontal) { y += 1; } else { // Vertical x += 1; } for (var i = 0; i < _options.Count; i++) { var isOptionSet = false; // Check if the Option is Set. switch (_config._type) { case SliderType.Single: case SliderType.Multiple: if (isSet && _setOptions.Contains (i)) { isOptionSet = true; } break; case SliderType.LeftRange: if (isSet && i <= _setOptions [0]) { isOptionSet = true; } break; case SliderType.RightRange: if (isSet && i >= _setOptions [0]) { isOptionSet = true; } break; case SliderType.Range when _setOptions.Count == 1: if (isSet && i == _setOptions [0]) { isOptionSet = true; } break; case SliderType.Range: if (isSet && ((i >= _setOptions [0] && i <= _setOptions [1]) || (i >= _setOptions [1] && i <= _setOptions [0]))) { isOptionSet = true; } break; } // Text || Abbreviation var text = string.Empty; if (_config._showLegendsAbbr) { text = _options [i].LegendAbbr.ToString () ?? new Rune (_options [i].Legend.First ()).ToString (); } else { text = _options [i].Legend; } switch (_config._sliderOrientation) { case Orientation.Horizontal: switch (_config._legendsOrientation) { case Orientation.Horizontal: text = AlignText (text, _config._cachedInnerSpacing + 1, Alignment.Center); break; case Orientation.Vertical: y = 1; break; } break; case Orientation.Vertical: switch (_config._legendsOrientation) { case Orientation.Horizontal: x = 1; break; case Orientation.Vertical: text = AlignText (text, _config._cachedInnerSpacing + 1, Alignment.Center); break; } break; } // Text int legendLeftSpacesCount = text.TakeWhile (e => e == ' ').Count (); int legendRightSpacesCount = text.Reverse ().TakeWhile (e => e == ' ').Count (); text = text.Trim (); // TODO(jmperricone): Improve the Orientation check. // Calculate Start Spacing if (_config._sliderOrientation == _config._legendsOrientation) { if (i == 0) { // The spacing for the slider use the StartSpacing but... // The spacing for the legends is the StartSpacing MINUS the total chars to the left of the first options. // ●────●────● // Hello Bye World // // chars_left is 2 for Hello => (5 - 1) / 2 // // then the spacing is 2 for the slider but 0 for the legends. int charsLeft = (text.Length - 1) / 2; legendLeftSpacesCount = _config._startSpacing - charsLeft; } // Option Left Spacing if (isTextVertical) { y += legendLeftSpacesCount; } else { x += legendLeftSpacesCount; } //Move (x, y); } // Legend Driver?.SetAttribute (isOptionSet ? setAttr : normalAttr); foreach (Rune c in text.EnumerateRunes ()) { MoveAndAdd (x, y, c); //Driver.AddRune (c); if (isTextVertical) { y += 1; } else { x += 1; } } // Calculate End Spacing if (i == _options.Count () - 1) { // See Start Spacing explanation. int charsRight = text.Length / 2; legendRightSpacesCount = _config._endSpacing - charsRight; } // Option Right Spacing of Option Driver?.SetAttribute (spaceAttr); if (isTextVertical) { y += legendRightSpacesCount; } else { x += legendRightSpacesCount; } if (_config._sliderOrientation == Orientation.Horizontal && _config._legendsOrientation == Orientation.Vertical) { x += _config._cachedInnerSpacing + 1; } else if (_config._sliderOrientation == Orientation.Vertical && _config._legendsOrientation == Orientation.Horizontal) { y += _config._cachedInnerSpacing + 1; } } } #endregion Drawing #region Keys and Mouse // Mouse coordinates of current drag private Point? _dragPosition; // Coordinates of where the "move cursor" is drawn (in OnDrawContent) private Point? _moveRenderPosition; /// protected internal override bool OnMouseEvent (MouseEvent mouseEvent) { // Note(jmperricone): Maybe we click to focus the cursor, and on next click we set the option. // That will makes OptionFocused Event more relevant. // (tig: I don't think so. Maybe an option if someone really wants it, but for now that // adss to much friction to UI. // TODO(jmperricone): Make Range Type work with mouse. if (!(mouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked) || mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed) || mouseEvent.Flags.HasFlag (MouseFlags.ReportMousePosition) || mouseEvent.Flags.HasFlag (MouseFlags.Button1Released))) { return false; } SetFocus (); if (!_dragPosition.HasValue && mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed)) { if (mouseEvent.Flags.HasFlag (MouseFlags.ReportMousePosition)) { _dragPosition = mouseEvent.Position; _moveRenderPosition = ClampMovePosition ((Point)_dragPosition); Application.GrabMouse (this); } SetNeedsDisplay (); return true; } if (_dragPosition.HasValue && mouseEvent.Flags.HasFlag (MouseFlags.ReportMousePosition) && mouseEvent.Flags.HasFlag (MouseFlags.Button1Pressed)) { // Continue Drag _dragPosition = mouseEvent.Position; _moveRenderPosition = ClampMovePosition ((Point)_dragPosition); var success = false; var option = 0; // how far has user dragged from original location? if (Orientation == Orientation.Horizontal) { success = TryGetOptionByPosition (mouseEvent.Position.X, 0, Math.Max (0, _config._cachedInnerSpacing / 2), out option); } else { success = TryGetOptionByPosition (0, mouseEvent.Position.Y, Math.Max (0, _config._cachedInnerSpacing / 2), out option); } if (!_config._allowEmpty && success) { if (!OnOptionFocused (option, new (GetSetOptionDictionary (), FocusedOption))) { SetFocusedOption (); } } SetNeedsDisplay (); return true; } if ((_dragPosition.HasValue && mouseEvent.Flags.HasFlag (MouseFlags.Button1Released)) || mouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked)) { // End Drag Application.UngrabMouse (); _dragPosition = null; _moveRenderPosition = null; // TODO: Add func to calc distance between options to use as the MouseClickXOptionThreshold var success = false; var option = 0; if (Orientation == Orientation.Horizontal) { success = TryGetOptionByPosition (mouseEvent.Position.X, 0, Math.Max (0, _config._cachedInnerSpacing / 2), out option); } else { success = TryGetOptionByPosition (0, mouseEvent.Position.Y, Math.Max (0, _config._cachedInnerSpacing / 2), out option); } if (success) { if (!OnOptionFocused (option, new (GetSetOptionDictionary (), FocusedOption))) { SetFocusedOption (); } } SetNeedsDisplay (); return true; } return false; Point ClampMovePosition (Point position) { int Clamp (int value, int min, int max) { return Math.Max (min, Math.Min (max, value)); } if (Orientation == Orientation.Horizontal) { int left = _config._startSpacing; int width = _options.Count + (_options.Count - 1) * _config._cachedInnerSpacing; int right = left + width - 1; int clampedX = Clamp (position.X, left, right); position = new (clampedX, 0); } else { int top = _config._startSpacing; int height = _options.Count + (_options.Count - 1) * _config._cachedInnerSpacing; int bottom = top + height - 1; int clampedY = Clamp (position.Y, top, bottom); position = new (0, clampedY); } return position; } } private void SetCommands () { AddCommand (Command.Right, () => MovePlus ()); AddCommand (Command.LineDown, () => MovePlus ()); AddCommand (Command.Left, () => MoveMinus ()); AddCommand (Command.LineUp, () => MoveMinus ()); AddCommand (Command.LeftHome, () => MoveStart ()); AddCommand (Command.RightEnd, () => MoveEnd ()); AddCommand (Command.RightExtend, () => ExtendPlus ()); AddCommand (Command.LeftExtend, () => ExtendMinus ()); AddCommand (Command.Accept, () => Set ()); SetKeyBindings (); } // This is called during initialization and anytime orientation changes private void SetKeyBindings () { if (_config._sliderOrientation == Orientation.Horizontal) { KeyBindings.Add (Key.CursorRight, Command.Right); KeyBindings.Remove (Key.CursorDown); KeyBindings.Add (Key.CursorLeft, Command.Left); KeyBindings.Remove (Key.CursorUp); KeyBindings.Add (Key.CursorRight.WithCtrl, Command.RightExtend); KeyBindings.Remove (Key.CursorDown.WithCtrl); KeyBindings.Add (Key.CursorLeft.WithCtrl, Command.LeftExtend); KeyBindings.Remove (Key.CursorUp.WithCtrl); } else { KeyBindings.Remove (Key.CursorRight); KeyBindings.Add (Key.CursorDown, Command.LineDown); KeyBindings.Remove (Key.CursorLeft); KeyBindings.Add (Key.CursorUp, Command.LineUp); KeyBindings.Remove (Key.CursorRight.WithCtrl); KeyBindings.Add (Key.CursorDown.WithCtrl, Command.RightExtend); KeyBindings.Remove (Key.CursorLeft.WithCtrl); KeyBindings.Add (Key.CursorUp.WithCtrl, Command.LeftExtend); } KeyBindings.Add (Key.Home, Command.LeftHome); KeyBindings.Add (Key.End, Command.RightEnd); KeyBindings.Add (Key.Enter, Command.Accept); KeyBindings.Add (Key.Space, Command.Accept); } private Dictionary> GetSetOptionDictionary () { return _setOptions.ToDictionary (e => e, e => _options [e]); } private void SetFocusedOption () { switch (_config._type) { case SliderType.Single: case SliderType.LeftRange: case SliderType.RightRange: if (_setOptions.Count == 1) { int prev = _setOptions [0]; if (!_config._allowEmpty && prev == FocusedOption) { break; } _setOptions.Clear (); _options [FocusedOption].OnUnSet (); if (FocusedOption != prev) { _setOptions.Add (FocusedOption); _options [FocusedOption].OnSet (); } } else { _setOptions.Add (FocusedOption); _options [FocusedOption].OnSet (); } // Raise slider changed event. OnOptionsChanged (); break; case SliderType.Multiple: if (_setOptions.Contains (FocusedOption)) { if (!_config._allowEmpty && _setOptions.Count () == 1) { break; } _setOptions.Remove (FocusedOption); _options [FocusedOption].OnUnSet (); } else { _setOptions.Add (FocusedOption); _options [FocusedOption].OnSet (); } OnOptionsChanged (); break; case SliderType.Range: if (_config._rangeAllowSingle) { if (_setOptions.Count == 1) { int prev = _setOptions [0]; if (!_config._allowEmpty && prev == FocusedOption) { break; } if (FocusedOption == prev) { // un-set _setOptions.Clear (); _options [FocusedOption].OnUnSet (); } else { _setOptions [0] = FocusedOption; _setOptions.Add (prev); _setOptions.Sort (); _options [FocusedOption].OnSet (); } } else if (_setOptions.Count == 0) { _setOptions.Add (FocusedOption); _options [FocusedOption].OnSet (); } else { // Extend/Shrink if (FocusedOption < _setOptions [0]) { // extend left _options [_setOptions [0]].OnUnSet (); _setOptions [0] = FocusedOption; } else if (FocusedOption > _setOptions [1]) { // extend right _options [_setOptions [1]].OnUnSet (); _setOptions [1] = FocusedOption; } else if (FocusedOption >= _setOptions [0] && FocusedOption <= _setOptions [1]) { if (FocusedOption < _lastFocusedOption) { // shrink to the left _options [_setOptions [1]].OnUnSet (); _setOptions [1] = FocusedOption; } else if (FocusedOption > _lastFocusedOption) { // shrink to the right _options [_setOptions [0]].OnUnSet (); _setOptions [0] = FocusedOption; } if (_setOptions.Count > 1 && _setOptions [0] == _setOptions [1]) { _setOptions.Clear (); _setOptions.Add (FocusedOption); } } } } else { if (_setOptions.Count == 1) { int prev = _setOptions [0]; if (!_config._allowEmpty && prev == FocusedOption) { break; } _setOptions [0] = FocusedOption; _setOptions.Add (prev); _setOptions.Sort (); _options [FocusedOption].OnSet (); } else if (_setOptions.Count == 0) { _setOptions.Add (FocusedOption); _options [FocusedOption].OnSet (); int next = FocusedOption < _options.Count - 1 ? FocusedOption + 1 : FocusedOption - 1; _setOptions.Add (next); _options [next].OnSet (); } else { // Extend/Shrink if (FocusedOption < _setOptions [0]) { // extend left _options [_setOptions [0]].OnUnSet (); _setOptions [0] = FocusedOption; } else if (FocusedOption > _setOptions [1]) { // extend right _options [_setOptions [1]].OnUnSet (); _setOptions [1] = FocusedOption; } else if (FocusedOption >= _setOptions [0] && FocusedOption <= _setOptions [1] && _setOptions [1] - _setOptions [0] > 1) { if (FocusedOption < _lastFocusedOption) { // shrink to the left _options [_setOptions [1]].OnUnSet (); _setOptions [1] = FocusedOption; } else if (FocusedOption > _lastFocusedOption) { // shrink to the right _options [_setOptions [0]].OnUnSet (); _setOptions [0] = FocusedOption; } } } } // Raise Slider Option Changed Event. OnOptionsChanged (); break; default: throw new ArgumentOutOfRangeException (_config._type.ToString ()); } } internal bool ExtendPlus () { int next = FocusedOption < _options.Count - 1 ? FocusedOption + 1 : FocusedOption; if (next != FocusedOption && !OnOptionFocused ( next, new ( GetSetOptionDictionary (), FocusedOption ) )) { SetFocusedOption (); } return true; //// TODO: Support RangeMultiple //if (_setOptions.Contains (FocusedOption)) { // var next = FocusedOption < _options.Count - 1 ? FocusedOption + 1 : FocusedOption; // if (!_setOptions.Contains (next)) { // if (_config._type == SliderType.Range) { // if (_setOptions.Count == 1) { // if (!OnOptionFocused (next, new SliderEventArgs (GetSetOptionDictionary (), FocusedOption))) { // _setOptions.Add (FocusedOption); // _setOptions.Sort (); // Range Type // OnOptionsChanged (); // } // } else if (_setOptions.Count == 2) { // if (!OnOptionFocused (next, new SliderEventArgs (GetSetOptionDictionary (), FocusedOption))) { // _setOptions [1] = FocusedOption; // _setOptions.Sort (); // Range Type // OnOptionsChanged (); // } // } // } else { // _setOptions.Remove (FocusedOption); // // Note(jmperricone): We are setting the option here, do we send the OptionFocused Event too ? // if (!OnOptionFocused (next, new SliderEventArgs (GetSetOptionDictionary (), FocusedOption))) { // _setOptions.Add (FocusedOption); // _setOptions.Sort (); // Range Type // OnOptionsChanged (); // } // } // } else { // if (_config._type == SliderType.Range) { // if (!OnOptionFocused (next, new SliderEventArgs (GetSetOptionDictionary (), FocusedOption))) { // _setOptions.Clear(); // _setOptions.Add (FocusedOption); // OnOptionsChanged (); // } // } else if (/*_settingRange == true ||*/ !AllowEmpty) { // SetFocusedOption (); // } // } //} //return true; } internal bool ExtendMinus () { int prev = FocusedOption > 0 ? FocusedOption - 1 : FocusedOption; if (prev != FocusedOption && !OnOptionFocused ( prev, new ( GetSetOptionDictionary (), FocusedOption ) )) { SetFocusedOption (); } return true; } internal bool Set () { SetFocusedOption (); return true; } internal bool MovePlus () { bool cancelled = OnOptionFocused ( FocusedOption + 1, new (GetSetOptionDictionary (), FocusedOption) ); if (cancelled) { return false; } if (!AllowEmpty) { SetFocusedOption (); } return true; } internal bool MoveMinus () { bool cancelled = OnOptionFocused ( FocusedOption - 1, new (GetSetOptionDictionary (), FocusedOption) ); if (cancelled) { return false; } if (!AllowEmpty) { SetFocusedOption (); } return true; } internal bool MoveStart () { if (OnOptionFocused (0, new (GetSetOptionDictionary (), FocusedOption))) { return false; } if (!AllowEmpty) { SetFocusedOption (); } return true; } internal bool MoveEnd () { if (OnOptionFocused (_options.Count - 1, new (GetSetOptionDictionary (), FocusedOption))) { return false; } if (!AllowEmpty) { SetFocusedOption (); } return true; } #endregion }