diff --git a/src/Eto.Gtk/Forms/Controls/TextAreaHandler.cs b/src/Eto.Gtk/Forms/Controls/TextAreaHandler.cs index 90928c611..28f36604c 100644 --- a/src/Eto.Gtk/Forms/Controls/TextAreaHandler.cs +++ b/src/Eto.Gtk/Forms/Controls/TextAreaHandler.cs @@ -15,6 +15,7 @@ public class TextAreaHandler : GtkControl TextReplacements.None; set { } } - public TextReplacements SupportedTextReplacements + public TextReplacements SupportedTextReplacements => TextReplacements.None; + + public virtual BorderType Border { - get { return TextReplacements.None; } + get => border; + set + { + border = value; + switch (border) + { + case BorderType.Bezel: + scroll.ShadowType = Gtk.ShadowType.In; + break; + case BorderType.Line: + scroll.ShadowType = Gtk.ShadowType.In; + break; + case BorderType.None: + scroll.ShadowType = Gtk.ShadowType.None; + break; + default: + throw new NotSupportedException(); + } + } + } + + public virtual int TextLength => Control.Buffer.CharCount; + + public virtual void ScrollTo(Range range) + { + var iter = Control.Buffer.GetIterAtOffset(range.Start + range.Length()); + var mark = Control.Buffer.CreateMark(null, iter, false); + Control.ScrollToMark(mark, 0, false, 0, 0); } + + public virtual void ScrollToEnd() + { + var end = Control.Buffer.EndIter; + var mark = Control.Buffer.CreateMark(null, end, false); + Control.ScrollToMark(mark, 0, false, 0, 0); + } + + public virtual void ScrollToStart() + { + var end = Control.Buffer.StartIter; + var mark = Control.Buffer.CreateMark(null, end, false); + Control.ScrollToMark(mark, 0, false, 0, 0); + } + + } } diff --git a/src/Eto.Mac/Forms/Controls/TextAreaHandler.cs b/src/Eto.Mac/Forms/Controls/TextAreaHandler.cs index 2b4921a6b..b55a89698 100644 --- a/src/Eto.Mac/Forms/Controls/TextAreaHandler.cs +++ b/src/Eto.Mac/Forms/Controls/TextAreaHandler.cs @@ -1,10 +1,12 @@ using Eto.Mac.Drawing; +using Range = Eto.Forms.Range; namespace Eto.Mac.Forms.Controls { public class TextAreaHandler : TextAreaHandler, TextArea.IHandler { - } + internal static readonly IntPtr selString = Selector.GetHandle("string"); + internal static readonly IntPtr selLength = Selector.GetHandle("length"); } public interface ITextAreaHandler { @@ -258,9 +260,9 @@ public Color TextColor } } } - + static readonly object DisabledBackgroundColor_Key = new object(); - + public virtual Color DisabledBackgroundColor { get => Widget.Properties.Get(DisabledBackgroundColor_Key) ?? NSColor.WindowBackground.ToEto(); @@ -380,11 +382,7 @@ public bool AcceptsReturn public void Append(string text, bool scrollToCursor) { - // get NSString object so we don't have to marshal the entire string to get its length - var stringValuePtr = Messaging.IntPtr_objc_msgSend(Control.Handle, selGetString); - var str = Runtime.GetNSObject(stringValuePtr); - - var range = new NSRange(str != null ? str.Length : 0, 0); + var range = new NSRange(TextLength, 0); Control.Replace(range, text); range.Location += text.Length; Control.SetSelectedRange(range); @@ -453,5 +451,19 @@ public TextReplacements SupportedTextReplacements { get { return TextReplacements.Quote | TextReplacements.Text | TextReplacements.Dash | TextReplacements.Spelling; } } + + public BorderType Border + { + get => Scroll.BorderType.ToEto(); + set => Scroll.BorderType = value.ToNS(); + } + + public int TextLength => (int)Control.TextStorage.Length; + + public void ScrollTo(Range range) => Control.ScrollRangeToVisible(range.ToNS()); + + public void ScrollToStart() => ScrollTo(new Range(0)); + + public void ScrollToEnd() => ScrollTo(Range.FromLength(TextLength, 0)); } } \ No newline at end of file diff --git a/src/Eto.WinForms/Forms/Controls/TextAreaHandler.cs b/src/Eto.WinForms/Forms/Controls/TextAreaHandler.cs index 8a523f8a9..b061d37d9 100644 --- a/src/Eto.WinForms/Forms/Controls/TextAreaHandler.cs +++ b/src/Eto.WinForms/Forms/Controls/TextAreaHandler.cs @@ -43,7 +43,7 @@ public class TextAreaHandler : WindowsControl true; - + public static Size DefaultMinimumSize = new Size(100, 60); public override Size? GetDefaultSize(Size availableSize) @@ -245,5 +245,50 @@ public TextReplacements SupportedTextReplacements { get { return TextReplacements.None; } } + + public BorderType Border + { + get => container.BorderStyle.ToEto(); + set => container.BorderStyle = value.ToSWF(); + } + + public int TextLength => Control.TextLength; + + public void ScrollTo(Range range) + { + var pos = Control.GetPositionFromCharIndex(range.End); + sd.Point scrollPosition = sd.Point.Empty; + Win32.SendMessage(Control.Handle, Win32.WM.EM_GETSCROLLPOS, IntPtr.Zero, ref scrollPosition); + + var si = new Win32.SCROLLINFO(); + si.cbSize = Marshal.SizeOf(si); + si.fMask = (int)Win32.ScrollInfoMask.SIF_ALL; + Win32.GetScrollInfo(Control.Handle, (int)Win32.SBOrientation.SB_VERT, ref si); + + if (si.nPage > 0) + scrollPosition.Y = Math.Min(si.nMax - si.nPage, Math.Max(si.nMin, scrollPosition.Y + pos.Y)); + + Win32.GetScrollInfo(Control.Handle, (int)Win32.SBOrientation.SB_HORZ, ref si); + + // only scroll X if not in view already + if (si.nPage > 0 && (pos.X < si.nPos || pos.X > si.nPos + si.nPage)) + scrollPosition.X = Math.Min(si.nMax - si.nPage, Math.Max(si.nMin, scrollPosition.X + pos.X)); + + Win32.SendMessage(Control.Handle, Win32.WM.EM_SETSCROLLPOS, IntPtr.Zero, ref scrollPosition); + } + + public void ScrollToStart() + { + Win32.SendMessage(Control.Handle, Win32.WM.VSCROLL, (IntPtr)Win32.SB.TOP, IntPtr.Zero); + Win32.SendMessage(Control.Handle, Win32.WM.HSCROLL, (IntPtr)Win32.SB.LEFT, IntPtr.Zero); + } + + public void ScrollToEnd() + { + Win32.SendMessage(Control.Handle, Win32.WM.VSCROLL, (IntPtr)Win32.SB.BOTTOM, IntPtr.Zero); + Win32.SendMessage(Control.Handle, Win32.WM.HSCROLL, (IntPtr)Win32.SB.LEFT, IntPtr.Zero); + } + + } } \ No newline at end of file diff --git a/src/Eto.WinForms/Win32.cs b/src/Eto.WinForms/Win32.cs index 04bc32bf9..438d083db 100755 --- a/src/Eto.WinForms/Win32.cs +++ b/src/Eto.WinForms/Win32.cs @@ -135,6 +135,15 @@ public enum WS_EX : uint NOACTIVATE = 0x08000000 } + public enum SB + { + THUMBTRACK = 5, + TOP = 6, + LEFT = 6, + BOTTOM = 7, + RIGHT = 7 + } + public enum WM { SETREDRAW = 0xB, @@ -180,9 +189,14 @@ public enum WM NCCREATE = 0x0081, NCLBUTTONDOWN = 0x00A1, PRINT = 0x0317, - SHOWWINDOW = 0x00000018 + SHOWWINDOW = 0x00000018, + HSCROLL = 0x114, + VSCROLL = 0x115, + USER = 0x400, + EM_GETSCROLLPOS = USER + 221, + EM_SETSCROLLPOS = USER + 222, } - + public enum VK : long { SHIFT = 0x10, @@ -332,6 +346,9 @@ public static swf.MouseButtons GetMouseButtonWParam(IntPtr wParam) [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] public static extern IntPtr SendMessage(IntPtr hWnd, WM msg, IntPtr wParam, [MarshalAs(UnmanagedType.LPWStr)] string lParam); + [DllImport("user32.dll", CharSet = CharSet.Auto)] + public static extern IntPtr SendMessage(IntPtr hWnd, WM wMsg, IntPtr wParam, ref sd.Point lParam); + [DllImport("user32.dll", CharSet = CharSet.Auto)] public static extern bool PeekMessage(ref swf.Message wMsg, IntPtr hwnd, int msgMin, int msgMax, int remove); @@ -411,7 +428,7 @@ public static string GetWindowText(IntPtr hwnd) } // for tray indicator - + public enum WH { KEYBOARD = 2, @@ -419,9 +436,9 @@ public enum WH MOUSE_LL = 14 } - - public static IntPtr SetHook(WH hookId, HookProc proc) - { + + public static IntPtr SetHook(WH hookId, HookProc proc) + { using (Process curProcess = Process.GetCurrentProcess()) using (ProcessModule curModule = curProcess.MainModule) { @@ -439,7 +456,7 @@ public static IntPtr SetHook(WH hookId, HookProc proc) public static extern IntPtr SetWindowsHookEx(IntPtr hookId, HookProc function, IntPtr instance, int threadId); [DllImport("user32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall, SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] + [return: MarshalAs(UnmanagedType.Bool)] public static extern bool UnhookWindowsHookEx(IntPtr hookId); [DllImportAttribute("user32.dll")] @@ -542,19 +559,19 @@ public static IntPtr GetThreadFocusWindow(uint? threadId = null) public static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert); [DllImport("user32.dll")] public static extern bool EnableMenuItem(IntPtr hMenu, SC uIDEnableItem, MF uEnable); - + [Flags] public enum MF : uint { BYCOMMAND = 0x00000000, GRAYED = 0x00000001 } - + public enum SC : uint { CLOSE = 0xF060 } - + [DllImport("kernel32.dll", CharSet = CharSet.Auto)] public static extern IntPtr GlobalLock(IntPtr handle); @@ -563,5 +580,45 @@ public enum SC : uint [DllImport("kernel32.dll", CharSet = CharSet.Auto)] public static extern int GlobalSize(IntPtr handle); + + + public enum SBB + { + HORZ = 0, + VERT = 1 + } + + public enum ScrollInfoMask : uint + { + SIF_RANGE = 0x1, + SIF_PAGE = 0x2, + SIF_POS = 0x4, + SIF_DISABLENOSCROLL = 0x8, + SIF_TRACKPOS = 0x10, + SIF_ALL = (SIF_RANGE | SIF_PAGE | SIF_POS | SIF_TRACKPOS), + } + public enum SBOrientation : int + { + SB_HORZ = 0x0, + SB_VERT = 0x1, + SB_CTL = 0x2, + SB_BOTH = 0x3 + } + + [Serializable, StructLayout(LayoutKind.Sequential)] + public struct SCROLLINFO + { + public int cbSize; // (uint) int is because of Marshal.SizeOf + public uint fMask; + public int nMin; + public int nMax; + public int nPage; + public int nPos; + public int nTrackPos; + } + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool GetScrollInfo(IntPtr hwnd, int fnBar, ref SCROLLINFO lpsi); } } diff --git a/src/Eto.Wpf/Forms/Controls/RichTextAreaHandler.cs b/src/Eto.Wpf/Forms/Controls/RichTextAreaHandler.cs index 0e9851911..6cee85bcc 100755 --- a/src/Eto.Wpf/Forms/Controls/RichTextAreaHandler.cs +++ b/src/Eto.Wpf/Forms/Controls/RichTextAreaHandler.cs @@ -984,6 +984,17 @@ public override TextAlignment TextAlignment ContentRange.ApplyPropertyValue(swd.Block.TextAlignmentProperty, value.ToWpfTextAlignment()); } } + + public override void ScrollTo(Range range) + { + var textRange = GetRange(range); + var rect = textRange.End.GetCharacterRect(swd.LogicalDirection.Backward); + Control.ScrollToVerticalOffset(rect.Top); + Control.ScrollToHorizontalOffset(rect.Left); + } + + public override int TextLength => ContentRange.GetLength(); + } static class FlowDocumentExtensions diff --git a/src/Eto.Wpf/Forms/Controls/TextAreaHandler.cs b/src/Eto.Wpf/Forms/Controls/TextAreaHandler.cs old mode 100644 new mode 100755 index e9b8a9d15..d8589a201 --- a/src/Eto.Wpf/Forms/Controls/TextAreaHandler.cs +++ b/src/Eto.Wpf/Forms/Controls/TextAreaHandler.cs @@ -78,6 +78,15 @@ public override void AttachEvent(string id) break; } } + + public override void ScrollTo(Range range) + { + var rect = Control.GetRectFromCharacterIndex(range.End); + Control.ScrollToVerticalOffset(rect.Top); + Control.ScrollToHorizontalOffset(rect.Left); + } + + public override int TextLength => Control.Text.Length; } public abstract class TextAreaHandler : WpfControl, TextArea.IHandler @@ -174,10 +183,7 @@ public void Append(string text, bool scrollToCursor) public abstract int CaretIndex { get; set; } - public void SelectAll() - { - Control.SelectAll(); - } + public void SelectAll() => Control.SelectAll(); static readonly object AcceptsTabKey = new object(); @@ -236,13 +242,27 @@ public bool SpellCheck public TextReplacements TextReplacements { - get { return TextReplacements.None; } + get => TextReplacements.None; set { } } - public TextReplacements SupportedTextReplacements + public TextReplacements SupportedTextReplacements => TextReplacements.None; + + static readonly object Border_Key = new object(); + + public virtual BorderType Border { - get { return TextReplacements.None; } + get { return Widget.Properties.Get(Border_Key, BorderType.Bezel); } + set { Widget.Properties.Set(Border_Key, value, () => Control.SetEtoBorderType(value)); } } + + public abstract void ScrollTo(Range range); + + public virtual void ScrollToEnd() => Control.ScrollToEnd(); + + public virtual void ScrollToStart() => Control.ScrollToHome(); + + public abstract int TextLength { get; } + } } diff --git a/src/Eto/Forms/Controls/TextArea.cs b/src/Eto/Forms/Controls/TextArea.cs index b796c440a..15c20a0a5 100644 --- a/src/Eto/Forms/Controls/TextArea.cs +++ b/src/Eto/Forms/Controls/TextArea.cs @@ -279,6 +279,24 @@ public TextReplacements SupportedTextReplacements { get { return Handler.SupportedTextReplacements; } } + + /// + /// Gets or sets the border type + /// + /// The border. + public BorderType Border + { + get => Handler.Border; + set => Handler.Border = value; + } + + /// + /// Gets the current length of the text + /// + /// + /// When there is lots of text, this can be more performant than using Text.Length. + /// + public int TextLength => Handler.TextLength; /// /// Append the specified text to the control and optionally scrolls to make the inserted text visible. @@ -292,6 +310,25 @@ public void Append(string text, bool scrollToCursor = false) { Handler.Append(text, scrollToCursor); } + + /// + /// Scrolls the specified range into view. + /// + /// + /// On some platforms, this may just scroll to the end of the range. + /// + public void ScrollTo(Range range) => Handler.ScrollTo(range); + + /// + /// Scrolls to the start of the text in the text area. + /// + public void ScrollToStart() => Handler.ScrollToStart(); + + /// + /// Scrolls to the end of the text in the text area. + /// + public void ScrollToEnd() => Handler.ScrollToEnd(); + static readonly object callback = new Callback(); @@ -403,6 +440,24 @@ public void OnCaretIndexChanged(TextArea widget, EventArgs e) /// When setting the selection, the control will be focussed and the associated keyboard may appear on mobile platforms. /// void SelectAll(); + + /// + /// Scrolls the specified range into view. + /// + /// + /// On some platforms, this may just scroll to the end of the range. + /// + void ScrollTo(Range range); + + /// + /// Scrolls to the start of the text in the text area. + /// + void ScrollToStart(); + + /// + /// Scrolls to the end of the text in the text area. + /// + void ScrollToEnd(); /// /// Gets or sets the index of the insertion caret. @@ -469,5 +524,16 @@ public void OnCaretIndexChanged(TextArea widget, EventArgs e) /// /// true if spell check is supported; otherwise, false. bool SpellCheckIsSupported { get; } + + /// + /// Gets or sets the border type + /// + /// The border. + BorderType Border { get; set; } + + /// + /// Gets the current length of the text + /// + int TextLength { get; } } } \ No newline at end of file diff --git a/test/Eto.Test/Sections/Controls/RichTextAreaSection.cs b/test/Eto.Test/Sections/Controls/RichTextAreaSection.cs index 61acbdfad..7a8422491 100644 --- a/test/Eto.Test/Sections/Controls/RichTextAreaSection.cs +++ b/test/Eto.Test/Sections/Controls/RichTextAreaSection.cs @@ -212,6 +212,7 @@ public RichTextAreaSection() TextAreaSection.TextAreaOptions(richText), TextAreaSection.TextAreaOptions2(richText), TextAreaSection.TextAreaOptions3(richText), + TextAreaSection.TextAreaOptions4(richText), formatting1, formatting2, formatting3, diff --git a/test/Eto.Test/Sections/Controls/TextAreaSection.cs b/test/Eto.Test/Sections/Controls/TextAreaSection.cs index fa7844615..1b3396f2a 100644 --- a/test/Eto.Test/Sections/Controls/TextAreaSection.cs +++ b/test/Eto.Test/Sections/Controls/TextAreaSection.cs @@ -78,11 +78,67 @@ public static Control TextAreaOptions4(TextArea text) { null, TextReplacementsDropDown(text), + ScrollToDropDown(text), + ShowTextLength(text), + SetBorderType(text), null } }; } + private static Control SetBorderType(TextArea text) + { + var dropDown = new EnumDropDown(); + dropDown.SelectedValueBinding.Bind(text, t => t.Border); + return dropDown; + } + + private static Control ShowTextLength(TextArea text) + { + var button = new Button { Text = "Show TextLength" }; + button.Click += (sender, e) => Log.Write(text, $"TextLength: {text.TextLength}"); + return button; + } + + private static Control ScrollToDropDown(TextArea text) + { + var button = new SegmentedButton { SelectionMode = SegmentedSelectionMode.None }; + + void ScrollToRange(object sender, EventArgs e) + { + var dlg = new Dialog { Title = "Select Range" }; + + var start = new NumericStepper { MinValue = 0, MaxValue = text.TextLength, Value = 10 }; + var end = new NumericStepper { MinValue = 0, MaxValue = text.TextLength, Value = 20 }; + + var layout = new TableLayout { Spacing = new Size(5, 5), Padding = 10 }; + layout.Rows.Add(new TableRow("Start:", start)); + layout.Rows.Add(new TableRow("End:", end)); + dlg.Content = layout; + + var done = new Button { Text = "Done" }; + done.Click += (sender, e) => dlg.Close(true); + + dlg.PositiveButtons.Add(done); + dlg.DefaultButton = done; + + if (dlg.ShowModal()) + { + text.ScrollTo(new Range((int)start.Value, (int)end.Value)); + } + } + + + var contextMenu = new ContextMenu(); + contextMenu.Items.Add(new ButtonMenuItem((sender, e) => text.ScrollToStart()) { Text = "ScrollToStart" }); + contextMenu.Items.Add(new ButtonMenuItem((sender, e) => text.ScrollToEnd()) { Text = "ScrollToEnd" }); + contextMenu.Items.Add(new ButtonMenuItem(ScrollToRange) { Text = "ScrollTo(Range)" }); + button.Items.Add(new MenuSegmentedItem { Text = "ScrollTo", Menu = contextMenu }); + + return button; + } + + static Control WrapCheckBox(TextArea text) { var control = new CheckBox { Text = "Wrap" };