diff --git a/Core/AppendOnlyCharacterDocument.cs b/Core/AppendOnlyCharacterDocument.cs new file mode 100644 index 00000000..24916255 --- /dev/null +++ b/Core/AppendOnlyCharacterDocument.cs @@ -0,0 +1,219 @@ +// Copyright 2019 The Poderosa Project. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Poderosa.View; +using System; +using System.Collections.Generic; +using System.Drawing; + +namespace Poderosa.Document { + + /// + /// implementation that only supports appending new lines. + /// + public abstract class AppendOnlyCharacterDocument : ICharacterDocument { + + protected readonly object _syncRoot = new object(); + + protected readonly InvalidatedRegion _invalidatedRegion = new InvalidatedRegion(); + + private readonly GLineBuffer _buffer; + + /// + /// Constructor (default capacity) + /// + protected AppendOnlyCharacterDocument() { + _buffer = new GLineBuffer(_syncRoot); + } + + /// + /// Constructor + /// + /// initial capacity in number of rows + protected AppendOnlyCharacterDocument(int capacity) { + _buffer = new GLineBuffer(_syncRoot, capacity); + } + + /// + /// Sets new capacity. + /// + /// new capacity in number of rows + protected void SetCapacity(int newCapacity) { + _buffer.SetCapacity(Math.Max(newCapacity, 1)); + } + + /// + /// Append a single line. + /// + /// line object + protected void Append(GLine line) { + lock (_syncRoot) { + int rowID = _buffer.NextRowID; + _buffer.Append(line); + _invalidatedRegion.InvalidateRow(rowID); + } + } + + /// + /// Append lines. + /// + /// sequence of the line objects + protected void Append(IEnumerable lines) { + lock (_syncRoot) { + int rowIDStart = _buffer.NextRowID; + _buffer.Append(lines); + int rowIDEnd = _buffer.NextRowID; + _invalidatedRegion.InvalidateRows(new RowIDSpan(rowIDStart, rowIDEnd - rowIDStart)); + } + } + + #region ICharacterDocument + + /// + /// Object for the synchronization. + /// + public object SyncRoot { + get { + return _syncRoot; + } + } + + /// + /// Invalidated region + /// + public InvalidatedRegion InvalidatedRegion { + get { + return _invalidatedRegion; + } + } + + /// + /// Gets range of the row ID in this document. + /// + /// span of the row ID + public RowIDSpan GetRowIDSpan() { + lock (_syncRoot) { + return _buffer.RowIDSpan; + } + } + + /// + /// Determines which color should be used as the background color of this document. + /// + /// current profile + /// background color + public Color DetermineBackgroundColor(RenderProfile profile) { + return profile.BackColor; + } + + /// + /// Determines which image should be painted (or should not be painted) in the background of this document. + /// + /// current profile + /// an image object to paint, or null. + public Image DetermineBackgroundImage(RenderProfile profile) { + return profile.GetImage(); + } + + /// + /// Apply action to each row in the specified range. + /// + /// + /// This method must guarantee that the specified action is called for all rows in the specified range. + /// If a row was missing in this document, null is passed to the action. + /// + /// start Row ID + /// number of rows + /// + /// a delegate function to apply. the first argument is a row ID. the second argument is a target GLine object. + /// + public void ForEach(int startRowID, int rows, Action action) { + if (rows < 0) { + throw new ArgumentException("invalid value", "rows"); + } + if (action == null) { + throw new ArgumentException("action is null", "action"); + } + + lock (_syncRoot) { + RowIDSpan buffSpan = _buffer.RowIDSpan; + RowIDSpan iterSpan = buffSpan.Intersect(new RowIDSpan(startRowID, rows)); + + int rowID = startRowID; + + if (iterSpan.Length > 0) { + while (rowID < iterSpan.Start) { + action(rowID, null); + rowID++; + } + + _buffer.Apply(iterSpan.Start, iterSpan.Length, s => { + foreach (var line in s.GLines()) { + action(rowID, line); + rowID++; + } + }); + } + + int endRowID = startRowID + rows; + while (rowID < endRowID) { + action(rowID, null); + rowID++; + } + } + } + + /// + /// Apply action to the specified row. + /// + /// + /// If a row was missing in this document, null is passed to the action. + /// + /// Row ID + /// + /// a delegate function to apply. the first argument may be null. + /// + public void Apply(int rowID, Action action) { + if (action == null) { + throw new ArgumentException("action is null", "action"); + } + + lock (_syncRoot) { + RowIDSpan buffSpan = _buffer.RowIDSpan; + + if (buffSpan.Includes(rowID)) { + _buffer.Apply(rowID, 1, s => { + action(s.Array[s.Offset]); + }); + } + else { + action(null); + } + } + } + + /// + /// Notifies document implementation from the document viewer + /// that the size of the visible area was changed. + /// + /// number of visible rows + /// number of visible columns + public void VisibleAreaSizeChanged(int rows, int cols) { + // do nothing + } + + #endregion + } + +} diff --git a/Core/BasicCommands.cs b/Core/BasicCommands.cs index 7c401379..008b905b 100644 --- a/Core/BasicCommands.cs +++ b/Core/BasicCommands.cs @@ -756,21 +756,46 @@ public SelectedTextCopyCommand() { } public CommandResult InternalExecute(ICommandTarget target, params IAdaptable[] args) { + ITextSelection s; + CharacterDocumentViewer control = (CharacterDocumentViewer)target.GetAdapter(typeof(CharacterDocumentViewer)); - ITextSelection s = control.ITextSelection; - if (s.IsEmpty || !control.EnabledEx) + if (control != null) { + s = control.Selection; + } + else { + CharacterDocumentViewer_Old controlOld = (CharacterDocumentViewer_Old)target.GetAdapter(typeof(CharacterDocumentViewer_Old)); + if (controlOld != null) { + s = controlOld.ITextSelection; + } else { + return CommandResult.Ignored; + } + } + + if (s.IsEmpty || !control.HasDocument) { return CommandResult.Ignored; + } string t = s.GetSelectedText(TextFormatOption.Default); - if (t.Length > 0) + if (t.Length > 0) { CopyToClipboard(t); + } + return CommandResult.Succeeded; } public bool CanExecute(ICommandTarget target) { CharacterDocumentViewer control = (CharacterDocumentViewer)target.GetAdapter(typeof(CharacterDocumentViewer)); - return control.EnabledEx && !control.ITextSelection.IsEmpty; + if (control != null) { + return control.HasDocument && !control.Selection.IsEmpty; + } + + CharacterDocumentViewer_Old controlOld = (CharacterDocumentViewer_Old)target.GetAdapter(typeof(CharacterDocumentViewer_Old)); + if (controlOld != null) { + return controlOld.EnabledEx && !controlOld.ITextSelection.IsEmpty; + } + + return false; } public IAdaptable GetAdapter(Type adapter) { diff --git a/Core/CharacterDocumentViewer.cs b/Core/CharacterDocumentViewer.cs index b5fcf176..d8259e96 100644 --- a/Core/CharacterDocumentViewer.cs +++ b/Core/CharacterDocumentViewer.cs @@ -1,4 +1,4 @@ -// Copyright 2004-2017 The Poderosa Project. +// Copyright 2004-2019 The Poderosa Project. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -22,66 +22,109 @@ using Poderosa.Sessions; using Poderosa.UI; using System; -using System.Collections.Generic; using System.Diagnostics; using System.Drawing; using System.Windows.Forms; namespace Poderosa.View { - /* - * CharacterDocumentの表示を行うコントロール。機能としては次がある。 - *  縦方向のみスクロールバーをサポート - *  再描画の最適化 - *  キャレットの表示。ただしキャレットを適切に移動する機能は含まれない - * - *  今後あってもいいかもしれない機能は、行間やPadding(HTML用語の)、行番号表示といったところ - */ /// - /// + /// Viewer control to show a . /// - /// - public class CharacterDocumentViewer : Control, IPoderosaControl, ISelectionListener, SplitMarkSupport.ISite { - - public const int BORDER = 2; //内側の枠線のサイズ - internal const int TIMER_INTERVAL = 50; //再描画最適化とキャレット処理を行うタイマーの間隔 + /// + /// This class shows the text with attributes, a scrollbar, and a caret. + /// Also, this class handles mouse input for the text-selection and the splitter. + /// This class doesn't handle key input. It will be handled by the derived class on its need. + /// + public abstract class CharacterDocumentViewer : Control, IPoderosaControl, ISelectionListener, SplitMarkSupport.ISite { + // inner padding in pixels + private const int BORDER = 2; + // timer interval for the periodic redraw + private const int TIMER_INTERVAL = 50; + + // vertical scrollbar + private VScrollBar _verticalScrollBar; + + // text-selection manager + private readonly TextSelection _textSelection; + // splitter manager + private readonly SplitMarkSupport _splitMark; + // mouse handler manager + private readonly MouseHandlerManager _mouseHandlerManager; + // caret manager + private readonly Caret _caret; + + // document + private ICharacterDocument _document = null; + + // timer for the periodic redraw + private ITimerSite _timer = null; + // timer tick counter for the periodic redraw + private int _tickCount = 0; + + // a flag for preventing repeat of error reports + private bool _errorRaisedInDrawing = false; + // a flag which indicates that one or more lines in this view need the periodic redraw + private bool _requiresPeriodicRedraw = false; + + // mouse pointer on the document (appears during a document is attached) + private Cursor _documentCursor = Cursors.IBeam; - private CharacterDocument _document; - private bool _errorRaisedInDrawing; - private readonly List _transientLines; //再描画するGLineを一時的に保管する - private readonly List _glinePool; - private bool _requiresPeriodicRedraw; - private TextSelection _textSelection; - private SplitMarkSupport _splitMark; - private bool _enabled; //ドキュメントがアタッチされていないときを示す 変更するときはEnabledExプロパティで! + // size of the viewport + private int _viewportRows = 0; + private int _viewportColumns = 0; - private Cursor _documentCursor = Cursors.IBeam; + // Row ID of the first row at the top of this view + private int _topRowID = 0; + // Row ID of the first row in the document + private int _docFirstRowID = 0; - protected MouseHandlerManager _mouseHandlerManager; - protected VScrollBar _VScrollBar; - protected bool _enableAutoScrollBarAdjustment; //リサイズ時に自動的に_VScrollBarの値を調整するかどうか - protected Caret _caret; - protected ITimerSite _timer; - protected int _tickCount; + // indicates whether OnResize event has been occurred + private bool _onResizeOccurred = false; - public delegate void OnPaintTimeObserver(Stopwatch s); + // temporal copy of lines + private readonly GLineChunk _linePool = new GLineChunk(0); #if ONPAINT_TIME_MEASUREMENT - private OnPaintTimeObserver _onPaintTimeObserver = null; + private Action _onPaintTimeObserver = null; #endif - public CharacterDocumentViewer() { - _enableAutoScrollBarAdjustment = true; - _transientLines = new List(); - _glinePool = new List(); + /// + /// Do extra work when the viewport size was changed. + /// + protected abstract void OnViewportSizeChanged(); + + /// + /// Do extra work when the document was attched or detached. + /// + protected abstract void OnCharacterDocumentChanged(); + + /// + /// Obtains a current render-profile. + /// + /// render-profile object. must not be null. + protected abstract RenderProfile GetCurrentRenderProfile(); + + /// + /// Determines whether the viewer is scrollable. + /// + /// true if the viewer is scrollable. + protected abstract bool DetermineScrollable(); + + /// + /// Constructor + /// + protected CharacterDocumentViewer() { InitializeComponent(); - //SetStyle(ControlStyles.UserPaint|ControlStyles.AllPaintingInWmPaint|ControlStyles.DoubleBuffer, true); + this.DoubleBuffered = true; + _caret = new Caret(); - _splitMark = new SplitMarkSupport(this, this); - Pen p = new Pen(SystemColors.ControlDark); - p.DashStyle = System.Drawing.Drawing2D.DashStyle.Dot; - _splitMark.Pen = p; + _splitMark = new SplitMarkSupport(this, this) { + Pen = new Pen(SystemColors.ControlDark) { + DashStyle = System.Drawing.Drawing2D.DashStyle.Dot + } + }; _textSelection = new TextSelection(this); _textSelection.AddSelectionListener(this); @@ -89,323 +132,620 @@ public CharacterDocumentViewer() { _mouseHandlerManager = new MouseHandlerManager(); _mouseHandlerManager.AddLastHandler(new TextSelectionUIHandler(this)); _mouseHandlerManager.AddLastHandler(new SplitMarkUIHandler(_splitMark)); + _mouseHandlerManager.AddLastHandler(new DefaultMouseWheelHandler(this)); _mouseHandlerManager.AttachControl(this); SetStyle(ControlStyles.SupportsTransparentBackColor, true); } - public CharacterDocument CharacterDocument { - get { - return _document; - } + private void InitializeComponent() { + this.SuspendLayout(); + this._verticalScrollBar = new System.Windows.Forms.VScrollBar(); + // + // _VScrollBar + // + this._verticalScrollBar.Enabled = false; + this._verticalScrollBar.Dock = DockStyle.Right; + this._verticalScrollBar.LargeChange = 1; + this._verticalScrollBar.Minimum = 0; + this._verticalScrollBar.Value = 0; + this._verticalScrollBar.Maximum = 2; + this._verticalScrollBar.Name = "_verticalScrollBar"; + this._verticalScrollBar.TabIndex = 0; + this._verticalScrollBar.TabStop = false; + this._verticalScrollBar.Cursor = Cursors.Default; + this._verticalScrollBar.Visible = false; + this._verticalScrollBar.ValueChanged += _verticalScrollBar_ValueChanged; + this.Controls.Add(_verticalScrollBar); + + this.ImeMode = System.Windows.Forms.ImeMode.NoControl; + this.ResumeLayout(); } - internal TextSelection TextSelection { - get { - return _textSelection; + + protected void AddFirstMouseHandler(IMouseHandler handler) { + _mouseHandlerManager.AddFirstHandler(handler); + } + + protected void AddLastMouseHandler(IMouseHandler handler) { + _mouseHandlerManager.AddLastHandler(handler); + } + + protected override void Dispose(bool disposing) { + base.Dispose(disposing); + if (disposing) { + _caret.Dispose(); + if (_timer != null) { + _timer.Close(); + } + _splitMark.Pen.Dispose(); } } - public ITextSelection ITextSelection { + + /// + /// A document attached to this view. + /// Null if no document was attached. + /// + public ICharacterDocument CharacterDocument { get { - return _textSelection; + return _document; } } - internal MouseHandlerManager MouseHandlerManager { + + /// + /// Whether this view has an attached document. + /// + public bool HasDocument { get { - return _mouseHandlerManager; + return _document != null; } } + /// + /// Caret manager + /// public Caret Caret { get { return _caret; } } - public bool EnabledEx { + /// + /// Text-selection manager + /// + public ITextSelection Selection { get { - return _enabled; - } - set { - _enabled = value; - _VScrollBar.Visible = value; //スクロールバーとは連動 - _splitMark.Pen.Color = value ? SystemColors.ControlDark : SystemColors.Window; //このBackColorと逆で - this.Cursor = GetDocumentCursor(); //Splitter.ISiteを援用 - this.BackColor = value ? GetRenderProfile().BackColor : SystemColors.ControlDark; - this.ImeMode = value ? ImeMode.NoControl : ImeMode.Disable; + return _textSelection; } } - public VScrollBar VScrollBar { + + /// + /// Number of rows in the viewport + /// + public int ViewportRows { get { - return _VScrollBar; + return _viewportRows; } } - public void ShowVScrollBar() { - _VScrollBar.Visible = true; - } - - public void HideVScrollBar() { - _VScrollBar.Visible = false; + /// + /// Number of columns in the viewport + /// + public int ViewportColumns { + get { + return _viewportColumns; + } } - public void SetDocumentCursor(Cursor cursor) { + /// + /// Set document. + /// + /// document to set. can be null. + public void SetDocument(ICharacterDocument doc) { if (this.InvokeRequired) { - this.BeginInvoke((MethodInvoker)delegate() { - SetDocumentCursor(cursor); - }); + this.BeginInvoke((Action)(() => SetDocument(doc))); return; } - _documentCursor = cursor; - if (_enabled) - this.Cursor = cursor; + + bool hasDocument = doc != null; + + _splitMark.Pen.Color = hasDocument ? SystemColors.ControlDark : SystemColors.Window; + this.Cursor = hasDocument ? _documentCursor : Cursors.Default; + this.ImeMode = hasDocument ? ImeMode.NoControl : ImeMode.Disable; + + // after a document was set, BackColor will be updated in OnPaint(). + // reset BackColor here if the document was detached. + if (!hasDocument) { + this.BackColor = SystemColors.ControlDark; + } + + // reset internal state + _viewportRows = 0; + _viewportColumns = 0; + _topRowID = 0; + _docFirstRowID = 0; + _errorRaisedInDrawing = false; + _requiresPeriodicRedraw = false; + + // set document (can be null) + _document = doc; + + // setup timer + if (_timer != null) { + _timer.Close(); + _timer = null; + } + + if (hasDocument) { + _timer = WindowManagerPlugin.Instance.CreateTimer(TIMER_INTERVAL, new TimerDelegate(OnWindowManagerTimer)); + _tickCount = 0; + } + + // reset mouse handlers + _mouseHandlerManager.ResetAll(); + + // clear selection + _textSelection.Clear(); + + // update viewport size + UpdateViewportSize(); + + // do extra work + OnCharacterDocumentChanged(); + + // request repaint + if (hasDocument) { + doc.InvalidatedRegion.InvalidatedAll = true; + RefreshViewer(); + // make sacrolbar visible after UpdateScrollBar() was called + this._verticalScrollBar.Visible = true; + } + else { + this._verticalScrollBar.Visible = false; + this.InvalidateFull(); + } } - public void ResetDocumentCursor() { + /// + /// Sets mouse pointer on the document. + /// + /// + protected void SetDocumentCursor(Cursor cursor) { if (this.InvokeRequired) { - this.BeginInvoke((MethodInvoker)delegate() { - ResetDocumentCursor(); - }); + this.BeginInvoke((Action)(() => SetDocumentCursor(cursor))); return; } - SetDocumentCursor(Cursors.IBeam); + + _documentCursor = cursor ?? Cursors.IBeam; + + if (_document != null) { + this.Cursor = cursor; + } } - private Cursor GetDocumentCursor() { - return _enabled ? _documentCursor : Cursors.Default; + /// + /// Resets mouse pointer on the document. + /// + protected void ResetDocumentCursor() { + SetDocumentCursor(null); } + /// + /// Get row ID + /// + /// row index on the screen + /// row ID + private int GetRowID(int rowIndex) { + return _topRowID + rowIndex; + } - #region IAdaptable - public virtual IAdaptable GetAdapter(Type adapter) { - return SessionManagerPlugin.Instance.PoderosaWorld.AdapterManager.GetAdapter(this, adapter); + #region static utility + + /// + /// Estimate view size. + /// + /// to use + /// number of rows + /// number of columns + /// estimated view size + public static Size EstimateViewSize(RenderProfile prof, int rows, int cols) { + int scrollBarWidth = SystemInformation.VerticalScrollBarWidth; + return new Size( + (int)Math.Ceiling(Math.Max(cols, 0) * prof.Pitch.Width) + BORDER * 2 + scrollBarWidth, + (int)Math.Ceiling(Math.Max(rows, 0) * prof.Pitch.Height + (Math.Max(rows - 1, 0) * prof.LineSpacing)) + BORDER * 2 + ); } + #endregion - #region OnPaint time measurement + #region IPoderosaControl - public void SetOnPaintTimeObserver(OnPaintTimeObserver observer) { -#if ONPAINT_TIME_MEASUREMENT - _onPaintTimeObserver = observer; -#endif + public Control AsControl() { + return this; } #endregion - //派生型であることを強制することなどのためにoverrideすることを許す - public virtual void SetContent(CharacterDocument doc) { - RenderProfile prof = GetRenderProfile(); - this.BackColor = prof.BackColor; - _document = doc; - this.EnabledEx = doc != null; + #region IAdaptable - if (_timer != null) - _timer.Close(); - if (this.EnabledEx) { - _timer = WindowManagerPlugin.Instance.CreateTimer(TIMER_INTERVAL, new TimerDelegate(OnWindowManagerTimer)); - _tickCount = 0; - } + public virtual IAdaptable GetAdapter(Type adapter) { + return SessionManagerPlugin.Instance.PoderosaWorld.AdapterManager.GetAdapter(this, adapter); + } + + #endregion - if (_enableAutoScrollBarAdjustment) - AdjustScrollBar(); + #region periodic repaint + + /// + /// Timer event handler + /// + private void OnWindowManagerTimer() { + int caretInterval = WindowManagerPlugin.Instance.WindowPreference.OriginalPreference.CaretInterval; + int caretIntervalTicks = Math.Max(1, caretInterval / TIMER_INTERVAL); + _tickCount = (_tickCount + 1) % caretIntervalTicks; + if (_tickCount == 0) { + CaretTick(); + } } - //タイマーの受信 private void CaretTick() { - if (_enabled) { + ICharacterDocument doc = _document; + if (doc != null) { // Note: // Currently, blinking status of the caret is used also for displaying "blink" characters. // So the blinking status of the caret have to be updated here even if the caret blinking was not enabled. _caret.Tick(); if (_requiresPeriodicRedraw) { _requiresPeriodicRedraw = false; - _document.InvalidatedRegion.InvalidatedAll = true; + doc.InvalidatedRegion.InvalidatedAll = true; } else { - _document.InvalidatedRegion.InvalidateLine(GetTopLine().ID + _caret.Y); + doc.InvalidatedRegion.InvalidateRow(_topRowID + _caret.Y); // FIXME: Caret.Y sould be a Row ID, not a position on the screen } - InvalidateEx(); + + InvalidateRowsRegion(); } } - protected virtual void OnWindowManagerTimer() { - //タイマーはTIMER_INTERVALごとにカウントされるので。 - int q = Math.Max(1, WindowManagerPlugin.Instance.WindowPreference.OriginalPreference.CaretInterval / TIMER_INTERVAL); - _tickCount = (_tickCount + 1) % q; - if (_tickCount == 0) { - CaretTick(); + + #endregion + + #region scrollbar + + /// + /// Type of scroll action in AdjustScrollBar() + /// + protected enum ScrollAction { + /// keep current position (row ID) + KeepRowID, + /// scroll to bottom + ScrollToBottom, + } + + /// + /// Updates scrollbar properties with considering position of the viewport. + /// + /// + /// Changes are made in UI thread later. + /// + protected void UpdateScrollBar() { + if (this.InvokeRequired) { + this.BeginInvoke((Action)UpdateScrollBar); + return; } + + bool scrollable = DetermineScrollable(); + + UpdateScrollBar(scrollable ? ScrollAction.KeepRowID : ScrollAction.ScrollToBottom); } - //自己サイズからScrollBarを適切にいじる - public void AdjustScrollBar() { - if (_document == null) + private void UpdateScrollBar(ScrollAction scrollAction) { + var doc = _document; + if (doc == null) { + _verticalScrollBar.Enabled = false; + _verticalScrollBar.Visible = false; return; - RenderProfile prof = GetRenderProfile(); - float ch = prof.Pitch.Height + prof.LineSpacing; - int largechange = (int)Math.Floor((this.ClientSize.Height - BORDER * 2 + prof.LineSpacing) / ch); //きちんと表示できる行数をLargeChangeにセット - int current = GetTopLine().ID - _document.FirstLineNumber; - int size = Math.Max(_document.Size, current + largechange); - if (size <= largechange) { - _VScrollBar.Enabled = false; } - else { - _VScrollBar.Enabled = true; - _VScrollBar.LargeChange = largechange; - _VScrollBar.Maximum = size - 1; //この-1が必要なのが妙な仕様だ + UpdateScrollBar(doc.GetRowIDSpan(), scrollAction); + } + + /// + /// Adjust settings of a vertical scrollbar to fit with size of the current document. + /// + /// row span of the document + /// specify the next scroll position + private void UpdateScrollBar(RowIDSpan docRowIDSpan, ScrollAction scrollAction) { + // at least one row is required for setup the scrololbar + int screenLines = Math.Max(_viewportRows, 1); + + if (docRowIDSpan.Length <= screenLines) { + _docFirstRowID = docRowIDSpan.Start; + _topRowID = docRowIDSpan.Start; + _verticalScrollBar.Enabled = false; + return; } - } - //このあたりの処置定まっていない - private RenderProfile _privateRenderProfile = null; - public void SetPrivateRenderProfile(RenderProfile prof) { - _privateRenderProfile = prof; + int oldTopRowID = _topRowID; + + // The size of the thumb of ScrollBar: + // => screenLines + // Minimum of the ScrollBar.Value: + // => 0 + // Maxuimum of the ScrollBar.Value should be: + // => docRowIDSpan.Length - screenLines + // Upper limits of the ScrollBar.Value is: ScrollBar.Maximum - ScrollBar.LargeChange + 1 + // So the ScrollBar.Maximum sould be: + // => (docRowIDSpan.Length - screenLines) + screenLines - 1 + // => docRowIDSpan.Length - 1 + + // fix current position + switch (scrollAction) { + case ScrollAction.ScrollToBottom: + _topRowID = Math.Max(docRowIDSpan.Start + docRowIDSpan.Length - screenLines, docRowIDSpan.Start); + break; + + case ScrollAction.KeepRowID: + default: + _topRowID = Math.Max(Math.Min(_topRowID, docRowIDSpan.Start + docRowIDSpan.Length - screenLines), docRowIDSpan.Start); + break; + } + _docFirstRowID = docRowIDSpan.Start; + + _verticalScrollBar.Enabled = true; + _verticalScrollBar.Maximum = docRowIDSpan.Length - 1; + _verticalScrollBar.LargeChange = screenLines; + _verticalScrollBar.Value = _topRowID - docRowIDSpan.Start; + + if (_topRowID != oldTopRowID) { + // repaint all + InvalidateFull(); + } } - //overrideして別の方法でRenderProfileを取得することもある - public virtual RenderProfile GetRenderProfile() { - return _privateRenderProfile; + /// + /// VScrollBar event handler + /// + /// + /// + private void _verticalScrollBar_ValueChanged(object sender, EventArgs e) { + _topRowID = _docFirstRowID + _verticalScrollBar.Value; + // repaint all + InvalidateFull(); } - protected virtual void CommitTransientScrollBar() { - //ViewerはUIによってしか切り取れないからここでは何もしなくていい + /// + /// Scroll specified number of rows + /// + /// number of rows (increase of the row index. positive value causes scroll-up, negative value causes scroll-down.) + protected void ScrollDocument(int rows) { + if (this.InvokeRequired) { + this.BeginInvoke((Action)(() => ScrollDocument(rows))); + return; + } + + if (_verticalScrollBar.Visible && _verticalScrollBar.Enabled) { + _verticalScrollBar.Value = + Math.Min( + Math.Max(_verticalScrollBar.Value + rows, 0), + _verticalScrollBar.Maximum - _verticalScrollBar.LargeChange + 1); + } } - //行数で表示可能な高さを返す - protected virtual int GetHeightInLines() { - RenderProfile prof = GetRenderProfile(); - float ch = prof.Pitch.Height + prof.LineSpacing; - int height = (int)Math.Floor((this.ClientSize.Height - BORDER * 2 + prof.LineSpacing) / ch); - return (height > 0) ? height : 0; + /// + /// Scrolls until the specified row is visible. + /// + /// target row ID + protected void ScrollToVisible(int rowID) { + if (this.InvokeRequired) { + this.BeginInvoke((Action)(() => ScrollToVisible(rowID))); + return; + } + + if (_verticalScrollBar.Visible && _verticalScrollBar.Enabled) { + int newVal; + if (rowID < _topRowID) { + newVal = rowID - _docFirstRowID; + } + else if (rowID - _topRowID >= _verticalScrollBar.LargeChange) { + newVal = rowID - _docFirstRowID - _verticalScrollBar.LargeChange + 1; + } + else { + return; + } + + _verticalScrollBar.Value = + Math.Min( + Math.Max(newVal, 0), + _verticalScrollBar.Maximum - _verticalScrollBar.LargeChange + 1); + } } - //_documentのうちどれを先頭(1行目)として表示するかを返す - public virtual GLine GetTopLine() { - return _document.FindLine(_document.FirstLine.ID + _VScrollBar.Value); + #endregion + + #region refresh viewer + + /// + /// Refresh viewer + /// + protected void RefreshViewer() { + if (this.InvokeRequired) { + this.BeginInvoke((Action)RefreshViewer); + return; + } + + UpdateScrollBar(); + InvalidateRowsRegion(); } - public void MousePosToTextPos(int mouseX, int mouseY, out int textX, out int textY) { - SizeF pitch = GetRenderProfile().Pitch; - textX = RuntimeUtil.AdjustIntRange((int)Math.Floor((mouseX - CharacterDocumentViewer.BORDER) / pitch.Width), 0, Int32.MaxValue); - textY = RuntimeUtil.AdjustIntRange((int)Math.Floor((mouseY - CharacterDocumentViewer.BORDER) / (pitch.Height + GetRenderProfile().LineSpacing)), 0, Int32.MaxValue); + /// + /// Refresh viewer + /// + protected void RefreshViewer(ScrollAction scrollAction) { + if (this.InvokeRequired) { + this.BeginInvoke((Action)(() => RefreshViewer(scrollAction))); + return; + } + + UpdateScrollBar(scrollAction); + InvalidateRowsRegion(); } - public void MousePosToTextPos_AllowNegative(int mouseX, int mouseY, out int textX, out int textY) { - SizeF pitch = GetRenderProfile().Pitch; - textX = (int)Math.Floor((mouseX - CharacterDocumentViewer.BORDER) / pitch.Width); - textY = (int)Math.Floor((mouseY - CharacterDocumentViewer.BORDER) / (pitch.Height + GetRenderProfile().LineSpacing)); + /// + /// Refresh viewer + /// + protected void RefreshViewerFull() { + if (this.InvokeRequired) { + this.BeginInvoke((Action)RefreshViewerFull); + return; + } + + UpdateScrollBar(); + InvalidateFull(); } - //_VScrollBar.ValueChangedイベント - protected virtual void VScrollBarValueChanged() { - if (_enableAutoScrollBarAdjustment) - Invalidate(); + /// + /// Refresh viewer + /// + protected void RefreshViewerFull(ScrollAction scrollAction) { + if (this.InvokeRequired) { + this.BeginInvoke((Action)(() => RefreshViewerFull(scrollAction))); + return; + } + + UpdateScrollBar(scrollAction); + InvalidateFull(); + } + + /// + /// Updates view port size + /// + private void UpdateViewportSize() { + // This method will be called: + // - when a new document was set, or was unset + // - when a new render-profile was set + // - every OnResize event + // + // The initial viewport size will be determined as the following: + // + // Case 1: a document and a render-profile are set before this view was rendered first + // The initial viewport size will be determined in the first OnResize event. + // + // Case 2: a document is set after this view was rendered first + // The initial viewport size will be determined when a new document was set. + + if (_document == null || !_onResizeOccurred) { + // cannot determine the size + _viewportRows = _viewportColumns = 0; + return; + } + + RenderProfile prof = GetCurrentRenderProfile(); + SizeF pitch = prof.Pitch; + Size viewSize = this.ClientSize; + viewSize.Width = Math.Max(viewSize.Width - _verticalScrollBar.Width, 0); // scrollbar is always visible during a document is set + _viewportColumns = Math.Max((int)Math.Floor((viewSize.Width - BORDER * 2) / pitch.Width), 0); + _viewportRows = Math.Max((int)Math.Floor((viewSize.Height - BORDER * 2 + prof.LineSpacing) / (pitch.Height + prof.LineSpacing)), 0); + + //Debug.WriteLine("Rows={0} Cols={1}", _viewportRows, _viewportColumns); + + _document.VisibleAreaSizeChanged(_viewportRows, _viewportColumns); + OnViewportSizeChanged(); } - //キャレットの座標設定、表示の可否を設定 - protected virtual void AdjustCaret(Caret caret) { + #endregion + + #region convert point + + /// + /// Convert the point in the client coordinate to the character position. + /// Negative position may be returned as it was. + /// + /// client coordinate position X in pixels + /// client coordinate position Y in pixels + /// character position column index (may be negative value) + /// character position row index (may be negative value) + protected void ClientPosToTextPos(int px, int py, out int colIndex, out int rowIndex) { + RenderProfile prof = GetCurrentRenderProfile(); + SizeF pitch = prof.Pitch; + colIndex = (int)Math.Floor((px - CharacterDocumentViewer.BORDER) / pitch.Width); + rowIndex = (int)Math.Floor((py - CharacterDocumentViewer.BORDER) / (pitch.Height + prof.LineSpacing)); } - //_documentの更新状況を見て適切な領域のControl.Invalidate()を呼ぶ。 - //また、コントロールを所有していないスレッドから呼んでもOKなようになっている。 - protected void InvalidateEx() { - if (this.IsDisposed) + #endregion + + #region invalidate region + + /// + /// Invalidates region for painting rows that need to be repainted. + /// + private void InvalidateRowsRegion() { + if (this.IsDisposed || this.Disposing) { return; - bool full_invalidate = true; + } + + ICharacterDocument doc = _document; + + bool fullInvalidate; Rectangle r = new Rectangle(); - if (_document != null) { - if (_document.InvalidatedRegion.IsEmpty) + if (doc != null) { + if (doc.InvalidatedRegion.IsEmpty) { return; - InvalidatedRegion rgn = _document.InvalidatedRegion.GetCopyAndReset(); - if (rgn.IsEmpty) + } + InvalidatedRegion rgn = doc.InvalidatedRegion.GetCopyAndClear(); + if (rgn.IsEmpty) { return; - if (!rgn.InvalidatedAll) { - full_invalidate = false; + } + if (rgn.InvalidatedAll) { + fullInvalidate = true; + } + else { + fullInvalidate = false; r.X = 0; r.Width = this.ClientSize.Width; - int topLine = GetTopLine().ID; - int y1 = rgn.LineIDStart - topLine; - int y2 = rgn.LineIDEnd + 1 - topLine; - RenderProfile prof = GetRenderProfile(); + int topRowID = _topRowID; + int y1 = rgn.StartRowID - topRowID; + int y2 = rgn.EndRowID - topRowID; + RenderProfile prof = GetCurrentRenderProfile(); r.Y = BORDER + (int)(y1 * (prof.Pitch.Height + prof.LineSpacing)); r.Height = (int)((y2 - y1) * (prof.Pitch.Height + prof.LineSpacing)) + 1; } } + else { + fullInvalidate = true; + } - if (this.InvokeRequired) { - if (full_invalidate) - this.BeginInvoke((MethodInvoker)delegate() { - Invalidate(); - }); - else { - this.BeginInvoke((MethodInvoker)delegate() { - Invalidate(r); - }); - } + if (fullInvalidate) { + Invalidate(); // Invalidate() can be called in the non-UI thread } else { - if (full_invalidate) - Invalidate(); - else - Invalidate(r); + Invalidate(r); // Invalidate() can be called in the non-UI thread } } - private void InitializeComponent() { - this.SuspendLayout(); - this._VScrollBar = new System.Windows.Forms.VScrollBar(); - // - // _VScrollBar - // - this._VScrollBar.Enabled = false; - //this._VScrollBar.Dock = DockStyle.Right; - this._VScrollBar.Anchor = AnchorStyles.Right | AnchorStyles.Top | AnchorStyles.Bottom; - this._VScrollBar.LargeChange = 1; - this._VScrollBar.Minimum = 0; - this._VScrollBar.Value = 0; - this._VScrollBar.Maximum = 2; - this._VScrollBar.Name = "_VScrollBar"; - this._VScrollBar.TabIndex = 0; - this._VScrollBar.TabStop = false; - this._VScrollBar.Cursor = Cursors.Default; - this._VScrollBar.Visible = false; - this._VScrollBar.ValueChanged += delegate(object sender, EventArgs args) { - VScrollBarValueChanged(); - }; - this.Controls.Add(_VScrollBar); - - this.ImeMode = ImeMode.NoControl; - //this.BorderStyle = BorderStyle.Fixed3D; //IMEPROBLEM - AdjustScrollBarPosition(); - this.ResumeLayout(); - } - - protected override void Dispose(bool disposing) { - base.Dispose(disposing); - if (disposing) { - _caret.Dispose(); - if (_timer != null) - _timer.Close(); - _splitMark.Pen.Dispose(); + /// + /// Invalidates full of the viewport + /// + private void InvalidateFull() { + ICharacterDocument doc = _document; + if (doc != null) { + doc.InvalidatedRegion.Clear(); } + Invalidate(); // Invalidate() can be called in the non-UI thread } - protected override void OnResize(EventArgs e) { - base.OnResize(e); - if (_VScrollBar.Visible) - AdjustScrollBarPosition(); - if (_enableAutoScrollBarAdjustment && _enabled) - AdjustScrollBar(); + #endregion - Invalidate(); - } + #region OnPaint time measurement - //NOTE 自分のDockがTopかLeftのとき、スクロールバーの位置が追随してくれないみたい - private void AdjustScrollBarPosition() { - _VScrollBar.Height = this.ClientSize.Height; - _VScrollBar.Left = this.ClientSize.Width - _VScrollBar.Width; + public void SetOnPaintTimeObserver(Action observer) { +#if ONPAINT_TIME_MEASUREMENT + _onPaintTimeObserver = observer; +#endif } - //描画の本体 + #endregion + + #region OnPaint + protected override sealed void OnPaint(PaintEventArgs e) { #if ONPAINT_TIME_MEASUREMENT Stopwatch onPaintSw = (_onPaintTimeObserver != null) ? Stopwatch.StartNew() : null; @@ -413,60 +753,54 @@ protected override sealed void OnPaint(PaintEventArgs e) { base.OnPaint(e); - try { - if (_document != null) - ShowVScrollBar(); - else - HideVScrollBar(); + ICharacterDocument doc = _document; - if (_enabled && !this.DesignMode) { + try { + if (!this.DesignMode) { Rectangle clip = e.ClipRectangle; Graphics g = e.Graphics; - RenderProfile profile = GetRenderProfile(); + RenderProfile profile = GetCurrentRenderProfile(); // determine background color of the view - Color backColor; - if (_document.IsApplicationMode) { - backColor = profile.GetBackColor(_document.ApplicationModeBackColor); - } - else { - backColor = profile.BackColor; - } + Color backColor = (doc != null) ? doc.DetermineBackgroundColor(profile) : profile.BackColor; - if (this.BackColor != backColor) + if (this.BackColor != backColor) { this.BackColor = backColor; // set background color of the view + } // draw background image if it is required. - if (!_document.IsApplicationMode) { - Image img = profile.GetImage(); + if (doc != null) { + Image img = doc.DetermineBackgroundImage(profile); if (img != null) { DrawBackgroundImage(g, img, profile.ImageStyle, clip); } } - //描画用にテンポラリのGLineを作り、描画中にdocumentをロックしないようにする - //!!ここは実行頻度が高いのでnewを毎回するのは避けたいところだ - RenderParameter param = new RenderParameter(); - _caret.Enabled = _caret.Enabled && this.Focused; //TODO さらにIME起動中はキャレットを表示しないように. TerminalControlだったらAdjustCaretでIMEをみてるので問題はない - lock (_document) { - CommitTransientScrollBar(); - BuildTransientDocument(e, param); - } + if (doc != null) { + RenderParameter param; + lock (doc.SyncRoot) { // synchronize the document during copying content + param = BuildTransientDocument(doc, profile, e); + } - DrawLines(g, param, backColor); + DrawLines(g, param, backColor); - if (_caret.Enabled && (!_caret.Blink || _caret.IsActiveTick)) { //点滅しなければEnabledによってのみ決まる - if (_caret.Style == CaretType.Line) - DrawBarCaret(g, param, _caret.X, _caret.Y); - else if (_caret.Style == CaretType.Underline) - DrawUnderLineCaret(g, param, _caret.X, _caret.Y); + if (_caret.Style == CaretType.Line) { + if (_caret.Enabled && this.Focused && (!_caret.Blink || _caret.IsActiveTick)) { + DrawBarCaret(g, _caret.X, _caret.Y); + } + } + else if (_caret.Style == CaretType.Underline) { + if (_caret.Enabled && this.Focused && (!_caret.Blink || _caret.IsActiveTick)) { + DrawUnderLineCaret(g, _caret.X, _caret.Y); + } + } } } - //マークの描画 + _splitMark.OnPaint(e); } catch (Exception ex) { - if (!_errorRaisedInDrawing) { //この中で一度例外が発生すると繰り返し起こってしまうことがままある。なので初回のみ表示してとりあえず切り抜ける + if (!_errorRaisedInDrawing) { // prevents repeated error reports _errorRaisedInDrawing = true; RuntimeUtil.ReportException(ex); } @@ -482,122 +816,92 @@ protected override sealed void OnPaint(PaintEventArgs e) { #endif } - private void BuildTransientDocument(PaintEventArgs e, RenderParameter param) { + private RenderParameter BuildTransientDocument(ICharacterDocument doc, RenderProfile profile, PaintEventArgs e) { Rectangle clip = e.ClipRectangle; - RenderProfile profile = GetRenderProfile(); - _transientLines.Clear(); - - //Win32.SystemMetrics sm = GEnv.SystemMetrics; - //param.TargetRect = new Rectangle(sm.ControlBorderWidth+1, sm.ControlBorderHeight, - // this.Width - _VScrollBar.Width - sm.ControlBorderWidth + 8, //この8がない値が正当だが、.NETの文字サイズ丸め問題のため行の最終文字が表示されないことがある。これを回避するためにちょっと増やす - // this.Height - sm.ControlBorderHeight); - param.TargetRect = this.ClientRectangle; - - int offset1 = (int)Math.Floor((clip.Top - BORDER) / (profile.Pitch.Height + profile.LineSpacing)); - if (offset1 < 0) - offset1 = 0; - param.LineFrom = offset1; - int offset2 = (int)Math.Floor((clip.Bottom - BORDER) / (profile.Pitch.Height + profile.LineSpacing)); - if (offset2 < 0) - offset2 = 0; - - param.LineCount = offset2 - offset1 + 1; - //Debug.WriteLine(String.Format("{0} {1} ", param.LineFrom, param.LineCount)); - - int topline_id = GetTopLine().ID; - GLine l = _document.FindLineOrNull(topline_id + param.LineFrom); - if (l != null) { - int poolIndex = 0; - for (int i = 0; i < param.LineCount; i++) { - GLine cloned; - if (poolIndex < _glinePool.Count) { - cloned = _glinePool[poolIndex]; - poolIndex++; - cloned.CopyFrom(l); + + int maxRows = _viewportRows; + + int rowOffset1 = (int)Math.Floor((clip.Top - BORDER) / (profile.Pitch.Height + profile.LineSpacing)); + int rowOffset2 = (int)Math.Floor((clip.Bottom - BORDER) / (profile.Pitch.Height + profile.LineSpacing)); + + if (rowOffset1 >= maxRows || rowOffset2 < 0) { + return new RenderParameter(_linePool.Span(0, 0), 0); + } + rowOffset1 = Math.Max(Math.Min(rowOffset1, maxRows - 1), 0); + rowOffset2 = Math.Max(Math.Min(rowOffset2, maxRows - 1), 0); + + int rowNum = rowOffset2 - rowOffset1 + 1; + int startRowID = _topRowID + rowOffset1; + //Debug.WriteLine(String.Format("{0} {1} ", rowIndex, rowNum)); + + // clone rows + _linePool.EnsureCapacity(rowNum); + doc.ForEach(startRowID, rowNum, + (rowID, line) => { + int arrayIndex = rowID - startRowID; + GLine srcLine = (line != null) ? line : new GLine(1); + GLine destLine = _linePool.Array[arrayIndex]; + if (destLine == null) { + _linePool.Array[arrayIndex] = destLine = new GLine(1); + } + if (line != null) { + destLine.CopyFrom(line); } else { - cloned = l.Clone(); - cloned.NextLine = cloned.PrevLine = null; - _glinePool.Add(cloned); // store for next use - poolIndex++; + destLine.Clear(); } - - _transientLines.Add(cloned); - l = l.NextLine; - if (l == null) - break; } - } - - //以下、_transientLinesにはparam.LineFromから示される値が入っていることに注意 - - //選択領域の描画 - if (!_textSelection.IsEmpty) { - TextSelection.TextPoint from = _textSelection.HeadPoint; - TextSelection.TextPoint to = _textSelection.TailPoint; - l = _document.FindLineOrNull(from.Line); - GLine t = _document.FindLineOrNull(to.Line); - if (l != null && t != null) { //本当はlがnullではいけないはずだが、それを示唆するバグレポートがあったので念のため - t = t.NextLine; - int pos = from.Column; //たとえば左端を越えてドラッグしたときの選択範囲は前行末になるので pos==TerminalWidthとなるケースがある。 - do { - int index = l.ID - (topline_id + param.LineFrom); - if (pos >= 0 && pos < l.DisplayLength && index >= 0 && index < _transientLines.Count) { - if (l.ID == to.Line) { - if (pos != to.Column) { - _transientLines[index].SetSelection(pos, to.Column); - } - } - else { - _transientLines[index].SetSelection(pos, l.DisplayLength); - } - } - pos = 0; //2行目からの選択は行頭から - l = l.NextLine; - } while (l != t); + ); + + // set selection to the cloned GLines + TextSelection.Region? selRegion = _textSelection.GetRegion(); + if (selRegion.HasValue) { + RowIDSpan drawRowsSpan = new RowIDSpan(startRowID, rowNum); + RowIDSpan selRowsSpan = new RowIDSpan(selRegion.Value.StartRowID, selRegion.Value.EndRowID - selRegion.Value.StartRowID + 1); + RowIDSpan drawSelRowsSpan = drawRowsSpan.Intersect(selRowsSpan); + + for (int i = 0; i < drawSelRowsSpan.Length; i++) { + int rowID = drawSelRowsSpan.Start + i; + GLine l = _linePool.Array[rowID - startRowID]; + int colFrom = (rowID == selRegion.Value.StartRowID) ? selRegion.Value.StartPos : 0; + int colTo = (rowID == selRegion.Value.EndRowID && selRegion.Value.EndPos.HasValue) ? selRegion.Value.EndPos.Value : l.DisplayLength; + l.SetSelection(colFrom, colTo); } } - AdjustCaret(_caret); - _caret.Enabled = _caret.Enabled && (param.LineFrom <= _caret.Y && _caret.Y < param.LineFrom + param.LineCount); - - //Caret画面外にあるなら処理はしなくてよい。2番目の条件は、Attach-ResizeTerminalの流れの中でこのOnPaintを実行した場合にTerminalHeight>lines.Countになるケースがあるのを防止するため - if (_caret.Enabled) { - //ヒクヒク問題のため、キャレットを表示しないときでもこの操作は省けない - if (_caret.Style == CaretType.Box) { - int y = _caret.Y - param.LineFrom; - if (y >= 0 && y < _transientLines.Count) { - _transientLines[y].SetCursor(_caret.X); - } + if (_caret.Enabled && _caret.Style == CaretType.Box) { + if (_caret.Y >= rowOffset1 && _caret.Y <= rowOffset2) { + _linePool.Array[_caret.Y - rowOffset1].SetCursor(_caret.X); } } + + return new RenderParameter(_linePool.Span(0, rowNum), rowOffset1); } private void DrawLines(Graphics g, RenderParameter param, Color baseBackColor) { - RenderProfile prof = GetRenderProfile(); + RenderProfile prof = GetCurrentRenderProfile(); Caret caret = _caret; //Rendering Core - if (param.LineFrom <= _document.LastLineNumber) { - IntPtr hdc = g.GetHdc(); - try { - float y = (prof.Pitch.Height + prof.LineSpacing) * param.LineFrom + BORDER; - for (int i = 0; i < _transientLines.Count; i++) { - GLine line = _transientLines[i]; - line.Render(hdc, prof, caret, baseBackColor, BORDER, (int)y); - if (line.IsPeriodicRedrawRequired()) { - _requiresPeriodicRedraw = true; - } - y += prof.Pitch.Height + prof.LineSpacing; + IntPtr hdc = g.GetHdc(); + try { + float y = (prof.Pitch.Height + prof.LineSpacing) * param.RowIndex + BORDER; + int lineNum = param.GLines.Length; + for (int i = 0; i < lineNum; i++) { + GLine line = param.GLines.Array[param.GLines.Offset + i]; + line.Render(hdc, prof, caret, baseBackColor, BORDER, (int)y); + if (line.IsPeriodicRedrawRequired()) { + _requiresPeriodicRedraw = true; } + y += prof.Pitch.Height + prof.LineSpacing; } - finally { - g.ReleaseHdc(hdc); - } + } + finally { + g.ReleaseHdc(hdc); } } - private void DrawBarCaret(Graphics g, RenderParameter param, int x, int y) { - RenderProfile profile = GetRenderProfile(); + private void DrawBarCaret(Graphics g, int x, int y) { + RenderProfile profile = GetCurrentRenderProfile(); PointF pt1 = new PointF(profile.Pitch.Width * x + BORDER, (profile.Pitch.Height + profile.LineSpacing) * y + BORDER + 2); PointF pt2 = new PointF(pt1.X, pt1.Y + profile.Pitch.Height - 2); Pen p = _caret.ToPen(profile); @@ -606,8 +910,9 @@ private void DrawBarCaret(Graphics g, RenderParameter param, int x, int y) { pt2.X += 1; g.DrawLine(p, pt1, pt2); } - private void DrawUnderLineCaret(Graphics g, RenderParameter param, int x, int y) { - RenderProfile profile = GetRenderProfile(); + + private void DrawUnderLineCaret(Graphics g, int x, int y) { + RenderProfile profile = GetCurrentRenderProfile(); PointF pt1 = new PointF(profile.Pitch.Width * x + BORDER + 2, (profile.Pitch.Height + profile.LineSpacing) * y + BORDER + profile.Pitch.Height); PointF pt2 = new PointF(pt1.X + profile.Pitch.Width - 2, pt1.Y); Pen p = _caret.ToPen(profile); @@ -631,17 +936,18 @@ private void DrawBackgroundImage(Graphics g, Image img, ImageStyle style, Rectan DrawBackgroundImage_Normal(g, img, style, clip); } } + private void DrawBackgroundImage_Scaled(Graphics g, Image img, Rectangle clip, bool fitWidth, bool fitHeight) { Size clientSize = this.ClientSize; PointF drawPoint; SizeF drawSize; if (fitWidth && fitHeight) { - drawSize = new SizeF(clientSize.Width - _VScrollBar.Width, clientSize.Height); + drawSize = new SizeF(clientSize.Width - _verticalScrollBar.Width, clientSize.Height); drawPoint = new PointF(0, 0); } else if (fitWidth) { - float drawWidth = clientSize.Width - _VScrollBar.Width; + float drawWidth = clientSize.Width - _verticalScrollBar.Width; float drawHeight = drawWidth * img.Height / img.Width; drawSize = new SizeF(drawWidth, drawHeight); drawPoint = new PointF(0, (clientSize.Height - drawSize.Height) / 2f); @@ -650,7 +956,7 @@ private void DrawBackgroundImage_Scaled(Graphics g, Image img, Rectangle clip, b float drawHeight = clientSize.Height; float drawWidth = drawHeight * img.Width / img.Height; drawSize = new SizeF(drawWidth, drawHeight); - drawPoint = new PointF((clientSize.Width - _VScrollBar.Width - drawSize.Width) / 2f, 0); + drawPoint = new PointF((clientSize.Width - _verticalScrollBar.Width - drawSize.Width) / 2f, 0); } Region oldClip = g.Clip; @@ -664,55 +970,42 @@ private void DrawBackgroundImage_Scaled(Graphics g, Image img, Rectangle clip, b private void DrawBackgroundImage_Normal(Graphics g, Image img, ImageStyle style, Rectangle clip) { int offset_x, offset_y; if (style == ImageStyle.Center) { - offset_x = (this.Width - _VScrollBar.Width - img.Width) / 2; + offset_x = (this.Width - _verticalScrollBar.Width - img.Width) / 2; offset_y = (this.Height - img.Height) / 2; } else { - offset_x = (style == ImageStyle.TopLeft || style == ImageStyle.BottomLeft) ? 0 : (this.ClientSize.Width - _VScrollBar.Width - img.Width); + offset_x = (style == ImageStyle.TopLeft || style == ImageStyle.BottomLeft) ? 0 : (this.ClientSize.Width - _verticalScrollBar.Width - img.Width); offset_y = (style == ImageStyle.TopLeft || style == ImageStyle.TopRight) ? 0 : (this.ClientSize.Height - img.Height); } - //if(offset_x < BORDER) offset_x = BORDER; - //if(offset_y < BORDER) offset_y = BORDER; - //画像内のコピー開始座標 Rectangle target = Rectangle.Intersect(new Rectangle(clip.Left - offset_x, clip.Top - offset_y, clip.Width, clip.Height), new Rectangle(0, 0, img.Width, img.Height)); - if (target != Rectangle.Empty) + if (target != Rectangle.Empty) { g.DrawImage(img, new Rectangle(target.Left + offset_x, target.Top + offset_y, target.Width, target.Height), target, GraphicsUnit.Pixel); + } } - //IPoderosaControl - public Control AsControl() { - return this; - } - - //マウスホイールでのスクロール - protected virtual void OnMouseWheelCore(MouseEventArgs e) { - if (!this.EnabledEx) - return; - - int d = e.Delta / 120; //開発環境だとDeltaに120。これで1か-1が入るはず - d *= 3; //可変にしてもいいかも + #endregion - int newval = _VScrollBar.Value - d; - if (newval < 0) - newval = 0; - if (newval > _VScrollBar.Maximum - _VScrollBar.LargeChange) - newval = _VScrollBar.Maximum - _VScrollBar.LargeChange + 1; - _VScrollBar.Value = newval; - } + #region OnResize - protected override void OnMouseWheel(MouseEventArgs e) { - base.OnMouseWheel(e); - OnMouseWheelCore(e); + protected override void OnResize(EventArgs e) { + base.OnResize(e); + _onResizeOccurred = true; + UpdateViewportSize(); + UpdateScrollBar(); + // repaint whole viewport is needed for erasing padding area + InvalidateFull(); } + #endregion - //SplitMark関係 #region SplitMark.ISite + protected override void OnMouseLeave(EventArgs e) { base.OnMouseLeave(e); - if (_splitMark.IsSplitMarkVisible) + if (_splitMark.IsSplitMarkVisible) { _mouseHandlerManager.EndCapture(); + } _splitMark.ClearMark(); } @@ -722,26 +1015,31 @@ public bool CanSplit { return v == null ? false : GetSplittableViewManager().CanSplit(v); } } + public int SplitClientWidth { get { - return this.ClientSize.Width - (_enabled ? _VScrollBar.Width : 0); + return this.ClientSize.Width - (_verticalScrollBar.Visible ? _verticalScrollBar.Width : 0); } } + public int SplitClientHeight { get { return this.ClientSize.Height; } } + public void OverrideCursor(Cursor cursor) { this.Cursor = cursor; } + public void RevertCursor() { - this.Cursor = GetDocumentCursor(); + this.Cursor = _documentCursor; } public void SplitVertically() { GetSplittableViewManager().SplitVertical(AsControlReplaceableView(), null); } + public void SplitHorizontally() { GetSplittableViewManager().SplitHorizontal(AsControlReplaceableView(), null); } @@ -752,23 +1050,23 @@ public SplitMarkSupport SplitMark { } } - #endregion - private ISplittableViewManager GetSplittableViewManager() { IContentReplaceableView v = AsControlReplaceableView(); - if (v == null) - return null; - else - return (ISplittableViewManager)v.ViewManager.GetAdapter(typeof(ISplittableViewManager)); + return (v == null) ? null : (ISplittableViewManager)v.ViewManager.GetAdapter(typeof(ISplittableViewManager)); } + private IContentReplaceableView AsControlReplaceableView() { IContentReplaceableViewSite site = (IContentReplaceableViewSite)this.GetAdapter(typeof(IContentReplaceableViewSite)); return site == null ? null : site.CurrentContentReplaceableView; } + #endregion + #region ISelectionListener + public void OnSelectionStarted() { } + public void OnSelectionFixed() { if (WindowManagerPlugin.Instance.WindowPreference.OriginalPreference.AutoCopyByLeftButton) { ICommandTarget ct = (ICommandTarget)this.GetAdapter(typeof(ICommandTarget)); @@ -781,193 +1079,272 @@ public void OnSelectionFixed() { else { //Debug.WriteLine("NormalCopy"); IGeneralViewCommands gv = (IGeneralViewCommands)GetAdapter(typeof(IGeneralViewCommands)); - if (gv != null) + if (gv != null) { cm.Execute(gv.Copy, ct); + } } } } - } - #endregion - } + #endregion - /* - * 何行目から何行目までを描画すべきかの情報を収録 - */ - internal class RenderParameter { - private int _linefrom; - private int _linecount; - private Rectangle _targetRect; + #region RenderParameter + + /// + /// Parameters for internal use. + /// + private class RenderParameter { + /// + /// GLines to draw + /// + public GLineChunkSpan GLines { + get; + private set; + } - public int LineFrom { - get { - return _linefrom; + /// + /// Row index on the screen + /// + public int RowIndex { + get; + private set; } - set { - _linefrom = value; + + /// + /// Constructor; + /// + /// GLines to draw + /// row index on the screen + public RenderParameter(GLineChunkSpan glines, int rowIndex) { + this.GLines = glines; + this.RowIndex = rowIndex; } } - public int LineCount { - get { - return _linecount; - } - set { - _linecount = value; + #endregion + + #region DefaultMouseWheelHandler + + /// + /// Default mouse wheel handler. + /// + private class DefaultMouseWheelHandler : DefaultMouseHandler { + private readonly CharacterDocumentViewer _viewer; + + public DefaultMouseWheelHandler(CharacterDocumentViewer viewer) + : base("defaultmousewheel") { + _viewer = viewer; } - } - public Rectangle TargetRect { - get { - return _targetRect; + + public override UIHandleResult OnMouseWheel(MouseEventArgs args) { + const int WHEEL_DELTA = 120; + const int ROWS_PER_NOTCH = 3; + int scroll = (args.Delta / WHEEL_DELTA) * ROWS_PER_NOTCH; + + _viewer.ScrollDocument(-scroll); + + return UIHandleResult.Stop; } - set { - _targetRect = value; + + public override void Reset() { } } - } - //テキスト選択のハンドラ - internal class TextSelectionUIHandler : DefaultMouseHandler { - private CharacterDocumentViewer _viewer; - public TextSelectionUIHandler(CharacterDocumentViewer v) - : base("textselection") { - _viewer = v; - } + #endregion - public override UIHandleResult OnMouseDown(MouseEventArgs args) { - if (args.Button != MouseButtons.Left || !_viewer.EnabledEx) - return UIHandleResult.Pass; + #region TextSelectionUIHandler + + /// + /// Mouse handler for the text selection. + /// + private class TextSelectionUIHandler : DefaultMouseHandler { + + private readonly CharacterDocumentViewer _viewer; + + // previous mouse position + private int _prevMouseX; + private int _prevMouseY; + + public TextSelectionUIHandler(CharacterDocumentViewer v) + : base("textselection") { + _viewer = v; + } + + public override UIHandleResult OnMouseDown(MouseEventArgs args) { + if (args.Button != MouseButtons.Left) { + return UIHandleResult.Pass; + } + + TextSelection sel = _viewer._textSelection; + + if (!sel.CanHandleMouseDown) { + return UIHandleResult.Pass; + } + + if (!_viewer.Focused) { + _viewer.Focus(); + } - //テキスト選択ではないのでちょっと柄悪いが。UserControl->Controlの置き換えに伴う - if (!_viewer.Focused) - _viewer.Focus(); - - - CharacterDocument document = _viewer.CharacterDocument; - lock (document) { - int col, row; - _viewer.MousePosToTextPos(args.X, args.Y, out col, out row); - int target_id = _viewer.GetTopLine().ID + row; - TextSelection sel = _viewer.TextSelection; - if (sel.State == SelectionState.Fixed) - sel.Clear(); //変なところでMouseDownしたとしてもClearだけはする - if (target_id <= document.LastLineNumber) { - //if(InFreeSelectionMode) ExitFreeSelectionMode(); - //if(InAutoSelectionMode) ExitAutoSelectionMode(); - RangeType rt; - //Debug.WriteLine(String.Format("MouseDown {0} {1}", sel.State, sel.PivotType)); - - //同じ場所でポチポチと押すとChar->Word->Line->Charとモード変化する - if (sel.StartX != args.X || sel.StartY != args.Y) - rt = RangeType.Char; - else - rt = sel.PivotType == RangeType.Char ? RangeType.Word : sel.PivotType == RangeType.Word ? RangeType.Line : RangeType.Char; - - //マウスを動かしていなくても、MouseDownとともにMouseMoveが来てしまうようだ - GLine tl = document.FindLine(target_id); - sel.StartSelection(tl, col, rt, args.X, args.Y); + TextSelection.Mode? selModeOverride = + ((Control.ModifierKeys & Keys.Control) != Keys.None) ? TextSelection.Mode.Word : + ((Control.ModifierKeys & Keys.Shift) != Keys.None) ? TextSelection.Mode.Line : + (TextSelection.Mode?)null; + + ICharacterDocument doc = _viewer._document; + if (doc != null) { + lock (doc.SyncRoot) { + int col, row; + _viewer.ClientPosToTextPos(args.X, args.Y, out col, out row); + if (row < 0) { + sel.Clear(); + } + else { + col = Math.Max(col, 0); + int targetRowID = _viewer.GetRowID(row); + doc.Apply(targetRowID, line => { + if (line != null) { + sel.OnMouseDown(targetRowID, line, col, selModeOverride, args.X, args.Y); + } + else { + sel.Clear(); + } + }); + } + } } + + _prevMouseX = args.X; + _prevMouseY = args.Y; + + // we need a full repaint because the old selection may be cleared + _viewer.Invalidate(); + + return UIHandleResult.Capture; } - _viewer.Invalidate(); //NOTE 選択状態に変化のあった行のみ更新すればなおよし - return UIHandleResult.Capture; - } - public override UIHandleResult OnMouseMove(MouseEventArgs args) { - if (args.Button != MouseButtons.Left) - return UIHandleResult.Pass; - TextSelection sel = _viewer.TextSelection; - if (sel.State == SelectionState.Fixed || sel.State == SelectionState.Empty) - return UIHandleResult.Pass; - //クリックだけでもなぜかMouseDownの直後にMouseMoveイベントが来るのでこのようにしてガード。でないと単発クリックでも選択状態になってしまう - if (sel.StartX == args.X && sel.StartY == args.Y) + + public override UIHandleResult OnMouseMove(MouseEventArgs args) { + if (args.Button != MouseButtons.Left) { + return UIHandleResult.Pass; + } + + TextSelection sel = _viewer._textSelection; + + if (!sel.CanHandleMouseMove) { + return UIHandleResult.Pass; + } + + if (args.X == _prevMouseX && args.Y == _prevMouseY) { + return UIHandleResult.Capture; + } + + TextSelection.Mode? selModeOverride = + ((Control.ModifierKeys & Keys.Control) != Keys.None) ? TextSelection.Mode.Word : + ((Control.ModifierKeys & Keys.Shift) != Keys.None) ? TextSelection.Mode.Line : + (TextSelection.Mode?)null; + + ICharacterDocument doc = _viewer._document; + if (doc != null) { + lock (doc.SyncRoot) { + int row, col; + _viewer.ClientPosToTextPos(args.X, args.Y, out col, out row); + int targetRowID = _viewer.GetRowID(row); + RowIDSpan rawIDSpan = doc.GetRowIDSpan(); + if (targetRowID < rawIDSpan.Start) { + targetRowID = rawIDSpan.Start; + col = 0; + } + else { + int lastRowID = rawIDSpan.Start + rawIDSpan.Length - 1; + if (targetRowID > lastRowID) { + targetRowID = lastRowID; + col = Int32.MaxValue; // fix later + } + } + + doc.Apply(targetRowID, line => { + if (line != null) { + int fixedCol = (col == Int32.MaxValue) ? line.DisplayLength : col; + sel.OnMouseMove(targetRowID, line, fixedCol, selModeOverride); + } + }); + + _viewer.ScrollToVisible(targetRowID); + } + } + + _prevMouseX = args.X; + _prevMouseY = args.Y; + + _viewer.Invalidate(); + return UIHandleResult.Capture; + } + + public override UIHandleResult OnMouseUp(MouseEventArgs args) { + if (args.Button == MouseButtons.Left) { + TextSelection sel = _viewer._textSelection; - CharacterDocument document = _viewer.CharacterDocument; - lock (document) { - int topline_id = _viewer.GetTopLine().ID; - SizeF pitch = _viewer.GetRenderProfile().Pitch; - int row, col; - _viewer.MousePosToTextPos_AllowNegative(args.X, args.Y, out col, out row); - int viewheight = (int)Math.Floor(_viewer.ClientSize.Height / pitch.Width); - int target_id = topline_id + row; - - GLine target_line = document.FindLineOrEdge(target_id); - TextSelection.TextPoint point = sel.ConvertSelectionPosition(target_line, col); - - point.Line = RuntimeUtil.AdjustIntRange(point.Line, document.FirstLineNumber, document.LastLineNumber); - - if (_viewer.VScrollBar.Enabled) { //スクロール可能なときは - VScrollBar vsc = _viewer.VScrollBar; - if (target_id < topline_id) //前方スクロール - vsc.Value = point.Line - document.FirstLineNumber; - else if (point.Line >= topline_id + vsc.LargeChange) { //後方スクロール - int newval = point.Line - document.FirstLineNumber - vsc.LargeChange + 1; - if (newval < 0) - newval = 0; - if (newval > vsc.Maximum - vsc.LargeChange) - newval = vsc.Maximum - vsc.LargeChange + 1; - vsc.Value = newval; + if (sel.CanHandleMouseUp) { + sel.OnMouseUp(); } } - else { //スクロール不可能なときは見えている範囲で - point.Line = RuntimeUtil.AdjustIntRange(point.Line, topline_id, topline_id + viewheight - 1); - } //ここさぼっている - //Debug.WriteLine(String.Format("MouseMove {0} {1} {2}", sel.State, sel.PivotType, args.X)); - RangeType rt = sel.PivotType; - if ((Control.ModifierKeys & Keys.Control) != Keys.None) - rt = RangeType.Word; - else if ((Control.ModifierKeys & Keys.Shift) != Keys.None) - rt = RangeType.Line; - - GLine tl = document.FindLine(point.Line); - sel.ExpandTo(tl, point.Column, rt); - } - _viewer.Invalidate(); //TODO 選択状態に変化のあった行のみ更新するようにすればなおよし - return UIHandleResult.Capture; - - } - public override UIHandleResult OnMouseUp(MouseEventArgs args) { - TextSelection sel = _viewer.TextSelection; - if (args.Button == MouseButtons.Left) { - if (sel.State == SelectionState.Expansion || sel.State == SelectionState.Pivot) - sel.FixSelection(); - else - sel.Clear(); + + return _viewer._mouseHandlerManager.CapturingHandler == this ? + UIHandleResult.EndCapture : UIHandleResult.Pass; } - return _viewer.MouseHandlerManager.CapturingHandler == this ? UIHandleResult.EndCapture : UIHandleResult.Pass; + public override void Reset() { + TextSelection sel = _viewer._textSelection; + if (sel != null) { + sel.Clear(); + } + } } - } - //スプリットマークのハンドラ - internal class SplitMarkUIHandler : DefaultMouseHandler { - private SplitMarkSupport _splitMark; - public SplitMarkUIHandler(SplitMarkSupport split) - : base("splitmark") { - _splitMark = split; - } + #endregion - public override UIHandleResult OnMouseDown(MouseEventArgs args) { - return UIHandleResult.Pass; - } - public override UIHandleResult OnMouseMove(MouseEventArgs args) { - bool v = _splitMark.IsSplitMarkVisible; - if (v || WindowManagerPlugin.Instance.WindowPreference.OriginalPreference.ViewSplitModifier == Control.ModifierKeys) - _splitMark.OnMouseMove(args); - //直前にキャプチャーしていたらEndCapture - return _splitMark.IsSplitMarkVisible ? UIHandleResult.Capture : v ? UIHandleResult.EndCapture : UIHandleResult.Pass; - } - public override UIHandleResult OnMouseUp(MouseEventArgs args) { - bool visible = _splitMark.IsSplitMarkVisible; - if (visible) { - //例えば、マーク表示位置から選択したいような場合を考慮し、マーク上で右クリックすると選択が消えるようにする。 - _splitMark.OnMouseUp(args); - return UIHandleResult.EndCapture; + #region SplitMarkUIHandler + + /// + /// Mouse handler for the splitter + /// + private class SplitMarkUIHandler : DefaultMouseHandler { + + private readonly SplitMarkSupport _splitMark; + + public SplitMarkUIHandler(SplitMarkSupport split) + : base("splitmark") { + _splitMark = split; + } + + public override UIHandleResult OnMouseDown(MouseEventArgs args) { + return UIHandleResult.Pass; } - else + + public override UIHandleResult OnMouseMove(MouseEventArgs args) { + bool isSplitMarkVisible = _splitMark.IsSplitMarkVisible; + if (isSplitMarkVisible || WindowManagerPlugin.Instance.WindowPreference.OriginalPreference.ViewSplitModifier == Control.ModifierKeys) { + _splitMark.OnMouseMove(args); + } + return _splitMark.IsSplitMarkVisible ? UIHandleResult.Capture : isSplitMarkVisible ? UIHandleResult.EndCapture : UIHandleResult.Pass; + } + + public override UIHandleResult OnMouseUp(MouseEventArgs args) { + bool isSplitMarkVisible = _splitMark.IsSplitMarkVisible; + if (isSplitMarkVisible) { + _splitMark.OnMouseUp(args); + return UIHandleResult.EndCapture; + } return UIHandleResult.Pass; + } + + public override void Reset() { + _splitMark.ClearMark(); + } } - } + #endregion + } } diff --git a/Core/CharacterDocumentViewer_Old.cs b/Core/CharacterDocumentViewer_Old.cs new file mode 100644 index 00000000..8b041dad --- /dev/null +++ b/Core/CharacterDocumentViewer_Old.cs @@ -0,0 +1,967 @@ +// Copyright 2004-2017 The Poderosa Project. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if DEBUG +#define ONPAINT_TIME_MEASUREMENT +#endif + +using Poderosa.Commands; +using Poderosa.Document; +using Poderosa.Forms; +using Poderosa.Sessions; +using Poderosa.UI; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Drawing; +using System.Windows.Forms; + +namespace Poderosa.View { + /* + * CharacterDocumentの表示を行うコントロール。機能としては次がある。 + *  縦方向のみスクロールバーをサポート + *  再描画の最適化 + *  キャレットの表示。ただしキャレットを適切に移動する機能は含まれない + * + *  今後あってもいいかもしれない機能は、行間やPadding(HTML用語の)、行番号表示といったところ + */ + /// + /// + /// + /// + public class CharacterDocumentViewer_Old : Control, IPoderosaControl, ISelectionListener, SplitMarkSupport.ISite { + + public const int BORDER = 2; //内側の枠線のサイズ + internal const int TIMER_INTERVAL = 50; //再描画最適化とキャレット処理を行うタイマーの間隔 + + private CharacterDocument_Old _document; + private bool _errorRaisedInDrawing; + private readonly List _transientLines; //再描画するGLineを一時的に保管する + private readonly List _glinePool; + private bool _requiresPeriodicRedraw; + private TextSelection_Old _textSelection; + private SplitMarkSupport _splitMark; + private bool _enabled; //ドキュメントがアタッチされていないときを示す 変更するときはEnabledExプロパティで! + + private Cursor _documentCursor = Cursors.IBeam; + + protected MouseHandlerManager _mouseHandlerManager; + protected VScrollBar _VScrollBar; + protected bool _enableAutoScrollBarAdjustment; //リサイズ時に自動的に_VScrollBarの値を調整するかどうか + protected Caret _caret; + protected ITimerSite _timer; + protected int _tickCount; + + public delegate void OnPaintTimeObserver(Stopwatch s); + +#if ONPAINT_TIME_MEASUREMENT + private OnPaintTimeObserver _onPaintTimeObserver = null; +#endif + + public CharacterDocumentViewer_Old() { + _enableAutoScrollBarAdjustment = true; + _transientLines = new List(); + _glinePool = new List(); + InitializeComponent(); + //SetStyle(ControlStyles.UserPaint|ControlStyles.AllPaintingInWmPaint|ControlStyles.DoubleBuffer, true); + this.DoubleBuffered = true; + _caret = new Caret(); + + _splitMark = new SplitMarkSupport(this, this); + Pen p = new Pen(SystemColors.ControlDark); + p.DashStyle = System.Drawing.Drawing2D.DashStyle.Dot; + _splitMark.Pen = p; + + _textSelection = new TextSelection_Old(this); + _textSelection.AddSelectionListener(this); + + _mouseHandlerManager = new MouseHandlerManager(); + _mouseHandlerManager.AddLastHandler(new TextSelectionUIHandler(this)); + _mouseHandlerManager.AddLastHandler(new SplitMarkUIHandler(_splitMark)); + _mouseHandlerManager.AttachControl(this); + + SetStyle(ControlStyles.SupportsTransparentBackColor, true); + } + + public CharacterDocument_Old CharacterDocument { + get { + return _document; + } + } + internal TextSelection_Old TextSelection { + get { + return _textSelection; + } + } + public ITextSelection ITextSelection { + get { + return _textSelection; + } + } + internal MouseHandlerManager MouseHandlerManager { + get { + return _mouseHandlerManager; + } + } + + public Caret Caret { + get { + return _caret; + } + } + + public bool EnabledEx { + get { + return _enabled; + } + set { + _enabled = value; + _VScrollBar.Visible = value; //スクロールバーとは連動 + _splitMark.Pen.Color = value ? SystemColors.ControlDark : SystemColors.Window; //このBackColorと逆で + this.Cursor = GetDocumentCursor(); //Splitter.ISiteを援用 + this.BackColor = value ? GetRenderProfile().BackColor : SystemColors.ControlDark; + this.ImeMode = value ? ImeMode.NoControl : ImeMode.Disable; + } + } + public VScrollBar VScrollBar { + get { + return _VScrollBar; + } + } + + public void ShowVScrollBar() { + _VScrollBar.Visible = true; + } + + public void HideVScrollBar() { + _VScrollBar.Visible = false; + } + + public void SetDocumentCursor(Cursor cursor) { + if (this.InvokeRequired) { + this.BeginInvoke((MethodInvoker)delegate() { + SetDocumentCursor(cursor); + }); + return; + } + _documentCursor = cursor; + if (_enabled) + this.Cursor = cursor; + } + + public void ResetDocumentCursor() { + if (this.InvokeRequired) { + this.BeginInvoke((MethodInvoker)delegate() { + ResetDocumentCursor(); + }); + return; + } + SetDocumentCursor(Cursors.IBeam); + } + + private Cursor GetDocumentCursor() { + return _enabled ? _documentCursor : Cursors.Default; + } + + + #region IAdaptable + public virtual IAdaptable GetAdapter(Type adapter) { + return SessionManagerPlugin.Instance.PoderosaWorld.AdapterManager.GetAdapter(this, adapter); + } + #endregion + + #region OnPaint time measurement + + public void SetOnPaintTimeObserver(OnPaintTimeObserver observer) { +#if ONPAINT_TIME_MEASUREMENT + _onPaintTimeObserver = observer; +#endif + } + + #endregion + + //派生型であることを強制することなどのためにoverrideすることを許す + public virtual void SetContent(CharacterDocument_Old doc) { + RenderProfile prof = GetRenderProfile(); + this.BackColor = prof.BackColor; + _document = doc; + this.EnabledEx = doc != null; + + if (_timer != null) + _timer.Close(); + if (this.EnabledEx) { + _timer = WindowManagerPlugin.Instance.CreateTimer(TIMER_INTERVAL, new TimerDelegate(OnWindowManagerTimer)); + _tickCount = 0; + } + + if (_enableAutoScrollBarAdjustment) + AdjustScrollBar(); + } + + //タイマーの受信 + private void CaretTick() { + if (_enabled) { + // Note: + // Currently, blinking status of the caret is used also for displaying "blink" characters. + // So the blinking status of the caret have to be updated here even if the caret blinking was not enabled. + _caret.Tick(); + if (_requiresPeriodicRedraw) { + _requiresPeriodicRedraw = false; + _document.InvalidatedRegion.InvalidatedAll = true; + } + else { + _document.InvalidatedRegion.InvalidateLine(GetTopLine().ID + _caret.Y); + } + InvalidateEx(); + } + } + protected virtual void OnWindowManagerTimer() { + //タイマーはTIMER_INTERVALごとにカウントされるので。 + int q = Math.Max(1, WindowManagerPlugin.Instance.WindowPreference.OriginalPreference.CaretInterval / TIMER_INTERVAL); + _tickCount = (_tickCount + 1) % q; + if (_tickCount == 0) { + CaretTick(); + } + } + + //自己サイズからScrollBarを適切にいじる + public void AdjustScrollBar() { + if (_document == null) + return; + RenderProfile prof = GetRenderProfile(); + float ch = prof.Pitch.Height + prof.LineSpacing; + int largechange = (int)Math.Floor((this.ClientSize.Height - BORDER * 2 + prof.LineSpacing) / ch); //きちんと表示できる行数をLargeChangeにセット + int current = GetTopLine().ID - _document.FirstLineNumber; + int size = Math.Max(_document.Size, current + largechange); + if (size <= largechange) { + _VScrollBar.Enabled = false; + } + else { + _VScrollBar.Enabled = true; + _VScrollBar.LargeChange = largechange; + _VScrollBar.Maximum = size - 1; //この-1が必要なのが妙な仕様だ + } + } + + //このあたりの処置定まっていない + private RenderProfile _privateRenderProfile = null; + public void SetPrivateRenderProfile(RenderProfile prof) { + _privateRenderProfile = prof; + } + + //overrideして別の方法でRenderProfileを取得することもある + public virtual RenderProfile GetRenderProfile() { + return _privateRenderProfile; + } + + protected virtual void CommitTransientScrollBar() { + //ViewerはUIによってしか切り取れないからここでは何もしなくていい + } + + //行数で表示可能な高さを返す + protected virtual int GetHeightInLines() { + RenderProfile prof = GetRenderProfile(); + float ch = prof.Pitch.Height + prof.LineSpacing; + int height = (int)Math.Floor((this.ClientSize.Height - BORDER * 2 + prof.LineSpacing) / ch); + return (height > 0) ? height : 0; + } + + //_documentのうちどれを先頭(1行目)として表示するかを返す + public virtual GLine GetTopLine() { + return _document.FindLine(_document.FirstLine.ID + _VScrollBar.Value); + } + + public void MousePosToTextPos(int mouseX, int mouseY, out int textX, out int textY) { + SizeF pitch = GetRenderProfile().Pitch; + textX = RuntimeUtil.AdjustIntRange((int)Math.Floor((mouseX - CharacterDocumentViewer_Old.BORDER) / pitch.Width), 0, Int32.MaxValue); + textY = RuntimeUtil.AdjustIntRange((int)Math.Floor((mouseY - CharacterDocumentViewer_Old.BORDER) / (pitch.Height + GetRenderProfile().LineSpacing)), 0, Int32.MaxValue); + } + + public void MousePosToTextPos_AllowNegative(int mouseX, int mouseY, out int textX, out int textY) { + SizeF pitch = GetRenderProfile().Pitch; + textX = (int)Math.Floor((mouseX - CharacterDocumentViewer_Old.BORDER) / pitch.Width); + textY = (int)Math.Floor((mouseY - CharacterDocumentViewer_Old.BORDER) / (pitch.Height + GetRenderProfile().LineSpacing)); + } + + //_VScrollBar.ValueChangedイベント + protected virtual void VScrollBarValueChanged() { + if (_enableAutoScrollBarAdjustment) + Invalidate(); + } + + //キャレットの座標設定、表示の可否を設定 + protected virtual void AdjustCaret(Caret caret) { + } + + //_documentの更新状況を見て適切な領域のControl.Invalidate()を呼ぶ。 + //また、コントロールを所有していないスレッドから呼んでもOKなようになっている。 + protected void InvalidateEx() { + if (this.IsDisposed) + return; + bool full_invalidate = true; + Rectangle r = new Rectangle(); + + if (_document != null) { + if (_document.InvalidatedRegion.IsEmpty) + return; + InvalidatedRegion_Old rgn = _document.InvalidatedRegion.GetCopyAndReset(); + if (rgn.IsEmpty) + return; + if (!rgn.InvalidatedAll) { + full_invalidate = false; + r.X = 0; + r.Width = this.ClientSize.Width; + int topLine = GetTopLine().ID; + int y1 = rgn.LineIDStart - topLine; + int y2 = rgn.LineIDEnd + 1 - topLine; + RenderProfile prof = GetRenderProfile(); + r.Y = BORDER + (int)(y1 * (prof.Pitch.Height + prof.LineSpacing)); + r.Height = (int)((y2 - y1) * (prof.Pitch.Height + prof.LineSpacing)) + 1; + } + } + + if (this.InvokeRequired) { + if (full_invalidate) + this.BeginInvoke((MethodInvoker)delegate() { + Invalidate(); + }); + else { + this.BeginInvoke((MethodInvoker)delegate() { + Invalidate(r); + }); + } + } + else { + if (full_invalidate) + Invalidate(); + else + Invalidate(r); + } + } + + private void InitializeComponent() { + this.SuspendLayout(); + this._VScrollBar = new System.Windows.Forms.VScrollBar(); + // + // _VScrollBar + // + this._VScrollBar.Enabled = false; + //this._VScrollBar.Dock = DockStyle.Right; + this._VScrollBar.Anchor = AnchorStyles.Right | AnchorStyles.Top | AnchorStyles.Bottom; + this._VScrollBar.LargeChange = 1; + this._VScrollBar.Minimum = 0; + this._VScrollBar.Value = 0; + this._VScrollBar.Maximum = 2; + this._VScrollBar.Name = "_VScrollBar"; + this._VScrollBar.TabIndex = 0; + this._VScrollBar.TabStop = false; + this._VScrollBar.Cursor = Cursors.Default; + this._VScrollBar.Visible = false; + this._VScrollBar.ValueChanged += delegate(object sender, EventArgs args) { + VScrollBarValueChanged(); + }; + this.Controls.Add(_VScrollBar); + + this.ImeMode = ImeMode.NoControl; + //this.BorderStyle = BorderStyle.Fixed3D; //IMEPROBLEM + AdjustScrollBarPosition(); + this.ResumeLayout(); + } + + protected override void Dispose(bool disposing) { + base.Dispose(disposing); + if (disposing) { + _caret.Dispose(); + if (_timer != null) + _timer.Close(); + _splitMark.Pen.Dispose(); + } + } + + protected override void OnResize(EventArgs e) { + base.OnResize(e); + if (_VScrollBar.Visible) + AdjustScrollBarPosition(); + if (_enableAutoScrollBarAdjustment && _enabled) + AdjustScrollBar(); + + Invalidate(); + } + + //NOTE 自分のDockがTopかLeftのとき、スクロールバーの位置が追随してくれないみたい + private void AdjustScrollBarPosition() { + _VScrollBar.Height = this.ClientSize.Height; + _VScrollBar.Left = this.ClientSize.Width - _VScrollBar.Width; + } + + //描画の本体 + protected override sealed void OnPaint(PaintEventArgs e) { +#if ONPAINT_TIME_MEASUREMENT + Stopwatch onPaintSw = (_onPaintTimeObserver != null) ? Stopwatch.StartNew() : null; +#endif + + base.OnPaint(e); + + try { + if (_document != null) + ShowVScrollBar(); + else + HideVScrollBar(); + + if (_enabled && !this.DesignMode) { + Rectangle clip = e.ClipRectangle; + Graphics g = e.Graphics; + RenderProfile profile = GetRenderProfile(); + + // determine background color of the view + Color backColor = _document.DetermineBackgroundColor(profile); + + if (this.BackColor != backColor) + this.BackColor = backColor; // set background color of the view + + // draw background image if it is required. + Image img = _document.DetermineBackgroundImage(profile); + if (img != null) { + DrawBackgroundImage(g, img, profile.ImageStyle, clip); + } + + //描画用にテンポラリのGLineを作り、描画中にdocumentをロックしないようにする + //!!ここは実行頻度が高いのでnewを毎回するのは避けたいところだ + RenderParameter param = new RenderParameter(); + _caret.Enabled = _caret.Enabled && this.Focused; //TODO さらにIME起動中はキャレットを表示しないように. TerminalControlだったらAdjustCaretでIMEをみてるので問題はない + lock (_document) { + CommitTransientScrollBar(); + BuildTransientDocument(e, param); + } + + DrawLines(g, param, backColor); + + if (_caret.Enabled && (!_caret.Blink || _caret.IsActiveTick)) { //点滅しなければEnabledによってのみ決まる + if (_caret.Style == CaretType.Line) + DrawBarCaret(g, param, _caret.X, _caret.Y); + else if (_caret.Style == CaretType.Underline) + DrawUnderLineCaret(g, param, _caret.X, _caret.Y); + } + } + //マークの描画 + _splitMark.OnPaint(e); + } + catch (Exception ex) { + if (!_errorRaisedInDrawing) { //この中で一度例外が発生すると繰り返し起こってしまうことがままある。なので初回のみ表示してとりあえず切り抜ける + _errorRaisedInDrawing = true; + RuntimeUtil.ReportException(ex); + } + } + +#if ONPAINT_TIME_MEASUREMENT + if (onPaintSw != null) { + onPaintSw.Stop(); + if (_onPaintTimeObserver != null) { + _onPaintTimeObserver(onPaintSw); + } + } +#endif + } + + private void BuildTransientDocument(PaintEventArgs e, RenderParameter param) { + Rectangle clip = e.ClipRectangle; + RenderProfile profile = GetRenderProfile(); + _transientLines.Clear(); + + //Win32.SystemMetrics sm = GEnv.SystemMetrics; + //param.TargetRect = new Rectangle(sm.ControlBorderWidth+1, sm.ControlBorderHeight, + // this.Width - _VScrollBar.Width - sm.ControlBorderWidth + 8, //この8がない値が正当だが、.NETの文字サイズ丸め問題のため行の最終文字が表示されないことがある。これを回避するためにちょっと増やす + // this.Height - sm.ControlBorderHeight); + param.TargetRect = this.ClientRectangle; + + int offset1 = (int)Math.Floor((clip.Top - BORDER) / (profile.Pitch.Height + profile.LineSpacing)); + if (offset1 < 0) + offset1 = 0; + param.LineFrom = offset1; + int offset2 = (int)Math.Floor((clip.Bottom - BORDER) / (profile.Pitch.Height + profile.LineSpacing)); + if (offset2 < 0) + offset2 = 0; + + param.LineCount = offset2 - offset1 + 1; + //Debug.WriteLine(String.Format("{0} {1} ", param.LineFrom, param.LineCount)); + + int topline_id = GetTopLine().ID; + GLine l = _document.FindLineOrNull(topline_id + param.LineFrom); + if (l != null) { + int poolIndex = 0; + for (int i = 0; i < param.LineCount; i++) { + GLine cloned; + if (poolIndex < _glinePool.Count) { + cloned = _glinePool[poolIndex]; + poolIndex++; + cloned.CopyFrom(l); + } + else { + cloned = l.Clone(); + cloned.NextLine = cloned.PrevLine = null; + _glinePool.Add(cloned); // store for next use + poolIndex++; + } + + _transientLines.Add(cloned); + l = l.NextLine; + if (l == null) + break; + } + } + + //以下、_transientLinesにはparam.LineFromから示される値が入っていることに注意 + + //選択領域の描画 + if (!_textSelection.IsEmpty) { + TextSelection_Old.TextPoint from = _textSelection.HeadPoint; + TextSelection_Old.TextPoint to = _textSelection.TailPoint; + l = _document.FindLineOrNull(from.Line); + GLine t = _document.FindLineOrNull(to.Line); + if (l != null && t != null) { //本当はlがnullではいけないはずだが、それを示唆するバグレポートがあったので念のため + t = t.NextLine; + int pos = from.Column; //たとえば左端を越えてドラッグしたときの選択範囲は前行末になるので pos==TerminalWidthとなるケースがある。 + do { + int index = l.ID - (topline_id + param.LineFrom); + if (pos >= 0 && pos < l.DisplayLength && index >= 0 && index < _transientLines.Count) { + if (l.ID == to.Line) { + if (pos != to.Column) { + _transientLines[index].SetSelection(pos, to.Column); + } + } + else { + _transientLines[index].SetSelection(pos, l.DisplayLength); + } + } + pos = 0; //2行目からの選択は行頭から + l = l.NextLine; + } while (l != t); + } + } + + AdjustCaret(_caret); + _caret.Enabled = _caret.Enabled && (param.LineFrom <= _caret.Y && _caret.Y < param.LineFrom + param.LineCount); + + //Caret画面外にあるなら処理はしなくてよい。2番目の条件は、Attach-ResizeTerminalの流れの中でこのOnPaintを実行した場合にTerminalHeight>lines.Countになるケースがあるのを防止するため + if (_caret.Enabled) { + //ヒクヒク問題のため、キャレットを表示しないときでもこの操作は省けない + if (_caret.Style == CaretType.Box) { + int y = _caret.Y - param.LineFrom; + if (y >= 0 && y < _transientLines.Count) { + _transientLines[y].SetCursor(_caret.X); + } + } + } + } + + private void DrawLines(Graphics g, RenderParameter param, Color baseBackColor) { + RenderProfile prof = GetRenderProfile(); + Caret caret = _caret; + //Rendering Core + if (param.LineFrom <= _document.LastLineNumber) { + IntPtr hdc = g.GetHdc(); + try { + float y = (prof.Pitch.Height + prof.LineSpacing) * param.LineFrom + BORDER; + for (int i = 0; i < _transientLines.Count; i++) { + GLine line = _transientLines[i]; + line.Render(hdc, prof, caret, baseBackColor, BORDER, (int)y); + if (line.IsPeriodicRedrawRequired()) { + _requiresPeriodicRedraw = true; + } + y += prof.Pitch.Height + prof.LineSpacing; + } + } + finally { + g.ReleaseHdc(hdc); + } + } + } + + private void DrawBarCaret(Graphics g, RenderParameter param, int x, int y) { + RenderProfile profile = GetRenderProfile(); + PointF pt1 = new PointF(profile.Pitch.Width * x + BORDER, (profile.Pitch.Height + profile.LineSpacing) * y + BORDER + 2); + PointF pt2 = new PointF(pt1.X, pt1.Y + profile.Pitch.Height - 2); + Pen p = _caret.ToPen(profile); + g.DrawLine(p, pt1, pt2); + pt1.X += 1; + pt2.X += 1; + g.DrawLine(p, pt1, pt2); + } + private void DrawUnderLineCaret(Graphics g, RenderParameter param, int x, int y) { + RenderProfile profile = GetRenderProfile(); + PointF pt1 = new PointF(profile.Pitch.Width * x + BORDER + 2, (profile.Pitch.Height + profile.LineSpacing) * y + BORDER + profile.Pitch.Height); + PointF pt2 = new PointF(pt1.X + profile.Pitch.Width - 2, pt1.Y); + Pen p = _caret.ToPen(profile); + g.DrawLine(p, pt1, pt2); + pt1.Y += 1; + pt2.Y += 1; + g.DrawLine(p, pt1, pt2); + } + + private void DrawBackgroundImage(Graphics g, Image img, ImageStyle style, Rectangle clip) { + if (style == ImageStyle.HorizontalFit) { + this.DrawBackgroundImage_Scaled(g, img, clip, true, false); + } + else if (style == ImageStyle.VerticalFit) { + this.DrawBackgroundImage_Scaled(g, img, clip, false, true); + } + else if (style == ImageStyle.Scaled) { + this.DrawBackgroundImage_Scaled(g, img, clip, true, true); + } + else { + DrawBackgroundImage_Normal(g, img, style, clip); + } + } + private void DrawBackgroundImage_Scaled(Graphics g, Image img, Rectangle clip, bool fitWidth, bool fitHeight) { + Size clientSize = this.ClientSize; + PointF drawPoint; + SizeF drawSize; + + if (fitWidth && fitHeight) { + drawSize = new SizeF(clientSize.Width - _VScrollBar.Width, clientSize.Height); + drawPoint = new PointF(0, 0); + } + else if (fitWidth) { + float drawWidth = clientSize.Width - _VScrollBar.Width; + float drawHeight = drawWidth * img.Height / img.Width; + drawSize = new SizeF(drawWidth, drawHeight); + drawPoint = new PointF(0, (clientSize.Height - drawSize.Height) / 2f); + } + else { + float drawHeight = clientSize.Height; + float drawWidth = drawHeight * img.Width / img.Height; + drawSize = new SizeF(drawWidth, drawHeight); + drawPoint = new PointF((clientSize.Width - _VScrollBar.Width - drawSize.Width) / 2f, 0); + } + + Region oldClip = g.Clip; + using (Region newClip = new Region(clip)) { + g.Clip = newClip; + g.DrawImage(img, new RectangleF(drawPoint, drawSize), new RectangleF(0, 0, img.Width, img.Height), GraphicsUnit.Pixel); + g.Clip = oldClip; + } + } + + private void DrawBackgroundImage_Normal(Graphics g, Image img, ImageStyle style, Rectangle clip) { + int offset_x, offset_y; + if (style == ImageStyle.Center) { + offset_x = (this.Width - _VScrollBar.Width - img.Width) / 2; + offset_y = (this.Height - img.Height) / 2; + } + else { + offset_x = (style == ImageStyle.TopLeft || style == ImageStyle.BottomLeft) ? 0 : (this.ClientSize.Width - _VScrollBar.Width - img.Width); + offset_y = (style == ImageStyle.TopLeft || style == ImageStyle.TopRight) ? 0 : (this.ClientSize.Height - img.Height); + } + //if(offset_x < BORDER) offset_x = BORDER; + //if(offset_y < BORDER) offset_y = BORDER; + + //画像内のコピー開始座標 + Rectangle target = Rectangle.Intersect(new Rectangle(clip.Left - offset_x, clip.Top - offset_y, clip.Width, clip.Height), new Rectangle(0, 0, img.Width, img.Height)); + if (target != Rectangle.Empty) + g.DrawImage(img, new Rectangle(target.Left + offset_x, target.Top + offset_y, target.Width, target.Height), target, GraphicsUnit.Pixel); + } + + //IPoderosaControl + public Control AsControl() { + return this; + } + + //マウスホイールでのスクロール + protected virtual void OnMouseWheelCore(MouseEventArgs e) { + if (!this.EnabledEx) + return; + + int d = e.Delta / 120; //開発環境だとDeltaに120。これで1か-1が入るはず + d *= 3; //可変にしてもいいかも + + int newval = _VScrollBar.Value - d; + if (newval < 0) + newval = 0; + if (newval > _VScrollBar.Maximum - _VScrollBar.LargeChange) + newval = _VScrollBar.Maximum - _VScrollBar.LargeChange + 1; + _VScrollBar.Value = newval; + } + + protected override void OnMouseWheel(MouseEventArgs e) { + base.OnMouseWheel(e); + OnMouseWheelCore(e); + } + + + //SplitMark関係 + #region SplitMark.ISite + protected override void OnMouseLeave(EventArgs e) { + base.OnMouseLeave(e); + if (_splitMark.IsSplitMarkVisible) + _mouseHandlerManager.EndCapture(); + _splitMark.ClearMark(); + } + + public bool CanSplit { + get { + IContentReplaceableView v = AsControlReplaceableView(); + return v == null ? false : GetSplittableViewManager().CanSplit(v); + } + } + public int SplitClientWidth { + get { + return this.ClientSize.Width - (_enabled ? _VScrollBar.Width : 0); + } + } + public int SplitClientHeight { + get { + return this.ClientSize.Height; + } + } + public void OverrideCursor(Cursor cursor) { + this.Cursor = cursor; + } + public void RevertCursor() { + this.Cursor = GetDocumentCursor(); + } + + public void SplitVertically() { + GetSplittableViewManager().SplitVertical(AsControlReplaceableView(), null); + } + public void SplitHorizontally() { + GetSplittableViewManager().SplitHorizontal(AsControlReplaceableView(), null); + } + + public SplitMarkSupport SplitMark { + get { + return _splitMark; + } + } + + #endregion + + private ISplittableViewManager GetSplittableViewManager() { + IContentReplaceableView v = AsControlReplaceableView(); + if (v == null) + return null; + else + return (ISplittableViewManager)v.ViewManager.GetAdapter(typeof(ISplittableViewManager)); + } + private IContentReplaceableView AsControlReplaceableView() { + IContentReplaceableViewSite site = (IContentReplaceableViewSite)this.GetAdapter(typeof(IContentReplaceableViewSite)); + return site == null ? null : site.CurrentContentReplaceableView; + } + + #region ISelectionListener + public void OnSelectionStarted() { + } + public void OnSelectionFixed() { + if (WindowManagerPlugin.Instance.WindowPreference.OriginalPreference.AutoCopyByLeftButton) { + ICommandTarget ct = (ICommandTarget)this.GetAdapter(typeof(ICommandTarget)); + if (ct != null) { + CommandManagerPlugin cm = CommandManagerPlugin.Instance; + if (Control.ModifierKeys == Keys.Shift) { //CopyAsLook + //Debug.WriteLine("CopyAsLook"); + cm.Execute(cm.Find("org.poderosa.terminalemulator.copyaslook"), ct); + } + else { + //Debug.WriteLine("NormalCopy"); + IGeneralViewCommands gv = (IGeneralViewCommands)GetAdapter(typeof(IGeneralViewCommands)); + if (gv != null) + cm.Execute(gv.Copy, ct); + } + } + } + + } + #endregion + + /* + * 何行目から何行目までを描画すべきかの情報を収録 + */ + private class RenderParameter { + private int _linefrom; + private int _linecount; + private Rectangle _targetRect; + + public int LineFrom { + get { + return _linefrom; + } + set { + _linefrom = value; + } + } + + public int LineCount { + get { + return _linecount; + } + set { + _linecount = value; + } + } + public Rectangle TargetRect { + get { + return _targetRect; + } + set { + _targetRect = value; + } + } + } + + //テキスト選択のハンドラ + private class TextSelectionUIHandler : DefaultMouseHandler { + private CharacterDocumentViewer_Old _viewer; + public TextSelectionUIHandler(CharacterDocumentViewer_Old v) + : base("textselection") { + _viewer = v; + } + + public override UIHandleResult OnMouseDown(MouseEventArgs args) { + if (args.Button != MouseButtons.Left || !_viewer.EnabledEx) + return UIHandleResult.Pass; + + //テキスト選択ではないのでちょっと柄悪いが。UserControl->Controlの置き換えに伴う + if (!_viewer.Focused) + _viewer.Focus(); + + + CharacterDocument_Old document = _viewer.CharacterDocument; + lock (document) { + int col, row; + _viewer.MousePosToTextPos(args.X, args.Y, out col, out row); + int target_id = _viewer.GetTopLine().ID + row; + TextSelection_Old sel = _viewer.TextSelection; + if (sel.State == TextSelection_Old.SelectionState.Fixed) + sel.Clear(); //変なところでMouseDownしたとしてもClearだけはする + if (target_id <= document.LastLineNumber) { + //if(InFreeSelectionMode) ExitFreeSelectionMode(); + //if(InAutoSelectionMode) ExitAutoSelectionMode(); + TextSelection_Old.RangeType rt; + //Debug.WriteLine(String.Format("MouseDown {0} {1}", sel.State, sel.PivotType)); + + //同じ場所でポチポチと押すとChar->Word->Line->Charとモード変化する + if (sel.StartX != args.X || sel.StartY != args.Y) + rt = TextSelection_Old.RangeType.Char; + else + rt = sel.PivotType == TextSelection_Old.RangeType.Char ? TextSelection_Old.RangeType.Word : sel.PivotType == TextSelection_Old.RangeType.Word ? TextSelection_Old.RangeType.Line : TextSelection_Old.RangeType.Char; + + //マウスを動かしていなくても、MouseDownとともにMouseMoveが来てしまうようだ + GLine tl = document.FindLine(target_id); + sel.StartSelection(tl, col, rt, args.X, args.Y); + } + } + _viewer.Invalidate(); //NOTE 選択状態に変化のあった行のみ更新すればなおよし + return UIHandleResult.Capture; + } + public override UIHandleResult OnMouseMove(MouseEventArgs args) { + if (args.Button != MouseButtons.Left) + return UIHandleResult.Pass; + TextSelection_Old sel = _viewer.TextSelection; + if (sel.State == TextSelection_Old.SelectionState.Fixed || sel.State == TextSelection_Old.SelectionState.Empty) + return UIHandleResult.Pass; + //クリックだけでもなぜかMouseDownの直後にMouseMoveイベントが来るのでこのようにしてガード。でないと単発クリックでも選択状態になってしまう + if (sel.StartX == args.X && sel.StartY == args.Y) + return UIHandleResult.Capture; + + CharacterDocument_Old document = _viewer.CharacterDocument; + lock (document) { + int topline_id = _viewer.GetTopLine().ID; + SizeF pitch = _viewer.GetRenderProfile().Pitch; + int row, col; + _viewer.MousePosToTextPos_AllowNegative(args.X, args.Y, out col, out row); + int viewheight = (int)Math.Floor(_viewer.ClientSize.Height / pitch.Width); + int target_id = topline_id + row; + + GLine target_line = document.FindLineOrEdge(target_id); + TextSelection_Old.TextPoint point = sel.ConvertSelectionPosition(target_line, col); + + point.Line = RuntimeUtil.AdjustIntRange(point.Line, document.FirstLineNumber, document.LastLineNumber); + + if (_viewer.VScrollBar.Enabled) { //スクロール可能なときは + VScrollBar vsc = _viewer.VScrollBar; + if (target_id < topline_id) //前方スクロール + vsc.Value = point.Line - document.FirstLineNumber; + else if (point.Line >= topline_id + vsc.LargeChange) { //後方スクロール + int newval = point.Line - document.FirstLineNumber - vsc.LargeChange + 1; + if (newval < 0) + newval = 0; + if (newval > vsc.Maximum - vsc.LargeChange) + newval = vsc.Maximum - vsc.LargeChange + 1; + vsc.Value = newval; + } + } + else { //スクロール不可能なときは見えている範囲で + point.Line = RuntimeUtil.AdjustIntRange(point.Line, topline_id, topline_id + viewheight - 1); + } //ここさぼっている + //Debug.WriteLine(String.Format("MouseMove {0} {1} {2}", sel.State, sel.PivotType, args.X)); + TextSelection_Old.RangeType rt = sel.PivotType; + if ((Control.ModifierKeys & Keys.Control) != Keys.None) + rt = TextSelection_Old.RangeType.Word; + else if ((Control.ModifierKeys & Keys.Shift) != Keys.None) + rt = TextSelection_Old.RangeType.Line; + + GLine tl = document.FindLine(point.Line); + sel.ExpandTo(tl, point.Column, rt); + } + _viewer.Invalidate(); //TODO 選択状態に変化のあった行のみ更新するようにすればなおよし + return UIHandleResult.Capture; + + } + public override UIHandleResult OnMouseUp(MouseEventArgs args) { + TextSelection_Old sel = _viewer.TextSelection; + if (args.Button == MouseButtons.Left) { + if (sel.State == TextSelection_Old.SelectionState.Expansion || sel.State == TextSelection_Old.SelectionState.Pivot) + sel.FixSelection(); + else + sel.Clear(); + } + return _viewer.MouseHandlerManager.CapturingHandler == this ? UIHandleResult.EndCapture : UIHandleResult.Pass; + + } + public override void Reset() { + } + } + + //スプリットマークのハンドラ + private class SplitMarkUIHandler : DefaultMouseHandler { + private SplitMarkSupport _splitMark; + public SplitMarkUIHandler(SplitMarkSupport split) + : base("splitmark") { + _splitMark = split; + } + + public override UIHandleResult OnMouseDown(MouseEventArgs args) { + return UIHandleResult.Pass; + } + public override UIHandleResult OnMouseMove(MouseEventArgs args) { + bool v = _splitMark.IsSplitMarkVisible; + if (v || WindowManagerPlugin.Instance.WindowPreference.OriginalPreference.ViewSplitModifier == Control.ModifierKeys) + _splitMark.OnMouseMove(args); + //直前にキャプチャーしていたらEndCapture + return _splitMark.IsSplitMarkVisible ? UIHandleResult.Capture : v ? UIHandleResult.EndCapture : UIHandleResult.Pass; + } + public override UIHandleResult OnMouseUp(MouseEventArgs args) { + bool visible = _splitMark.IsSplitMarkVisible; + if (visible) { + //例えば、マーク表示位置から選択したいような場合を考慮し、マーク上で右クリックすると選択が消えるようにする。 + _splitMark.OnMouseUp(args); + return UIHandleResult.EndCapture; + } + else + return UIHandleResult.Pass; + } + public override void Reset() { + } + } + } + +} diff --git a/Core/CharacterDocument.cs b/Core/CharacterDocument_Old.cs similarity index 90% rename from Core/CharacterDocument.cs rename to Core/CharacterDocument_Old.cs index dd926af9..49efc05a 100644 --- a/Core/CharacterDocument.cs +++ b/Core/CharacterDocument_Old.cs @@ -21,6 +21,7 @@ using Poderosa.Sessions; using Poderosa.Commands; +using Poderosa.View; namespace Poderosa.Document { //文字ベースのドキュメント。画面表示のみ。 @@ -40,20 +41,17 @@ namespace Poderosa.Document { /// This class has not explained yet. /// /// - public class CharacterDocument : IPoderosaDocument, IPoderosaContextMenuPoint { + public class CharacterDocument_Old : IPoderosaDocument, IPoderosaContextMenuPoint { protected string _caption; protected Image _icon; protected ISession _owner; - protected InvalidatedRegion _invalidatedRegion; + protected InvalidatedRegion_Old _invalidatedRegion; protected GLine _firstLine; protected GLine _lastLine; protected int _size; //サイズは_firstLine/lastLineから計算可能だがよく使うのでキャッシュ - protected ColorSpec _appModeBgColor = ColorSpec.Default; - protected bool _bApplicationMode; - - public InvalidatedRegion InvalidatedRegion { + public InvalidatedRegion_Old InvalidatedRegion { get { return _invalidatedRegion; } @@ -84,25 +82,17 @@ public int Size { return _size; } } - public ColorSpec ApplicationModeBackColor { - get { - return _appModeBgColor; - } - set { - _appModeBgColor = value; - } + + public CharacterDocument_Old() { + _invalidatedRegion = new InvalidatedRegion_Old(); } - public bool IsApplicationMode { - get { - return _bApplicationMode; - } - set { - _bApplicationMode = value; - } + + public virtual Color DetermineBackgroundColor(RenderProfile profile) { + return profile.BackColor; } - public CharacterDocument() { - _invalidatedRegion = new InvalidatedRegion(); + public virtual Image DetermineBackgroundImage(RenderProfile profile) { + return profile.GetImage(); } public GLine FindLineOrNull(int index) { @@ -292,8 +282,8 @@ public void LoadForTest(string filename) { } } //単一行からの作成 - public static CharacterDocument SingleLine(string content) { - CharacterDocument doc = new CharacterDocument(); + public static CharacterDocument_Old SingleLine(string content) { + CharacterDocument_Old doc = new CharacterDocument_Old(); doc.AddLine(GLine.CreateSimpleGLine(content, TextDecoration.Default)); return doc; } @@ -324,7 +314,7 @@ public virtual string Caption { //描画の必要のあるIDの範囲 /// - public class InvalidatedRegion { + public class InvalidatedRegion_Old { private const int NOT_SET = -1; private int _lineIDStart; @@ -333,7 +323,7 @@ public class InvalidatedRegion { private bool _empty; - public InvalidatedRegion() { + public InvalidatedRegion_Old() { Reset(); } @@ -382,9 +372,9 @@ public void Reset() { } } - public InvalidatedRegion GetCopyAndReset() { + public InvalidatedRegion_Old GetCopyAndReset() { lock (this) { - InvalidatedRegion copy = (InvalidatedRegion)MemberwiseClone(); + InvalidatedRegion_Old copy = (InvalidatedRegion_Old)MemberwiseClone(); Reset(); return copy; } diff --git a/Core/Core.csproj b/Core/Core.csproj index ab5566fb..2617fb2a 100644 --- a/Core/Core.csproj +++ b/Core/Core.csproj @@ -78,13 +78,18 @@ Form + - Component + + + + Component + @@ -102,6 +107,10 @@ + + + + @@ -117,6 +126,9 @@ Form + + + @@ -124,7 +136,7 @@ - + Component diff --git a/Core/GLine.cs b/Core/GLine.cs index 0f2bb3fd..54fca7cd 100644 --- a/Core/GLine.cs +++ b/Core/GLine.cs @@ -833,8 +833,9 @@ public void Clear() { /// /// text decoration for specifying the background color, or null for using default attributes. public void Clear(TextDecoration dec) { - GAttr attr = dec.Attr; - GColor24 color = dec.Color24; + TextDecoration d = dec ?? TextDecoration.Default; + GAttr attr = d.Attr; + GColor24 color = d.Color24; lock (this) { Fill(0, _cell.Length, GChar.ASCII_NUL, attr, color); diff --git a/Core/GLineBuffer.cs b/Core/GLineBuffer.cs new file mode 100644 index 00000000..a52ae51c --- /dev/null +++ b/Core/GLineBuffer.cs @@ -0,0 +1,533 @@ +// Copyright 2019 The Poderosa Project. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; + +namespace Poderosa.Document { + + /// + /// GLine buffer. + /// + /// + /// This buffer is designed for containing "out-of-screen" lines. + /// When the number of lines exceeds the capacity, oldest lines are removed from this buffer automatically. + /// Removing / inserting lines between two lines are not supported. + /// + public class GLineBuffer { + +#if UNITTEST + internal +#else + private +#endif + const int ROWS_PER_PAGE = 2048; + + public const int DEFAULT_CAPACITY = ROWS_PER_PAGE; + + // This buffer consists of a list of `page`s. + // + // The page contains contiguious rows. + // Each page have `ROWS_PER_PAGE` rows, but the first page and the last page of the list may have less rows. + // + // When a new row is added to this buffer, a new page will be added to the list as its necessary. + // When the total number of rows in this buffer exceeds the capacity of this buffer, oldest rows are removed from the oldest page. + // If the oldest page becomes empty, the page is removed from the list. + + #region GLinePage + + /// + /// A group of the GLines. + /// +#if UNITTEST + internal +#else + private +#endif + class GLinePage { + private readonly GLine[] _glines = new GLine[ROWS_PER_PAGE]; + private int _startIndex = 0; // start index of the range (inclusive) + private int _endIndex = 0; // end index of the range (exclusive) + + public int Size { + get { + return _endIndex - _startIndex; + } + } + + public int Available { + get { + return ROWS_PER_PAGE - _endIndex; + } + } + + public bool IsEmpty { + get { + return _endIndex <= _startIndex; + } + } + + public void Apply(int index, int length, Action action) { + if (index < 0) { + throw new ArgumentException("invalid index"); + } + + int srcIndex = _startIndex + index; + int endIndex = srcIndex + length; + if (srcIndex >= _endIndex || endIndex > _endIndex) { + throw new IndexOutOfRangeException(); + } + + action(new GLineChunkSpan(_glines, srcIndex, length)); + } + + public IEnumerable Peek(int offset, int length) { + for (int i = 0; i < length; i++) { + yield return _glines[offset + i]; + } + } + + public bool Append(GLine line) { + if (_endIndex >= ROWS_PER_PAGE) { + return false; + } + _glines[_endIndex] = line; + _endIndex++; + return true; + } + + public void RemoveFromHead(int rows) { + if (rows < 0) { + throw new ArgumentException("invalid value", "rows"); + } + if (rows > this.Size) { + throw new ArgumentException("too many rows", "rows"); + } + Array.Clear(_glines, _startIndex, rows); + _startIndex += rows; + } + + public void RemoveFromTail(GLineChunkSpan span) { + if (span.Length > this.Size) { + throw new ArgumentException("too many rows", "rows"); + } + int newEndIndex = _endIndex - span.Length; + Array.Copy(_glines, newEndIndex, span.Array, span.Offset, span.Length); + Array.Clear(_glines, newEndIndex, span.Length); + _endIndex = newEndIndex; + } + } + + #endregion + + #region GLinePageList + + /// + /// List of the GLinePages which is maintained with the circular buffer. + /// +#if UNITTEST + internal +#else + private +#endif + class GLinePageList { + // circular buffer of the GLinePage + private readonly int _capacity; + private readonly GLinePage[] _pages; + private int _startIndex = 0; + private int _size = 0; + + public GLinePage this[int index] { + get { + if (index < 0 || index >= _size) { + throw new IndexOutOfRangeException(); + } + return _pages[(_startIndex + index) % _capacity]; + } + } + + public GLinePage Head { + get { + return this[0]; + } + } + + public GLinePage Tail { + get { + return this[_size - 1]; + } + } + + public int Size { + get { + return _size; + } + } + + public int Capacity { + get { + return _capacity; + } + } + + public GLinePageList(int maxRows) { + // Required number of pages for retainig maxRows is: + // ceil(maxRows / ROWS_PER_PAGE) + 1 + // And more 1 page is required because new rows are added before remove oldest rows. + _capacity = (maxRows + ROWS_PER_PAGE - 1) / ROWS_PER_PAGE + 2; + _pages = new GLinePage[_capacity]; + } + + public GLinePageList(int maxRows, GLinePageList source) + : this(maxRows) { + // copy pages from `source` + if (source._size > _capacity) { + throw new InvalidOperationException("too many pages in the source list"); + } + int sourceIndex = source._startIndex; + for (int i = 0; i < source._size; i++) { + _pages[_size] = source._pages[sourceIndex]; + _size++; + sourceIndex = (sourceIndex + 1) % source._capacity; + } + } + + public void Append(GLinePage page) { + if (_size >= _capacity) { + throw new InvalidOperationException("GLinePageList full"); + } + _pages[(_startIndex + _size) % _capacity] = page; + _size++; + } + + public void RemoveHead() { + if (_size > 0) { + _pages[_startIndex] = null; + _startIndex = (_startIndex + 1) % _capacity; + _size--; + } + } + + public void RemoveTail() { + if (_size > 0) { + _pages[(_startIndex + _size - 1) % _capacity] = null; + _size--; + } + } + + public GLinePage Peek(int internalIndex) { + return _pages[internalIndex % _capacity]; + } + } + + #endregion + + private GLinePageList _pageList; + private int _capacity; + + private int _rowCount = 0; + private int _firstRowID = 1; // Row ID of the first row + + private readonly object _syncRoot; + + /// + /// Object for synchronization of the buffer operations. + /// + public object SyncRoot { + get { + return _syncRoot; + } + } + + /// + /// Row ID span of this document. + /// + public RowIDSpan RowIDSpan { + get { + lock (_syncRoot) { + return new RowIDSpan(_firstRowID, _rowCount); + } + } + } + + /// + /// Next row ID + /// + public int NextRowID { + get { + lock (_syncRoot) { + return _firstRowID + _rowCount; + } + } + } + + /// + /// Constructs an instance with the default capacity. + /// + public GLineBuffer() + : this(null, DEFAULT_CAPACITY) { + } + + /// + /// Constructs an instance with the default capacity. + /// + public GLineBuffer(object sync) + : this(sync, DEFAULT_CAPACITY) { + } + + /// + /// Constructs an instance with the specified capacity. + /// + /// capacity of this buffer in number of rows. + public GLineBuffer(int capacity) + : this(null, capacity) { + } + + /// + /// Constructs an instance with the specified capacity. + /// + /// an object to be used for the synchronization + /// capacity of this buffer in number of rows. + public GLineBuffer(object sync, int capacity) { + _syncRoot = sync ?? new object(); + capacity = Math.Max(capacity, 1); + _capacity = capacity; + _pageList = new GLinePageList(capacity); + } + + /// + /// Set capacity of this buffer. + /// If new capacity was smaller than the current content size, oldest rows are removed. + /// + /// new capacity of this buffer in number of rows. + public void SetCapacity(int newCapacity) { + lock (_syncRoot) { + newCapacity = Math.Max(newCapacity, 1); + // if the capacity was shrinked, oldest rows have to be removed before the page list is re-created. + _capacity = newCapacity; + TrimHead(); // note that this call does nothing if the capacity was enlarged. + _pageList = new GLinePageList(newCapacity, _pageList); + } + } + +#if UNITTEST + /// + /// Gets the capacity of the page list for testing purpose. + /// + /// the capacity of the page list in the number of pages + internal int GetPageCapacity() { + return _pageList.Capacity; + } +#endif + /// + /// Append a line. + /// If this buffer was full, an oldest row is removed. + /// + /// line to add + public void Append(GLine line) { + lock (_syncRoot) { + if (_pageList.Size == 0) { + _pageList.Append(new GLinePage()); + } + if (!_pageList.Tail.Append(line)) { // page-full + _pageList.Append(new GLinePage()); + _pageList.Tail.Append(line); + } + _rowCount++; + TrimHead(); + } + } + + /// + /// Append lines. + /// If this buffer was full, oldest rows are removed. + /// + /// lines to add + public void Append(IEnumerable lines) { + lock (_syncRoot) { + foreach (GLine line in lines) { + if (_pageList.Size == 0) { + _pageList.Append(new GLinePage()); + } + if (!_pageList.Tail.Append(line)) { // page-full + TrimHead(); // reduce active pages for avoiding list-full + _pageList.Append(new GLinePage()); + _pageList.Tail.Append(line); + } + _rowCount++; + } + TrimHead(); + } + } + + /// + /// Remove tail (newest) rows. + /// + /// + /// a span of the GLine array to store the removed rows. + /// the chunk length dictates the number of rows to remove. + /// + /// array of the removed rows. + public void RemoveFromTail(GLineChunkSpan span) { + lock (_syncRoot) { + if (span.Length > _rowCount) { + throw new ArgumentException("too many rows", "rows"); + } + + int removedLinesCount = 0; + int subSpanOffset = span.Length; + while (_pageList.Size > 0 && removedLinesCount < span.Length) { + GLinePage tailPage = _pageList.Tail; + int rowsToRemove = Math.Min(span.Length - removedLinesCount, tailPage.Size); + subSpanOffset -= rowsToRemove; + tailPage.RemoveFromTail(span.Span(subSpanOffset, rowsToRemove)); + removedLinesCount += rowsToRemove; + _rowCount -= rowsToRemove; + if (tailPage.IsEmpty) { + _pageList.RemoveTail(); + } + } + } + } + + /// + /// Gets lines starting with the specified row ID. + /// + /// row ID of the first row + /// + /// a span of the GLine array to store the copied rows. + /// the chunk length dictates the number of rows to copy. + /// + /// buffer is empty + /// length is smaller than zero + /// + /// the range specified by and doesn't match with this buffer + /// + public void GetLinesByID(int startRowID, GLineChunkSpan span) { + int spanOffset = 0; + Apply(startRowID, span.Length, s => { + Array.Copy(s.Array, s.Offset, span.Array, span.Offset + spanOffset, s.Length); + spanOffset += s.Length; + }); + } + + /// + /// Calls action for the specified range. + /// + /// row ID of the first row + /// number of rows + /// + /// action to be called for each span. + /// + /// buffer is empty + /// length is smaller than zero + /// + /// the range specified by and doesn't match with this buffer + /// + public void Apply(int startRowID, int length, Action action) { + lock (_syncRoot) { + if (_rowCount <= 0) { + if (length == 0) { + return; + } + throw new InvalidOperationException("no lines"); + } + + // determine row index in this buffer + int rowIndex = startRowID - _firstRowID; + + // check range + if (rowIndex < 0 || rowIndex + length > _rowCount) { + throw new IndexOutOfRangeException(); + } + + // determine page index + int firstPageRows = _pageList.Head.Size; + int pageIndex; + int pageRowIndex; + if (rowIndex < firstPageRows) { + pageIndex = 0; + pageRowIndex = rowIndex; + } + else { + int r = rowIndex - firstPageRows; + pageIndex = r / ROWS_PER_PAGE + 1; + pageRowIndex = r % ROWS_PER_PAGE; + } + + // get lines + if (length > 0) { + int lineCount = 0; + for (; ; ) { + GLinePage page = _pageList[pageIndex]; + int rowsToRead = Math.Min(page.Size - pageRowIndex, length - lineCount); + page.Apply(pageRowIndex, rowsToRead, action); + lineCount += rowsToRead; + if (lineCount >= length) { + break; + } + pageIndex++; + pageRowIndex = 0; + } + } + } + } + +#if UNITTEST + /// + /// Copy internal s for the testing purpose. + /// + /// + internal GLinePage[] GetAllPages() { + lock (_syncRoot) { + int size = _pageList.Size; + GLinePage[] pages = new GLinePage[size]; + for (int i = 0; i < size; i++) { + pages[i] = _pageList[i]; + } + return pages; + } + } +#endif + + /// + /// Removes oldest rows that were pushed out from this buffer. + /// + private void TrimHead() { + int rowsToRemove = _rowCount - _capacity; + while (_pageList.Size > 0 && rowsToRemove > 0) { + GLinePage headPage = _pageList.Head; + int pageSize = headPage.Size; + + int rowsRemoved; + if (rowsToRemove >= pageSize) { + _pageList.RemoveHead(); + rowsRemoved = pageSize; + } + else { + headPage.RemoveFromHead(rowsToRemove); + rowsRemoved = rowsToRemove; + } + + rowsToRemove -= rowsRemoved; + _rowCount -= rowsRemoved; + _firstRowID += rowsRemoved; + } + } + + } + + + +} diff --git a/Core/GLineChunk.cs b/Core/GLineChunk.cs new file mode 100644 index 00000000..dfd8bf6c --- /dev/null +++ b/Core/GLineChunk.cs @@ -0,0 +1,148 @@ +// Copyright 2019 The Poderosa Project. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Collections.Generic; + +namespace Poderosa.Document { + + /// + /// Manages reusable array. + /// + public class GLineChunk { + + private GLine[] _array; + + /// + /// Underlying array + /// + public GLine[] Array { + get { + return _array; + } + } + + /// + /// Constructor + /// + /// initial capacity + public GLineChunk(int capacity) { + _array = new GLine[capacity]; + } + + /// + /// Clear array + /// + public void Clear() { + System.Array.Clear(_array, 0, _array.Length); + } + + /// + /// Ensures that the capacity of this instance is at least the specified value. + /// + /// minimum capacity + public void EnsureCapacity(int capacity) { + GLine[] oldArray = _array; + if (oldArray.Length < capacity) { + GLine[] newArray = new GLine[capacity]; + System.Array.Copy(oldArray, 0, newArray, 0, oldArray.Length); + _array = newArray; + } + } + + /// + /// Creates which is based on this instance. + /// + /// offset of the underlying array + /// span length in the number of rows + /// new span + /// and doesn't fit the underlying array. + public GLineChunkSpan Span(int offset, int length) { + return new GLineChunkSpan(_array, offset, length); + } + } + + /// + /// A struct that specifies a range on the array. + /// + public struct GLineChunkSpan { + + /// + /// Underlying array + /// + public readonly GLine[] Array; + + /// + /// Offset from the head of the array + /// + public readonly int Offset; + + /// + /// Length of this span + /// + public readonly int Length; + + internal GLineChunkSpan(GLine[] array, int offset, int length) { +#if DEBUG || UNITTEST + if (offset < 0) { + throw new ArgumentException("invalid offset", "offset"); + } + if (length < 0) { + throw new ArgumentException("invalid length", "length"); + } + if (offset + length > array.Length) { + throw new ArgumentException("size of the underlying array is not enough"); + } +#endif + this.Array = array; + this.Offset = offset; + this.Length = length; + } + + /// + /// Gets a sub span of this span. + /// + /// offset in this span. 0 indicates that the new offset of the underlying array is same with this span. + /// length of the new span. + /// new span + public GLineChunkSpan Span(int offset, int length) { +#if DEBUG || UNITTEST + if (offset < 0) { + throw new ArgumentException("invalid offset", "offset"); + } + if (length < 0) { + throw new ArgumentException("invalid length", "length"); + } + if (offset + length > this.Length) { + throw new ArgumentException("offset and length exceed length of the based span."); + } +#endif + return new GLineChunkSpan(this.Array, this.Offset + offset, length); + } + + /// + /// Iterate GLines in this span. + /// + /// IEnumerable + public IEnumerable GLines() { + var array = this.Array; + var offset = this.Offset; + var length = this.Length; + for (int i = 0; i < length; i++) { + yield return array[offset + i]; + } + } + } + +} diff --git a/Core/GLineScreenBuffer.cs b/Core/GLineScreenBuffer.cs new file mode 100644 index 00000000..1a810bd4 --- /dev/null +++ b/Core/GLineScreenBuffer.cs @@ -0,0 +1,515 @@ +// Copyright 2019 The Poderosa Project. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Runtime.CompilerServices; + +namespace Poderosa.Document { + + /// + /// GLine screen buffer. + /// + /// + /// This buffer is designed for containing visible lines on the screen. + /// + public class GLineScreenBuffer { + + // Internal buffer is a circular buffer. + + private GLine[] _buff; + private int _startIndex; + private int _size; + + private readonly object _syncRoot; + + /// + /// Synchronization object + /// + public object SyncRoot { + get { + return _syncRoot; + } + } + + /// + /// Screen size in the number of rows. + /// + public int Size { + get { + return _size; + } + } + + /// + /// + /// Gets or sets a object at the specified row. + /// + /// + /// Note that each access to this property does internal synchronization. + /// Consider to use or . + /// + /// + /// row index of the screen + public GLine this[int rowIndex] { + get { + lock (_syncRoot) { + if (rowIndex < 0 || rowIndex >= _size) { + throw new IndexOutOfRangeException("invalid index"); + } + + return _buff[RowIndexToBuffIndex(rowIndex)]; + } + } + set { + lock (_syncRoot) { + if (rowIndex < 0 || rowIndex >= _size) { + throw new IndexOutOfRangeException("invalid index"); + } + + _buff[RowIndexToBuffIndex(rowIndex)] = value; + } + } + } + + /// + /// Constructor + /// + /// number of visible rows + /// a function to get the initial GLine objects + public GLineScreenBuffer(int rows, Func createLine) + : this(null, rows, createLine) { + } + + /// + /// Constructor + /// + /// an object to be used for the synchronization + /// number of visible rows + /// a function to get the initial GLine objects + public GLineScreenBuffer(object sync, int rows, Func createLine) { + if (rows <= 0) { + throw new ArgumentException("invalid value", "rows"); + } + + _syncRoot = sync ?? new object(); + + _buff = new GLine[CalcBufferSize(rows)]; + _startIndex = 0; + _size = rows; + + for (int i = 0; i < rows; i++) { + _buff[i] = createLine(i); + } + } + + /// + /// Constructor (for testing) + /// + /// index of the internal buffer + /// number of visible rows + /// a function to get the initial GLine objects + internal GLineScreenBuffer(int startIndex, int rows, Func createLine) { + if (rows <= 0) { + throw new ArgumentException("invalid value", "rows"); + } + + _syncRoot = new object(); + + _buff = new GLine[CalcBufferSize(rows)]; + _startIndex = startIndex; + _size = rows; + + for (int i = 0; i < rows; i++) { + _buff[RowIndexToBuffIndex(i)] = createLine(i); + } + } + + /// + /// Determine new buffer size + /// + /// number of visible rows + /// buffer size + private static int CalcBufferSize(int rows) { + // round-up to multiply of 32. 64 at minimum. + return Math.Max((rows + 31) & ~31, 64); + } + + /// + /// Gets index of the internal buffer from the row index. + /// + /// row index + /// index of the internal buffer + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int RowIndexToBuffIndex(int rowIndex) { + return (_startIndex + rowIndex) % _buff.Length; + } + + /// + /// Gets index of the internal buffer from the row index. + /// + /// + /// row index towards the negative direction. + /// a value "1" indicatess the previous row of the top row of the screen. + /// + /// index of the internal buffer + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int RowIndexToBuffIndexNegative(int offset) { + int buffSize = _buff.Length; + return (_startIndex + buffSize - offset) % buffSize; + } + + /// + /// Increases the buffer size with extending the top of the screen. + /// + /// + /// rows to insert at the top of the screen. + /// the length of the span dictates the new screen size. + /// + public void ExtendHead(GLineChunkSpan rows) { + lock (_syncRoot) { + int newSize = _size + rows.Length; + int buffSize = CalcBufferSize(newSize); + + if (buffSize <= _buff.Length) { + // buffer size is already enough. + // insert rows. + int newStartIndex = RowIndexToBuffIndexNegative(rows.Length); + CopyToBuffer(rows.Array, rows.Offset, newStartIndex, rows.Length); + _startIndex = newStartIndex; + _size = newSize; + } + else { + // buffer size is not enough. + // allocate new buffer. + GLine[] newBuff = new GLine[buffSize]; + // copy rows. + Array.Copy(rows.Array, rows.Offset, newBuff, 0, rows.Length); + CopyFromBuffer(_startIndex, newBuff, rows.Length, _size); + _buff = newBuff; + _startIndex = 0; + _size = newSize; + } + } + } + + /// + /// Increases the buffer size with extending the bottom of the screen. + /// + /// + /// rows to insert at the top of the screen. + /// the length of the span dictates the new screen size. + /// + public void ExtendTail(GLineChunkSpan rows) { + lock (_syncRoot) { + int newSize = _size + rows.Length; + int buffSize = CalcBufferSize(newSize); + + if (buffSize <= _buff.Length) { + // buffer size is already enough. + // insert rows. + CopyToBuffer(rows.Array, rows.Offset, RowIndexToBuffIndex(_size), rows.Length); + _size = newSize; + } + else { + // buffer size is not enough. + // allocate new buffer. + GLine[] newBuff = new GLine[buffSize]; + // copy rows. + CopyFromBuffer(_startIndex, newBuff, 0, _size); + Array.Copy(rows.Array, rows.Offset, newBuff, _size, rows.Length); + _buff = newBuff; + _startIndex = 0; + _size = newSize; + } + } + } + + /// + /// Decreases the buffer size with shrinking the top of the screen. + /// + /// number of rows to decrease + public void ShrinkHead(int rows) { + if (rows < 0) { + throw new ArgumentException("invalid value", "rows"); + } + + lock (_syncRoot) { + if (rows >= _size) { + throw new ArgumentException("too large shrink size", "rows"); + } + + ClearBuffer(_startIndex, rows); + _startIndex = RowIndexToBuffIndex(rows); + _size -= rows; + } + } + + /// + /// Decreases the buffer size with shrinking the bottom of the screen. + /// + /// number of rows to decrease + public void ShrinkTail(int rows) { + if (rows < 0) { + throw new ArgumentException("invalid value", "rows"); + } + + lock (_syncRoot) { + if (rows >= _size) { + throw new ArgumentException("too large shrink size", "rows"); + } + + _size -= rows; + ClearBuffer(RowIndexToBuffIndex(_size), rows); + } + } + + /// + /// Gets rows starting at the specified index. + /// + /// row index. 0 indicates the first row at the top of the screen. + /// + /// array buffer to store the copied object. + /// the length of the span is used as the number of rows to get. + /// + public void GetRows(int rowIndex, GLineChunkSpan span) { + if (rowIndex < 0) { + throw new ArgumentException("invalid value", "rowIndex"); + } + + lock (_syncRoot) { + if (rowIndex + span.Length > _size) { + throw new ArgumentException("invalid range"); + } + + CopyFromBuffer(RowIndexToBuffIndex(rowIndex), span.Array, span.Offset, span.Length); + } + } + + /// + /// Sets rows starting at the specified index. + /// + /// row index. 0 indicates the first row at the top of the screen. + /// + /// array buffer to get objects to be copied. + /// the length of the span is used as the number of rows to set. + /// + public void SetRows(int rowIndex, GLineChunkSpan span) { + if (rowIndex < 0) { + throw new ArgumentException("invalid value", "rowIndex"); + } + + lock (_syncRoot) { + if (rowIndex + span.Length > _size) { + throw new ArgumentException("invalid range"); + } + + CopyToBuffer(span.Array, span.Offset, RowIndexToBuffIndex(rowIndex), span.Length); + } + } + + /// + /// Scroll-up entire of the screen. + /// + /// + /// rows to append at the bottom of the screen. + /// the length of the span is used as the number of rows to scroll. + /// + public void ScrollUp(GLineChunkSpan newRows) { + lock (_syncRoot) { + int oldEndIndex = RowIndexToBuffIndex(_size); + // scroll-up + int scrollSize = Math.Min(newRows.Length, _size); + ClearBuffer(_startIndex, scrollSize); + _startIndex = RowIndexToBuffIndex(scrollSize); + // append new rows + CopyToBuffer(newRows.Array, newRows.Offset + newRows.Length - scrollSize, oldEndIndex, scrollSize); + } + } + + /// + /// Scroll-down entire of the screen. + /// + /// + /// rows to insert at the top of the screen. + /// the length of the span is used as the number of rows to scroll. + /// + public void ScrollDown(GLineChunkSpan newRows) { + lock (_syncRoot) { + // scroll-down + int scrollSize = Math.Min(newRows.Length, _size); + _startIndex = RowIndexToBuffIndexNegative(scrollSize); + ClearBuffer(RowIndexToBuffIndex(_size), scrollSize); + // insert new rows + CopyToBuffer(newRows.Array, newRows.Offset, _startIndex, scrollSize); + } + } + + /// + /// Scroll-up rows in the specified region. + /// + /// start row indedx of the scroll region (inclusive) + /// end row indedx of the scroll region (exclusive) + /// + /// rows to insert at the bottom of the region. + /// the length of the span is used as the number of rows to scroll. + /// + public void ScrollUpRegion(int startRowIndex, int endRowIndex, GLineChunkSpan newRows) { + lock (_syncRoot) { + // adjust range + startRowIndex = Math.Max(startRowIndex, 0); + endRowIndex = Math.Min(endRowIndex, _size); + if (startRowIndex >= endRowIndex) { + return; + } + // scroll-up + int scrollSize = Math.Min(newRows.Length, endRowIndex - startRowIndex); + int destRowIndex = startRowIndex; + int srcRowIndex = destRowIndex + scrollSize; + while (srcRowIndex < endRowIndex) { + _buff[RowIndexToBuffIndex(destRowIndex)] = _buff[RowIndexToBuffIndex(srcRowIndex)]; + destRowIndex++; + srcRowIndex++; + } + CopyToBuffer(newRows.Array, newRows.Offset + newRows.Length - scrollSize, RowIndexToBuffIndex(destRowIndex), scrollSize); + } + } + + /// + /// Scroll-down rows in the specified region. + /// + /// start row indedx of the scroll region (inclusive) + /// end row indedx of the scroll region (exclusive) + /// + /// rows to insert at the top of the region. + /// the length of the span is used as the number of rows to scroll. + /// + public void ScrollDownRegion(int startRowIndex, int endRowIndex, GLineChunkSpan newRows) { + lock (_syncRoot) { + // adjust range + startRowIndex = Math.Max(startRowIndex, 0); + endRowIndex = Math.Min(endRowIndex, _size); + if (startRowIndex >= endRowIndex) { + return; + } + // scroll-down + int scrollSize = Math.Min(newRows.Length, endRowIndex - startRowIndex); + int destRowIndex = endRowIndex - 1; + int srcRowIndex = destRowIndex - scrollSize; + while (srcRowIndex >= startRowIndex) { + _buff[RowIndexToBuffIndex(destRowIndex)] = _buff[RowIndexToBuffIndex(srcRowIndex)]; + destRowIndex--; + srcRowIndex--; + } + CopyToBuffer(newRows.Array, newRows.Offset, RowIndexToBuffIndex(startRowIndex), scrollSize); + } + } + + /// + /// Calls action for the specified range. + /// + /// row index + /// number of rows + /// + /// action to be called for each span. + /// + /// + /// + /// the range specified by and doesn't match with this buffer + /// + public void Apply(int rowIndex, int length, Action action) { + lock (_syncRoot) { + if (length < 0) { + throw new ArgumentException("invalid value", "length"); + } + + // check range + if (rowIndex < 0 || rowIndex + length > _size) { + throw new IndexOutOfRangeException(); + } + + // determine buffer index + int buffIndex = RowIndexToBuffIndex(rowIndex); + int sizeToEnd = _buff.Length - buffIndex; + if (sizeToEnd >= length) { + action(new GLineChunkSpan(_buff, buffIndex, length)); + } + else { + action(new GLineChunkSpan(_buff, buffIndex, sizeToEnd)); + action(new GLineChunkSpan(_buff, 0, length - sizeToEnd)); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CopyToBuffer(GLine[] src, int srcIndex, int buffIndex, int length) { + int sizeToEnd = _buff.Length - buffIndex; + if (sizeToEnd >= length) { + Array.Copy(src, srcIndex, _buff, buffIndex, length); + } + else { + Array.Copy(src, srcIndex, _buff, buffIndex, sizeToEnd); + Array.Copy(src, srcIndex + sizeToEnd, _buff, 0, length - sizeToEnd); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CopyFromBuffer(int buffIndex, GLine[] dest, int destIndex, int length) { + int sizeToEnd = _buff.Length - buffIndex; + if (sizeToEnd >= length) { + Array.Copy(_buff, buffIndex, dest, destIndex, length); + } + else { + Array.Copy(_buff, buffIndex, dest, destIndex, sizeToEnd); + Array.Copy(_buff, 0, dest, destIndex + sizeToEnd, length - sizeToEnd); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ClearBuffer(int buffIndex, int length) { + int sizeToEnd = _buff.Length - buffIndex; + if (sizeToEnd >= length) { + Array.Clear(_buff, buffIndex, length); + } + else { + Array.Clear(_buff, buffIndex, sizeToEnd); + Array.Clear(_buff, 0, length - sizeToEnd); + } + } + +#if UNITTEST + internal void InternalCopyToBuffer(GLine[] src, int srcIndex, int buffIndex, int length) { + CopyToBuffer(src, srcIndex, buffIndex, length); + } + + internal void InternalCopyFromBuffer(int buffIndex, GLine[] dest, int destIndex, int length) { + CopyFromBuffer(buffIndex, dest, destIndex, length); + } + + internal void InternalClearBuffer(int buffIndex, int length) { + ClearBuffer(buffIndex, length); + } + + internal int StartIndex { + get { + return _startIndex; + } + } + + internal GLine[] GetRawBuff() { + return (GLine[])_buff.Clone(); + } +#endif + } + +} diff --git a/Core/ICharacterDocument.cs b/Core/ICharacterDocument.cs new file mode 100644 index 00000000..0be353f6 --- /dev/null +++ b/Core/ICharacterDocument.cs @@ -0,0 +1,88 @@ +// Copyright 2019 The Poderosa Project. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Poderosa.View; +using System; +using System.Drawing; + +namespace Poderosa.Document { + + /// + /// An interface of the document object that is displayed by . + /// + public interface ICharacterDocument { + + object SyncRoot { + get; + } + + InvalidatedRegion InvalidatedRegion { + get; + } + + /// + /// Gets range of the row ID in this document. + /// + /// span of the row ID + RowIDSpan GetRowIDSpan(); + + /// + /// Determines which color should be used as the background color of this document. + /// + /// current profile + /// background color + Color DetermineBackgroundColor(RenderProfile profile); + + /// + /// Determines which image should be painted (or should not be painted) in the background of this document. + /// + /// current profile + /// an image object to paint, or null. + Image DetermineBackgroundImage(RenderProfile profile); + + /// + /// Apply action to each row in the specified range. + /// + /// + /// This method must guarantee that the specified action is called for all rows in the specified range. + /// If a row was missing in this document, null is passed to the action. + /// + /// start Row ID + /// number of rows + /// + /// a delegate function to apply. the first argument is a row ID. the second argument is a target GLine object. + /// + void ForEach(int startRowID, int rows, Action action); + + /// + /// Apply action to the specified row. + /// + /// + /// If a row was missing in this document, null is passed to the action. + /// + /// Row ID + /// + /// a delegate function to apply. the first argument may be null. + /// + void Apply(int rowID, Action action); + + /// + /// Notifies document implementation from the document viewer + /// that the size of the visible area was changed. + /// + /// number of visible rows + /// number of visible columns + void VisibleAreaSizeChanged(int rows, int cols); + } +} diff --git a/Core/InvalidatedRegion.cs b/Core/InvalidatedRegion.cs new file mode 100644 index 00000000..9e552450 --- /dev/null +++ b/Core/InvalidatedRegion.cs @@ -0,0 +1,151 @@ +// Copyright 2019 The Poderosa Project. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +namespace Poderosa.Document { + + /// + /// A row region that need to be redrawn. + /// + public class InvalidatedRegion { + + private enum Status { + Empty, + All, + Range, + } + + private int _startRowID = 0; // inclusive + private int _endRowID = 0; // exclusive + private Status _status = Status.Empty; + + /// + /// Constructor + /// + public InvalidatedRegion() { + } + + /// + /// Starting row ID (inclusive) + /// + public int StartRowID { + get { + return _startRowID; + } + } + + /// + /// Ending row ID (exclusive) + /// + public int EndRowID { + get { + return _endRowID; + } + } + + /// + /// Whether this range is empty + /// + public bool IsEmpty { + get { + lock (this) { + return _endRowID <= _startRowID; + } + } + } + + /// + /// Whether all rows need to be redrawn + /// + public bool InvalidatedAll { + get { + return _status == Status.All; + } + set { + lock (this) { + _startRowID = _endRowID = 0; + _status = Status.All; + } + } + } + + /// + /// Invalidate a single row + /// + /// + public void InvalidateRow(int rowID) { + lock (this) { + if (_status == Status.Range) { + if (rowID < _startRowID) { + _startRowID = rowID; + } + if (rowID >= _endRowID) { + _endRowID = rowID + 1; + } + } + else if (_status == Status.Empty) { + _startRowID = rowID; + _endRowID = rowID + 1; + _status = Status.Range; + } + } + } + + /// + /// Invalidate a span of rows + /// + /// + public void InvalidateRows(RowIDSpan span) { + lock (this) { + if (_status == Status.Range) { + if (span.Start < _startRowID) { + _startRowID = span.Start; + } + int endSpan = span.Start + span.Length; + if (endSpan > _endRowID) { + _endRowID = endSpan; + } + } + else if (_status == Status.Empty) { + _startRowID = span.Start; + _endRowID = span.Start + span.Length; + _status = Status.Range; + } + // if the status was Status.All, no need to update. + } + } + + /// + /// Clear + /// + public void Clear() { + lock (this) { + _startRowID = _endRowID = 0; + _status = Status.Empty; + } + } + + /// + /// Copy region information then clear + /// + /// copied information + public InvalidatedRegion GetCopyAndClear() { + lock (this) { + InvalidatedRegion copy = (InvalidatedRegion)MemberwiseClone(); + Clear(); + return copy; + } + } + } +} diff --git a/Core/PopupViewContainer.cs b/Core/PopupViewContainer.cs index debcd903..23fb8381 100644 --- a/Core/PopupViewContainer.cs +++ b/Core/PopupViewContainer.cs @@ -34,7 +34,6 @@ public PopupViewContainer(PopupViewCreationParam cp) { InitializeComponent(); _view = cp.ViewFactory.CreateNew(this); _view.AsControl().Dock = DockStyle.Fill; - _view.AsControl().Size = cp.InitialSize; this.Controls.Add(_view.AsControl()); this.ClientSize = cp.InitialSize; this.ResumeLayout(false); @@ -53,7 +52,7 @@ private void InitializeComponent() { // PopupViewContainer // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 12F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None; this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.SizableToolWindow; this.Name = "PopupViewContainer"; this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; diff --git a/Core/RowIDSpan.cs b/Core/RowIDSpan.cs new file mode 100644 index 00000000..98dd91f9 --- /dev/null +++ b/Core/RowIDSpan.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; +// Copyright 2019 The Poderosa Project. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Text; +using System.Threading.Tasks; + +namespace Poderosa.Document { + + /// + /// A struct to represent a span of the row IDs. + /// + public struct RowIDSpan { + + private readonly int _start; + private readonly int _length; + + /// + /// Start row ID + /// + public int Start { + get { + return _start; + } + } + + /// + /// Number of rows in this range + /// + public int Length { + get { + return _length; + } + } + + /// + /// Constructor + /// + /// start row ID + /// number of rows + public RowIDSpan(int start, int length) { + this._start = start; + this._length = Math.Max(length, 0); + } + + /// + /// Gets an intersection of this span and another span. + /// + /// another span + /// intersection span + public RowIDSpan Intersect(RowIDSpan other) { + int otherEnd = other._start + other._length; + if (otherEnd <= this._start) { + return new RowIDSpan(this._start, 0); + } + int thisEnd = this._start + this._length; + if (thisEnd <= other._start) { + return new RowIDSpan(this._start, 0); + } + int intersectStart = Math.Max(this._start, other._start); + int intersectEnd = Math.Min(thisEnd, otherEnd); + return new RowIDSpan(intersectStart, intersectEnd - intersectStart); + } + + /// + /// Returns whether this span includes a specified row ID. + /// + /// + /// true if this span includes a specified row ID. + public bool Includes(int rowID) { + return rowID >= this._start && rowID < this._start + this._length; + } + } +} diff --git a/Core/TerminalCharacterDocument.cs b/Core/TerminalCharacterDocument.cs new file mode 100644 index 00000000..d972693d --- /dev/null +++ b/Core/TerminalCharacterDocument.cs @@ -0,0 +1,525 @@ +// Copyright 2019 The Poderosa Project. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Poderosa.View; +using System; +using System.Collections.Generic; +using System.Drawing; + +namespace Poderosa.Document { + + /// + /// implementation which consists of a screen buffer and a log buffer. + /// + public abstract class TerminalCharacterDocument : ICharacterDocument { + + protected readonly object _syncRoot = new object(); + + protected readonly InvalidatedRegion _invalidatedRegion = new InvalidatedRegion(); + + private readonly GLineBuffer _logBuffer; + + private readonly GLineScreenBuffer _screenBuffer; + + private readonly GLineChunk _workGLineBuff = new GLineChunk(10); + + private readonly GLine[] _workSingleGLine = new GLine[1]; + + /// + /// Gets whether the screen buffer is isolated from the log buffer. + /// + /// if true, the rows scroll-out from the screen are not moved to the log buffer. + protected abstract bool IsScreenIsolated(); + + /// + /// Notifies document implementation from the document viewer + /// that the size of the visible area was changed. + /// + /// number of visible rows + /// number of visible columns + protected abstract void OnVisibleAreaSizeChanged(int rows, int cols); + + /// + /// Creates a new line object. + /// + /// new line object + protected abstract GLine CreateEmptyLine(); + + #region ICharacterDocument + + public object SyncRoot { + get { + return _syncRoot; + } + } + + public InvalidatedRegion InvalidatedRegion { + get { + return _invalidatedRegion; + } + } + + #endregion + + /// + /// Constructor + /// + /// initial screen width + /// initial screen height + protected TerminalCharacterDocument(int width, int height) { + if (width <= 0) { + throw new ArgumentException("invalid width", "width"); + } + if (height <= 0) { + throw new ArgumentException("invalid height", "height"); + } + + _logBuffer = new GLineBuffer(_syncRoot); + _screenBuffer = new GLineScreenBuffer(_syncRoot, height, (n) => new GLine(width)); + } + + #region ICharacterDocument + + /// + /// Gets range of the row ID in this document. + /// + /// span of the row ID + public RowIDSpan GetRowIDSpan() { + lock (_syncRoot) { + var logSpan = _logBuffer.RowIDSpan; + return new RowIDSpan(logSpan.Start, logSpan.Length + _screenBuffer.Size); + } + } + + /// + /// Determines which color should be used as the background color of this document. + /// + /// current profile + /// background color + public virtual Color DetermineBackgroundColor(RenderProfile profile) { + return profile.BackColor; + } + + /// + /// Determines which image should be painted (or should not be painted) in the background of this document. + /// + /// current profile + /// an image object to paint, or null. + public Image DetermineBackgroundImage(RenderProfile profile) { + return profile.GetImage(); + } + + /// + /// Apply action to each row in the specified range. + /// + /// + /// This method must guarantee that the specified action is called for all rows in the specified range. + /// If a row was missing in this document, null is passed to the action. + /// + /// start Row ID + /// number of rows + /// + /// a delegate function to apply. the first argument is a row ID. the second argument is a target GLine object. + /// + public void ForEach(int startRowID, int rows, Action action) { + if (rows < 0) { + throw new ArgumentException("invalid value", "rows"); + } + if (action == null) { + throw new ArgumentException("action is null", "action"); + } + + lock (_syncRoot) { + RowIDSpan logBuffSpan; + RowIDSpan screenBuffSpan; + GetRowIDSpans(out logBuffSpan, out screenBuffSpan); + + int rowID = startRowID; + + { + RowIDSpan logIterSpan = logBuffSpan.Intersect(new RowIDSpan(startRowID, rows)); + + if (logIterSpan.Length > 0) { + while (rowID < logIterSpan.Start) { + action(rowID, null); + rowID++; + } + + _logBuffer.Apply(logIterSpan.Start, logIterSpan.Length, s => { + foreach (var line in s.GLines()) { + action(rowID, line); + rowID++; + } + }); + } + } + + { + RowIDSpan screenIterSpan = screenBuffSpan.Intersect(new RowIDSpan(startRowID, rows)); + + if (screenIterSpan.Length > 0) { + while (rowID < screenIterSpan.Start) { + action(rowID, null); + rowID++; + } + + _screenBuffer.Apply(screenIterSpan.Start - screenBuffSpan.Start, screenIterSpan.Length, s => { + foreach (var line in s.GLines()) { + action(rowID, line); + rowID++; + } + }); + } + } + + int endRowID = startRowID + rows; + while (rowID < endRowID) { + action(rowID, null); + rowID++; + } + } + } + + /// + /// Apply action to the specified row. + /// + /// + /// If a row was missing in this document, null is passed to the action. + /// + /// Row ID + /// + /// a delegate function to apply. the first argument may be null. + /// + public void Apply(int rowID, Action action) { + if (action == null) { + throw new ArgumentException("action is null", "action"); + } + + lock (_syncRoot) { + RowIDSpan logBuffSpan; + RowIDSpan screenBuffSpan; + GetRowIDSpans(out logBuffSpan, out screenBuffSpan); + + if (logBuffSpan.Includes(rowID)) { + _logBuffer.Apply(rowID, 1, s => { + action(s.Array[s.Offset]); + }); + } + else if (screenBuffSpan.Includes(rowID)) { + _screenBuffer.Apply(rowID - screenBuffSpan.Start, 1, s => { + action(s.Array[s.Offset]); + }); + } + else { + action(null); + } + } + } + + /// + /// Notifies document implementation from the document viewer + /// that the size of the visible area was changed. + /// + /// number of visible rows + /// number of visible columns + public void VisibleAreaSizeChanged(int rows, int cols) { + int newRows = Math.Max(rows, 1); + int newCols = Math.Max(cols, 1); + + lock (_syncRoot) { + int curRows = _screenBuffer.Size; + if (newRows < curRows) { + int shrinkRows = curRows - newRows; + if (IsScreenIsolated()) { + // the first row must not be moved. + _screenBuffer.ShrinkTail(shrinkRows); + } + else { + // copy rows from the screen buffer to the log buffer + _workGLineBuff.EnsureCapacity(shrinkRows); + var buffSpan = _workGLineBuff.Span(0, shrinkRows); + _screenBuffer.GetRows(0, buffSpan); + _logBuffer.Append(buffSpan.GLines()); + _workGLineBuff.Clear(); + // shrink the screen buffer + _screenBuffer.ShrinkHead(shrinkRows); + } + } + else if (newRows > curRows) { + int extendRows = newRows - curRows; + if (IsScreenIsolated()) { + // the first row must not be moved. + _screenBuffer.ExtendTail(FillNewLines(_workGLineBuff, extendRows)); + _workGLineBuff.Clear(); + } + else { + int logRows = _logBuffer.RowIDSpan.Length; + // move rows from the log buffer to the screen buffer (only available rows) + int moveRows = Math.Min(extendRows, logRows); + if (moveRows > 0) { + _workGLineBuff.EnsureCapacity(moveRows); + var buffSpan = _workGLineBuff.Span(0, moveRows); + _logBuffer.RemoveFromTail(buffSpan); + _screenBuffer.ExtendHead(buffSpan); + _workGLineBuff.Clear(); + } + // extend screen if the moved rows were not enough + int extendTailRows = extendRows - moveRows; + if (extendTailRows > 0) { + _screenBuffer.ExtendTail(FillNewLines(_workGLineBuff, extendTailRows)); + _workGLineBuff.Clear(); + } + } + } + + OnVisibleAreaSizeChanged(rows, cols); + } + } + + #endregion + + //------------------------------------------------------------- + // Internal screen operations + //------------------------------------------------------------- + + /// + /// Append a single line. + /// + /// line object + protected void ScreenAppend(GLine line) { + lock (_syncRoot) { + _workSingleGLine[0] = line; + ScreenScrollUp(new GLineChunkSpan(_workSingleGLine, 0, 1)); + _workSingleGLine[0] = null; + } + } + + /// + /// + /// Gets a object at the specified row. + /// + /// + /// Note that each access to this property does internal synchronization. + /// Consider to use or . + /// + /// + /// row index of the screen + /// was out-of-range. + protected GLine ScreenGetRow(int rowIndex) { + return _screenBuffer[rowIndex]; + } + + /// + /// + /// Gets or sets a object at the specified row. + /// + /// + /// Note that each access to this property does internal synchronization. + /// Consider to use or . + /// + /// + /// row index of the screen + /// line object to set + /// was out-of-range. + protected void ScreenSetRow(int rowIndex, GLine line) { + _screenBuffer[rowIndex] = line; + } + + /// + /// Gets rows starting at the specified index. + /// + /// row index. 0 indicates the first row at the top of the screen. + /// + /// array buffer to store the copied object. + /// the length of the span is used as the number of rows to get. + /// + /// and the length of the span was out-of-range. + protected void ScreenGetRows(int rowIndex, GLineChunkSpan span) { + _screenBuffer.GetRows(rowIndex, span); + } + + /// + /// Sets rows starting at the specified index. + /// + /// row index. 0 indicates the first row at the top of the screen. + /// + /// array buffer to get objects to be copied. + /// the length of the span is used as the number of rows to set. + /// + /// and the length of the span was out-of-range. + protected void ScreenSetRows(int rowIndex, GLineChunkSpan span) { + _screenBuffer.SetRows(rowIndex, span); + } + + /// + /// Scroll-up entire of the screen. + /// + /// number of rows to scroll + protected void ScreenScrollUp(int scrollRows) { + lock (_syncRoot) { + int screenRows = _screenBuffer.Size; + int scrollOutRows = Math.Min(scrollRows, screenRows); + + if (!IsScreenIsolated()) { + // copy rows from the screen buffer to the log buffer + _workGLineBuff.EnsureCapacity(scrollOutRows); + var buffSpan = _workGLineBuff.Span(0, scrollOutRows); + _screenBuffer.GetRows(0, buffSpan); + _logBuffer.Append(buffSpan.GLines()); + _workGLineBuff.Clear(); + // append empty rows if the copied rows were not enough + if (scrollOutRows < scrollRows) { + _logBuffer.Append(GenerateNewLines(scrollRows - scrollOutRows)); + } + } + _screenBuffer.ScrollUp(FillNewLines(_workGLineBuff, scrollOutRows)); + _workGLineBuff.Clear(); + } + } + + /// + /// Scroll-up entire of the screen. + /// + /// rows to append at the bottom of the screen. + protected void ScreenScrollUp(GLineChunkSpan newRows) { + lock (_syncRoot) { + int scrollRows = newRows.Length; + int screenRows = _screenBuffer.Size; + int scrollOutRows = Math.Min(scrollRows, screenRows); + + if (!IsScreenIsolated()) { + // copy rows from the screen buffer to the log buffer + _workGLineBuff.EnsureCapacity(scrollOutRows); + var buffSpan = _workGLineBuff.Span(0, scrollOutRows); + _screenBuffer.GetRows(0, buffSpan); + _logBuffer.Append(buffSpan.GLines()); + _workGLineBuff.Clear(); + // copy rows from newRows if the copied rows were not enough + if (scrollOutRows < scrollRows) { + _logBuffer.Append(newRows.Span(0, scrollRows - scrollOutRows).GLines()); + } + } + + _screenBuffer.ScrollUp(newRows.Span(scrollRows - scrollOutRows, scrollOutRows)); + } + } + + /// + /// Scroll-down entire of the screen. + /// + /// number of rows to scroll + protected void ScreenScrollDown(int scrollRows) { + lock (_syncRoot) { + if (!IsScreenIsolated()) { + var logRowIDSpan = _logBuffer.RowIDSpan; + // copy rows from the log buffer to the screen buffer + _workGLineBuff.EnsureCapacity(scrollRows); + int rowsToRemove = Math.Min(scrollRows, logRowIDSpan.Length); + int emptyRows = scrollRows - rowsToRemove; + if (emptyRows > 0) { + FillNewLines(_workGLineBuff.Span(0, emptyRows)); + } + _logBuffer.RemoveFromTail(_workGLineBuff.Span(emptyRows, rowsToRemove)); + _screenBuffer.ScrollDown(_workGLineBuff.Span(0, scrollRows)); + _workGLineBuff.Clear(); + } + else { + _screenBuffer.ScrollDown(FillNewLines(_workGLineBuff, scrollRows)); + } + } + } + + /// + /// Scroll-up rows in the specified region. + /// + /// start row indedx of the scroll region (inclusive) + /// end row indedx of the scroll region (exclusive) + /// + /// rows to insert at the bottom of the region. + /// the length of the span is used as the number of rows to scroll. + /// + protected void ScreenScrollUpRegion(int startRowIndex, int endRowIndex, GLineChunkSpan newRows) { + _screenBuffer.ScrollUpRegion(startRowIndex, endRowIndex, newRows); + } + + /// + /// Scroll-down rows in the specified region. + /// + /// start row indedx of the scroll region (inclusive) + /// end row indedx of the scroll region (exclusive) + /// + /// rows to insert at the top of the region. + /// the length of the span is used as the number of rows to scroll. + /// + protected void ScreenScrollDownRegion(int startRowIndex, int endRowIndex, GLineChunkSpan newRows) { + _screenBuffer.ScrollDownRegion(startRowIndex, endRowIndex, newRows); + } + +#if UNITTEST + internal int ScreenBufferSize { + get { + return _screenBuffer.Size; + } + } + + internal int LogBufferSize { + get { + return _logBuffer.RowIDSpan.Length; + } + } + + internal void StoreGLines(GLine[] screenBuff) { + for (int i = 0; i < screenBuff.Length; i++) { + _screenBuffer[i] = screenBuff[i]; + } + } + + internal void PeekGLines(out GLine[] screenBuff, out GLine[] logBuff) { + lock (_syncRoot) { + int screenSize = _screenBuffer.Size; + screenBuff = new GLine[screenSize]; + _screenBuffer.GetRows(0, new GLineChunkSpan(screenBuff, 0, screenSize)); + + var logSpan = _logBuffer.RowIDSpan; + logBuff = new GLine[logSpan.Length]; + _logBuffer.GetLinesByID(logSpan.Start, new GLineChunkSpan(logBuff, 0, logSpan.Length)); + } + } +#endif + + private void GetRowIDSpans(out RowIDSpan logBuffSpan, out RowIDSpan screenBuffSpan) { + logBuffSpan = _logBuffer.RowIDSpan; + screenBuffSpan = new RowIDSpan(logBuffSpan.Start + logBuffSpan.Length, _screenBuffer.Size); + } + + private GLineChunkSpan FillNewLines(GLineChunk chunk, int rows) { + chunk.EnsureCapacity(rows); + var span = chunk.Span(0, rows); + FillNewLines(span); + return span; + } + + private void FillNewLines(GLineChunkSpan span) { + for (int i = 0; i < span.Length; i++) { + span.Array[span.Offset + i] = CreateEmptyLine(); + } + } + + private IEnumerable GenerateNewLines(int rows) { + for (int i = 0; i < rows; i++) { + yield return CreateEmptyLine(); + } + } + } +} diff --git a/Core/TextSelection.cs b/Core/TextSelection.cs index 11aacfcd..a4b1d554 100644 --- a/Core/TextSelection.cs +++ b/Core/TextSelection.cs @@ -1,4 +1,4 @@ -// Copyright 2004-2017 The Poderosa Project. +// Copyright 2004-2019 The Poderosa Project. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,345 +12,228 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; -using System.Text; -using System.Collections.Generic; -using System.Diagnostics; - -using Poderosa.Sessions; +using Poderosa.Commands; using Poderosa.Document; using Poderosa.Forms; -using Poderosa.Commands; +using Poderosa.Sessions; +using System; +using System.Collections.Generic; +using System.Text; namespace Poderosa.View { - internal enum RangeType { - Char, - Word, - Line - } - internal enum SelectionState { - Empty, //無選択 - Pivot, //選択開始 - Expansion, //選択中 - Fixed //選択領域確定 - } - //CharacterDocumentの一部を選択するための機能 + /// + /// A class that manages the text selection on the document. + /// internal class TextSelection : ITextSelection { - //端点 - internal class TextPoint : ICloneable { - private int _line; - private int _column; - - public int Line { - get { - return _line; - } - set { - _line = value; - } - } - public int Column { - get { - return _column; - } - set { - _column = value; - } - } - - public TextPoint() { - Clear(); - } - public TextPoint(int line, int column) { - _line = line; - _column = column; - } - + internal enum Mode { + Char, + Word, + Line + } - public void Clear() { - Line = -1; - Column = 0; - } + internal enum State { + /// Empty. (no selection) + Empty, + /// The first anchor point has been set. (mouse-down) + Started, + /// Expanding the region. (during mouse-move) + Expanding, + /// The second anchor point has been set. (mouse-up) + Fixed, + } - public object Clone() { - return MemberwiseClone(); + internal struct Region { + /// starting row ID (inclusive) + public readonly int StartRowID; + /// starting caret position + public readonly int StartPos; + /// ending row ID (inclusive) + public readonly int EndRowID; + /// ending caret position. null represents the end-of-line. + public readonly int? EndPos; + + public Region(int startRowID, int startPos, int endRowID, int? endPos) { + this.StartRowID = startRowID; + this.StartPos = startPos; + this.EndRowID = endRowID; + this.EndPos = endPos; } } - private SelectionState _state; + private readonly CharacterDocumentViewer _ownerViewer; + + private readonly List _listeners = new List(); - private List _listeners; + private State _state = State.Empty; - private CharacterDocumentViewer _owner; - //最初の選択点。単語や行を選択したときのために2つ(forward/backward)設ける。 - private TextPoint _forwardPivot; - private TextPoint _backwardPivot; - //選択の最終点 - private TextPoint _forwardDestination; - private TextPoint _backwardDestination; + // Row ID of the first anchor point + private int _firstRowID; + // Caret position of the first anchor point + private int _firstPosLeft; // left-end position of the word + private int? _firstPosRight; // right-end position of the word, or null (end-of-line) - //pivotの状態 - private RangeType _pivotType; + // Row ID of the second anchor point + private int _secondRowID; + // Caret position of the first anchor point + private int _secondPosLeft; // left-end position of the word + private int? _secondPosRight; // right-end position of the word, or null (end-of-line) - //選択を開始したときのマウス座標 - private int _startX; - private int _startY; + private Mode _mode = Mode.Char; - //ちょっと汚いフラグ - //private bool _disabledTemporary; + // Mouse position at the first anchor point of the previous selection + private int _prevStartMouseX; + private int _prevStartMouseY; - public TextSelection(CharacterDocumentViewer owner) { - _owner = owner; - _forwardPivot = new TextPoint(); - _backwardPivot = new TextPoint(); - _forwardDestination = new TextPoint(); - _backwardDestination = new TextPoint(); - _listeners = new List(); + /// + /// Constructor + /// + /// + public TextSelection(CharacterDocumentViewer viewer) { + _ownerViewer = viewer; } - public SelectionState State { + /// + /// Current state of the text-selection + /// + public State CurrentState { get { return _state; } } - public RangeType PivotType { - get { - return _pivotType; - } - } - //マウスを動かさなくてもクリックだけでMouseMoveイベントが発生してしまうので、位置のチェックのためにマウス座標記憶が必要 - public int StartX { + /// + /// Current mode of the text-selection + /// + public Mode CurrentMode { get { - return _startX; + return _mode; } } - public int StartY { - get { - return _startY; - } - } - - - public void Clear() { - //if(_owner!=null) - // _owner.ExitTextSelection(); - _state = SelectionState.Empty; - _forwardPivot.Clear(); - _backwardPivot.Clear(); - _forwardDestination.Clear(); - _backwardDestination.Clear(); - //_disabledTemporary = false; - } - - /* - public void DisableTemporary() { - _disabledTemporary = true; - }*/ #region ISelection + public IPoderosaView OwnerView { get { - return (IPoderosaView)_owner.GetAdapter(typeof(IPoderosaView)); + return (IPoderosaView)_ownerViewer.GetAdapter(typeof(IPoderosaView)); } } + public IPoderosaCommand TranslateCommand(IGeneralCommand command) { return null; } + public IAdaptable GetAdapter(Type adapter) { return WindowManagerPlugin.Instance.PoderosaWorld.AdapterManager.GetAdapter(this, adapter); } - #endregion - - //ドキュメントがDiscardされたときに呼ばれる。first_lineより前に選択領域が重なっていたらクリアする - public void ClearIfOverlapped(int first_line) { - if (_forwardPivot.Line != -1 && _forwardPivot.Line < first_line) { - _forwardPivot.Line = first_line; - _forwardPivot.Column = 0; - _backwardPivot.Line = first_line; - _backwardPivot.Column = 0; - } - if (_forwardDestination.Line != -1 && _forwardDestination.Line < first_line) { - _forwardDestination.Line = first_line; - _forwardDestination.Column = 0; - _backwardDestination.Line = first_line; - _backwardDestination.Column = 0; - } + public void AddSelectionListener(ISelectionListener listener) { + _listeners.Add(listener); } - - public bool IsEmpty { - get { - return _forwardPivot.Line == -1 || _backwardPivot.Line == -1 || - _forwardDestination.Line == -1 || _backwardDestination.Line == -1; - } + public void RemoveSelectionListener(ISelectionListener listener) { + _listeners.Remove(listener); } - public bool StartSelection(GLine line, int position, RangeType type, int x, int y) { - Debug.Assert(position >= 0); - //日本語文字の右側からの選択は左側に修正 - line.ExpandBuffer(position + 1); - if (line.IsRightSideOfZenkaku(position)) - position--; + #endregion - //_disabledTemporary = false; - _pivotType = type; - _forwardPivot.Line = line.ID; - _backwardPivot.Line = line.ID; - _forwardDestination.Line = line.ID; - _forwardDestination.Column = position; - _backwardDestination.Line = line.ID; - _backwardDestination.Column = position; - switch (type) { - case RangeType.Char: - _forwardPivot.Column = position; - _backwardPivot.Column = position; - break; - case RangeType.Word: { - int start; - int end; - line.FindWordBreakPoint(position, out start, out end); - _forwardPivot.Column = start; - _backwardPivot.Column = end; - } - break; - case RangeType.Line: - _forwardPivot.Column = 0; - _backwardPivot.Column = line.DisplayLength; - break; - } - _state = SelectionState.Pivot; - _startX = x; - _startY = y; - FireSelectionStarted(); - return true; - } + #region ITextSelection - public bool ExpandTo(GLine line, int position, RangeType type) { - line.ExpandBuffer(position + 1); - //_disabledTemporary = false; - _state = SelectionState.Expansion; - - _forwardDestination.Line = line.ID; - _backwardDestination.Line = line.ID; - //Debug.WriteLine(String.Format("ExpandTo Line{0} Position{1}", line.ID, position)); - switch (type) { - case RangeType.Char: - _forwardDestination.Column = position; - _backwardDestination.Column = position; - break; - case RangeType.Word: { - int start; - int end; - line.FindWordBreakPoint(position, out start, out end); - _forwardDestination.Column = start; - _backwardDestination.Column = end; - } - break; - case RangeType.Line: - _forwardDestination.Column = 0; - _backwardDestination.Column = line.DisplayLength; - break; + public bool IsEmpty { + get { + return _state == State.Empty + || (_firstRowID == _secondRowID + && _firstPosLeft == _firstPosRight + && _secondPosLeft == _secondPosRight + && _firstPosLeft == _secondPosLeft); } - - return true; } - public void SelectAll() { - //_disabledTemporary = false; - _forwardPivot.Line = _owner.CharacterDocument.FirstLine.ID; - _forwardPivot.Column = 0; - _backwardPivot = (TextPoint)_forwardPivot.Clone(); - _forwardDestination.Line = _owner.CharacterDocument.LastLine.ID; - _forwardDestination.Column = _owner.CharacterDocument.LastLine.DisplayLength; - _backwardDestination = (TextPoint)_forwardDestination.Clone(); - - _pivotType = RangeType.Char; - FixSelection(); + /// + /// Clear selection + /// + public void Clear() { + _state = State.Empty; + _firstRowID = _secondRowID = 0; + _firstPosLeft = _secondPosLeft = 0; + _firstPosRight = _secondPosRight = 0; + _mode = Mode.Char; } - //選択モードに応じて範囲を定める。マウスでドラッグすることもあるので、column<0のケースも存在する - public TextPoint ConvertSelectionPosition(GLine line, int column) { - TextPoint result = new TextPoint(line.ID, column); - - int line_length = line.DisplayLength; - if (_pivotType == RangeType.Line) { - //行選択のときは、選択開始点以前のであったらその行の先頭、そうでないならその行のラスト。 - //言い換えると(Pivot-Destination)を行頭・行末方向に拡大したものになるように - if (result.Line <= _forwardPivot.Line) - result.Column = 0; - else - result.Column = line.DisplayLength; + /// + /// Select entire screen + /// + public void SelectAll() { + ICharacterDocument doc = _ownerViewer.CharacterDocument; + if (doc == null) { + return; } - else { //Word,Char選択 - if (result.Line < _forwardPivot.Line) { //開始点より前のときは - if (result.Column < 0) - result.Column = 0; //行頭まで。 - else if (result.Column >= line_length) { //行の右端の右まで選択しているときは、次行の先頭まで - result.Line++; - result.Column = 0; - } - } - else if (result.Line == _forwardPivot.Line) { //同一行内選択.その行におさまるように - result.Column = RuntimeUtil.AdjustIntRange(result.Column, 0, line_length); - } - else { //開始点の後方への選択 - if (result.Column < 0) { - result.Line--; - result.Column = line.PrevLine == null ? 0 : line.PrevLine.DisplayLength; - } - else if (result.Column >= line_length) - result.Column = line_length; - } + RowIDSpan rowIDSpan = doc.GetRowIDSpan(); + if (rowIDSpan.Length <= 0) { + Clear(); + return; } - - return result; - } - - public void FixSelection() { - _state = SelectionState.Fixed; + _firstRowID = rowIDSpan.Start; + _firstPosLeft = 0; + _firstPosRight = 0; + _secondRowID = rowIDSpan.Start + rowIDSpan.Length - 1; + _secondPosLeft = 0; + _secondPosRight = null; // end-of-line + _state = State.Fixed; FireSelectionFixed(); } + /// + /// Gets text in the selection. + /// + /// + /// public string GetSelectedText(TextFormatOption opt) { - StringBuilder bld = new StringBuilder(); - TextPoint selStart = HeadPoint; - TextPoint selEnd = TailPoint; - - GLine l = _owner.CharacterDocument.FindLineOrEdge(selStart.Line); - int pos = selStart.Column; - if (pos < 0) { + Region? regionOrNull = GetRegion(); + if (!regionOrNull.HasValue) { return ""; } - while (true) { - bool eolRequired = (opt == TextFormatOption.AsLook || l.EOLType != EOLType.Continue); + Region region = regionOrNull.Value; - if (l.ID == selEnd.Line) { // the last line - CopyGLineContent(l, bld, pos, selEnd.Column); - if (eolRequired && _pivotType == RangeType.Line) { - bld.Append("\r\n"); - } - break; - } + StringBuilder buff = new StringBuilder(); - CopyGLineContent(l, bld, pos, null); + ICharacterDocument doc = _ownerViewer.CharacterDocument; + if (doc != null) { + lock (doc.SyncRoot) { + RowIDSpan docSpan = doc.GetRowIDSpan(); + RowIDSpan selSpan = docSpan.Intersect( + new RowIDSpan(region.StartRowID, region.EndRowID - region.StartRowID + 1)); - if (eolRequired) { - bld.Append("\r\n"); - } + if (selSpan.Length <= 0) { + // selected rows were already missing in the current document + return ""; + } - l = l.NextLine; - if (l == null) { - // this should not be happened... - break; + doc.ForEach(selSpan.Start, selSpan.Length, (rowID, line) => { + if (line != null) { + bool eolRequired = (opt == TextFormatOption.AsLook || line.EOLType != EOLType.Continue); + int lineLen = line.DisplayLength; + int startCol = (rowID == region.StartRowID) ? Math.Min(region.StartPos, lineLen) : 0; + if (rowID == region.EndRowID) { + // the last line + CopyGLineContent(line, buff, startCol, region.EndPos); + if (eolRequired && _mode == Mode.Line) { + buff.Append("\r\n"); + } + } + else { + CopyGLineContent(line, buff, startCol, null); + if (eolRequired) { + buff.Append("\r\n"); + } + } + } + }); } - pos = 0; } - return bld.ToString(); + return buff.ToString(); } private void CopyGLineContent(GLine line, StringBuilder buff, int start, int? end) { @@ -370,64 +253,194 @@ private void CopyGLineContent(GLine line, StringBuilder buff, int start, int? en } } - internal TextPoint HeadPoint { - get { - return Min(_forwardPivot, _forwardDestination); + #endregion + + /// + /// Gets current selected region. + /// + /// region, or null if the selection is empty. + public Region? GetRegion() { + if (IsEmpty) { + return null; + } + + if (_firstRowID < _secondRowID) { + return new Region( + startRowID: _firstRowID, + startPos: _firstPosLeft, + endRowID: _secondRowID, + endPos: _secondPosRight); + } + else if (_firstRowID == _secondRowID) { + int? selEndPos; + if (_firstPosRight.HasValue && _secondPosRight.HasValue) { + selEndPos = Math.Max(_firstPosRight.Value, _secondPosRight.Value); + } + else { + selEndPos = null; // end-of-line + } + + return new Region( + startRowID: _firstRowID, + startPos: Math.Min(_firstPosLeft, _secondPosLeft), + endRowID: _firstRowID, + endPos: selEndPos); + } + else { + return new Region( + startRowID: _secondRowID, + startPos: _secondPosLeft, + endRowID: _firstRowID, + endPos: _firstPosRight); } } - internal TextPoint TailPoint { + + public bool CanHandleMouseDown { get { - return Max(_backwardPivot, _backwardDestination); + return true; } } - private static TextPoint Min(TextPoint p1, TextPoint p2) { - int id1 = p1.Line; - int id2 = p2.Line; - if (id1 == id2) { - int pos1 = p1.Column; - int pos2 = p2.Column; - if (pos1 == pos2) - return p1; - else - return pos1 < pos2 ? p1 : p2; + + public bool CanHandleMouseMove { + get { + return _state == State.Expanding || _state == State.Started; } - else - return id1 < id2 ? p1 : p2; + } + public bool CanHandleMouseUp { + get { + return _state == State.Expanding || _state == State.Started; + } } - private static TextPoint Max(TextPoint p1, TextPoint p2) { - int id1 = p1.Line; - int id2 = p2.Line; - if (id1 == id2) { - int pos1 = p1.Column; - int pos2 = p2.Column; - if (pos1 == pos2) - return p1; - else - return pos1 > pos2 ? p1 : p2; + + /// + /// Handle mouse-down + /// + /// row ID of the target row. + /// line data at the target row. + /// caret position + /// mode to override, or null to use a mode determined. + /// raw mouse position + /// raw mouse position + public void OnMouseDown(int rowID, GLine line, int position, Mode? modeOverride, int mouseX, int mouseY) { + // adjust position with the boundary of the wide-width character + line.ExpandBuffer(position + 1); + if (line.IsRightSideOfZenkaku(position)) { + position--; } - else - return id1 > id2 ? p1 : p2; + if (modeOverride.HasValue) { + _mode = modeOverride.Value; + } + else if (Math.Abs(mouseX - _prevStartMouseX) <= 1 && Math.Abs(mouseY - _prevStartMouseY) <= 1) { + // switch modes + switch (_mode) { + case Mode.Char: + _mode = Mode.Word; + break; + case Mode.Word: + _mode = Mode.Line; + break; + case Mode.Line: + default: + _mode = Mode.Char; + break; + } + } + else { + _mode = Mode.Char; + } + + _firstRowID = _secondRowID = rowID; + + switch (_mode) { + case Mode.Word: { + int start; + int end; + line.FindWordBreakPoint(position, out start, out end); + _firstPosLeft = _secondPosLeft = start; + _firstPosRight = _secondPosRight = end; + } + break; + case Mode.Line: + _firstPosLeft = _secondPosLeft = 0; + _firstPosRight = _secondPosRight = null; // end-of-line + break; + case Mode.Char: + default: + _firstPosLeft = _secondPosLeft = position; + _firstPosRight = _secondPosRight = position; + break; + } + _state = State.Started; + _prevStartMouseX = mouseX; + _prevStartMouseY = mouseY; + FireSelectionStarted(); } - //Listener系 - public void AddSelectionListener(ISelectionListener listener) { - _listeners.Add(listener); + /// + /// Handle mouse-move + /// + /// row ID of the target row. + /// line data at the target row. + /// caret position + /// mode to override, or null to use a mode determined at the mouse-down. + public void OnMouseMove(int rowID, GLine line, int position, Mode? modeOverride) { + // adjust position with the boundary of the wide-width character + line.ExpandBuffer(position + 1); + if (line.IsRightSideOfZenkaku(position)) { + position--; + } + + _secondRowID = rowID; + + switch (modeOverride ?? _mode) { + case Mode.Word: { + int start; + int end; + line.FindWordBreakPoint(position, out start, out end); + _secondPosLeft = start; + _secondPosRight = end; + } + break; + case Mode.Line: + _secondPosLeft = 0; + _secondPosRight = null; // end-of-line + break; + case Mode.Char: + default: + _secondPosLeft = position; + _secondPosRight = position; + break; + } + _state = State.Expanding; } - public void RemoveSelectionListener(ISelectionListener listener) { - _listeners.Remove(listener); + + /// + /// Handle mouse-up + /// + public void OnMouseUp() { + if (IsEmpty) { + Clear(); + } + else { + _state = State.Fixed; + } + FireSelectionFixed(); } - void FireSelectionStarted() { - foreach (ISelectionListener listener in _listeners) + + private void FireSelectionStarted() { + foreach (ISelectionListener listener in _listeners) { listener.OnSelectionStarted(); + } } - void FireSelectionFixed() { - foreach (ISelectionListener listener in _listeners) + + private void FireSelectionFixed() { + foreach (ISelectionListener listener in _listeners) { listener.OnSelectionFixed(); + } } - } } diff --git a/Core/TextSelection_Old.cs b/Core/TextSelection_Old.cs new file mode 100644 index 00000000..d4fd2d6e --- /dev/null +++ b/Core/TextSelection_Old.cs @@ -0,0 +1,435 @@ +// Copyright 2004-2017 The Poderosa Project. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System; +using System.Text; +using System.Collections.Generic; +using System.Diagnostics; + +using Poderosa.Sessions; +using Poderosa.Document; +using Poderosa.Forms; +using Poderosa.Commands; + +namespace Poderosa.View { + + //CharacterDocumentの一部を選択するための機能 + internal class TextSelection_Old : ITextSelection { + + internal enum RangeType { + Char, + Word, + Line + } + + internal enum SelectionState { + Empty, //無選択 + Pivot, //選択開始 + Expansion, //選択中 + Fixed //選択領域確定 + } + + //端点 + internal class TextPoint : ICloneable { + private int _line; + private int _column; + + public int Line { + get { + return _line; + } + set { + _line = value; + } + } + public int Column { + get { + return _column; + } + set { + _column = value; + } + } + + public TextPoint() { + Clear(); + } + public TextPoint(int line, int column) { + _line = line; + _column = column; + } + + + public void Clear() { + Line = -1; + Column = 0; + } + + public object Clone() { + return MemberwiseClone(); + } + } + + private SelectionState _state; + + private List _listeners; + + private CharacterDocumentViewer_Old _owner; + //最初の選択点。単語や行を選択したときのために2つ(forward/backward)設ける。 + private TextPoint _forwardPivot; + private TextPoint _backwardPivot; + //選択の最終点 + private TextPoint _forwardDestination; + private TextPoint _backwardDestination; + + //pivotの状態 + private RangeType _pivotType; + + //選択を開始したときのマウス座標 + private int _startX; + private int _startY; + + //ちょっと汚いフラグ + //private bool _disabledTemporary; + + public TextSelection_Old(CharacterDocumentViewer_Old owner) { + _owner = owner; + _forwardPivot = new TextPoint(); + _backwardPivot = new TextPoint(); + _forwardDestination = new TextPoint(); + _backwardDestination = new TextPoint(); + _listeners = new List(); + } + + public SelectionState State { + get { + return _state; + } + } + public RangeType PivotType { + get { + return _pivotType; + } + } + + //マウスを動かさなくてもクリックだけでMouseMoveイベントが発生してしまうので、位置のチェックのためにマウス座標記憶が必要 + public int StartX { + get { + return _startX; + } + } + public int StartY { + get { + return _startY; + } + } + + + public void Clear() { + //if(_owner!=null) + // _owner.ExitTextSelection(); + _state = SelectionState.Empty; + _forwardPivot.Clear(); + _backwardPivot.Clear(); + _forwardDestination.Clear(); + _backwardDestination.Clear(); + //_disabledTemporary = false; + } + + /* + public void DisableTemporary() { + _disabledTemporary = true; + }*/ + + #region ISelection + public IPoderosaView OwnerView { + get { + return (IPoderosaView)_owner.GetAdapter(typeof(IPoderosaView)); + } + } + public IPoderosaCommand TranslateCommand(IGeneralCommand command) { + return null; + } + public IAdaptable GetAdapter(Type adapter) { + return WindowManagerPlugin.Instance.PoderosaWorld.AdapterManager.GetAdapter(this, adapter); + } + #endregion + + //ドキュメントがDiscardされたときに呼ばれる。first_lineより前に選択領域が重なっていたらクリアする + public void ClearIfOverlapped(int first_line) { + if (_forwardPivot.Line != -1 && _forwardPivot.Line < first_line) { + _forwardPivot.Line = first_line; + _forwardPivot.Column = 0; + _backwardPivot.Line = first_line; + _backwardPivot.Column = 0; + } + + if (_forwardDestination.Line != -1 && _forwardDestination.Line < first_line) { + _forwardDestination.Line = first_line; + _forwardDestination.Column = 0; + _backwardDestination.Line = first_line; + _backwardDestination.Column = 0; + } + } + + public bool IsEmpty { + get { + return _forwardPivot.Line == -1 || _backwardPivot.Line == -1 || + _forwardDestination.Line == -1 || _backwardDestination.Line == -1; + } + } + + public bool StartSelection(GLine line, int position, RangeType type, int x, int y) { + Debug.Assert(position >= 0); + //日本語文字の右側からの選択は左側に修正 + line.ExpandBuffer(position + 1); + if (line.IsRightSideOfZenkaku(position)) + position--; + + //_disabledTemporary = false; + _pivotType = type; + _forwardPivot.Line = line.ID; + _backwardPivot.Line = line.ID; + _forwardDestination.Line = line.ID; + _forwardDestination.Column = position; + _backwardDestination.Line = line.ID; + _backwardDestination.Column = position; + switch (type) { + case RangeType.Char: + _forwardPivot.Column = position; + _backwardPivot.Column = position; + break; + case RangeType.Word: { + int start; + int end; + line.FindWordBreakPoint(position, out start, out end); + _forwardPivot.Column = start; + _backwardPivot.Column = end; + } + break; + case RangeType.Line: + _forwardPivot.Column = 0; + _backwardPivot.Column = line.DisplayLength; + break; + } + _state = SelectionState.Pivot; + _startX = x; + _startY = y; + FireSelectionStarted(); + return true; + } + + public bool ExpandTo(GLine line, int position, RangeType type) { + line.ExpandBuffer(position + 1); + //_disabledTemporary = false; + _state = SelectionState.Expansion; + + _forwardDestination.Line = line.ID; + _backwardDestination.Line = line.ID; + //Debug.WriteLine(String.Format("ExpandTo Line{0} Position{1}", line.ID, position)); + switch (type) { + case RangeType.Char: + _forwardDestination.Column = position; + _backwardDestination.Column = position; + break; + case RangeType.Word: { + int start; + int end; + line.FindWordBreakPoint(position, out start, out end); + _forwardDestination.Column = start; + _backwardDestination.Column = end; + } + break; + case RangeType.Line: + _forwardDestination.Column = 0; + _backwardDestination.Column = line.DisplayLength; + break; + } + + return true; + } + + public void SelectAll() { + //_disabledTemporary = false; + _forwardPivot.Line = _owner.CharacterDocument.FirstLine.ID; + _forwardPivot.Column = 0; + _backwardPivot = (TextPoint)_forwardPivot.Clone(); + _forwardDestination.Line = _owner.CharacterDocument.LastLine.ID; + _forwardDestination.Column = _owner.CharacterDocument.LastLine.DisplayLength; + _backwardDestination = (TextPoint)_forwardDestination.Clone(); + + _pivotType = RangeType.Char; + FixSelection(); + } + + //選択モードに応じて範囲を定める。マウスでドラッグすることもあるので、column<0のケースも存在する + public TextPoint ConvertSelectionPosition(GLine line, int column) { + TextPoint result = new TextPoint(line.ID, column); + + int line_length = line.DisplayLength; + if (_pivotType == RangeType.Line) { + //行選択のときは、選択開始点以前のであったらその行の先頭、そうでないならその行のラスト。 + //言い換えると(Pivot-Destination)を行頭・行末方向に拡大したものになるように + if (result.Line <= _forwardPivot.Line) + result.Column = 0; + else + result.Column = line.DisplayLength; + } + else { //Word,Char選択 + if (result.Line < _forwardPivot.Line) { //開始点より前のときは + if (result.Column < 0) + result.Column = 0; //行頭まで。 + else if (result.Column >= line_length) { //行の右端の右まで選択しているときは、次行の先頭まで + result.Line++; + result.Column = 0; + } + } + else if (result.Line == _forwardPivot.Line) { //同一行内選択.その行におさまるように + result.Column = RuntimeUtil.AdjustIntRange(result.Column, 0, line_length); + } + else { //開始点の後方への選択 + if (result.Column < 0) { + result.Line--; + result.Column = line.PrevLine == null ? 0 : line.PrevLine.DisplayLength; + } + else if (result.Column >= line_length) + result.Column = line_length; + } + } + + return result; + } + + public void FixSelection() { + _state = SelectionState.Fixed; + FireSelectionFixed(); + } + + public string GetSelectedText(TextFormatOption opt) { + StringBuilder bld = new StringBuilder(); + TextPoint selStart = HeadPoint; + TextPoint selEnd = TailPoint; + + GLine l = _owner.CharacterDocument.FindLineOrEdge(selStart.Line); + int pos = selStart.Column; + if (pos < 0) { + return ""; + } + + while (true) { + bool eolRequired = (opt == TextFormatOption.AsLook || l.EOLType != EOLType.Continue); + + if (l.ID == selEnd.Line) { // the last line + CopyGLineContent(l, bld, pos, selEnd.Column); + if (eolRequired && _pivotType == RangeType.Line) { + bld.Append("\r\n"); + } + break; + } + + CopyGLineContent(l, bld, pos, null); + + if (eolRequired) { + bld.Append("\r\n"); + } + + l = l.NextLine; + if (l == null) { + // this should not be happened... + break; + } + pos = 0; + } + + return bld.ToString(); + } + + private void CopyGLineContent(GLine line, StringBuilder buff, int start, int? end) { + if (start > 0 && line.IsRightSideOfZenkaku(start)) { + start--; + } + + if (end.HasValue) { + line.WriteTo( + (data, len) => buff.Append(data, 0, len), + start, end.Value); + } + else { + line.WriteTo( + (data, len) => buff.Append(data, 0, len), + start); + } + } + + internal TextPoint HeadPoint { + get { + return Min(_forwardPivot, _forwardDestination); + } + } + internal TextPoint TailPoint { + get { + return Max(_backwardPivot, _backwardDestination); + } + } + private static TextPoint Min(TextPoint p1, TextPoint p2) { + int id1 = p1.Line; + int id2 = p2.Line; + if (id1 == id2) { + int pos1 = p1.Column; + int pos2 = p2.Column; + if (pos1 == pos2) + return p1; + else + return pos1 < pos2 ? p1 : p2; + } + else + return id1 < id2 ? p1 : p2; + + } + private static TextPoint Max(TextPoint p1, TextPoint p2) { + int id1 = p1.Line; + int id2 = p2.Line; + if (id1 == id2) { + int pos1 = p1.Column; + int pos2 = p2.Column; + if (pos1 == pos2) + return p1; + else + return pos1 > pos2 ? p1 : p2; + } + else + return id1 > id2 ? p1 : p2; + + } + + //Listener系 + public void AddSelectionListener(ISelectionListener listener) { + _listeners.Add(listener); + } + public void RemoveSelectionListener(ISelectionListener listener) { + _listeners.Remove(listener); + } + + void FireSelectionStarted() { + foreach (ISelectionListener listener in _listeners) + listener.OnSelectionStarted(); + } + void FireSelectionFixed() { + foreach (ISelectionListener listener in _listeners) + listener.OnSelectionFixed(); + } + + + } +} diff --git a/Core/UIEventHandler.cs b/Core/UIEventHandler.cs index 9bc87698..01d6d3b0 100644 --- a/Core/UIEventHandler.cs +++ b/Core/UIEventHandler.cs @@ -51,6 +51,7 @@ public interface IMouseHandler : IUIHandler { UIHandleResult OnMouseMove(MouseEventArgs args); UIHandleResult OnMouseUp(MouseEventArgs args); UIHandleResult OnMouseWheel(MouseEventArgs args); + void Reset(); } //ProcessCmdKey/ProcessDialogKeyの周辺に関しての処理を行う @@ -93,6 +94,8 @@ public virtual UIHandleResult OnMouseUp(MouseEventArgs args) { public virtual UIHandleResult OnMouseWheel(MouseEventArgs args) { return UIHandleResult.Pass; } + + abstract public void Reset(); } /// @@ -102,14 +105,9 @@ public virtual UIHandleResult OnMouseWheel(MouseEventArgs args) { /// /// public abstract class UIHandlerManager where HANDLER : class, IUIHandler { - /// - /// - /// - /// - /// - /// - /// + public delegate UIHandleResult HandlerDelegate(HANDLER handler, ARG args); + public delegate void ResetDelegate(HANDLER handler); private List _handlers; //先頭が最高優先度 private HANDLER _capturingHandler; //イベントをキャプチャしているハンドラ。存在しないときはnull @@ -178,6 +176,18 @@ protected UIHandleResult Process(HandlerDelegate action, ARG args) { return UIHandleResult.Pass; } + + protected void Reset(ResetDelegate reset) { + try { + foreach (HANDLER h in _handlers) { + reset(h); + } + _capturingHandler = null; + } + catch (Exception ex) { + RuntimeUtil.ReportException(ex); + } + } } //ハンドラのマネージャ @@ -204,6 +214,10 @@ public class MouseHandlerManager : UIHandlerManager diff --git a/CoreTest/CoreTest.csproj b/CoreTest/CoreTest.csproj index 1aa09755..33d4701b 100644 --- a/CoreTest/CoreTest.csproj +++ b/CoreTest/CoreTest.csproj @@ -1,7 +1,7 @@  + - Debug @@ -13,7 +13,7 @@ Poderosa.CoreTest v4.5 512 - f5253711 + b3ba9388 true @@ -52,10 +52,14 @@ + + + + @@ -86,8 +90,8 @@ This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - + page[0][PAGE_SIZE - 3] Row ID: PAGE_SIZE - 2 + // start = 1 --> page[0][PAGE_SIZE - 2] Row ID: PAGE_SIZE - 1 + // start = 2 --> page[0][PAGE_SIZE - 1] Row ID: PAGE_SIZE + // start = 3 --> page[1][0] Row ID: PAGE_SIZE + 1 + // start = 4 --> page[1][1] Row ID: PAGE_SIZE + 2 + // start = 5 --> page[1][2] Row ID: PAGE_SIZE + 3 + + int startRowID = start + PAGE_SIZE - 2; + + for (int end = 0; end < 6; end++) { + // end = 0 --> page[1][PAGE_SIZE - 3] Row ID: PAGE_SIZE * 2 - 2 + // end = 1 --> page[1][PAGE_SIZE - 2] Row ID: PAGE_SIZE * 2 - 1 + // end = 2 --> page[1][PAGE_SIZE - 1] Row ID: PAGE_SIZE * 2 + // end = 3 --> page[2][0] Row ID: PAGE_SIZE * 2 + 1 + // end = 4 --> page[2][1] Row ID: PAGE_SIZE * 2 + 2 + // end = 5 --> page[2][2] Row ID: PAGE_SIZE * 2 + 3 + + int endRowID = end + PAGE_SIZE * 2 - 2; + + int rowCount = endRowID - startRowID + 1; + + var chunk = new GLineChunk(rowCount + 5); + buff.GetLinesByID(startRowID, chunk.Span(5, rowCount)); + + CollectionAssert.AreEqual( + GLineSequenceUtil.Concat( + new GLine[] { null, null, null, null, null }, + appender.Last(PAGE_SIZE + 6).Skip(start).Take(rowCount) + ), + chunk.Array + ); + } + } + } + + private GLine[] CreateLines(int num) { + return Enumerable.Range(0, num).Select(_ => new GLine(1)).ToArray(); + } + + private void CheckPages(GLineBuffer buff, ILineAppender appender, int[] pageSize) { + CheckPages(buff, appender.Last(pageSize.Sum()), pageSize); + } + + private void CheckPages(GLineBuffer buff, IEnumerable lineSource, int[] pageSize) { + GLineBuffer.GLinePage[] pages = buff.GetAllPages(); + Assert.AreEqual(pageSize.Length, pages.Length); + + int skipLines = 0; + for (int i = 0; i < pageSize.Length; i++) { + int expectedSize = pageSize[i]; + Assert.AreEqual(expectedSize, pages[i].Size); + + List list = new List(); + pages[i].Apply(0, expectedSize, s => { + for (int k = 0; k < s.Length; k++) { + list.Add(s.Array[s.Offset + k]); + } + }); + CollectionAssert.AreEqual(lineSource.Skip(skipLines).Take(expectedSize), list); + skipLines += expectedSize; + } + } + + private void CheckBuffContent(GLineBuffer buff, ILineAppender appender, int expectedSize) { + CheckBuffContent(buff, appender.Last(expectedSize), expectedSize); + } + + private void CheckBuffContent(GLineBuffer buff, IEnumerable lineSource, int expectedSize) { + var chunk = new GLineChunk(expectedSize); + buff.GetLinesByID(buff.RowIDSpan.Start, chunk.Span(0, expectedSize)); + CollectionAssert.AreEqual(lineSource.Take(expectedSize), chunk.Array); + } + } +} + +#endif // UNITTEST diff --git a/CoreTest/GLineScreenBufferTest.cs b/CoreTest/GLineScreenBufferTest.cs new file mode 100644 index 00000000..8d944d49 --- /dev/null +++ b/CoreTest/GLineScreenBufferTest.cs @@ -0,0 +1,801 @@ +// Copyright 2019 The Poderosa Project. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if UNITTEST + +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Poderosa.Document { + + [TestFixture] + class GLineScreenBufferTest { + + private readonly GLine[] _glines = Enumerable.Range(0, 200).Select(_ => new GLine(1)).ToArray(); + + private IEnumerable GLines(int start, int count) { + for (int i = 0; i < count; i++) { + yield return _glines[start + i]; + } + } + + private IEnumerable Nulls(int count) { + for (int i = 0; i < count; i++) { + yield return null; + } + } + + [TestCase(1, 64)] + [TestCase(64, 64)] + [TestCase(65, 96)] + [TestCase(96, 96)] + [TestCase(97, 128)] + public void Constructor_NormalSize(int size, int expectedBufferSize) { + var buff = new GLineScreenBuffer(size, (index) => _glines[index]); + + Assert.AreEqual(size, buff.Size); + Assert.AreEqual(0, buff.StartIndex); + + CheckNullRows(buff); + + var expectedScreenRows = GLines(0, size); + CollectionAssert.AreEqual(expectedScreenRows, GetScreenRows(buff)); + } + + [TestCase(-1)] + [TestCase(0)] + public void Constructor_InvalidSize(int size) { + Assert.Throws(() => new GLineScreenBuffer(size, (index) => _glines[index])); + } + + [TestCase(40, 23, 64)] + [TestCase(40, 24, 64)] + [TestCase(40, 25, 64)] + [TestCase(40, 64, 64)] + [TestCase(0, 64, 64)] + public void ConstructorWithSartIndex(int startIndex, int size, int expectedBufferSize) { + var buff = new GLineScreenBuffer(startIndex, size, (index) => _glines[index]); + + Assert.AreEqual(size, buff.Size); + Assert.AreEqual(startIndex, buff.StartIndex); + + CheckNullRows(buff); + + var expectedScreenRows = GLines(0, size); + CollectionAssert.AreEqual(expectedScreenRows, GetScreenRows(buff)); + } + + // BufferSize = 64 + [TestCase(0, 0)] + [TestCase(0, 1)] + [TestCase(0, 64)] // to end + [TestCase(30, 0)] + [TestCase(30, 1)] + [TestCase(30, 34)] // to end + [TestCase(63, 0)] + [TestCase(63, 1)] // to end + public void CopyToBuffer_NoWrapAround(int buffIndex, int length) { + var buff = new GLineScreenBuffer(1, (index) => (GLine)null); + // BufferSize = 64, filled by null. + buff.InternalCopyToBuffer(_glines, 3, buffIndex, length); + + var expectedBuffContent = + GLineSequenceUtil.Concat( + Nulls(buffIndex), + GLines(3, length), + Nulls(64 - buffIndex - length) + ); + + CollectionAssert.AreEqual(expectedBuffContent, buff.GetRawBuff()); + } + + // BufferSize = 64 + [TestCase(30, 35)] + [TestCase(30, 36)] + [TestCase(30, 64)] + [TestCase(63, 2)] + [TestCase(63, 3)] + [TestCase(63, 64)] + public void CopyToBuffer_WrapAround(int buffIndex, int length) { + var buff = new GLineScreenBuffer(1, (index) => (GLine)null); + // BufferSize = 64, filled by null. + buff.InternalCopyToBuffer(_glines, 3, buffIndex, length); + + int expectedNonWrapAroundRows = 64 - buffIndex; + int expectedWrapAroundRows = length - expectedNonWrapAroundRows; + + var expectedBuffContent = + GLineSequenceUtil.Concat( + GLines(3 + expectedNonWrapAroundRows, expectedWrapAroundRows), + Nulls(64 - expectedWrapAroundRows - expectedNonWrapAroundRows), + GLines(3, expectedNonWrapAroundRows) + ); + + CollectionAssert.AreEqual(expectedBuffContent, buff.GetRawBuff()); + } + + // BufferSize = 64 + [TestCase(0, 0)] + [TestCase(0, 1)] + [TestCase(0, 64)] // to end + [TestCase(30, 0)] + [TestCase(30, 1)] + [TestCase(30, 34)] // to end + [TestCase(63, 0)] + [TestCase(63, 1)] // to end + public void CopyFromBuffer_NoWrapAround(int buffIndex, int length) { + var buff = new GLineScreenBuffer(64, (index) => _glines[index]); + // BufferSize = 64, filled by GLines. + GLine[] copiedRows = new GLine[length + 3]; + buff.InternalCopyFromBuffer(buffIndex, copiedRows, 3, length); + + var expectedCopiedRows = + GLineSequenceUtil.Concat( + Nulls(3), + GLines(buffIndex, length) + ); + + CollectionAssert.AreEqual(expectedCopiedRows, copiedRows); + } + + // BufferSize = 64 + [TestCase(30, 35)] + [TestCase(30, 36)] + [TestCase(30, 64)] + [TestCase(63, 2)] + [TestCase(63, 3)] + [TestCase(63, 64)] + public void CopyFromBuffer_WrapAround(int buffIndex, int length) { + var buff = new GLineScreenBuffer(64, (index) => _glines[index]); + // BufferSize = 64, filled by GLines. + GLine[] copiedRows = new GLine[length + 3]; + buff.InternalCopyFromBuffer(buffIndex, copiedRows, 3, length); + + int expectedNonWrapAroundRows = 64 - buffIndex; + int expectedWrapAroundRows = length - expectedNonWrapAroundRows; + + var expectedCopiedRows = + GLineSequenceUtil.Concat( + Nulls(3), + GLines(buffIndex, expectedNonWrapAroundRows), + GLines(0, expectedWrapAroundRows) + ); + + CollectionAssert.AreEqual(expectedCopiedRows, copiedRows); + } + + // BufferSize = 64 + [TestCase(0, 0)] + [TestCase(0, 1)] + [TestCase(0, 64)] // to end + [TestCase(30, 0)] + [TestCase(30, 1)] + [TestCase(30, 34)] // to end + [TestCase(63, 0)] + [TestCase(63, 1)] // to end + public void ClearBuffer_NoWrapAround(int buffIndex, int length) { + var buff = new GLineScreenBuffer(64, (index) => _glines[index]); + // BufferSize = 64, filled by GLines. + buff.InternalClearBuffer(buffIndex, length); + + var expectedBuffContent = + GLineSequenceUtil.Concat( + GLines(0, buffIndex), + Nulls(length), + GLines(buffIndex + length, 64 - buffIndex - length) + ); + + CollectionAssert.AreEqual(expectedBuffContent, buff.GetRawBuff()); + } + + // BufferSize = 64 + [TestCase(30, 35)] + [TestCase(30, 36)] + [TestCase(30, 64)] + [TestCase(63, 2)] + [TestCase(63, 3)] + [TestCase(63, 64)] + public void ClearBuffer_WrapAround(int buffIndex, int length) { + var buff = new GLineScreenBuffer(64, (index) => _glines[index]); + // BufferSize = 64, filled by GLines. + buff.InternalClearBuffer(buffIndex, length); + + int expectedNonWrapAroundRows = 64 - buffIndex; + int expectedWrapAroundRows = length - expectedNonWrapAroundRows; + + var expectedBuffContent = + GLineSequenceUtil.Concat( + Nulls(expectedWrapAroundRows), + GLines(expectedWrapAroundRows, 64 - expectedWrapAroundRows - expectedNonWrapAroundRows), + Nulls(expectedNonWrapAroundRows) + ); + + CollectionAssert.AreEqual(expectedBuffContent, buff.GetRawBuff()); + } + + [TestCase(10, 20, 0, 10)] + [TestCase(10, 20, 1, 9)] + [TestCase(10, 20, 10, 0)] + [TestCase(10, 54, 10, 0)] // use all spare rows in the buffer + [TestCase(10, 20, 11, 63)] + [TestCase(10, 20, 44, 30)] + [TestCase(0, 20, 1, 63)] + public void ExtendHead_NoReallocate(int initialStartIndex, int initialSize, int extendSize, int expectedNewStartIndex) { + var buff = new GLineScreenBuffer(initialStartIndex, initialSize, (index) => _glines[index]); + GLineChunk chunk = GetFilledGLineChunk(3 + extendSize + 3); + + buff.ExtendHead(chunk.Span(3, extendSize)); + + Assert.AreEqual(expectedNewStartIndex, buff.StartIndex); + Assert.AreEqual(initialSize + extendSize, buff.Size); + + CheckNullRows(buff); + + var expectedScreenRows = + GLineSequenceUtil.Concat( + chunk.Array.Skip(3).Take(extendSize), + GLines(0, initialSize) + ); + + CollectionAssert.AreEqual(expectedScreenRows, GetScreenRows(buff)); + } + + [TestCase(10, 20, 45, 96)] + [TestCase(54, 20, 45, 96)] // wrap-arounded + public void ExtendHead_Reallocate(int initialStartIndex, int initialSize, int extendSize, int expectedBufferSize) { + var buff = new GLineScreenBuffer(initialStartIndex, initialSize, (index) => _glines[index]); + + GLineChunk chunk = GetFilledGLineChunk(3 + extendSize + 3); + + buff.ExtendHead(chunk.Span(3, extendSize)); + + Assert.AreEqual(0, buff.StartIndex); + Assert.AreEqual(extendSize + initialSize, buff.Size); + + CheckNullRows(buff); + + Assert.AreEqual(expectedBufferSize, buff.GetRawBuff().Length); + + var expectedScreenRows = + GLineSequenceUtil.Concat( + chunk.Array.Skip(3).Take(extendSize), + GLines(0, initialSize) + ); + + CollectionAssert.AreEqual(expectedScreenRows, GetScreenRows(buff)); + } + + [TestCase(34, 20, 0)] + [TestCase(34, 20, 1)] + [TestCase(34, 20, 10)] + [TestCase(34, 20, 11)] + [TestCase(34, 20, 44)] + [TestCase(44, 20, 1)] + public void ExtendTail_NoReallocate(int initialStartIndex, int initialSize, int extendSize) { + var buff = new GLineScreenBuffer(initialStartIndex, initialSize, (index) => _glines[index]); + + GLineChunk chunk = GetFilledGLineChunk(3 + extendSize + 3); + + buff.ExtendTail(chunk.Span(3, extendSize)); + + Assert.AreEqual(initialStartIndex, buff.StartIndex); + Assert.AreEqual(initialSize + extendSize, buff.Size); + + CheckNullRows(buff); + + var expectedScreenRows = + GLineSequenceUtil.Concat( + GLines(0, initialSize), + chunk.Array.Skip(3).Take(extendSize) + ); + + CollectionAssert.AreEqual(expectedScreenRows, GetScreenRows(buff)); + } + + [TestCase(34, 20, 45, 96)] + [TestCase(54, 20, 45, 96)] // wrap-arounded + public void ExtendTail_Reallocate(int initialStartIndex, int initialSize, int extendSize, int expectedBufferSize) { + var buff = new GLineScreenBuffer(initialStartIndex, initialSize, (index) => _glines[index]); + + GLineChunk chunk = GetFilledGLineChunk(3 + extendSize + 3); + + buff.ExtendTail(chunk.Span(3, extendSize)); + + Assert.AreEqual(0, buff.StartIndex); + Assert.AreEqual(initialSize + extendSize, buff.Size); + + CheckNullRows(buff); + + Assert.AreEqual(expectedBufferSize, buff.GetRawBuff().Length); + + var expectedScreenRows = + GLineSequenceUtil.Concat( + GLines(0, initialSize), + chunk.Array.Skip(3).Take(extendSize) + ); + + CollectionAssert.AreEqual(expectedScreenRows, GetScreenRows(buff)); + } + + [TestCase(10, 20, 5, 15)] + [TestCase(44, 20, 19, 63)] + [TestCase(54, 20, 10, 0)] + [TestCase(54, 20, 11, 1)] + [TestCase(54, 64, 10, 0)] // no spare rows in the buffer + public void ShrinkHead_Success(int initialStartIndex, int initialSize, int shrinkSize, int expectedNewStartIndex) { + var buff = new GLineScreenBuffer(initialStartIndex, initialSize, (index) => _glines[index]); + + buff.ShrinkHead(shrinkSize); + + Assert.AreEqual(expectedNewStartIndex, buff.StartIndex); + Assert.AreEqual(initialSize - shrinkSize, buff.Size); + + CheckNullRows(buff); + + var expectedScreenRows = GLines(shrinkSize, initialSize - shrinkSize); + + CollectionAssert.AreEqual(expectedScreenRows, GetScreenRows(buff)); + } + + [TestCase(20, 20)] + [TestCase(20, 21)] + [TestCase(20, -1)] + public void ShrinkHead_InvalidParam(int initialSize, int shrinkSize) { + var buff = new GLineScreenBuffer(0, initialSize, (index) => _glines[index]); + + Assert.Throws(() => buff.ShrinkHead(shrinkSize)); + } + + [TestCase(10, 20, 5)] + [TestCase(0, 20, 19)] + [TestCase(54, 20, 10)] + [TestCase(54, 20, 11)] + [TestCase(54, 20, 19)] + [TestCase(54, 64, 1)] // no spare rows in the buffer + public void ShrinkTail_Success(int initialStartIndex, int initialSize, int shrinkSize) { + var buff = new GLineScreenBuffer(initialStartIndex, initialSize, (index) => _glines[index]); + + buff.ShrinkTail(shrinkSize); + + Assert.AreEqual(initialStartIndex, buff.StartIndex); + Assert.AreEqual(initialSize - shrinkSize, buff.Size); + + CheckNullRows(buff); + + var expectedScreenRows = GLines(0, initialSize - shrinkSize); + + CollectionAssert.AreEqual(expectedScreenRows, GetScreenRows(buff)); + } + + [TestCase(20, 20)] + [TestCase(20, 21)] + [TestCase(20, -1)] + public void ShrinkTail_InvalidParam(int initialSize, int shrinkSize) { + var buff = new GLineScreenBuffer(0, initialSize, (index) => _glines[index]); + + Assert.Throws(() => buff.ShrinkTail(shrinkSize)); + } + + [TestCase(0, 20, 0, 0)] + [TestCase(0, 20, 0, 20)] + [TestCase(0, 20, 3, 10)] + [TestCase(0, 20, 20, 0)] + [TestCase(54, 20, 0, 0)] // splitted in 2 regions + [TestCase(54, 20, 0, 20)] // splitted in 2 regions + [TestCase(54, 20, 3, 10)] // splitted in 2 regions + [TestCase(54, 20, 20, 0)] // splitted in 2 regions + [TestCase(54, 64, 0, 64)] // no spare rows in the buffer + public void GetRows_Success(int startIndex, int size, int rowIndex, int length) { + var buff = new GLineScreenBuffer(startIndex, size, (index) => _glines[index]); + + GLineChunk chunk = new GLineChunk(3 + length + 3); + buff.GetRows(rowIndex, chunk.Span(3, length)); + + var expectedDest = + GLineSequenceUtil.Concat( + Nulls(3), + GLines(rowIndex, length), + Nulls(3) + ); + + CollectionAssert.AreEqual(expectedDest, chunk.Array); + } + + [TestCase(0, 20, -1, 1)] + [TestCase(0, 20, 3, -1)] + [TestCase(0, 20, 0, 21)] + [TestCase(0, 20, 3, 18)] + public void GetRows_Error(int startIndex, int size, int rowIndex, int length) { + var buff = new GLineScreenBuffer(startIndex, size, (index) => _glines[index]); + + GLineChunk chunk = new GLineChunk(30); + + Assert.Throws(() => buff.GetRows(rowIndex, chunk.Span(3, length))); + } + + [TestCase(0, 20, 0, 0)] + [TestCase(0, 20, 0, 20)] + [TestCase(0, 20, 3, 10)] + [TestCase(0, 20, 20, 0)] + [TestCase(54, 20, 0, 0)] // splitted in 2 regions + [TestCase(54, 20, 0, 20)] // splitted in 2 regions + [TestCase(54, 20, 3, 10)] // splitted in 2 regions + [TestCase(54, 20, 20, 0)] // splitted in 2 regions + [TestCase(54, 64, 0, 64)] // no spare rows in the buffer + public void SetRows_Success(int startIndex, int size, int rowIndex, int length) { + var buff = new GLineScreenBuffer(startIndex, size, (index) => _glines[index]); + + GLineChunk chunk = GetFilledGLineChunk(3 + length + 3); + + buff.SetRows(rowIndex, chunk.Span(3, length)); + + Assert.AreEqual(startIndex, buff.StartIndex); + Assert.AreEqual(size, buff.Size); + + var expectedScreenRows = + GLineSequenceUtil.Concat( + GLines(0, rowIndex), + chunk.Array.Skip(3).Take(length), + GLines(rowIndex + length, size - rowIndex - length) + ); + + CollectionAssert.AreEqual(expectedScreenRows, GetScreenRows(buff)); + } + + [TestCase(0, 20, -1, 1)] + [TestCase(0, 20, 3, -1)] + [TestCase(0, 20, 0, 21)] + [TestCase(0, 20, 3, 18)] + public void SetRows_Error(int startIndex, int size, int rowIndex, int length) { + var buff = new GLineScreenBuffer(startIndex, size, (index) => _glines[index]); + + GLineChunk chunk = GetFilledGLineChunk(3 + length + 3); + + Assert.Throws(() => buff.SetRows(rowIndex, chunk.Span(3, length))); + + Assert.AreEqual(startIndex, buff.StartIndex); + Assert.AreEqual(size, buff.Size); + } + + [TestCase(10, 20, 0, 10)] + [TestCase(10, 20, 1, 11)] + [TestCase(10, 20, 10, 20)] + [TestCase(10, 20, 20, 30)] // screen size + [TestCase(54, 20, 10, 0)] + [TestCase(54, 20, 11, 1)] + [TestCase(54, 20, 20, 10)] // screen size + [TestCase(54, 64, 20, 10)] // no spare rows in the buffer + public void ScrollUp_NotExceedScreenSize(int startIndex, int size, int scrollRows, int expectedNextStartIndex) { + var buff = new GLineScreenBuffer(startIndex, size, (index) => _glines[index]); + + GLineChunk chunk = GetFilledGLineChunk(3 + scrollRows + 3); + + buff.ScrollUp(chunk.Span(3, scrollRows)); + + Assert.AreEqual(expectedNextStartIndex, buff.StartIndex); + Assert.AreEqual(size, buff.Size); + + CheckNullRows(buff); + + var expectedScreenRows = + GLineSequenceUtil.Concat( + GLines(scrollRows, size - scrollRows), + chunk.Array.Skip(3).Take(scrollRows) + ); + + CollectionAssert.AreEqual(expectedScreenRows, GetScreenRows(buff)); + } + + [TestCase(10, 20, 21, 30)] // exceeds screen size + [TestCase(10, 20, 200, 30)] // exceeds screen size + [TestCase(54, 20, 21, 10)] // exceeds screen size + [TestCase(54, 20, 200, 10)] // exceeds screen size + public void ScrollUp_ExceedScreenSize(int startIndex, int size, int scrollRows, int expectedNextStartIndex) { + var buff = new GLineScreenBuffer(startIndex, size, (index) => _glines[index]); + + GLineChunk chunk = GetFilledGLineChunk(3 + scrollRows + 3); + + buff.ScrollUp(chunk.Span(3, scrollRows)); + + Assert.AreEqual(expectedNextStartIndex, buff.StartIndex); + Assert.AreEqual(size, buff.Size); + + CheckNullRows(buff); + + var expectedScreenRows = chunk.Array.Skip(3 + scrollRows - size).Take(size); + + CollectionAssert.AreEqual(expectedScreenRows, GetScreenRows(buff)); + } + + [TestCase(10, 20, 0, 10)] + [TestCase(10, 20, 1, 9)] + [TestCase(10, 20, 10, 0)] + [TestCase(10, 20, 20, 54)] // screen size + [TestCase(54, 20, 10, 44)] + [TestCase(54, 20, 11, 43)] + [TestCase(54, 20, 20, 34)] // screen size + [TestCase(10, 64, 20, 54)] // no spare rows in the buffer + public void ScrollDown_NotExceedScreenSize(int startIndex, int size, int scrollRows, int expectedNextStartIndex) { + var buff = new GLineScreenBuffer(startIndex, size, (index) => _glines[index]); + + GLineChunk chunk = GetFilledGLineChunk(3 + scrollRows + 3); + + buff.ScrollDown(chunk.Span(3, scrollRows)); + + Assert.AreEqual(expectedNextStartIndex, buff.StartIndex); + Assert.AreEqual(size, buff.Size); + + CheckNullRows(buff); + + var expectedScreenRows = + GLineSequenceUtil.Concat( + chunk.Array.Skip(3).Take(scrollRows), + GLines(0, size - scrollRows) + ); + + CollectionAssert.AreEqual(expectedScreenRows, GetScreenRows(buff)); + } + + [TestCase(10, 20, 21, 54)] // exceeds screen size + [TestCase(10, 20, 200, 54)] // exceeds screen size + [TestCase(54, 20, 21, 34)] // exceeds screen size + [TestCase(54, 20, 200, 34)] // exceeds screen size + public void ScrollDown_ExceedScreenSize(int startIndex, int size, int scrollRows, int expectedNextStartIndex) { + var buff = new GLineScreenBuffer(startIndex, size, (index) => _glines[index]); + + GLineChunk chunk = GetFilledGLineChunk(3 + scrollRows + 3); + + buff.ScrollDown(chunk.Span(3, scrollRows)); + + Assert.AreEqual(expectedNextStartIndex, buff.StartIndex); + Assert.AreEqual(size, buff.Size); + + CheckNullRows(buff); + + var expectedScreenRows = chunk.Array.Skip(3).Take(size); + + CollectionAssert.AreEqual(expectedScreenRows, GetScreenRows(buff)); + } + + [TestCase(54, 20, 5, 15, 5, 15, 0)] + [TestCase(54, 20, 5, 15, 5, 15, 1)] + [TestCase(54, 20, 5, 15, 5, 15, 10)] // scroll region size + [TestCase(54, 20, 0, 15, 0, 15, 1)] + [TestCase(54, 20, -1, 15, 0, 15, 1)] + [TestCase(54, 20, 5, 20, 5, 20, 1)] + [TestCase(54, 20, 5, 21, 5, 20, 1)] + [TestCase(54, 20, 0, 20, 0, 20, 1)] // scroll entire of the screen + [TestCase(54, 20, 0, 20, 0, 20, 20)] // scroll entire of the screen + [TestCase(54, 64, 0, 64, 0, 64, 20)] // no spare rows in the buffer + public void ScrollUpRegion_NotExceedRegionSize( + int startIndex, int size, + int startRowIndex, int endRowIndex, + int actualStartRowIndex, int actualEndRowIndex, + int scrollRows) { + + var buff = new GLineScreenBuffer(startIndex, size, (index) => _glines[index]); + + GLineChunk chunk = GetFilledGLineChunk(3 + scrollRows + 3); + + buff.ScrollUpRegion(startRowIndex, endRowIndex, chunk.Span(3, scrollRows)); + + Assert.AreEqual(startIndex, buff.StartIndex); + Assert.AreEqual(size, buff.Size); + + CheckNullRows(buff); + + var expectedScreenRows = + GLineSequenceUtil.Concat( + GLines(0, actualStartRowIndex), + GLines(actualStartRowIndex + scrollRows, (actualEndRowIndex - actualStartRowIndex) - scrollRows), + chunk.Array.Skip(3).Take(scrollRows), + GLines(actualEndRowIndex, size - actualEndRowIndex) + ); + + CollectionAssert.AreEqual(expectedScreenRows, GetScreenRows(buff)); + } + + [TestCase(54, 20, 5, 15, 5, 15, 11)] + [TestCase(54, 20, 5, 15, 5, 15, 200)] + public void ScrollUpRegion_ExceedRegionSize( + int startIndex, int size, + int startRowIndex, int endRowIndex, + int actualStartRowIndex, int actualEndRowIndex, + int scrollRows) { + + var buff = new GLineScreenBuffer(startIndex, size, (index) => _glines[index]); + + GLineChunk chunk = GetFilledGLineChunk(3 + scrollRows + 3); + + buff.ScrollUpRegion(startRowIndex, endRowIndex, chunk.Span(3, scrollRows)); + + Assert.AreEqual(startIndex, buff.StartIndex); + Assert.AreEqual(size, buff.Size); + + CheckNullRows(buff); + + int regionSize = actualEndRowIndex - actualStartRowIndex; + + var expectedScreenRows = + GLineSequenceUtil.Concat( + GLines(0, actualStartRowIndex), + chunk.Array.Skip(3 + scrollRows - regionSize).Take(regionSize), + GLines(actualEndRowIndex, size - actualEndRowIndex) + ); + + CollectionAssert.AreEqual(expectedScreenRows, GetScreenRows(buff)); + } + + [TestCase(54, 20, 5, 15, 5, 15, 0)] + [TestCase(54, 20, 5, 15, 5, 15, 1)] + [TestCase(54, 20, 5, 15, 5, 15, 10)] // scroll region size + [TestCase(54, 20, 0, 15, 0, 15, 1)] + [TestCase(54, 20, -1, 15, 0, 15, 1)] + [TestCase(54, 20, 5, 20, 5, 20, 1)] + [TestCase(54, 20, 5, 21, 5, 20, 1)] + [TestCase(54, 20, 0, 20, 0, 20, 1)] // scroll entire of the screen + [TestCase(54, 20, 0, 20, 0, 20, 20)] // scroll entire of the screen + [TestCase(54, 64, 0, 64, 0, 64, 20)] // no spare rows in the buffer + public void ScrollDownRegion_NotExceedRegionSize( + int startIndex, int size, + int startRowIndex, int endRowIndex, + int actualStartRowIndex, int actualEndRowIndex, + int scrollRows) { + + var buff = new GLineScreenBuffer(startIndex, size, (index) => _glines[index]); + + GLineChunk chunk = GetFilledGLineChunk(3 + scrollRows + 3); + + buff.ScrollDownRegion(startRowIndex, endRowIndex, chunk.Span(3, scrollRows)); + + Assert.AreEqual(startIndex, buff.StartIndex); + Assert.AreEqual(size, buff.Size); + + CheckNullRows(buff); + + var expectedScreenRows = + GLineSequenceUtil.Concat( + GLines(0, actualStartRowIndex), + chunk.Array.Skip(3).Take(scrollRows), + GLines(actualStartRowIndex, (actualEndRowIndex - actualStartRowIndex) - scrollRows), + GLines(actualEndRowIndex, size - actualEndRowIndex) + ); + + CollectionAssert.AreEqual(expectedScreenRows, GetScreenRows(buff)); + } + + [TestCase(54, 20, 5, 15, 5, 15, 11)] + [TestCase(54, 20, 5, 15, 5, 15, 200)] + public void ScrollDownRegion_ExceedRegionSize( + int startIndex, int size, + int startRowIndex, int endRowIndex, + int actualStartRowIndex, int actualEndRowIndex, + int scrollRows) { + + var buff = new GLineScreenBuffer(startIndex, size, (index) => _glines[index]); + + GLineChunk chunk = GetFilledGLineChunk(3 + scrollRows + 3); + + buff.ScrollDownRegion(startRowIndex, endRowIndex, chunk.Span(3, scrollRows)); + + Assert.AreEqual(startIndex, buff.StartIndex); + Assert.AreEqual(size, buff.Size); + + CheckNullRows(buff); + + int regionSize = actualEndRowIndex - actualStartRowIndex; + + var expectedScreenRows = + GLineSequenceUtil.Concat( + GLines(0, actualStartRowIndex), + chunk.Array.Skip(3).Take(regionSize), + GLines(actualEndRowIndex, size - actualEndRowIndex) + ); + + CollectionAssert.AreEqual(expectedScreenRows, GetScreenRows(buff)); + } + + [TestCase(0, 20, 0, 0)] + [TestCase(0, 20, 0, 1)] + [TestCase(0, 20, 0, 20)] + [TestCase(0, 20, 5, 10)] + [TestCase(0, 20, 5, 15)] + [TestCase(54, 20, 0, 10)] + [TestCase(54, 20, 0, 11)] + [TestCase(54, 20, 0, 20)] + [TestCase(54, 20, 5, 10)] + [TestCase(54, 20, 5, 15)] + public void Apply_Success(int startIndex, int size, int rowIndex, int length) { + + var buff = new GLineScreenBuffer(startIndex, size, (index) => _glines[index]); + + List list = new List(); + + buff.Apply(rowIndex, length, s => { + for (int i = 0; i < s.Length; i++) { + list.Add(s.Array[s.Offset + i]); + } + }); + + var expectedScreenRows = GLines(rowIndex, length); + + CollectionAssert.AreEqual(expectedScreenRows, list); + } + + [TestCase(0, 20, -1, 1)] + [TestCase(0, 20, 0, -1)] + [TestCase(0, 20, -1, 1)] + [TestCase(0, 20, 0, 21)] + [TestCase(0, 20, 5, 16)] + public void Apply_Error(int startIndex, int size, int rowIndex, int length) { + + var buff = new GLineScreenBuffer(startIndex, size, (index) => _glines[index]); + + List list = new List(); + + if (length < 0) { + Assert.Throws(() => { + buff.Apply(rowIndex, length, s => { + }); + }); + } + else { + Assert.Throws(() => { + buff.Apply(rowIndex, length, s => { + }); + }); + } + } + + private GLineChunk GetFilledGLineChunk(int rows) { + GLineChunk chunk = new GLineChunk(rows); + for (int i = 0; i < chunk.Array.Length; i++) { + chunk.Array[i] = new GLine(1); + } + return chunk; + } + + private IEnumerable GetScreenRows(GLineScreenBuffer buff) { + for (int i = 0; i < buff.Size; i++) { + yield return buff[i]; + } + } + + // checks whether the row is null or not. + // an active row must have a GLine object. + // an inactive row must be null. + private void CheckNullRows(GLineScreenBuffer buff) { + int startIndex = buff.StartIndex; + int size = buff.Size; + var rawBuff = buff.GetRawBuff(); + + int endIndex = (startIndex + size) % rawBuff.Length; + + for (int i = 0; i < rawBuff.Length; i++) { + int offset = (i < startIndex) ? (i + rawBuff.Length - startIndex) : (i - startIndex); + if (offset < size) { + Assert.IsNotNull(rawBuff[i]); + } + else { + Assert.IsNull(rawBuff[i]); + } + } + } + + } +} + +#endif diff --git a/CoreTest/RowIDSpanTest.cs b/CoreTest/RowIDSpanTest.cs new file mode 100644 index 00000000..305babbf --- /dev/null +++ b/CoreTest/RowIDSpanTest.cs @@ -0,0 +1,71 @@ +// Copyright 2019 The Poderosa Project. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if UNITTEST + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using NUnit.Framework; + +namespace Poderosa.Document { + + [TestFixture] + public class RowIDSpanTest { + [TestCase(10, 10, 5, 4, 10, 0)] // [10..19] x [5..8] => [] + [TestCase(10, 10, 5, 5, 10, 0)] // [10..19] x [5..9] => [] + [TestCase(10, 10, 5, 6, 10, 1)] // [10..19] x [5..10] => [10] + [TestCase(10, 10, 5, 15, 10, 10)] // [10..19] x [5..19] => [10..19] + [TestCase(10, 10, 5, 16, 10, 10)] // [10..19] x [5..20] => [10..19] + [TestCase(10, 10, 10, 0, 10, 0)] // [10..19] x [] => [] + [TestCase(10, 10, 10, 10, 10, 10)] // [10..19] x [10..19] => [10..19] + [TestCase(10, 10, 11, 1, 11, 1)] // [10..19] x [11] => [11] + [TestCase(10, 10, 11, 9, 11, 9)] // [10..19] x [11..19] => [11..19] + [TestCase(10, 10, 11, 10, 11, 9)] // [10..19] x [11..20] => [11..19] + [TestCase(10, 10, 19, 1, 19, 1)] // [10..19] x [19] => [19] + [TestCase(10, 10, 19, 2, 19, 1)] // [10..19] x [19..20] => [19] + [TestCase(10, 10, 20, 5, 10, 0)] // [10..19] x [20..24] => [] + [TestCase(10, 10, 21, 5, 10, 0)] // [10..19] x [21..25] => [] + [TestCase(10, 0, 5, 6, 10, 0)] // [] x [5..10] => [] + [TestCase(10, 0, 10, 0, 10, 0)] // [] x [] => [] + [TestCase(10, 0, 10, 10, 10, 0)] // [] x [10..19] => [] + [TestCase(10, 0, 11, 1, 10, 0)] // [] x [11] => [] + public void TestIntersect(int start1, int len1, int start2, int len2, int expectedStart, int expectedLength) { + + var s1 = new RowIDSpan(start1, len1); + var s2 = new RowIDSpan(start2, len2); + + var r = s1.Intersect(s2); + + Assert.AreEqual(expectedStart, r.Start); + Assert.AreEqual(expectedLength, r.Length); + } + + [TestCase(10, 5, 9, false)] + [TestCase(10, 5, 10, true)] + [TestCase(10, 5, 11, true)] + [TestCase(10, 5, 12, true)] + [TestCase(10, 5, 13, true)] + [TestCase(10, 5, 14, true)] + [TestCase(10, 5, 15, false)] + public void TestIncludes(int start, int len, int testRowId, bool expected) { + Assert.AreEqual(expected, new RowIDSpan(start, len).Includes(testRowId)); + } + } +} + +#endif diff --git a/CoreTest/TerminalCharacterDocumentTest.cs b/CoreTest/TerminalCharacterDocumentTest.cs new file mode 100644 index 00000000..2635f81f --- /dev/null +++ b/CoreTest/TerminalCharacterDocumentTest.cs @@ -0,0 +1,738 @@ +// Copyright 2019 The Poderosa Project. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if UNITTEST + +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace Poderosa.Document { + + [TestFixture] + class TerminalCharacterDocumentTest { + + private readonly GLine[] newLines = new GLine[] { + null, null, null, null, null, + GLine.CreateSimpleGLine("A", TextDecoration.Default), + GLine.CreateSimpleGLine("B", TextDecoration.Default), + GLine.CreateSimpleGLine("C", TextDecoration.Default), + GLine.CreateSimpleGLine("D", TextDecoration.Default), + GLine.CreateSimpleGLine("E", TextDecoration.Default), + GLine.CreateSimpleGLine("F", TextDecoration.Default), + GLine.CreateSimpleGLine("G", TextDecoration.Default), + GLine.CreateSimpleGLine("H", TextDecoration.Default), + GLine.CreateSimpleGLine("I", TextDecoration.Default), + GLine.CreateSimpleGLine("J", TextDecoration.Default), + GLine.CreateSimpleGLine("K", TextDecoration.Default), + GLine.CreateSimpleGLine("L", TextDecoration.Default), + GLine.CreateSimpleGLine("M", TextDecoration.Default), + GLine.CreateSimpleGLine("N", TextDecoration.Default), + GLine.CreateSimpleGLine("O", TextDecoration.Default), + GLine.CreateSimpleGLine("P", TextDecoration.Default), + GLine.CreateSimpleGLine("Q", TextDecoration.Default), + GLine.CreateSimpleGLine("R", TextDecoration.Default), + GLine.CreateSimpleGLine("S", TextDecoration.Default), + GLine.CreateSimpleGLine("T", TextDecoration.Default), + GLine.CreateSimpleGLine("U", TextDecoration.Default), + GLine.CreateSimpleGLine("V", TextDecoration.Default), + GLine.CreateSimpleGLine("W", TextDecoration.Default), + GLine.CreateSimpleGLine("X", TextDecoration.Default), + GLine.CreateSimpleGLine("Y", TextDecoration.Default), + GLine.CreateSimpleGLine("Z", TextDecoration.Default), + null, null, null, null, null, + }; + + [Test] + public void Test_InitialState() { + TestTerminalDoc doc = new TestTerminalDoc(); + CheckLines(doc, + new string[] { }, + new string[] { "", "", "", "", "", "", "", "", "", "", } + ); + } + + [Test] + public void Test_VisibleAreaSizeChanged_ScreenIsNotIsolated() { + TestTerminalDoc doc = new TestTerminalDoc(); + SetupScreenLines(doc, 1, 10); + + CheckLines(doc, + new string[] { }, + new string[] { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", } + ); + + doc.ScreenIsolated = false; + + // 80x10 -> 60x6 + doc.VisibleAreaSizeChanged(6, 60); + + CheckLines(doc, + new string[] { "1", "2", "3", "4", }, + new string[] { "5", "6", "7", "8", "9", "10", } + ); + + // 60x6 -> 70x6 + doc.VisibleAreaSizeChanged(6, 70); + + CheckLines(doc, + new string[] { "1", "2", "3", "4", }, + new string[] { "5", "6", "7", "8", "9", "10", } + ); + + // 70x6 -> 70x8 + doc.VisibleAreaSizeChanged(8, 70); + + CheckLines(doc, + new string[] { "1", "2", }, + new string[] { "3", "4", "5", "6", "7", "8", "9", "10", } + ); + + // 70x8 -> 70x12 + doc.VisibleAreaSizeChanged(12, 70); + + CheckLines(doc, + new string[] { }, + new string[] { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "", "", } + ); + + // 70x12 -> 70x13 + doc.VisibleAreaSizeChanged(15, 70); + + CheckLines(doc, + new string[] { }, + new string[] { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "", "", "", "", "", } + ); + } + + [Test] + public void Test_VisibleAreaSizeChanged_ScreenIsIsolated() { + TestTerminalDoc doc = new TestTerminalDoc(); + SetupScreenLines(doc, 1, 10); + + CheckLines(doc, + new string[] { }, + new string[] { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", } + ); + + doc.ScreenIsolated = true; + + // 80x10 -> 60x6 + doc.VisibleAreaSizeChanged(6, 60); + + CheckLines(doc, + new string[] { }, + new string[] { "1", "2", "3", "4", "5", "6", } + ); + + // 60x6 -> 70x6 + doc.VisibleAreaSizeChanged(6, 70); + + CheckLines(doc, + new string[] { }, + new string[] { "1", "2", "3", "4", "5", "6", } + ); + + // 70x6 -> 70x8 + doc.VisibleAreaSizeChanged(8, 70); + + CheckLines(doc, + new string[] { }, + new string[] { "1", "2", "3", "4", "5", "6", "", "", } + ); + + // 70x8 -> 70x12 + doc.VisibleAreaSizeChanged(12, 70); + + CheckLines(doc, + new string[] { }, + new string[] { "1", "2", "3", "4", "5", "6", "", "", "", "", "", "", } + ); + } + + [Test] + public void Test_ScreenAppend() { + TestTerminalDoc doc = new TestTerminalDoc(); + SetupScreenLines(doc, 1, 10); + + CheckLines(doc, + new string[] { }, + new string[] { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", } + ); + + doc.ScreenAppend_(GLine.CreateSimpleGLine("X", TextDecoration.Default)); + + CheckLines(doc, + new string[] { "1", }, + new string[] { "2", "3", "4", "5", "6", "7", "8", "9", "10", "X", } + ); + + doc.ScreenAppend_(GLine.CreateSimpleGLine("Y", TextDecoration.Default)); + + CheckLines(doc, + new string[] { "1", "2", }, + new string[] { "3", "4", "5", "6", "7", "8", "9", "10", "X", "Y", } + ); + } + + [Test] + public void Test_ScreenGetRow() { + TestTerminalDoc doc = new TestTerminalDoc(); + SetupScreenLines(doc, 1, 10); + + Assert.AreEqual("1", doc.ScreenGetRow_(0).ToNormalString()); + Assert.AreEqual("10", doc.ScreenGetRow_(9).ToNormalString()); + } + + [TestCase(-1)] + [TestCase(10)] + public void Test_ScreenGetRow_Error(int rowIndex) { + TestTerminalDoc doc = new TestTerminalDoc(); + SetupScreenLines(doc, 1, 10); + + Assert.Throws(() => doc.ScreenGetRow_(rowIndex)); + } + + [Test] + public void Test_ScreenSetRow() { + TestTerminalDoc doc = new TestTerminalDoc(); + SetupScreenLines(doc, 1, 10); + + doc.ScreenSetRow_(0, GLine.CreateSimpleGLine("X", TextDecoration.Default)); + doc.ScreenSetRow_(9, GLine.CreateSimpleGLine("Y", TextDecoration.Default)); + + CheckLines(doc, + new string[] { }, + new string[] { "X", "2", "3", "4", "5", "6", "7", "8", "9", "Y", } + ); + } + + [TestCase(-1)] + [TestCase(10)] + public void Test_ScreenSetRow_Error(int rowIndex) { + TestTerminalDoc doc = new TestTerminalDoc(); + SetupScreenLines(doc, 1, 10); + + Assert.Throws( + () => doc.ScreenSetRow_(rowIndex, GLine.CreateSimpleGLine("Z", TextDecoration.Default))); + } + + [TestCase(0, 10, new string[] { null, null, null, null, null, "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", null, null, null, null, null, })] + [TestCase(9, 1, new string[] { null, null, null, null, null, "10", null, null, null, null, null, null, null, null, null, null, null, null, null, null, })] + public void Test_ScreenGetRows(int rowIndex, int length, string[] expected) { + TestTerminalDoc doc = new TestTerminalDoc(); + SetupScreenLines(doc, 1, 10); + + GLineChunk chunk = new GLineChunk(20); + doc.ScreenGetRows_(rowIndex, chunk.Span(5, length)); + string[] chunkContent = chunk.Array.Select(r => (r != null) ? r.ToNormalString() : null).ToArray(); + CollectionAssert.AreEqual(expected, chunkContent); + } + + [TestCase(-1, 2)] + [TestCase(0, 11)] + [TestCase(9, 2)] + [TestCase(10, 1)] + public void Test_ScreenGetRows_Error(int rowIndex, int length) { + TestTerminalDoc doc = new TestTerminalDoc(); + SetupScreenLines(doc, 1, 10); + + GLineChunk chunk = new GLineChunk(20); + Assert.Throws(() => doc.ScreenGetRows_(rowIndex, chunk.Span(5, length))); + } + + [TestCase(0, 10, new string[] { "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", })] + [TestCase(9, 1, new string[] { "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", })] + public void Test_ScreenSetRows(int rowIndex, int length, string[] expected) { + TestTerminalDoc doc = new TestTerminalDoc(); + SetupScreenLines(doc, 1, 10); + + doc.ScreenSetRows_(rowIndex, new GLineChunkSpan(newLines, 5, length)); + + CheckLines(doc, + new string[] { }, + expected); + } + + [TestCase(-1, 2)] + [TestCase(0, 11)] + [TestCase(9, 2)] + [TestCase(10, 1)] + public void Test_ScreenSetRows_Error(int rowIndex, int length) { + TestTerminalDoc doc = new TestTerminalDoc(); + SetupScreenLines(doc, 1, 10); + Assert.Throws( + () => doc.ScreenSetRows_(rowIndex, new GLineChunkSpan(newLines, 5, length))); + } + + [Test] + public void Test_ScreenScrollUp_AppendEmptyLines_ScreenIsNotIsolated() { + TestTerminalDoc doc = new TestTerminalDoc(); + SetupScreenLines(doc, 1, 10); + + doc.ScreenIsolated = false; + + doc.ScreenScrollUp_(1); + + CheckLines(doc, + new string[] { "1", }, + new string[] { "2", "3", "4", "5", "6", "7", "8", "9", "10", "", } + ); + + doc.ScreenScrollUp_(2); + + CheckLines(doc, + new string[] { "1", "2", "3", }, + new string[] { "4", "5", "6", "7", "8", "9", "10", "", "", "", } + ); + + doc.ScreenScrollUp_(15); + + CheckLines(doc, + new string[] { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "", "", "", "", "", "", "", "", }, + new string[] { "", "", "", "", "", "", "", "", "", "", } + ); + } + + [Test] + public void Test_ScreenScrollUp_AppendEmptyLines_ScreenIsIsolated() { + TestTerminalDoc doc = new TestTerminalDoc(); + SetupScreenLines(doc, 1, 10); + + doc.ScreenIsolated = true; + + doc.ScreenScrollUp_(1); + + CheckLines(doc, + new string[] { }, + new string[] { "2", "3", "4", "5", "6", "7", "8", "9", "10", "", } + ); + + doc.ScreenScrollUp_(2); + + CheckLines(doc, + new string[] { }, + new string[] { "4", "5", "6", "7", "8", "9", "10", "", "", "", } + ); + + doc.ScreenScrollUp_(15); + + CheckLines(doc, + new string[] { }, + new string[] { "", "", "", "", "", "", "", "", "", "", } + ); + } + + [Test] + public void Test_ScreenScrollUp_AppendLines_ScreenIsNotIsolated() { + TestTerminalDoc doc = new TestTerminalDoc(); + SetupScreenLines(doc, 1, 10); + + doc.ScreenIsolated = false; + + doc.ScreenScrollUp_(new GLineChunkSpan(newLines, 5, 1)); + + CheckLines(doc, + new string[] { "1", }, + new string[] { "2", "3", "4", "5", "6", "7", "8", "9", "10", "A", } + ); + + doc.ScreenScrollUp_(new GLineChunkSpan(newLines, 6, 2)); + + CheckLines(doc, + new string[] { "1", "2", "3", }, + new string[] { "4", "5", "6", "7", "8", "9", "10", "A", "B", "C", } + ); + + doc.ScreenScrollUp_(new GLineChunkSpan(newLines, 8, 15)); + + CheckLines(doc, + new string[] { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "A", "B", "C", "D", "E", "F", "G", "H", }, + new string[] { "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", } + ); + } + + [Test] + public void Test_ScreenScrollUp_AppendLines_ScreenIsIsolated() { + TestTerminalDoc doc = new TestTerminalDoc(); + SetupScreenLines(doc, 1, 10); + + doc.ScreenIsolated = true; + + doc.ScreenScrollUp_(new GLineChunkSpan(newLines, 5, 1)); + + CheckLines(doc, + new string[] { }, + new string[] { "2", "3", "4", "5", "6", "7", "8", "9", "10", "A", } + ); + + doc.ScreenScrollUp_(new GLineChunkSpan(newLines, 6, 2)); + + CheckLines(doc, + new string[] { }, + new string[] { "4", "5", "6", "7", "8", "9", "10", "A", "B", "C", } + ); + + doc.ScreenScrollUp_(new GLineChunkSpan(newLines, 8, 15)); + + CheckLines(doc, + new string[] { }, + new string[] { "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", } + ); + } + + [Test] + public void Test_ScreenScrollDown_ScreenIsNotIsolated_1() { + TestTerminalDoc doc = new TestTerminalDoc(); + SetupScreenLines(doc, 1, 10); + doc.ScreenAppend_(GLine.CreateSimpleGLine("11", TextDecoration.Default)); + doc.ScreenAppend_(GLine.CreateSimpleGLine("12", TextDecoration.Default)); + doc.ScreenAppend_(GLine.CreateSimpleGLine("13", TextDecoration.Default)); + + CheckLines(doc, + new string[] { "1", "2", "3", }, + new string[] { "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", } + ); + + doc.ScreenIsolated = false; + + doc.ScreenScrollDown_(1); + + CheckLines(doc, + new string[] { "1", "2", }, + new string[] { "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", } + ); + + doc.ScreenScrollDown_(2); + + CheckLines(doc, + new string[] { }, + new string[] { "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", } + ); + + doc.ScreenScrollDown_(3); + + CheckLines(doc, + new string[] { }, + new string[] { "", "", "", "1", "2", "3", "4", "5", "6", "7", } + ); + } + + [Test] + public void Test_ScreenScrollDown_ScreenIsNotIsolated_2() { + TestTerminalDoc doc = new TestTerminalDoc(); + SetupScreenLines(doc, 1, 10); + doc.ScreenAppend_(GLine.CreateSimpleGLine("11", TextDecoration.Default)); + doc.ScreenAppend_(GLine.CreateSimpleGLine("12", TextDecoration.Default)); + doc.ScreenAppend_(GLine.CreateSimpleGLine("13", TextDecoration.Default)); + + CheckLines(doc, + new string[] { "1", "2", "3", }, + new string[] { "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", } + ); + + doc.ScreenIsolated = false; + + doc.ScreenScrollDown_(12); + + CheckLines(doc, + new string[] { }, + new string[] { "", "", "", "", "", "", "", "", "", "1", } + ); + } + + [Test] + public void Test_ScreenScrollDown_ScreenIsIsolated() { + TestTerminalDoc doc = new TestTerminalDoc(); + SetupScreenLines(doc, 1, 10); + doc.ScreenAppend_(GLine.CreateSimpleGLine("11", TextDecoration.Default)); + doc.ScreenAppend_(GLine.CreateSimpleGLine("12", TextDecoration.Default)); + doc.ScreenAppend_(GLine.CreateSimpleGLine("13", TextDecoration.Default)); + + CheckLines(doc, + new string[] { "1", "2", "3", }, + new string[] { "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", } + ); + + doc.ScreenIsolated = true; + + doc.ScreenScrollDown_(1); + + CheckLines(doc, + new string[] { "1", "2", "3", }, + new string[] { "", "4", "5", "6", "7", "8", "9", "10", "11", "12", } + ); + + doc.ScreenScrollDown_(2); + + CheckLines(doc, + new string[] { "1", "2", "3", }, + new string[] { "", "", "", "4", "5", "6", "7", "8", "9", "10", } + ); + + doc.ScreenScrollDown_(15); + + CheckLines(doc, + new string[] { "1", "2", "3", }, + new string[] { "", "", "", "", "", "", "", "", "", "", } + ); + } + + [Test] + public void Test_GetRowIDSpan() { + TestTerminalDoc doc = new TestTerminalDoc(); + SetupScreenLines(doc, 1, 10); + + { + var span = doc.GetRowIDSpan(); + Assert.AreEqual(1, span.Start); + Assert.AreEqual(10, span.Length); + } + + foreach (var line in + Enumerable.Range(11, 10) + .Select(n => n.ToString(NumberFormatInfo.InvariantInfo)) + .Select(s => GLine.CreateSimpleGLine(s, TextDecoration.Default))) { + + doc.ScreenAppend_(line); + } + + { + var span = doc.GetRowIDSpan(); + Assert.AreEqual(1, span.Start); + Assert.AreEqual(20, span.Length); + } + + foreach (var line in + Enumerable.Range(21, 3980) + .Select(n => n.ToString(NumberFormatInfo.InvariantInfo)) + .Select(s => GLine.CreateSimpleGLine(s, TextDecoration.Default))) { + + doc.ScreenAppend_(line); + } + + { + var span = doc.GetRowIDSpan(); + Assert.AreEqual(4000 - (GLineBuffer.DEFAULT_CAPACITY + 10) + 1, span.Start); + Assert.AreEqual(GLineBuffer.DEFAULT_CAPACITY + 10, span.Length); + } + } + + [Test] + public void Test_Apply() { + TestTerminalDoc doc = new TestTerminalDoc(); + SetupScreenLines(doc, 1, 10); + foreach (var line in + Enumerable.Range(11, 3990) + .Select(n => n.ToString(NumberFormatInfo.InvariantInfo)) + .Select(s => GLine.CreateSimpleGLine(s, TextDecoration.Default))) { + + doc.ScreenAppend_(line); + } + + var span = doc.GetRowIDSpan(); + Assert.AreEqual(4000 - (GLineBuffer.DEFAULT_CAPACITY + 10) + 1, span.Start); + Assert.AreEqual(GLineBuffer.DEFAULT_CAPACITY + 10, span.Length); + + // note: the content of each row equal to its RowID + + foreach (var testCase in new[] { + new { RowID= span.Start, ExpectNotNull = true }, + new { RowID= span.Start - 1, ExpectNotNull = false }, + new { RowID= span.Start + GLineBuffer.DEFAULT_CAPACITY - 1, ExpectNotNull = true }, + new { RowID= span.Start + GLineBuffer.DEFAULT_CAPACITY, ExpectNotNull = true }, + new { RowID= span.Start + GLineBuffer.DEFAULT_CAPACITY + 9, ExpectNotNull = true }, + new { RowID= span.Start + GLineBuffer.DEFAULT_CAPACITY + 10, ExpectNotNull = false }, + }) { + string actualContent = "xxxxx"; + doc.Apply(testCase.RowID, line => { + actualContent = (line != null) ? line.ToNormalString() : null; + }); + if (testCase.ExpectNotNull) { + string expectedContent = testCase.RowID.ToString(NumberFormatInfo.InvariantInfo); + Assert.AreEqual(expectedContent, actualContent); + } + else { + Assert.IsNull(actualContent); + } + } + } + + [Test] + public void Test_ForEach_1() { + TestTerminalDoc doc = new TestTerminalDoc(); + SetupScreenLines(doc, 1, 10); + foreach (var line in + Enumerable.Range(11, 3990) + .Select(n => n.ToString(NumberFormatInfo.InvariantInfo)) + .Select(s => GLine.CreateSimpleGLine(s, TextDecoration.Default))) { + + doc.ScreenAppend_(line); + } + + var span = doc.GetRowIDSpan(); + Assert.AreEqual(4000 - (GLineBuffer.DEFAULT_CAPACITY + 10) + 1, span.Start); + Assert.AreEqual(GLineBuffer.DEFAULT_CAPACITY + 10, span.Length); + + // note: the content of each row equal to its RowID + + foreach (var testCase in new[] { + new { StartRowID= span.Start - 10, Length = GLineBuffer.DEFAULT_CAPACITY + 30 }, + new { StartRowID= span.Start, Length = GLineBuffer.DEFAULT_CAPACITY }, + new { StartRowID= span.Start + GLineBuffer.DEFAULT_CAPACITY, Length = 10 }, + }) { + int nextExpectedRowID = testCase.StartRowID; + + doc.ForEach(testCase.StartRowID, testCase.Length, (rowID, line) => { + Assert.AreEqual(nextExpectedRowID, rowID); + if (span.Includes(rowID)) { + string expectedContent = rowID.ToString(NumberFormatInfo.InvariantInfo); + string actualContent = line.ToNormalString(); + Assert.AreEqual(expectedContent, actualContent); + } + else { + Assert.IsNull(line); + } + nextExpectedRowID++; + }); + + Assert.AreEqual(testCase.StartRowID + testCase.Length, nextExpectedRowID); + } + } + + [Test] + public void Test_ForEach_2() { + TestTerminalDoc doc = new TestTerminalDoc(); + SetupScreenLines(doc, 1, 10); + + var span = doc.GetRowIDSpan(); + Assert.AreEqual(1, span.Start); + Assert.AreEqual(10, span.Length); + + // note: the content of each row equal to its RowID + + foreach (var testCase in new[] { + new { StartRowID= -10, Length = 30 }, + new { StartRowID= 1, Length = 10 }, + }) { + int nextExpectedRowID = testCase.StartRowID; + + doc.ForEach(testCase.StartRowID, testCase.Length, (rowID, line) => { + Assert.AreEqual(nextExpectedRowID, rowID); + if (span.Includes(rowID)) { + string expectedContent = rowID.ToString(NumberFormatInfo.InvariantInfo); + string actualContent = line.ToNormalString(); + Assert.AreEqual(expectedContent, actualContent); + } + else { + Assert.IsNull(line); + } + nextExpectedRowID++; + }); + + Assert.AreEqual(testCase.StartRowID + testCase.Length, nextExpectedRowID); + } + } + + private void SetupScreenLines(TestTerminalDoc doc, int start, int length) { + bool screenIsolated = doc.ScreenIsolated; + doc.ScreenIsolated = false; + doc.StoreGLines( + Enumerable.Range(start, length) + .Select(n => n.ToString(NumberFormatInfo.InvariantInfo)) + .Select(s => GLine.CreateSimpleGLine(s, TextDecoration.Default)) + .ToArray()); + doc.ScreenIsolated = screenIsolated; + } + + private void CheckLines(TerminalCharacterDocument doc, IEnumerable expectedLogRows, IEnumerable expectedScreenRows) { + GLine[] screenRows; + GLine[] logRows; + doc.PeekGLines(out screenRows, out logRows); + var actualScreenRows = screenRows.Select(r => r.ToNormalString()).ToArray(); + var actualLogRows = logRows.Select(r => r.ToNormalString()).ToArray(); + CollectionAssert.AreEqual(expectedScreenRows, actualScreenRows); + CollectionAssert.AreEqual(expectedLogRows, actualLogRows); + } + } + + // A class for exposing protected methods of the TerminalCharacterDocument + class TestTerminalDoc : TerminalCharacterDocument { + + public TestTerminalDoc() + : base(80, 10) { + } + + public bool ScreenIsolated { + get; + set; + } + + protected override bool IsScreenIsolated() { + return this.ScreenIsolated; + } + + protected override void OnVisibleAreaSizeChanged(int rows, int cols) { + // do nothing + } + + protected override GLine CreateEmptyLine() { + return GLine.CreateSimpleGLine("", TextDecoration.Default); + } + + public void ScreenAppend_(GLine line) { + ScreenAppend(line); + } + + public GLine ScreenGetRow_(int rowIndex) { + return ScreenGetRow(rowIndex); + } + + public void ScreenSetRow_(int rowIndex, GLine line) { + ScreenSetRow(rowIndex, line); + } + + public void ScreenGetRows_(int rowIndex, GLineChunkSpan span) { + ScreenGetRows(rowIndex, span); + } + + public void ScreenSetRows_(int rowIndex, GLineChunkSpan span) { + ScreenSetRows(rowIndex, span); + } + + public void ScreenScrollUp_(int scrollRows) { + ScreenScrollUp(scrollRows); + } + + public void ScreenScrollUp_(GLineChunkSpan newRows) { + ScreenScrollUp(newRows); + } + + public void ScreenScrollDown_(int scrollRows) { + ScreenScrollDown(scrollRows); + } + + public void ScreenScrollUpRegion_(int startRowIndex, int endRowIndex, GLineChunkSpan newRows) { + ScreenScrollUpRegion(startRowIndex, endRowIndex, newRows); + } + + public void ScreenScrollDownRegion_(int startRowIndex, int endRowIndex, GLineChunkSpan newRows) { + ScreenScrollDownRegion(startRowIndex, endRowIndex, newRows); + } + } +} + +#endif diff --git a/CoreTest/packages.config b/CoreTest/packages.config index e3afc87f..4386ac91 100644 --- a/CoreTest/packages.config +++ b/CoreTest/packages.config @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/GranadosTest/GranadosTest.csproj b/GranadosTest/GranadosTest.csproj index d5705805..3ba21c5d 100644 --- a/GranadosTest/GranadosTest.csproj +++ b/GranadosTest/GranadosTest.csproj @@ -1,7 +1,7 @@  + - Debug @@ -13,7 +13,7 @@ GranadosTest v4.5 512 - 8362b0f2 + 827634a9 true @@ -71,8 +71,8 @@ This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - + + + + + + + + + @@ -110,7 +118,6 @@ - Form diff --git a/TerminalEmulator/VT100.cs b/TerminalEmulator/VT100.cs deleted file mode 100644 index 68a4b606..00000000 --- a/TerminalEmulator/VT100.cs +++ /dev/null @@ -1,751 +0,0 @@ -// Copyright 2004-2017 The Poderosa Project. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -using System; -using System.IO; -using System.Text; -using System.Diagnostics; -using System.Drawing; -using System.Windows.Forms; - -using Poderosa.Util.Drawing; -using Poderosa.Document; -using Poderosa.ConnectionParam; -using Poderosa.View; - -namespace Poderosa.Terminal { - internal class VT100Terminal : EscapeSequenceTerminal { - - private int _savedRow; - private int _savedCol; - protected bool _insertMode; - protected bool _scrollRegionRelative; - - //接続の種類によってエスケープシーケンスの解釈を変える部分 - //protected bool _homePositionOnCSIJ2; - - public VT100Terminal(TerminalInitializeInfo info) - : base(info) { - _insertMode = false; - _scrollRegionRelative = false; - //bool sfu = _terminalSettings is SFUTerminalParam; - //_homePositionOnCSIJ2 = sfu; - } - protected override void ResetInternal() { - base.ResetInternal(); - _insertMode = false; - _scrollRegionRelative = false; - } - - internal override byte[] GetPasteLeadingBytes() { - return new byte[0]; - } - - internal override byte[] GetPasteTrailingBytes() { - return new byte[0]; - } - - protected override ProcessCharResult ProcessEscapeSequence(char code, char[] seq, int offset) { - string param; - switch (code) { - case '[': - if (seq.Length - offset - 1 >= 0) { - param = new string(seq, offset, seq.Length - offset - 1); - return ProcessAfterCSI(param, seq[seq.Length - 1]); - } - break; - //throw new UnknownEscapeSequenceException(String.Format("unknown command after CSI {0}", code)); - case ']': - if (seq.Length - offset - 1 >= 0) { - param = new string(seq, offset, seq.Length - offset - 1); - return ProcessAfterOSC(param, seq[seq.Length - 1]); - } - break; - case '=': - ChangeMode(TerminalMode.Application); - return ProcessCharResult.Processed; - case '>': - ChangeMode(TerminalMode.Normal); - return ProcessCharResult.Processed; - case 'E': - ProcessNextLine(); - return ProcessCharResult.Processed; - case 'M': - ReverseIndex(); - return ProcessCharResult.Processed; - case 'D': - Index(); - return ProcessCharResult.Processed; - case '7': - SaveCursor(); - return ProcessCharResult.Processed; - case '8': - RestoreCursor(); - return ProcessCharResult.Processed; - case 'c': - FullReset(); - return ProcessCharResult.Processed; - } - return ProcessCharResult.Unsupported; - } - - protected virtual ProcessCharResult ProcessAfterCSI(string param, char code) { - - switch (code) { - case 'c': - ProcessDeviceAttributes(param); - break; - case 'm': //SGR - ProcessSGR(param); - break; - case 'h': - case 'l': - return ProcessDECSETMulti(param, code); - case 'r': - if (param.Length > 0 && param[0] == '?') - return ProcessRestoreDECSET(param.Substring(1), code); - else - ProcessSetScrollingRegion(param); - break; - case 's': - if (param.Length > 0 && param[0] == '?') - return ProcessSaveDECSET(param.Substring(1), code); - else - return ProcessCharResult.Unsupported; - case 'n': - ProcessDeviceStatusReport(param); - break; - case 'A': - case 'B': - case 'C': - case 'D': - case 'E': - case 'F': - ProcessCursorMove(param, code); - break; - case 'H': - case 'f': //fは本当はxterm固有 - ProcessCursorPosition(param); - break; - case 'J': - ProcessEraseInDisplay(param); - break; - case 'K': - ProcessEraseInLine(param); - break; - case 'L': - ProcessInsertLines(param); - break; - case 'M': - ProcessDeleteLines(param); - break; - default: - return ProcessCharResult.Unsupported; - } - - return ProcessCharResult.Processed; - } - protected virtual ProcessCharResult ProcessAfterOSC(string param, char code) { - return ProcessCharResult.Unsupported; - } - - protected virtual void ProcessSGR(string param) { - string[] ps = param.Split(';'); - TextDecoration dec = _currentdecoration; - foreach (string cmd in ps) { - int code = ParseSGRCode(cmd); - ProcessSGRParameterANSI(code, ref dec); - } - _currentdecoration = dec; - } - - protected void ProcessSGRParameterANSI(int code, ref TextDecoration dec) { - switch (code) { - case 0: // default rendition (implementation-defined) (ECMA-48,VT100,VT220) - dec = TextDecoration.Default; - break; - case 1: // bold or increased intensity (ECMA-48,VT100,VT220) - dec = dec.GetCopyWithBold(true); - break; - case 2: // faint, decreased intensity or second colour (ECMA-48) - break; - case 3: // italicized (ECMA-48) - break; - case 4: // singly underlined (ECMA-48,VT100,VT220) - dec = dec.GetCopyWithUnderline(true); - break; - case 5: // slowly blinking (ECMA-48,VT100,VT220) - case 6: // rapidly blinking (ECMA-48) - dec = dec.GetCopyWithBlink(true); - break; - case 7: // negative image (ECMA-48,VT100,VT220) - dec = dec.GetCopyWithInverted(true); - break; - case 8: // concealed characters (ECMA-48,VT300) - case 9: // crossed-out (ECMA-48) - case 10: // primary (default) font (ECMA-48) - case 11: // first alternative font (ECMA-48) - case 12: // second alternative font (ECMA-48) - case 13: // third alternative font (ECMA-48) - case 14: // fourth alternative font (ECMA-48) - case 15: // fifth alternative font (ECMA-48) - case 16: // sixth alternative font (ECMA-48) - case 17: // seventh alternative font (ECMA-48) - case 18: // eighth alternative font (ECMA-48) - case 19: // ninth alternative font (ECMA-48) - case 20: // Fraktur (Gothic) (ECMA-48) - case 21: // doubly underlined (ECMA-48) - break; - case 22: // normal colour or normal intensity (neither bold nor faint) (ECMA-48,VT220,VT300) - dec = TextDecoration.Default; - break; - case 23: // not italicized, not fraktur (ECMA-48) - break; - case 24: // not underlined (neither singly nor doubly) (ECMA-48,VT220,VT300) - dec = dec.GetCopyWithUnderline(false); - break; - case 25: // steady (not blinking) (ECMA-48,VT220,VT300) - dec = dec.GetCopyWithBlink(false); - break; - case 26: // reserved (ECMA-48) - break; - case 27: // positive image (ECMA-48,VT220,VT300) - dec = dec.GetCopyWithInverted(false); - break; - case 28: // revealed characters (ECMA-48) - case 29: // not crossed out (ECMA-48) - break; - case 30: // black display (ECMA-48) - case 31: // red display (ECMA-48) - case 32: // green display (ECMA-48) - case 33: // yellow display (ECMA-48) - case 34: // blue display (ECMA-48) - case 35: // magenta display (ECMA-48) - case 36: // cyan display (ECMA-48) - case 37: // white display (ECMA-48) - dec = SelectForeColor(dec, code - 30); - break; - case 38: // reserved (ECMA-48) - break; - case 39: // default display colour (implementation-defined) (ECMA-48) - dec = dec.GetCopyWithForeColor(ColorSpec.Default); - break; - case 40: // black background (ECMA-48) - case 41: // red background (ECMA-48) - case 42: // green background (ECMA-48) - case 43: // yellow background (ECMA-48) - case 44: // blue background (ECMA-48) - case 45: // magenta background (ECMA-48) - case 46: // cyan background (ECMA-48) - case 47: // white background (ECMA-48) - dec = SelectBackgroundColor(dec, code - 40); - break; - case 48: // reserved (ECMA-48) - break; - case 49: // default background colour (implementation-defined) (ECMA-48) - dec = dec.GetCopyWithBackColor(ColorSpec.Default); - break; - case 50: // reserved (ECMA-48) - case 51: // framed (ECMA-48) - case 52: // encircled (ECMA-48) - case 53: // overlined (ECMA-48) - case 54: // not framed, not encircled (ECMA-48) - case 55: // not overlined (ECMA-48) - case 56: // reserved (ECMA-48) - case 57: // reserved (ECMA-48) - case 58: // reserved (ECMA-48) - case 59: // reserved (ECMA-48) - case 60: // ideogram underline or right side line (ECMA-48) - case 61: // ideogram double underline or double line on the right side (ECMA-48) - case 62: // ideogram overline or left side line (ECMA-48) - case 63: // ideogram double overline or double line on the left side (ECMA-48) - case 64: // ideogram stress marking (ECMA-48) - case 65: // cancels the effect of the rendition aspects established by parameter values 60 to 64 (ECMA-48) - break; - default: - // other values are ignored without notification to the user - Debug.WriteLine("unknown SGR code (ANSI) : {0}", code); - break; - } - } - - protected TextDecoration SelectForeColor(TextDecoration dec, int index) { - return dec.GetCopyWithForeColor(new ColorSpec(index)); - } - - protected TextDecoration SelectBackgroundColor(TextDecoration dec, int index) { - return dec.GetCopyWithBackColor(new ColorSpec(index)); - } - - protected int ParseSGRCode(string param) { - if (param.Length == 0) - return 0; - else if (param.Length == 1) - return param[0] - '0'; - else if (param.Length == 2) - return (param[0] - '0') * 10 + (param[1] - '0'); - else if (param.Length == 3) - return (param[0] - '0') * 100 + (param[1] - '0') * 10 + (param[2] - '0'); - else - throw new UnknownEscapeSequenceException(String.Format("unknown SGR parameter {0}", param)); - } - - protected virtual void ProcessDeviceAttributes(string param) { - byte[] data = Encoding.ASCII.GetBytes(" [?1;2c"); //なんかよくわからないがMindTerm等をみるとこれでいいらしい - data[0] = 0x1B; //ESC - TransmitDirect(data); - } - protected virtual void ProcessDeviceStatusReport(string param) { - string response; - if (param == "5") - response = " [0n"; //これでOKの意味らしい - else if (param == "6") - response = String.Format(" [{0};{1}R", GetDocument().CurrentLineNumber - GetDocument().TopLineNumber + 1, _manipulator.CaretColumn + 1); - else - throw new UnknownEscapeSequenceException("DSR " + param); - - byte[] data = Encoding.ASCII.GetBytes(response); - data[0] = 0x1B; //ESC - TransmitDirect(data); - } - - protected void ProcessCursorMove(string param, char method) { - int count = ParseInt(param, 1); //パラメータが省略されたときの移動量は1 - - int column = _manipulator.CaretColumn; - switch (method) { - case 'A': - GetDocument().UpdateCurrentLine(_manipulator); - GetDocument().CurrentLineNumber = (GetDocument().CurrentLineNumber - count); - _manipulator.Load(GetDocument().CurrentLine, column); - break; - case 'B': - GetDocument().UpdateCurrentLine(_manipulator); - GetDocument().CurrentLineNumber = (GetDocument().CurrentLineNumber + count); - _manipulator.Load(GetDocument().CurrentLine, column); - break; - case 'C': { - int newvalue = column + count; - if (newvalue >= GetDocument().TerminalWidth) - newvalue = GetDocument().TerminalWidth - 1; - _manipulator.CaretColumn = newvalue; - } - break; - case 'D': { - int newvalue = column - count; - if (newvalue < 0) - newvalue = 0; - _manipulator.CaretColumn = newvalue; - } - break; - } - } - - //CSI H - protected void ProcessCursorPosition(string param) { - IntPair t = ParseIntPair(param, 1, 1); - int row = t.first, col = t.second; - if (_scrollRegionRelative && GetDocument().ScrollingTop != -1) { - row += GetDocument().ScrollingTop; - } - - if (row < 1) - row = 1; - else if (row > GetDocument().TerminalHeight) - row = GetDocument().TerminalHeight; - if (col < 1) - col = 1; - else if (col > GetDocument().TerminalWidth) - col = GetDocument().TerminalWidth; - ProcessCursorPosition(row, col); - } - protected void ProcessCursorPosition(int row, int col) { - GetDocument().UpdateCurrentLine(_manipulator); - GetDocument().CurrentLineNumber = (GetDocument().TopLineNumber + row - 1); - //int cc = GetDocument().CurrentLine.DisplayPosToCharPos(col-1); - //Debug.Assert(cc>=0); - _manipulator.Load(GetDocument().CurrentLine, col - 1); - } - - //CSI J - protected void ProcessEraseInDisplay(string param) { - int d = ParseInt(param, 0); - - TerminalDocument doc = GetDocument(); - int cur = doc.CurrentLineNumber; - int top = doc.TopLineNumber; - int bottom = top + doc.TerminalHeight; - int col = _manipulator.CaretColumn; - switch (d) { - case 0: //erase below - { - if (col == 0 && cur == top) - goto ERASE_ALL; - - EraseRight(); - doc.UpdateCurrentLine(_manipulator); - doc.EnsureLine(bottom - 1); - doc.RemoveAfter(bottom); - doc.ClearRange(cur + 1, bottom, _currentdecoration); - _manipulator.Load(doc.CurrentLine, col); - } - break; - case 1: //erase above - { - if (col == doc.TerminalWidth - 1 && cur == bottom - 1) - goto ERASE_ALL; - - EraseLeft(); - doc.UpdateCurrentLine(_manipulator); - doc.ClearRange(top, cur, _currentdecoration); - _manipulator.Load(doc.CurrentLine, col); - } - break; - case 2: //erase all - ERASE_ALL: { - GetDocument().ApplicationModeBackColor = - (_currentdecoration != null) ? _currentdecoration.GetBackColorSpec() : ColorSpec.Default; - - doc.UpdateCurrentLine(_manipulator); - //if(_homePositionOnCSIJ2) { //SFUではこうなる - // ProcessCursorPosition(1,1); - // col = 0; - //} - doc.EnsureLine(bottom - 1); - doc.RemoveAfter(bottom); - doc.ClearRange(top, bottom, _currentdecoration); - _manipulator.Load(doc.CurrentLine, col); - } - break; - default: - throw new UnknownEscapeSequenceException(String.Format("unknown ED option {0}", param)); - } - - } - - //CSI K - private void ProcessEraseInLine(string param) { - int d = ParseInt(param, 0); - - switch (d) { - case 0: //erase right - EraseRight(); - break; - case 1: //erase left - EraseLeft(); - break; - case 2: //erase all - EraseLine(); - break; - default: - throw new UnknownEscapeSequenceException(String.Format("unknown EL option {0}", param)); - } - } - - private void EraseRight() { - _manipulator.FillSpace(_manipulator.CaretColumn, _manipulator.BufferSize, _currentdecoration); - } - - private void EraseLeft() { - _manipulator.FillSpace(0, _manipulator.CaretColumn + 1, _currentdecoration); - } - - private void EraseLine() { - _manipulator.FillSpace(0, _manipulator.BufferSize, _currentdecoration); - } - - - protected virtual void SaveCursor() { - _savedRow = GetDocument().CurrentLineNumber - GetDocument().TopLineNumber; - _savedCol = _manipulator.CaretColumn; - } - protected virtual void RestoreCursor() { - GetDocument().UpdateCurrentLine(_manipulator); - GetDocument().CurrentLineNumber = GetDocument().TopLineNumber + _savedRow; - _manipulator.Load(GetDocument().CurrentLine, _savedCol); - } - - protected void Index() { - GetDocument().UpdateCurrentLine(_manipulator); - int current = GetDocument().CurrentLineNumber; - if (current == GetDocument().TopLineNumber + GetDocument().TerminalHeight - 1 || current == GetDocument().ScrollingBottom) - GetDocument().ScrollDown(); - else - GetDocument().CurrentLineNumber = current + 1; - _manipulator.Load(GetDocument().CurrentLine, _manipulator.CaretColumn); - } - protected void ReverseIndex() { - GetDocument().UpdateCurrentLine(_manipulator); - int current = GetDocument().CurrentLineNumber; - if (current == GetDocument().TopLineNumber || current == GetDocument().ScrollingTop) - GetDocument().ScrollUp(); - else - GetDocument().CurrentLineNumber = current - 1; - _manipulator.Load(GetDocument().CurrentLine, _manipulator.CaretColumn); - } - - protected void ProcessSetScrollingRegion(string param) { - int height = GetDocument().TerminalHeight; - IntPair v = ParseIntPair(param, 1, height); - - if (v.first < 1) - v.first = 1; - else if (v.first > height) - v.first = height; - if (v.second < 1) - v.second = 1; - else if (v.second > height) - v.second = height; - if (v.first > v.second) { //問答無用でエラーが良いようにも思うが - int t = v.first; - v.first = v.second; - v.second = t; - } - - //指定は1-originだが処理は0-origin - GetDocument().SetScrollingRegion(v.first - 1, v.second - 1); - } - - protected void ProcessNextLine() { - GetDocument().UpdateCurrentLine(_manipulator); - GetDocument().CurrentLineNumber = (GetDocument().CurrentLineNumber + 1); - _manipulator.Load(GetDocument().CurrentLine, 0); - } - - protected override void ChangeMode(TerminalMode mode) { - if (_terminalMode == mode) - return; - - if (mode == TerminalMode.Normal) { - GetDocument().ClearScrollingRegion(); - GetConnection().TerminalOutput.Resize(GetDocument().TerminalWidth, GetDocument().TerminalHeight); //たとえばemacs起動中にリサイズし、シェルへ戻るとシェルは新しいサイズを認識していない - //RMBoxで確認されたことだが、無用に後方にドキュメントを広げてくる奴がいる。カーソルを123回後方へ、など。 - //場当たり的だが、ノーマルモードに戻る際に後ろの空行を削除することで対応する。 - GLine l = GetDocument().LastLine; - while (l != null && l.DisplayLength == 0 && l.ID > GetDocument().CurrentLineNumber) - l = l.PrevLine; - - if (l != null) - l = l.NextLine; - if (l != null) - GetDocument().RemoveAfter(l.ID); - - GetDocument().IsApplicationMode = false; - } - else { - GetDocument().ApplicationModeBackColor = ColorSpec.Default; - GetDocument().SetScrollingRegion(0, GetDocument().TerminalHeight - 1); - GetDocument().IsApplicationMode = true; - } - - GetDocument().InvalidateAll(); - - _terminalMode = mode; - } - - private ProcessCharResult ProcessDECSETMulti(string param, char code) { - if (param.Length == 0) - return ProcessCharResult.Processed; - bool question = param[0] == '?'; - string[] ps = question ? param.Substring(1).Split(';') : param.Split(';'); - bool unsupported = false; - foreach (string p in ps) { - ProcessCharResult r = question ? ProcessDECSET(p, code) : ProcessSetMode(p, code); - if (r == ProcessCharResult.Unsupported) - unsupported = true; - } - return unsupported ? ProcessCharResult.Unsupported : ProcessCharResult.Processed; - } - - //CSI ? Pm h, CSI ? Pm l - protected virtual ProcessCharResult ProcessDECSET(string param, char code) { - //Debug.WriteLine(String.Format("DECSET {0} {1}", param, code)); - switch (param) { - case "25": - return ProcessCharResult.Processed; //!!Show/Hide Cursorだがとりあえず無視 - case "1": - ChangeCursorKeyMode(code == 'h' ? TerminalMode.Application : TerminalMode.Normal); - return ProcessCharResult.Processed; - default: - return ProcessCharResult.Unsupported; - } - } - protected virtual ProcessCharResult ProcessSetMode(string param, char code) { - bool set = code == 'h'; - switch (param) { - case "4": - _insertMode = set; //hで始まってlで終わる - return ProcessCharResult.Processed; - case "12": //local echo - _afterExitLockActions.Add(new AfterExitLockDelegate(new LocalEchoChanger(GetTerminalSettings(), !set).Do)); - return ProcessCharResult.Processed; - case "20": - return ProcessCharResult.Processed; //!!WinXPのTelnetで確認した - case "25": - return ProcessCharResult.Processed; - case "34": //MakeCursorBig, puttyにはある - //!setでカーソルを強制的に箱型にし、setで通常に戻すというのが正しい動作だが実害はないので無視 - return ProcessCharResult.Processed; - default: - return ProcessCharResult.Unsupported; - } - } - - //これはさぼり。ちゃんと保存しないといけない状態はほとんどないので - protected virtual ProcessCharResult ProcessSaveDECSET(string param, char code) { - //このparamは複数個パラメータ - return ProcessCharResult.Processed; - } - protected virtual ProcessCharResult ProcessRestoreDECSET(string param, char code) { - //このparamは複数個パラメータ - return ProcessCharResult.Processed; - } - - //これを送ってくるアプリケーションは viで上方スクロール - protected void ProcessInsertLines(string param) { - int d = ParseInt(param, 1); - - TerminalDocument doc = GetDocument(); - int caret_pos = _manipulator.CaretColumn; - int offset = doc.CurrentLineNumber - doc.TopLineNumber; - doc.UpdateCurrentLine(_manipulator); - if (doc.ScrollingBottom == -1) - doc.SetScrollingRegion(0, GetDocument().TerminalHeight - 1); - - for (int i = 0; i < d; i++) { - doc.ScrollUp(doc.CurrentLineNumber, doc.ScrollingBottom); - doc.CurrentLineNumber = doc.TopLineNumber + offset; - } - _manipulator.Load(doc.CurrentLine, caret_pos); - } - - //これを送ってくるアプリケーションは viで下方スクロール - protected void ProcessDeleteLines(string param) { - int d = ParseInt(param, 1); - - /* - TerminalDocument doc = GetDocument(); - _manipulator.Clear(GetConnection().TerminalWidth); - GLine target = doc.CurrentLine; - for(int i=0; i= 20) - tail = (modifier & Keys.Control) != Keys.None ? '@' : '$'; - else - tail = (modifier & Keys.Control) != Keys.None ? '^' : '~'; - string f = FUNCTIONKEY_MAP[n]; - r[2] = (byte)f[0]; - r[3] = (byte)f[1]; - r[4] = (byte)tail; - return r; - } - else if (GUtil.IsCursorKey(body)) { - byte[] r = new byte[3]; - r[0] = 0x1B; - if (_cursorKeyMode == TerminalMode.Normal) - r[1] = (byte)'['; - else - r[1] = (byte)'O'; - - switch (body) { - case Keys.Up: - r[2] = (byte)'A'; - break; - case Keys.Down: - r[2] = (byte)'B'; - break; - case Keys.Right: - r[2] = (byte)'C'; - break; - case Keys.Left: - r[2] = (byte)'D'; - break; - default: - throw new ArgumentException("unknown cursor key code", "key"); - } - return r; - } - else { - byte[] r = new byte[4]; - r[0] = 0x1B; - r[1] = (byte)'['; - r[3] = (byte)'~'; - if (body == Keys.Insert) - r[2] = (byte)'1'; - else if (body == Keys.Home) - r[2] = (byte)'2'; - else if (body == Keys.PageUp) - r[2] = (byte)'3'; - else if (body == Keys.Delete) - r[2] = (byte)'4'; - else if (body == Keys.End) - r[2] = (byte)'5'; - else if (body == Keys.PageDown) - r[2] = (byte)'6'; - else - throw new ArgumentException("unknown key " + body.ToString()); - return r; - } - } - - private class LocalEchoChanger { - private ITerminalSettings _settings; - private bool _value; - public LocalEchoChanger(ITerminalSettings settings, bool value) { - _settings = settings; - _value = value; - } - public void Do() { - _settings.BeginUpdate(); - _settings.LocalEcho = _value; - _settings.EndUpdate(); - } - } - } -} diff --git a/TerminalEmulator/XTerm.cs b/TerminalEmulator/XTerm.cs index b422c26d..8d7b15e8 100644 --- a/TerminalEmulator/XTerm.cs +++ b/TerminalEmulator/XTerm.cs @@ -29,68 +29,70 @@ using Poderosa.Preferences; namespace Poderosa.Terminal { - internal class XTerm : VT100Terminal { - - private enum MouseTrackingState { - Off, - Normal, - Drag, - Any, + internal class XTerm : AbstractTerminal { + + private class ControlCode { + public const char NUL = '\u0000'; + public const char BEL = '\u0007'; + public const char BS = '\u0008'; + public const char HT = '\u0009'; + public const char LF = '\u000a'; + public const char VT = '\u000b'; + public const char CR = '\u000d'; + public const char SO = '\u000e'; + public const char SI = '\u000f'; + public const char ESC = '\u001b'; + public const char ST = '\u009c'; } - private enum MouseTrackingProtocol { - Normal, - Utf8, - Urxvt, - Sgr, - } + private StringBuilder _escapeSequence; + private IModalCharacterTask _currentCharacterTask; + + protected bool _insertMode; + protected bool _scrollRegionRelative; private bool _gotEscape; private bool _wrapAroundMode; private bool _reverseVideo; private bool[] _tabStops; - private readonly List[] _savedScreen = new List[2]; // { main, alternate } 別のバッファに移行したときにGLineを退避しておく - private bool _isAlternateBuffer; - private bool _savedMode_isAlternateBuffer; - private readonly int[] _xtermSavedRow = new int[2]; // { main, alternate } - private readonly int[] _xtermSavedCol = new int[2]; // { main, alternate } - - private bool _bracketedPasteMode = false; - private readonly byte[] _bracketedPasteModeLeadingBytes = new byte[] { 0x1b, (byte)'[', (byte)'2', (byte)'0', (byte)'0', (byte)'~' }; - private readonly byte[] _bracketedPasteModeTrailingBytes = new byte[] { 0x1b, (byte)'[', (byte)'2', (byte)'0', (byte)'1', (byte)'~' }; - private readonly byte[] _bracketedPasteModeEmptyBytes = new byte[0]; - - private MouseTrackingState _mouseTrackingState = MouseTrackingState.Off; - private MouseTrackingProtocol _mouseTrackingProtocol = MouseTrackingProtocol.Normal; - private bool _focusReportingMode = false; - private int _prevMouseRow = -1; - private int _prevMouseCol = -1; - private MouseButtons _mouseButton = MouseButtons.None; - - private const int MOUSE_POS_LIMIT = 255 - 32; // mouse position limit - private const int MOUSE_POS_EXT_LIMIT = 2047 - 32; // mouse position limit in extended mode - private const int MOUSE_POS_EXT_START = 127 - 32; // mouse position to start using extended format + + private readonly ScreenBufferManager _screenBuffer; + private readonly BracketedPasteModeManager _bracketedPasteMode; + private readonly MouseTrackingManager _mouseTracking; + private readonly FocusReportingManager _focusReporting; public XTerm(TerminalInitializeInfo info) : base(info) { + _escapeSequence = new StringBuilder(); + _processCharResult = ProcessCharResult.Processed; + + _insertMode = false; + _scrollRegionRelative = false; + _wrapAroundMode = true; _tabStops = new bool[GetDocument().TerminalWidth]; - _isAlternateBuffer = false; - _savedMode_isAlternateBuffer = false; InitTabStops(); + + _screenBuffer = new ScreenBufferManager(this); + _bracketedPasteMode = new BracketedPasteModeManager(this); + _mouseTracking = new MouseTrackingManager(this); + _focusReporting = new FocusReportingManager(this); } - public override bool GetFocusReportingMode() { - return _focusReportingMode; + protected override void ResetInternal() { + _escapeSequence = new StringBuilder(); + _processCharResult = ProcessCharResult.Processed; + _insertMode = false; + _scrollRegionRelative = false; } internal override byte[] GetPasteLeadingBytes() { - return _bracketedPasteMode ? _bracketedPasteModeLeadingBytes : _bracketedPasteModeEmptyBytes; + return _bracketedPasteMode.GetPasteLeadingBytes(); } internal override byte[] GetPasteTrailingBytes() { - return _bracketedPasteMode ? _bracketedPasteModeTrailingBytes : _bracketedPasteModeEmptyBytes; + return _bracketedPasteMode.GetPasteTrailingBytes(); } public override void ProcessChar(char ch) { @@ -104,283 +106,280 @@ public override void ProcessChar(char ch) { // for terminating the escape sequence. // After this conversion, we can consider ESC as the start // of the new escape sequence. - base.ProcessChar(ControlCode.ST); + ProcessChar2(ControlCode.ST); return; } - base.ProcessChar(ControlCode.ESC); + ProcessChar2(ControlCode.ESC); } if (ch == ControlCode.ESC) { _gotEscape = true; } else { - base.ProcessChar(ch); + ProcessChar2(ch); } } - public bool ReverseVideo { - get { - return _reverseVideo; - } - } + private void ProcessChar2(char ch) { + if (_processCharResult != ProcessCharResult.Escaping) { + if (ch == ControlCode.ESC) { + _processCharResult = ProcessCharResult.Escaping; + } + else { + if (_currentCharacterTask != null) { //マクロなど、charを取るタイプ + _currentCharacterTask.ProcessChar(ch); + } - public override bool ProcessMouse(TerminalMouseAction action, MouseButtons button, Keys modKeys, int row, int col) { - MouseTrackingState currentState = _mouseTrackingState; // copy value because _mouseTrackingState may be changed in another thread. + this.LogService.XmlLogger.Write(ch); - if (currentState == MouseTrackingState.Off) { - _prevMouseRow = -1; - _prevMouseCol = -1; - switch (action) { - case TerminalMouseAction.ButtonUp: - case TerminalMouseAction.ButtonDown: - _mouseButton = MouseButtons.None; - break; + if (Unicode.IsControlCharacter(ch)) + _processCharResult = ProcessControlChar(ch); + else + _processCharResult = ProcessNormalChar(ch); } - return false; } + else { + if (ch == ControlCode.NUL) + return; //シーケンス中にNULL文字が入っているケースが確認された なお今はXmlLoggerにもこのデータは行かない。 + + if (ch == ControlCode.ESC) { + // escape sequence restarted ? + // save log silently + RuntimeUtil.SilentReportException(new UnknownEscapeSequenceException("Incomplete escape sequence: ESC " + _escapeSequence.ToString())); + _escapeSequence.Remove(0, _escapeSequence.Length); + return; + } - // Note: from here, return value must be true even if nothing has been processed actually. - - MouseTrackingProtocol protocol = _mouseTrackingProtocol; // copy value because _mouseTrackingProtocol may be changed in another thread. - - int posLimit = protocol == MouseTrackingProtocol.Normal ? MOUSE_POS_LIMIT : MOUSE_POS_EXT_LIMIT; - - if (row < 0) - row = 0; - else if (row > posLimit) - row = posLimit; - - if (col < 0) - col = 0; - else if (col > posLimit) - col = posLimit; - - int statBits; - switch (action) { - case TerminalMouseAction.ButtonDown: - if (_mouseButton != MouseButtons.None) - return true; // another button is already pressed - - switch (button) { - case MouseButtons.Left: - statBits = 0x00; - break; - case MouseButtons.Middle: - statBits = 0x01; - break; - case MouseButtons.Right: - statBits = 0x02; - break; - default: - return true; // unsupported button - } + _escapeSequence.Append(ch); + bool end_flag = false; //escape sequenceの終わりかどうかを示すフラグ + if (_escapeSequence.Length == 1) { //ESC+1文字である場合 + end_flag = ('0' <= ch && ch <= '9') || ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z' && ch != 'P') || ch == '>' || ch == '=' || ch == '|' || ch == '}' || ch == '~'; + } + else if (_escapeSequence[0] == ']') { //OSCの終端はBELかST(String Terminator) + end_flag = (ch == ControlCode.BEL) || (ch == ControlCode.ST); + // Note: The conversion from "ESC \" to ST would be done in XTerm.ProcessChar(char). + } + else if (this._escapeSequence[0] == '@') { + end_flag = (ch == '0') || (ch == '1'); + } + else if (this._escapeSequence[0] == 'P') { // DCS + end_flag = (ch == ControlCode.ST); + } + else { + end_flag = ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') || ch == '@' || ch == '~' || ch == '|' || ch == '{'; + } - _mouseButton = button; - break; + if (end_flag) { //シーケンスのおわり + char[] seq = _escapeSequence.ToString().ToCharArray(); - case TerminalMouseAction.ButtonUp: - if (button != _mouseButton) - return true; // ignore + this.LogService.XmlLogger.EscapeSequence(seq); - if (protocol == MouseTrackingProtocol.Sgr) { - switch (button) { - case MouseButtons.Left: - statBits = 0x00; - break; - case MouseButtons.Middle: - statBits = 0x01; - break; - case MouseButtons.Right: - statBits = 0x02; - break; - default: - return true; // unsupported button - } + try { + char code = seq[0]; + _processCharResult = ProcessCharResult.Unsupported; //ProcessEscapeSequenceで例外が来た後で状態がEscapingはひどい結果を招くので + _processCharResult = ProcessEscapeSequence(code, seq, 1); + if (_processCharResult == ProcessCharResult.Unsupported) + throw new UnknownEscapeSequenceException("Unknown escape sequence: ESC " + new string(seq)); } - else { - statBits = 0x03; + catch (UnknownEscapeSequenceException ex) { + CharDecodeError(GEnv.Strings.GetString("Message.EscapesequenceTerminal.UnsupportedSequence") + ex.Message); + RuntimeUtil.SilentReportException(ex); } - - _mouseButton = MouseButtons.None; - break; - - case TerminalMouseAction.WheelUp: - statBits = 0x40; - break; - - case TerminalMouseAction.WheelDown: - statBits = 0x41; - break; - - case TerminalMouseAction.MouseMove: - if (currentState != MouseTrackingState.Any && currentState != MouseTrackingState.Drag) - return true; // no need to send - - if (currentState == MouseTrackingState.Drag && _mouseButton == MouseButtons.None) - return true; // no need to send - - if (row == _prevMouseRow && col == _prevMouseCol) - return true; // no need to send - - switch (_mouseButton) { - case MouseButtons.Left: - statBits = 0x20; - break; - case MouseButtons.Middle: - statBits = 0x21; - break; - case MouseButtons.Right: - statBits = 0x22; - break; - default: - statBits = 0x20; - break; + finally { + _escapeSequence.Remove(0, _escapeSequence.Length); } - break; - - default: - return true; // unknown action + } + else + _processCharResult = ProcessCharResult.Escaping; } + } - if ((modKeys & Keys.Shift) != Keys.None) - statBits |= 0x04; - - if ((modKeys & Keys.Alt) != Keys.None) - statBits |= 0x08; // Meta key - - if ((modKeys & Keys.Control) != Keys.None) - statBits |= 0x10; - - if (protocol != MouseTrackingProtocol.Sgr) - statBits += 0x20; + protected ProcessCharResult ProcessNormalChar(char ch) { + UnicodeChar unicodeChar; + if (!base.UnicodeCharConverter.Feed(ch, out unicodeChar)) { + return ProcessCharResult.Processed; + } - _prevMouseRow = row; - _prevMouseCol = col; + return ProcessNormalUnicodeChar(unicodeChar); + } - byte[] data; - int dataLen; + public bool ReverseVideo { + get { + return _reverseVideo; + } + } - switch (protocol) { + public override bool ProcessMouse(TerminalMouseAction action, MouseButtons button, Keys modKeys, int row, int col) { - case MouseTrackingProtocol.Normal: - data = new byte[] { - (byte)27, // ESCAPE - (byte)91, // [ - (byte)77, // M - (byte)statBits, - (col == posLimit) ? - (byte)0 : // emulate xterm's bug - (byte)(col + (1 + 0x20)), // column 0 --> send as 1 - (row == posLimit) ? - (byte)0 : // emulate xterm's bug - (byte)(row + (1 + 0x20)), // row 0 --> send as 1 - }; - dataLen = 6; - break; + return _mouseTracking.ProcessMouse( + action: action, + button: button, + modKeys: modKeys, + row: row, + col: col); + } - case MouseTrackingProtocol.Utf8: - data = new byte[8] { - (byte)27, // ESCAPE - (byte)91, // [ - (byte)77, // M - (byte)statBits, - 0,0,0,0, - }; - - dataLen = 4; - - if (col < MOUSE_POS_EXT_START) - data[dataLen++] = (byte)(col + (1 + 0x20)); // column 0 --> send as 1 - else { // encode in UTF-8 - int val = col + 1 + 0x20; - data[dataLen++] = (byte)(0xc0 + (val >> 6)); - data[dataLen++] = (byte)(0x80 + (val & 0x3f)); - } + public override void OnGotFocus() { + _focusReporting.OnGotFocus(); + } - if (row < MOUSE_POS_EXT_START) - data[dataLen++] = (byte)(row + (1 + 0x20)); // row 0 --> send as 1 - else { // encode in UTF-8 - int val = row + (1 + 0x20); - data[dataLen++] = (byte)(0xc0 + (val >> 6)); - data[dataLen++] = (byte)(0x80 + (val & 0x3f)); - } - break; + public override void OnLostFocus() { + _focusReporting.OnLostFocus(); + } - case MouseTrackingProtocol.Urxvt: - data = Encoding.ASCII.GetBytes( - new StringBuilder() - .Append("\x1b[") - .Append(statBits.ToString(NumberFormatInfo.InvariantInfo)) - .Append(';') - .Append((col + 1).ToString(NumberFormatInfo.InvariantInfo)) - .Append(';') - .Append((row + 1).ToString(NumberFormatInfo.InvariantInfo)) - .Append("M") - .ToString()); - dataLen = data.Length; - break; + protected ProcessCharResult ProcessNormalUnicodeChar(UnicodeChar ch) { + //WrapAroundがfalseで、キャレットが右端のときは何もしない + if (!_wrapAroundMode && _manipulator.CaretColumn >= GetDocument().TerminalWidth - 1) + return ProcessCharResult.Processed; - case MouseTrackingProtocol.Sgr: - data = Encoding.ASCII.GetBytes( - new StringBuilder() - .Append("\x1b[<") - .Append(statBits.ToString(NumberFormatInfo.InvariantInfo)) - .Append(';') - .Append((col + 1).ToString(NumberFormatInfo.InvariantInfo)) - .Append(';') - .Append((row + 1).ToString(NumberFormatInfo.InvariantInfo)) - .Append(action == TerminalMouseAction.ButtonUp ? 'm' : 'M') - .ToString()); - dataLen = data.Length; - break; + if (_insertMode) + _manipulator.InsertBlanks(_manipulator.CaretColumn, ch.IsWideWidth ? 2 : 1, _currentdecoration); - default: - return true; // unknown protocol + //既に画面右端にキャレットがあるのに文字が来たら改行をする + int tw = GetDocument().TerminalWidth; + if (_manipulator.CaretColumn + (ch.IsWideWidth ? 2 : 1) > tw) { + _manipulator.EOLType = EOLType.Continue; + GLine lineUpdated = GetDocument().UpdateCurrentLine(_manipulator); + if (lineUpdated != null) { + this.LogService.TextLogger.WriteLine(lineUpdated); + } + GetDocument().LineFeed(); + _manipulator.Load(GetDocument().CurrentLine, 0); } - TransmitDirect(data, 0, dataLen); + //画面のリサイズがあったときは、_manipulatorのバッファサイズが不足の可能性がある + if (tw > _manipulator.BufferSize) + _manipulator.ExpandBuffer(tw); - return true; + //通常文字の処理 + _manipulator.PutChar(ch, _currentdecoration); + + return ProcessCharResult.Processed; } - protected override ProcessCharResult ProcessNormalUnicodeChar(UnicodeChar ch) { - //WrapAroundがfalseで、キャレットが右端のときは何もしない - if (!_wrapAroundMode && _manipulator.CaretColumn >= GetDocument().TerminalWidth - 1) + protected ProcessCharResult ProcessControlChar(char ch) { + if (ch == ControlCode.LF || ch == ControlCode.VT) { //Vertical TabはLFと等しい + LineFeedRule rule = GetTerminalSettings().LineFeedRule; + if (rule == LineFeedRule.Normal) { + DoLineFeed(); + } + else if (rule == LineFeedRule.LFOnly) { + DoCarriageReturn(); + DoLineFeed(); + } return ProcessCharResult.Processed; + } + else if (ch == ControlCode.CR) { + LineFeedRule rule = GetTerminalSettings().LineFeedRule; + if (rule == LineFeedRule.Normal) { + DoCarriageReturn(); + } + else if (rule == LineFeedRule.CROnly) { + DoCarriageReturn(); + DoLineFeed(); + } + return ProcessCharResult.Processed; + } + else if (ch == ControlCode.BEL) { + this.IndicateBell(); + return ProcessCharResult.Processed; + } + else if (ch == ControlCode.BS) { + //行頭で、直前行の末尾が継続であった場合行を戻す + if (_manipulator.CaretColumn == 0) { + TerminalDocument doc = GetDocument(); + int line = doc.CurrentLineNumber - 1; + if (line >= 0 && doc.FindLineOrEdge(line).EOLType == EOLType.Continue) { + doc.InvalidatedRegion.InvalidateLine(doc.CurrentLineNumber); + doc.CurrentLineNumber = line; + if (doc.CurrentLine == null) + _manipulator.Reset(doc.TerminalWidth); + else + _manipulator.Load(doc.CurrentLine, doc.CurrentLine.DisplayLength - 1); //NOTE ここはCharLengthだったが同じだと思って改名した + doc.InvalidatedRegion.InvalidateLine(doc.CurrentLineNumber); + } + } + else + _manipulator.BackCaret(); - if (_insertMode) - _manipulator.InsertBlanks(_manipulator.CaretColumn, ch.IsWideWidth ? 2 : 1, _currentdecoration); - return base.ProcessNormalUnicodeChar(ch); - } - protected override ProcessCharResult ProcessControlChar(char ch) { - return base.ProcessControlChar(ch); - /* 文字コードが誤っているとこのあたりを不意に実行してしまうことがあり、よろしくない。 - switch(ch) { - //単純な変換なら他にもできるが、サポートしているのはいまのところこれしかない - case (char)0x8D: - base.ProcessChar((char)0x1B); - base.ProcessChar('M'); - return ProcessCharResult.Processed; - case (char)0x9B: - base.ProcessChar((char)0x1B); - base.ProcessChar('['); - return ProcessCharResult.Processed; - case (char)0x9D: - base.ProcessChar((char)0x1B); - base.ProcessChar(']'); - return ProcessCharResult.Processed; - default: - return base.ProcessControlChar(ch); + return ProcessCharResult.Processed; + } + else if (ch == ControlCode.HT) { + _manipulator.CaretColumn = GetNextTabStop(_manipulator.CaretColumn); + return ProcessCharResult.Processed; + } + else if (ch == ControlCode.SO) { + return ProcessCharResult.Processed; //以下2つはCharDecoderの中で処理されているはずなので無視 + } + else if (ch == ControlCode.SI) { + return ProcessCharResult.Processed; + } + else if (ch == ControlCode.NUL) { + return ProcessCharResult.Processed; //null charは無視 !!CR NULをCR LFとみなす仕様があるが、CR LF CR NULとくることもあって難しい + } + else { + //Debug.WriteLine("Unknown char " + (int)ch); + //適当なグラフィック表示ほしい + return ProcessCharResult.Unsupported; } - */ } + private void DoLineFeed() { + _manipulator.EOLType = (_manipulator.EOLType == EOLType.CR || _manipulator.EOLType == EOLType.CRLF) ? EOLType.CRLF : EOLType.LF; + GLine lineUpdated = GetDocument().UpdateCurrentLine(_manipulator); + if (lineUpdated != null) { + this.LogService.TextLogger.WriteLine(lineUpdated); + } + GetDocument().LineFeed(); - protected override ProcessCharResult ProcessEscapeSequence(char code, char[] seq, int offset) { - ProcessCharResult v = base.ProcessEscapeSequence(code, seq, offset); - if (v != ProcessCharResult.Unsupported) - return v; + //カラム保持は必要。サンプル:linuxconf.log + int col = _manipulator.CaretColumn; + _manipulator.Load(GetDocument().CurrentLine, col); + } + private void DoCarriageReturn() { + _manipulator.CarriageReturn(); + _manipulator.EOLType = EOLType.CR; // will be changed to CRLF in DoLineFeed() + } + protected ProcessCharResult ProcessEscapeSequence(char code, char[] seq, int offset) { + string param; switch (code) { + case '[': + if (seq.Length - offset - 1 >= 0) { + param = new string(seq, offset, seq.Length - offset - 1); + return ProcessAfterCSI(param, seq[seq.Length - 1]); + } + break; + //throw new UnknownEscapeSequenceException(String.Format("unknown command after CSI {0}", code)); + case ']': + if (seq.Length - offset - 1 >= 0) { + param = new string(seq, offset, seq.Length - offset - 1); + return ProcessAfterOSC(param, seq[seq.Length - 1]); + } + break; + case '=': + ChangeMode(TerminalMode.Application); + return ProcessCharResult.Processed; + case '>': + ChangeMode(TerminalMode.Normal); + return ProcessCharResult.Processed; + case 'E': + ProcessNextLine(); + return ProcessCharResult.Processed; + case 'M': + ReverseIndex(); + return ProcessCharResult.Processed; + case 'D': + Index(); + return ProcessCharResult.Processed; + case '7': + _screenBuffer.SaveCursor(); + return ProcessCharResult.Processed; + case '8': + _screenBuffer.RestoreCursor(); + return ProcessCharResult.Processed; + case 'c': + FullReset(); + return ProcessCharResult.Processed; case 'F': if (seq.Length == offset) { //パラメータなしの場合 ProcessCursorPosition(1, 1); @@ -404,12 +403,88 @@ protected override ProcessCharResult ProcessEscapeSequence(char code, char[] seq return ProcessCharResult.Unsupported; } - protected override ProcessCharResult ProcessAfterCSI(string param, char code) { - ProcessCharResult v = base.ProcessAfterCSI(param, code); - if (v != ProcessCharResult.Unsupported) - return v; + protected override void ChangeMode(TerminalMode mode) { + if (_terminalMode == mode) + return; + + if (mode == TerminalMode.Normal) { + GetDocument().ClearScrollingRegion(); + GetConnection().TerminalOutput.Resize(GetDocument().TerminalWidth, GetDocument().TerminalHeight); //たとえばemacs起動中にリサイズし、シェルへ戻るとシェルは新しいサイズを認識していない + //RMBoxで確認されたことだが、無用に後方にドキュメントを広げてくる奴がいる。カーソルを123回後方へ、など。 + //場当たり的だが、ノーマルモードに戻る際に後ろの空行を削除することで対応する。 + GLine l = GetDocument().LastLine; + while (l != null && l.DisplayLength == 0 && l.ID > GetDocument().CurrentLineNumber) + l = l.PrevLine; + + if (l != null) + l = l.NextLine; + if (l != null) + GetDocument().RemoveAfter(l.ID); + + GetDocument().IsApplicationMode = false; + } + else { + GetDocument().ApplicationModeBackColor = ColorSpec.Default; + GetDocument().SetScrollingRegion(0, GetDocument().TerminalHeight - 1); + GetDocument().IsApplicationMode = true; + } + + GetDocument().InvalidateAll(); + + _terminalMode = mode; + } + + protected ProcessCharResult ProcessAfterCSI(string param, char code) { switch (code) { + case 'c': + ProcessDeviceAttributes(param); + return ProcessCharResult.Processed; + case 'm': //SGR + ProcessSGR(param); + return ProcessCharResult.Processed; + case 'h': + case 'l': + return ProcessDECSETMulti(param, code); + case 'r': + if (param.Length > 0 && param[0] == '?') + return ProcessRestoreDECSET(param.Substring(1), code); + else { + ProcessSetScrollingRegion(param); + return ProcessCharResult.Processed; + } + case 's': + if (param.Length > 0 && param[0] == '?') + return ProcessSaveDECSET(param.Substring(1), code); + else + return ProcessCharResult.Unsupported; + case 'n': + ProcessDeviceStatusReport(param); + return ProcessCharResult.Processed; + case 'A': + case 'B': + case 'C': + case 'D': + case 'E': + case 'F': + ProcessCursorMove(param, code); + return ProcessCharResult.Processed; + case 'H': + case 'f': //fは本当はxterm固有 + ProcessCursorPosition(param); + return ProcessCharResult.Processed; + case 'J': + ProcessEraseInDisplay(param); + return ProcessCharResult.Processed; + case 'K': + ProcessEraseInLine(param); + return ProcessCharResult.Processed; + case 'L': + ProcessInsertLines(param); + return ProcessCharResult.Processed; + case 'M': + ProcessDeleteLines(param); + return ProcessCharResult.Processed; case 'd': ProcessLinePositionAbsolute(param); return ProcessCharResult.Processed; @@ -447,7 +522,7 @@ protected override ProcessCharResult ProcessAfterCSI(string param, char code) { //!!パラメータによって無視してよい場合と、応答を返すべき場合がある。応答の返し方がよくわからないので保留中 return ProcessCharResult.Processed; case 'U': //これはSFUでしか確認できてない - base.ProcessCursorPosition(GetDocument().TerminalHeight, 1); + ProcessCursorPosition(GetDocument().TerminalHeight, 1); return ProcessCharResult.Processed; case 'u': //SFUでのみ確認。特にbは続く文字を繰り返すらしいが、意味のある動作になっているところを見ていない case 'b': @@ -457,22 +532,227 @@ protected override ProcessCharResult ProcessAfterCSI(string param, char code) { } } - protected override void ProcessDeviceAttributes(string param) { + protected void ProcessDeviceAttributes(string param) { if (param.StartsWith(">")) { byte[] data = Encoding.ASCII.GetBytes(" [>82;1;0c"); data[0] = 0x1B; //ESC TransmitDirect(data); } + else { + byte[] data = Encoding.ASCII.GetBytes(" [?1;2c"); //なんかよくわからないがMindTerm等をみるとこれでいいらしい + data[0] = 0x1B; //ESC + TransmitDirect(data); + } + } + + protected void ProcessDeviceStatusReport(string param) { + string response; + if (param == "5") + response = " [0n"; //これでOKの意味らしい + else if (param == "6") + response = String.Format(" [{0};{1}R", GetDocument().CurrentLineNumber - GetDocument().TopLineNumber + 1, _manipulator.CaretColumn + 1); + else + throw new UnknownEscapeSequenceException("DSR " + param); + + byte[] data = Encoding.ASCII.GetBytes(response); + data[0] = 0x1B; //ESC + TransmitDirect(data); + } + + protected void ProcessCursorMove(string param, char method) { + int count = ParseInt(param, 1); //パラメータが省略されたときの移動量は1 + + int column = _manipulator.CaretColumn; + switch (method) { + case 'A': + GetDocument().UpdateCurrentLine(_manipulator); + GetDocument().CurrentLineNumber = (GetDocument().CurrentLineNumber - count); + _manipulator.Load(GetDocument().CurrentLine, column); + break; + case 'B': + GetDocument().UpdateCurrentLine(_manipulator); + GetDocument().CurrentLineNumber = (GetDocument().CurrentLineNumber + count); + _manipulator.Load(GetDocument().CurrentLine, column); + break; + case 'C': { + int newvalue = column + count; + if (newvalue >= GetDocument().TerminalWidth) + newvalue = GetDocument().TerminalWidth - 1; + _manipulator.CaretColumn = newvalue; + } + break; + case 'D': { + int newvalue = column - count; + if (newvalue < 0) + newvalue = 0; + _manipulator.CaretColumn = newvalue; + } + break; + } + } + + //CSI H + protected void ProcessCursorPosition(string param) { + IntPair t = ParseIntPair(param, 1, 1); + int row = t.first, col = t.second; + if (_scrollRegionRelative && GetDocument().ScrollingTop != -1) { + row += GetDocument().ScrollingTop; + } + + if (row < 1) + row = 1; + else if (row > GetDocument().TerminalHeight) + row = GetDocument().TerminalHeight; + if (col < 1) + col = 1; + else if (col > GetDocument().TerminalWidth) + col = GetDocument().TerminalWidth; + ProcessCursorPosition(row, col); + } + protected void ProcessCursorPosition(int row, int col) { + GetDocument().UpdateCurrentLine(_manipulator); + GetDocument().CurrentLineNumber = (GetDocument().TopLineNumber + row - 1); + //int cc = GetDocument().CurrentLine.DisplayPosToCharPos(col-1); + //Debug.Assert(cc>=0); + _manipulator.Load(GetDocument().CurrentLine, col - 1); + } + + //CSI J + protected void ProcessEraseInDisplay(string param) { + int d = ParseInt(param, 0); + + TerminalDocument doc = GetDocument(); + int cur = doc.CurrentLineNumber; + int top = doc.TopLineNumber; + int bottom = top + doc.TerminalHeight; + int col = _manipulator.CaretColumn; + switch (d) { + case 0: //erase below + { + if (col == 0 && cur == top) + goto ERASE_ALL; + + EraseRight(); + doc.UpdateCurrentLine(_manipulator); + doc.EnsureLine(bottom - 1); + doc.RemoveAfter(bottom); + doc.ClearRange(cur + 1, bottom, _currentdecoration); + _manipulator.Load(doc.CurrentLine, col); + } + break; + case 1: //erase above + { + if (col == doc.TerminalWidth - 1 && cur == bottom - 1) + goto ERASE_ALL; + + EraseLeft(); + doc.UpdateCurrentLine(_manipulator); + doc.ClearRange(top, cur, _currentdecoration); + _manipulator.Load(doc.CurrentLine, col); + } + break; + case 2: //erase all + ERASE_ALL: { + GetDocument().ApplicationModeBackColor = + (_currentdecoration != null) ? _currentdecoration.GetBackColorSpec() : ColorSpec.Default; + + doc.UpdateCurrentLine(_manipulator); + //if(_homePositionOnCSIJ2) { //SFUではこうなる + // ProcessCursorPosition(1,1); + // col = 0; + //} + doc.EnsureLine(bottom - 1); + doc.RemoveAfter(bottom); + doc.ClearRange(top, bottom, _currentdecoration); + _manipulator.Load(doc.CurrentLine, col); + } + break; + default: + throw new UnknownEscapeSequenceException(String.Format("unknown ED option {0}", param)); + } + + } + + //CSI K + private void ProcessEraseInLine(string param) { + int d = ParseInt(param, 0); + + switch (d) { + case 0: //erase right + EraseRight(); + break; + case 1: //erase left + EraseLeft(); + break; + case 2: //erase all + EraseLine(); + break; + default: + throw new UnknownEscapeSequenceException(String.Format("unknown EL option {0}", param)); + } + } + + private void EraseRight() { + _manipulator.FillSpace(_manipulator.CaretColumn, _manipulator.BufferSize, _currentdecoration); + } + + private void EraseLeft() { + _manipulator.FillSpace(0, _manipulator.CaretColumn + 1, _currentdecoration); + } + + private void EraseLine() { + _manipulator.FillSpace(0, _manipulator.BufferSize, _currentdecoration); + } + + protected void Index() { + GetDocument().UpdateCurrentLine(_manipulator); + int current = GetDocument().CurrentLineNumber; + if (current == GetDocument().TopLineNumber + GetDocument().TerminalHeight - 1 || current == GetDocument().ScrollingBottom) + GetDocument().ScrollDown(); + else + GetDocument().CurrentLineNumber = current + 1; + _manipulator.Load(GetDocument().CurrentLine, _manipulator.CaretColumn); + } + protected void ReverseIndex() { + GetDocument().UpdateCurrentLine(_manipulator); + int current = GetDocument().CurrentLineNumber; + if (current == GetDocument().TopLineNumber || current == GetDocument().ScrollingTop) + GetDocument().ScrollUp(); else - base.ProcessDeviceAttributes(param); + GetDocument().CurrentLineNumber = current - 1; + _manipulator.Load(GetDocument().CurrentLine, _manipulator.CaretColumn); + } + + protected void ProcessSetScrollingRegion(string param) { + int height = GetDocument().TerminalHeight; + IntPair v = ParseIntPair(param, 1, height); + + if (v.first < 1) + v.first = 1; + else if (v.first > height) + v.first = height; + if (v.second < 1) + v.second = 1; + else if (v.second > height) + v.second = height; + if (v.first > v.second) { //問答無用でエラーが良いようにも思うが + int t = v.first; + v.first = v.second; + v.second = t; + } + + //指定は1-originだが処理は0-origin + GetDocument().SetScrollingRegion(v.first - 1, v.second - 1); } + protected void ProcessNextLine() { + GetDocument().UpdateCurrentLine(_manipulator); + GetDocument().CurrentLineNumber = (GetDocument().CurrentLineNumber + 1); + _manipulator.Load(GetDocument().CurrentLine, 0); + } - protected override ProcessCharResult ProcessAfterOSC(string param, char code) { - ProcessCharResult v = base.ProcessAfterOSC(param, code); - if (v != ProcessCharResult.Unsupported) - return v; + protected ProcessCharResult ProcessAfterOSC(string param, char code) { int semicolon = param.IndexOf(';'); if (semicolon == -1) return ProcessCharResult.Unsupported; @@ -486,7 +766,7 @@ protected override ProcessCharResult ProcessAfterOSC(string param, char code) { if (fmts.Length > 0) { ITerminalSettings settings = GetTerminalSettings(); string title = fmts[0].FormatCaptionUsingWindowTitle(GetConnection().Destination, settings, pt); - _afterExitLockActions.Add(new AfterExitLockDelegate(new CaptionChanger(GetTerminalSettings(), title).Do)); + _afterExitLockActions.Add(new CaptionChanger(GetTerminalSettings(), title).Do); } //Quick Test //_afterExitLockActions.Add(new AfterExitLockDelegate(new CaptionChanger(GetTerminalSettings(), pt).Do)); @@ -623,7 +903,7 @@ protected override ProcessCharResult ProcessAfterOSC(string param, char code) { return ProcessCharResult.Unsupported; } - protected override void ProcessSGR(string param) { + protected void ProcessSGR(string param) { int state = 0, target = 0, r = 0, g = 0, b = 0; string[] ps = param.Split(';'); TextDecoration dec = _currentdecoration; @@ -740,95 +1020,228 @@ protected override void ProcessSGR(string param) { _currentdecoration = dec; } - private TextDecoration SetForeColorByRGB(TextDecoration dec, int r, int g, int b) { - return dec.GetCopyWithForeColor(new ColorSpec(Color.FromArgb(r, g, b))); - } - - private TextDecoration SetBackColorByRGB(TextDecoration dec, int r, int g, int b) { - return dec.GetCopyWithBackColor(new ColorSpec(Color.FromArgb(r, g, b))); + protected int ParseSGRCode(string param) { + if (param.Length == 0) + return 0; + else if (param.Length == 1) + return param[0] - '0'; + else if (param.Length == 2) + return (param[0] - '0') * 10 + (param[1] - '0'); + else if (param.Length == 3) + return (param[0] - '0') * 100 + (param[1] - '0') * 10 + (param[2] - '0'); + else + throw new UnknownEscapeSequenceException(String.Format("unknown SGR parameter {0}", param)); } - protected override ProcessCharResult ProcessDECSET(string param, char code) { - ProcessCharResult v = base.ProcessDECSET(param, code); - if (v != ProcessCharResult.Unsupported) - return v; - bool set = code == 'h'; - - switch (param) { - case "1047": //Alternate Buffer - if (set) { - SwitchBuffer(true); - // XTerm doesn't clear screen. + protected void ProcessSGRParameterANSI(int code, ref TextDecoration dec) { + switch (code) { + case 0: // default rendition (implementation-defined) (ECMA-48,VT100,VT220) + dec = TextDecoration.Default; + break; + case 1: // bold or increased intensity (ECMA-48,VT100,VT220) + dec = dec.GetCopyWithBold(true); + break; + case 2: // faint, decreased intensity or second colour (ECMA-48) + break; + case 3: // italicized (ECMA-48) + break; + case 4: // singly underlined (ECMA-48,VT100,VT220) + dec = dec.GetCopyWithUnderline(true); + break; + case 5: // slowly blinking (ECMA-48,VT100,VT220) + case 6: // rapidly blinking (ECMA-48) + dec = dec.GetCopyWithBlink(true); + break; + case 7: // negative image (ECMA-48,VT100,VT220) + dec = dec.GetCopyWithInverted(true); + break; + case 8: // concealed characters (ECMA-48,VT300) + case 9: // crossed-out (ECMA-48) + case 10: // primary (default) font (ECMA-48) + case 11: // first alternative font (ECMA-48) + case 12: // second alternative font (ECMA-48) + case 13: // third alternative font (ECMA-48) + case 14: // fourth alternative font (ECMA-48) + case 15: // fifth alternative font (ECMA-48) + case 16: // sixth alternative font (ECMA-48) + case 17: // seventh alternative font (ECMA-48) + case 18: // eighth alternative font (ECMA-48) + case 19: // ninth alternative font (ECMA-48) + case 20: // Fraktur (Gothic) (ECMA-48) + case 21: // doubly underlined (ECMA-48) + break; + case 22: // normal colour or normal intensity (neither bold nor faint) (ECMA-48,VT220,VT300) + dec = TextDecoration.Default; + break; + case 23: // not italicized, not fraktur (ECMA-48) + break; + case 24: // not underlined (neither singly nor doubly) (ECMA-48,VT220,VT300) + dec = dec.GetCopyWithUnderline(false); + break; + case 25: // steady (not blinking) (ECMA-48,VT220,VT300) + dec = dec.GetCopyWithBlink(false); + break; + case 26: // reserved (ECMA-48) + break; + case 27: // positive image (ECMA-48,VT220,VT300) + dec = dec.GetCopyWithInverted(false); + break; + case 28: // revealed characters (ECMA-48) + case 29: // not crossed out (ECMA-48) + break; + case 30: // black display (ECMA-48) + case 31: // red display (ECMA-48) + case 32: // green display (ECMA-48) + case 33: // yellow display (ECMA-48) + case 34: // blue display (ECMA-48) + case 35: // magenta display (ECMA-48) + case 36: // cyan display (ECMA-48) + case 37: // white display (ECMA-48) + dec = SelectForeColor(dec, code - 30); + break; + case 38: // reserved (ECMA-48) + break; + case 39: // default display colour (implementation-defined) (ECMA-48) + dec = dec.GetCopyWithForeColor(ColorSpec.Default); + break; + case 40: // black background (ECMA-48) + case 41: // red background (ECMA-48) + case 42: // green background (ECMA-48) + case 43: // yellow background (ECMA-48) + case 44: // blue background (ECMA-48) + case 45: // magenta background (ECMA-48) + case 46: // cyan background (ECMA-48) + case 47: // white background (ECMA-48) + dec = SelectBackgroundColor(dec, code - 40); + break; + case 48: // reserved (ECMA-48) + break; + case 49: // default background colour (implementation-defined) (ECMA-48) + dec = dec.GetCopyWithBackColor(ColorSpec.Default); + break; + case 50: // reserved (ECMA-48) + case 51: // framed (ECMA-48) + case 52: // encircled (ECMA-48) + case 53: // overlined (ECMA-48) + case 54: // not framed, not encircled (ECMA-48) + case 55: // not overlined (ECMA-48) + case 56: // reserved (ECMA-48) + case 57: // reserved (ECMA-48) + case 58: // reserved (ECMA-48) + case 59: // reserved (ECMA-48) + case 60: // ideogram underline or right side line (ECMA-48) + case 61: // ideogram double underline or double line on the right side (ECMA-48) + case 62: // ideogram overline or left side line (ECMA-48) + case 63: // ideogram double overline or double line on the left side (ECMA-48) + case 64: // ideogram stress marking (ECMA-48) + case 65: // cancels the effect of the rendition aspects established by parameter values 60 to 64 (ECMA-48) + break; + default: + // other values are ignored without notification to the user + Debug.WriteLine("unknown SGR code (ANSI) : {0}", code); + break; + } + } + + protected TextDecoration SelectForeColor(TextDecoration dec, int index) { + return dec.GetCopyWithForeColor(new ColorSpec(index)); + } + + protected TextDecoration SelectBackgroundColor(TextDecoration dec, int index) { + return dec.GetCopyWithBackColor(new ColorSpec(index)); + } + + private TextDecoration SetForeColorByRGB(TextDecoration dec, int r, int g, int b) { + return dec.GetCopyWithForeColor(new ColorSpec(Color.FromArgb(r, g, b))); + } + + private TextDecoration SetBackColorByRGB(TextDecoration dec, int r, int g, int b) { + return dec.GetCopyWithBackColor(new ColorSpec(Color.FromArgb(r, g, b))); + } + + protected ProcessCharResult ProcessDECSET(string param, char code) { + switch (param) { + case "25": + return ProcessCharResult.Processed; //!!Show/Hide Cursorだがとりあえず無視 + case "1": + ChangeCursorKeyMode(code == 'h' ? TerminalMode.Application : TerminalMode.Normal); + return ProcessCharResult.Processed; + } + + bool set = code == 'h'; + + switch (param) { + case "1047": //Alternate Buffer + if (set) { + _screenBuffer.SwitchBuffer(true); + // XTerm doesn't clear screen. } else { ClearScreen(); - SwitchBuffer(false); + _screenBuffer.SwitchBuffer(false); } return ProcessCharResult.Processed; case "1048": //Save/Restore Cursor if (set) - SaveCursor(); + _screenBuffer.SaveCursor(); else - RestoreCursor(); + _screenBuffer.RestoreCursor(); return ProcessCharResult.Processed; case "1049": //Save/Restore Cursor and Alternate Buffer if (set) { - SaveCursor(); - SwitchBuffer(true); + _screenBuffer.SaveCursor(); + _screenBuffer.SwitchBuffer(true); ClearScreen(); } else { // XTerm doesn't clear screen for enabling copy/paste from the alternate buffer. // But we need ClearScreen for emulating the buffer-switch. ClearScreen(); - SwitchBuffer(false); - RestoreCursor(); + _screenBuffer.SwitchBuffer(false); + _screenBuffer.RestoreCursor(); } return ProcessCharResult.Processed; case "1000": // DEC VT200 compatible: Send button press and release event with mouse position. - ResetMouseTracking((set) ? MouseTrackingState.Normal : MouseTrackingState.Off); + ResetMouseTracking(set ? + MouseTrackingManager.MouseTrackingState.Normal : + MouseTrackingManager.MouseTrackingState.Off); return ProcessCharResult.Processed; case "1001": // DEC VT200 highlight tracking // Not supported - ResetMouseTracking(MouseTrackingState.Off); + ResetMouseTracking(MouseTrackingManager.MouseTrackingState.Off); return ProcessCharResult.Processed; case "1002": // Button-event tracking: Send button press, release, and drag event. - ResetMouseTracking((set) ? MouseTrackingState.Drag : MouseTrackingState.Off); + ResetMouseTracking(set ? + MouseTrackingManager.MouseTrackingState.Drag : + MouseTrackingManager.MouseTrackingState.Off); return ProcessCharResult.Processed; case "1003": // Any-event tracking: Send button press, release, and motion. - ResetMouseTracking((set) ? MouseTrackingState.Any : MouseTrackingState.Off); + ResetMouseTracking(set ? + MouseTrackingManager.MouseTrackingState.Any : + MouseTrackingManager.MouseTrackingState.Off); return ProcessCharResult.Processed; case "1004": // Send FocusIn/FocusOut events - _focusReportingMode = set; + _focusReporting.SetFocusReportingMode(set); return ProcessCharResult.Processed; case "1005": // Enable UTF8 Mouse Mode - if (set) { - _mouseTrackingProtocol = MouseTrackingProtocol.Utf8; - } - else { - _mouseTrackingProtocol = MouseTrackingProtocol.Normal; - } + SetMouseTrackingProtocol(set ? + MouseTrackingManager.MouseTrackingProtocol.Utf8 : + MouseTrackingManager.MouseTrackingProtocol.Normal); return ProcessCharResult.Processed; case "1006": // Enable SGR Extended Mouse Mode - if (set) { - _mouseTrackingProtocol = MouseTrackingProtocol.Sgr; - } - else { - _mouseTrackingProtocol = MouseTrackingProtocol.Normal; - } + SetMouseTrackingProtocol(set ? + MouseTrackingManager.MouseTrackingProtocol.Sgr : + MouseTrackingManager.MouseTrackingProtocol.Normal); return ProcessCharResult.Processed; case "1015": // Enable UTF8 Extended Mouse Mode - if (set) { - _mouseTrackingProtocol = MouseTrackingProtocol.Urxvt; - } - else { - _mouseTrackingProtocol = MouseTrackingProtocol.Normal; - } + SetMouseTrackingProtocol(set ? + MouseTrackingManager.MouseTrackingProtocol.Urxvt : + MouseTrackingManager.MouseTrackingProtocol.Normal); return ProcessCharResult.Processed; case "1034": // Input 8 bits return ProcessCharResult.Processed; case "2004": // Set/Reset bracketed paste mode - _bracketedPasteMode = set; + _bracketedPasteMode.SetBracketedPasteMode(set); return ProcessCharResult.Processed; case "3": //132 Column Mode return ProcessCharResult.Processed; @@ -847,48 +1260,39 @@ protected override ProcessCharResult ProcessDECSET(string param, char code) { //一応報告あったので。SETMODEの12ならローカルエコーなんだがな return ProcessCharResult.Processed; case "47": - if (set) - SwitchBuffer(true); - else - SwitchBuffer(false); + _screenBuffer.SwitchBuffer(set); return ProcessCharResult.Processed; default: return ProcessCharResult.Unsupported; } } - protected override ProcessCharResult ProcessSaveDECSET(string param, char code) { + protected ProcessCharResult ProcessSaveDECSET(string param, char code) { switch (param) { case "1047": case "47": - _savedMode_isAlternateBuffer = _isAlternateBuffer; + _screenBuffer.SaveBufferMode(); break; } return ProcessCharResult.Processed; } - protected override ProcessCharResult ProcessRestoreDECSET(string param, char code) { + protected ProcessCharResult ProcessRestoreDECSET(string param, char code) { switch (param) { case "1047": case "47": - SwitchBuffer(_savedMode_isAlternateBuffer); + _screenBuffer.RestoreBufferMode(); break; } return ProcessCharResult.Processed; } - private void ResetMouseTracking(MouseTrackingState newState) { - if (newState != MouseTrackingState.Off) { - if (_mouseTrackingState == MouseTrackingState.Off) { - SetDocumentCursor(Cursors.Arrow); - } - } - else { - if (_mouseTrackingState != MouseTrackingState.Off) { - ResetDocumentCursor(); - } - } - _mouseTrackingState = newState; + private void ResetMouseTracking(MouseTrackingManager.MouseTrackingState newState) { + _mouseTracking.SetMouseTrackingState(newState); + } + + private void SetMouseTrackingProtocol(MouseTrackingManager.MouseTrackingProtocol newProtocol) { + _mouseTracking.SetMouseTrackingProtocol(newProtocol); } private void ProcessLinePositionAbsolute(string param) { @@ -1008,7 +1412,7 @@ private void ClearAllTabStop() { _tabStops[i] = false; } } - protected override int GetNextTabStop(int start) { + protected int GetNextTabStop(int start) { EnsureTabStops(Math.Max(start + 1, GetDocument().TerminalWidth)); int index = start + 1; @@ -1032,64 +1436,10 @@ protected int GetPrevTabStop(int start) { return 0; } - protected void SwitchBuffer(bool toAlternate) { - if (_isAlternateBuffer != toAlternate) { - SaveScreen(toAlternate ? 0 : 1); - RestoreScreen(toAlternate ? 1 : 0); - _isAlternateBuffer = toAlternate; - } - } - - private void SaveScreen(int sw) { - List lines = new List(); - GLine l = GetDocument().TopLine; - int m = l.ID + GetDocument().TerminalHeight; - while (l != null && l.ID < m) { - lines.Add(l.Clone()); - l = l.NextLine; - } - _savedScreen[sw] = lines; - } - - private void RestoreScreen(int sw) { - if (_savedScreen[sw] == null) { - ClearScreen(); // emulate new buffer - return; - } - TerminalDocument doc = GetDocument(); - int w = doc.TerminalWidth; - int m = doc.TerminalHeight; - GLine t = doc.TopLine; - foreach (GLine l in _savedScreen[sw]) { - l.ExpandBuffer(w); - if (t == null) - doc.AddLine(l); - else { - doc.Replace(t, l); - t = l.NextLine; - } - if (--m == 0) - break; - } - } - protected void ClearScreen() { ProcessEraseInDisplay("2"); } - protected override void SaveCursor() { - int sw = _isAlternateBuffer ? 1 : 0; - _xtermSavedRow[sw] = GetDocument().CurrentLineNumber - GetDocument().TopLineNumber; - _xtermSavedCol[sw] = _manipulator.CaretColumn; - } - - protected override void RestoreCursor() { - int sw = _isAlternateBuffer ? 1 : 0; - GetDocument().UpdateCurrentLine(_manipulator); - GetDocument().CurrentLineNumber = GetDocument().TopLineNumber + _xtermSavedRow[sw]; - _manipulator.Load(GetDocument().CurrentLine, _xtermSavedCol[sw]); - } - //画面の反転 private void SetReverseVideo(bool reverse) { if (reverse == _reverseVideo) @@ -1115,7 +1465,7 @@ internal override byte[] SequenceKeyData(Keys modifier, Keys key) { byte[] data = ModifyCursorKey(modifier, key); if (data != null) return data; - return base.SequenceKeyData(modifier, key); + return SequenceKeyData2(modifier, key); } else { byte[] r = new byte[4]; @@ -1141,6 +1491,81 @@ internal override byte[] SequenceKeyData(Keys modifier, Keys key) { } } + + private static string[] FUNCTIONKEY_MAP = { + // F1 F2 F3 F4 F5 F6 F7 F8 F9 F10 F11 F12 + "11", "12", "13", "14", "15", "17", "18", "19", "20", "21", "23", "24", + // F13 F14 F15 F16 F17 F18 F19 F20 F21 F22 + "25", "26", "28", "29", "31", "32", "33", "34", "23", "24" }; + //特定のデータを流すタイプ。現在、カーソルキーとファンクションキーが該当する + internal byte[] SequenceKeyData2(Keys modifier, Keys body) { + if ((int)Keys.F1 <= (int)body && (int)body <= (int)Keys.F12) { + byte[] r = new byte[5]; + r[0] = 0x1B; + r[1] = (byte)'['; + int n = (int)body - (int)Keys.F1; + if ((modifier & Keys.Shift) != Keys.None) + n += 10; //shiftは値を10ずらす + char tail; + if (n >= 20) + tail = (modifier & Keys.Control) != Keys.None ? '@' : '$'; + else + tail = (modifier & Keys.Control) != Keys.None ? '^' : '~'; + string f = FUNCTIONKEY_MAP[n]; + r[2] = (byte)f[0]; + r[3] = (byte)f[1]; + r[4] = (byte)tail; + return r; + } + else if (GUtil.IsCursorKey(body)) { + byte[] r = new byte[3]; + r[0] = 0x1B; + if (_cursorKeyMode == TerminalMode.Normal) + r[1] = (byte)'['; + else + r[1] = (byte)'O'; + + switch (body) { + case Keys.Up: + r[2] = (byte)'A'; + break; + case Keys.Down: + r[2] = (byte)'B'; + break; + case Keys.Right: + r[2] = (byte)'C'; + break; + case Keys.Left: + r[2] = (byte)'D'; + break; + default: + throw new ArgumentException("unknown cursor key code", "key"); + } + return r; + } + else { + byte[] r = new byte[4]; + r[0] = 0x1B; + r[1] = (byte)'['; + r[3] = (byte)'~'; + if (body == Keys.Insert) + r[2] = (byte)'1'; + else if (body == Keys.Home) + r[2] = (byte)'2'; + else if (body == Keys.PageUp) + r[2] = (byte)'3'; + else if (body == Keys.Delete) + r[2] = (byte)'4'; + else if (body == Keys.End) + r[2] = (byte)'5'; + else if (body == Keys.PageDown) + r[2] = (byte)'6'; + else + throw new ArgumentException("unknown key " + body.ToString()); + return r; + } + } + private byte[] XtermFunctionKey(Keys modifier, Keys key) { int m = 1; if ((modifier & Keys.Shift) != Keys.None) { @@ -1258,6 +1683,150 @@ public override void FullReset() { base.FullReset(); } + private ProcessCharResult ProcessDECSETMulti(string param, char code) { + if (param.Length == 0) + return ProcessCharResult.Processed; + bool question = param[0] == '?'; + string[] ps = question ? param.Substring(1).Split(';') : param.Split(';'); + bool unsupported = false; + foreach (string p in ps) { + ProcessCharResult r = question ? ProcessDECSET(p, code) : ProcessSetMode(p, code); + if (r == ProcessCharResult.Unsupported) + unsupported = true; + } + return unsupported ? ProcessCharResult.Unsupported : ProcessCharResult.Processed; + } + + protected virtual ProcessCharResult ProcessSetMode(string param, char code) { + bool set = code == 'h'; + switch (param) { + case "4": + _insertMode = set; //hで始まってlで終わる + return ProcessCharResult.Processed; + case "12": //local echo + _afterExitLockActions.Add(new LocalEchoChanger(GetTerminalSettings(), !set).Do); + return ProcessCharResult.Processed; + case "20": + return ProcessCharResult.Processed; //!!WinXPのTelnetで確認した + case "25": + return ProcessCharResult.Processed; + case "34": //MakeCursorBig, puttyにはある + //!setでカーソルを強制的に箱型にし、setで通常に戻すというのが正しい動作だが実害はないので無視 + return ProcessCharResult.Processed; + default: + return ProcessCharResult.Unsupported; + } + } + + private class LocalEchoChanger { + private ITerminalSettings _settings; + private bool _value; + public LocalEchoChanger(ITerminalSettings settings, bool value) { + _settings = settings; + _value = value; + } + public void Do() { + _settings.BeginUpdate(); + _settings.LocalEcho = _value; + _settings.EndUpdate(); + } + } + + //これを送ってくるアプリケーションは viで上方スクロール + protected void ProcessInsertLines(string param) { + int d = ParseInt(param, 1); + + TerminalDocument doc = GetDocument(); + int caret_pos = _manipulator.CaretColumn; + int offset = doc.CurrentLineNumber - doc.TopLineNumber; + doc.UpdateCurrentLine(_manipulator); + if (doc.ScrollingBottom == -1) + doc.SetScrollingRegion(0, GetDocument().TerminalHeight - 1); + + for (int i = 0; i < d; i++) { + doc.ScrollUp(doc.CurrentLineNumber, doc.ScrollingBottom); + doc.CurrentLineNumber = doc.TopLineNumber + offset; + } + _manipulator.Load(doc.CurrentLine, caret_pos); + } + + //これを送ってくるアプリケーションは viで下方スクロール + protected void ProcessDeleteLines(string param) { + int d = ParseInt(param, 1); + + /* + TerminalDocument doc = GetDocument(); + _manipulator.Clear(GetConnection().TerminalWidth); + GLine target = doc.CurrentLine; + for(int i=0; i 0) + return Int32.Parse(param); + else + return default_value; + } + catch (Exception ex) { + throw new UnknownEscapeSequenceException(String.Format("bad number format [{0}] : {1}", param, ex.Message)); + } + } + + protected static IntPair ParseIntPair(string param, int default_first, int default_second) { + IntPair ret = new IntPair(default_first, default_second); + + string[] s = param.Split(';'); + + if (s.Length >= 1 && s[0].Length > 0) { + try { + ret.first = Int32.Parse(s[0]); + } + catch (Exception ex) { + throw new UnknownEscapeSequenceException(String.Format("bad number format [{0}] : {1}", s[0], ex.Message)); + } + } + + if (s.Length >= 2 && s[1].Length > 0) { + try { + ret.second = Int32.Parse(s[1]); + } + catch (Exception ex) { + throw new UnknownEscapeSequenceException(String.Format("bad number format [{0}] : {1}", s[1], ex.Message)); + } + } + + return ret; + } + + //ModalTaskのセットを見る + public override void StartModalTerminalTask(IModalTerminalTask task) { + base.StartModalTerminalTask(task); + _currentCharacterTask = (IModalCharacterTask)task.GetAdapter(typeof(IModalCharacterTask)); + } + public override void EndModalTerminalTask() { + base.EndModalTerminalTask(); + _currentCharacterTask = null; + } + //動的変更用 private class CaptionChanger { private ITerminalSettings _settings; @@ -1272,6 +1841,512 @@ public void Do() { _settings.EndUpdate(); } } + + #region ScreenBufferManager + + /// + /// Management of the screen buffer emulation. + /// + private class ScreenBufferManager { + + private readonly XTerm _term; + + private readonly List[] _savedScreen = new List[2]; // { main, alternate } + private bool _isAlternateBuffer = false; + private bool _saved_isAlternateBuffer = false; + private readonly int[] _xtermSavedRow = new int[2]; // { main, alternate } + private readonly int[] _xtermSavedCol = new int[2]; // { main, alternate } + + public ScreenBufferManager(XTerm term) { + _term = term; + } + + /// + /// Saves current buffer mode. + /// + public void SaveBufferMode() { + _saved_isAlternateBuffer = _isAlternateBuffer; + } + + /// + /// Restores buffer mode. + /// + public void RestoreBufferMode() { + SwitchBuffer(_saved_isAlternateBuffer); + } + + /// + /// Switches buffer. + /// + /// true if the alternate buffer is to be switched to. + public void SwitchBuffer(bool toAlternate) { + if (_isAlternateBuffer != toAlternate) { + SaveScreen(toAlternate ? 0 : 1); + RestoreScreen(toAlternate ? 1 : 0); + _isAlternateBuffer = toAlternate; + } + } + + /// + /// Saves current cursor position. + /// + public void SaveCursor() { + int sw = _isAlternateBuffer ? 1 : 0; + TerminalDocument doc = _term.GetDocument(); + _xtermSavedRow[sw] = doc.CurrentLineNumber - doc.TopLineNumber; + _xtermSavedCol[sw] = _term._manipulator.CaretColumn; + } + + /// + /// Restores cursor position. + /// + public void RestoreCursor() { + int sw = _isAlternateBuffer ? 1 : 0; + TerminalDocument doc = _term.GetDocument(); + doc.UpdateCurrentLine(_term._manipulator); + doc.CurrentLineNumber = doc.TopLineNumber + _xtermSavedRow[sw]; + _term._manipulator.Load(doc.CurrentLine, _xtermSavedCol[sw]); + } + + private void SaveScreen(int sw) { + List lines = new List(); + TerminalDocument doc = _term.GetDocument(); + GLine l = doc.TopLine; + int m = l.ID + doc.TerminalHeight; + while (l != null && l.ID < m) { + lines.Add(l.Clone()); + l = l.NextLine; + } + _savedScreen[sw] = lines; + } + + private void RestoreScreen(int sw) { + if (_savedScreen[sw] == null) { + _term.ClearScreen(); // emulate new buffer + return; + } + TerminalDocument doc = _term.GetDocument(); + int w = doc.TerminalWidth; + int m = doc.TerminalHeight; + GLine t = doc.TopLine; + foreach (GLine l in _savedScreen[sw]) { + l.ExpandBuffer(w); + if (t == null) + doc.AddLine(l); + else { + doc.Replace(t, l); + t = l.NextLine; + } + if (--m == 0) + break; + } + } + } + + #endregion + + #region MouseTrackingManager + + /// + /// Management of the mouse tracking. + /// + private class MouseTrackingManager { + + public enum MouseTrackingState { + Off, + Normal, + Drag, + Any, + } + + public enum MouseTrackingProtocol { + Normal, + Utf8, + Urxvt, + Sgr, + } + + private readonly XTerm _term; + + private MouseTrackingState _mouseTrackingState = MouseTrackingState.Off; + private MouseTrackingProtocol _mouseTrackingProtocol = MouseTrackingProtocol.Normal; + private int _prevMouseRow = -1; + private int _prevMouseCol = -1; + private MouseButtons _mouseButton = MouseButtons.None; + + private const int MOUSE_POS_LIMIT = 255 - 32; // mouse position limit + private const int MOUSE_POS_EXT_LIMIT = 2047 - 32; // mouse position limit in extended mode + private const int MOUSE_POS_EXT_START = 127 - 32; // mouse position to start using extended format + + public MouseTrackingManager(XTerm term) { + _term = term; + } + + /// + /// Set mouse tracking state. + /// + /// new state + public void SetMouseTrackingState(MouseTrackingState newState) { + if (_mouseTrackingState == newState) { + return; + } + + _mouseTrackingState = newState; + + if (newState == MouseTrackingManager.MouseTrackingState.Off) { + _term.ResetDocumentCursor(); + } + else { + _term.SetDocumentCursor(Cursors.Arrow); + } + } + + /// + /// Set mouse tracking protocol. + /// + /// new protocol + public void SetMouseTrackingProtocol(MouseTrackingProtocol newProtocol) { + _mouseTrackingProtocol = newProtocol; + } + + /// + /// Hande mouse action. + /// + /// Action type + /// Which mouse button caused the event + /// Modifier keys (Shift, Ctrl or Alt) being pressed + /// Row index (zero based) + /// Column index (zero based) + public bool ProcessMouse(TerminalMouseAction action, MouseButtons button, Keys modKeys, int row, int col) { + + MouseTrackingState currentState = _mouseTrackingState; // copy value because _mouseTrackingState may be changed in non-UI thread. + + if (currentState == MouseTrackingState.Off) { + _prevMouseRow = -1; + _prevMouseCol = -1; + switch (action) { + case TerminalMouseAction.ButtonUp: + case TerminalMouseAction.ButtonDown: + _mouseButton = MouseButtons.None; + break; + } + return false; + } + + // Note: from here, mouse event is consumed even if nothing has been processed actually. + + MouseTrackingProtocol protocol = _mouseTrackingProtocol; // copy value because _mouseTrackingProtocol may be changed in non-UI thread. + + int posLimit = protocol == MouseTrackingProtocol.Normal ? MOUSE_POS_LIMIT : MOUSE_POS_EXT_LIMIT; + + if (row < 0) + row = 0; + else if (row > posLimit) + row = posLimit; + + if (col < 0) + col = 0; + else if (col > posLimit) + col = posLimit; + + int statBits; + switch (action) { + case TerminalMouseAction.ButtonDown: + if (_mouseButton != MouseButtons.None) { + return true; // another button is already pressed + } + + switch (button) { + case MouseButtons.Left: + statBits = 0x00; + break; + case MouseButtons.Middle: + statBits = 0x01; + break; + case MouseButtons.Right: + statBits = 0x02; + break; + default: + return true; // unsupported button + } + + _mouseButton = button; + break; + + case TerminalMouseAction.ButtonUp: + if (button != _mouseButton) { + return true; // ignore + } + + if (protocol == MouseTrackingProtocol.Sgr) { + switch (button) { + case MouseButtons.Left: + statBits = 0x00; + break; + case MouseButtons.Middle: + statBits = 0x01; + break; + case MouseButtons.Right: + statBits = 0x02; + break; + default: + return true; // unsupported button + } + } + else { + statBits = 0x03; + } + + _mouseButton = MouseButtons.None; + break; + + case TerminalMouseAction.WheelUp: + statBits = 0x40; + break; + + case TerminalMouseAction.WheelDown: + statBits = 0x41; + break; + + case TerminalMouseAction.MouseMove: + if (currentState != MouseTrackingState.Any && currentState != MouseTrackingState.Drag) { + return true; // no need to send + } + + if (currentState == MouseTrackingState.Drag && _mouseButton == MouseButtons.None) { + return true; // no need to send + } + + if (row == _prevMouseRow && col == _prevMouseCol) { + return true; // no need to send + } + + switch (_mouseButton) { + case MouseButtons.Left: + statBits = 0x20; + break; + case MouseButtons.Middle: + statBits = 0x21; + break; + case MouseButtons.Right: + statBits = 0x22; + break; + default: + statBits = 0x20; + break; + } + break; + + default: + return true; // unknown action + } + + if ((modKeys & Keys.Shift) != Keys.None) { + statBits |= 0x04; + } + + if ((modKeys & Keys.Alt) != Keys.None) { + statBits |= 0x08; // Meta key + } + + if ((modKeys & Keys.Control) != Keys.None) { + statBits |= 0x10; + } + + if (protocol != MouseTrackingProtocol.Sgr) { + statBits += 0x20; + } + + _prevMouseRow = row; + _prevMouseCol = col; + + byte[] data; + int dataLen; + + switch (protocol) { + + case MouseTrackingProtocol.Normal: { + data = new byte[] { + (byte)27, // ESCAPE + (byte)91, // [ + (byte)77, // M + (byte)statBits, + (col == posLimit) ? + (byte)0 : // emulate xterm's bug + (byte)(col + (1 + 0x20)), // column 0 --> send as 1 + (row == posLimit) ? + (byte)0 : // emulate xterm's bug + (byte)(row + (1 + 0x20)), // row 0 --> send as 1 + }; + dataLen = data.Length; + } + break; + + case MouseTrackingProtocol.Utf8: { + data = new byte[8] { + (byte)27, // ESCAPE + (byte)91, // [ + (byte)77, // M + (byte)statBits, + 0,0,0,0, + }; + + dataLen = 4; + + if (col < MOUSE_POS_EXT_START) { + data[dataLen++] = (byte)(col + (1 + 0x20)); // column 0 --> send as 1 + } + else { // encode in UTF-8 + int val = col + 1 + 0x20; + data[dataLen++] = (byte)(0xc0 + (val >> 6)); + data[dataLen++] = (byte)(0x80 + (val & 0x3f)); + } + + if (row < MOUSE_POS_EXT_START) { + data[dataLen++] = (byte)(row + (1 + 0x20)); // row 0 --> send as 1 + } + else { // encode in UTF-8 + int val = row + (1 + 0x20); + data[dataLen++] = (byte)(0xc0 + (val >> 6)); + data[dataLen++] = (byte)(0x80 + (val & 0x3f)); + } + } + break; + + case MouseTrackingProtocol.Urxvt: { + data = Encoding.ASCII.GetBytes( + new StringBuilder() + .Append("\x1b[") + .Append(statBits.ToString(NumberFormatInfo.InvariantInfo)) + .Append(';') + .Append((col + 1).ToString(NumberFormatInfo.InvariantInfo)) + .Append(';') + .Append((row + 1).ToString(NumberFormatInfo.InvariantInfo)) + .Append("M") + .ToString()); + dataLen = data.Length; + } + break; + + case MouseTrackingProtocol.Sgr: { + data = Encoding.ASCII.GetBytes( + new StringBuilder() + .Append("\x1b[<") + .Append(statBits.ToString(NumberFormatInfo.InvariantInfo)) + .Append(';') + .Append((col + 1).ToString(NumberFormatInfo.InvariantInfo)) + .Append(';') + .Append((row + 1).ToString(NumberFormatInfo.InvariantInfo)) + .Append(action == TerminalMouseAction.ButtonUp ? 'm' : 'M') + .ToString()); + dataLen = data.Length; + } + break; + + default: + return true; // unknown protocol + } + + _term.TransmitDirect(data, 0, dataLen); + + return true; + } + } + + #endregion + + #region FocusReportingManager + + /// + /// Management of the focus reporting. + /// + private class FocusReportingManager { + + private readonly XTerm _term; + + private bool _focusReportingMode = false; + + private readonly byte[] _gotFocusBytes = new byte[] { 0x1b, 0x5b, 0x49 }; + private readonly byte[] _lostFocusBytes = new byte[] { 0x1b, 0x5b, 0x4f }; + + public FocusReportingManager(XTerm term) { + _term = term; + } + + /// + /// Sets focus reporting mode. + /// + /// true if the focus reporting is enabled. + public void SetFocusReportingMode(bool sw) { + _focusReportingMode = sw; + } + + /// + /// Handles got-focus event. + /// + public void OnGotFocus() { + if (_focusReportingMode) { + _term.TransmitDirect(_gotFocusBytes, 0, _gotFocusBytes.Length); + } + } + + /// + /// Handles lost-focus event. + /// + public void OnLostFocus() { + if (_focusReportingMode) { + _term.TransmitDirect(_lostFocusBytes, 0, _lostFocusBytes.Length); + } + } + } + + #endregion + + #region BracketedPasteModeManager + + /// + /// Management of the bracketed paste mode. + /// + private class BracketedPasteModeManager { + + private readonly XTerm _term; + + private bool _bracketedPasteMode = false; + + private readonly byte[] _bracketedPasteModeLeadingBytes = new byte[] { 0x1b, (byte)'[', (byte)'2', (byte)'0', (byte)'0', (byte)'~' }; + private readonly byte[] _bracketedPasteModeTrailingBytes = new byte[] { 0x1b, (byte)'[', (byte)'2', (byte)'0', (byte)'1', (byte)'~' }; + private readonly byte[] _bracketedPasteModeEmptyBytes = new byte[0]; + + public BracketedPasteModeManager(XTerm term) { + _term = term; + } + + /// + /// Sets the bracketed paste mode. + /// + /// true if the bracketed paste mode is enabled. + public void SetBracketedPasteMode(bool sw) { + _bracketedPasteMode = sw; + } + + /// + /// Gets leading bytes for the pasted data. + /// + /// leading bytes + public byte[] GetPasteLeadingBytes() { + return _bracketedPasteMode ? _bracketedPasteModeLeadingBytes : _bracketedPasteModeEmptyBytes; + } + + /// + /// Gets trailing bytes for the pasted data. + /// + /// trailing bytes + public byte[] GetPasteTrailingBytes() { + return _bracketedPasteMode ? _bracketedPasteModeTrailingBytes : _bracketedPasteModeEmptyBytes; + } + } + + #endregion } /// diff --git a/TerminalEmulatorTest/EscapeSequenceEngine/DfaTest.cs b/TerminalEmulatorTest/EscapeSequenceEngine/DfaTest.cs new file mode 100644 index 00000000..766afe0a --- /dev/null +++ b/TerminalEmulatorTest/EscapeSequenceEngine/DfaTest.cs @@ -0,0 +1,873 @@ +// Copyright 2019 The Poderosa Project. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#if UNITTEST +using NUnit.Framework; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Poderosa.Terminal.EscapeSequenceEngine { + /// + /// DFA test + /// + [TestFixture] + class EscapeSequenceDFATest { + private readonly string[] XTERM_ESCAPE_SEQUENCE_PATTERNS = new string[] { + @"{ESC}{SP}F", + @"{ESC}{SP}G", + @"{ESC}{SP}L", + @"{ESC}{SP}M", + @"{ESC}{SP}N", + @"{ESC}#3", + @"{ESC}#4", + @"{ESC}#5", + @"{ESC}#6", + @"{ESC}#8", + @"{ESC}%@", + @"{ESC}%G", + @"{ESC}[()*+][AB4C5RfQ9KY6ZH7=0<>]", + @"{ESC}[()*+]"">", + @"{ESC}[()*+]%=", + @"{ESC}[()*+]`,E", + @"{ESC}[()*+]%6", + @"{ESC}[()*+]%2", + @"{ESC}[()*+]%5", + @"{ESC}[()*+]""?", + @"{ESC}[()*+]""4", + @"{ESC}[()*+]%0", + @"{ESC}[()*+]&4", + @"{ESC}[()*+]&5", + @"{ESC}[()*+]%3", + @"{ESC}[-\./][AFHLM]", + @"{ESC}6", + @"{ESC}7", + @"{ESC}8", + @"{ESC}9", + @"{ESC}=", + @"{ESC}>", + @"{ESC}F", + @"{ESC}c", + @"{ESC}l", + @"{ESC}m", + @"{ESC}n", + @"{ESC}o", + @"{ESC}|", + @"{ESC}\}", + @"{ESC}~", + @"{APC}{Pt}{ST}", + @"{DCS}{P2}|{Pt}{ST}", + @"{DCS}$q{Pt}{ST}", + @"{DCS}{P1}$t{Pt}{ST}", + @"{DCS}+p{Pt}{ST}", + @"{DCS}+q{Pt}{ST}", + @"{CSI}{P1}@", + @"{CSI}{P1}{SP}@", + @"{CSI}{P1}A", + @"{CSI}{P1}{SP}A", + @"{CSI}{P1}B", + @"{CSI}{P1}C", + @"{CSI}{P1}D", + @"{CSI}{P1}E", + @"{CSI}{P1}F", + @"{CSI}{P1}G", + @"{CSI}{P2}H", + @"{CSI}{P1}I", + @"{CSI}{P1}J", + @"{CSI}?{P1}J", + @"{CSI}{P1}K", + @"{CSI}?{P1}K", + @"{CSI}{P1}L", + @"{CSI}{P1}M", + @"{CSI}{P1}P", + @"{CSI}{P1}S", + @"{CSI}?{P3}S", + @"{CSI}{P1}T", + @"{CSI}{P5}T", + @"{CSI}>{P2}T", + @"{CSI}{P1}X", + @"{CSI}{P1}Z", + @"{CSI}{P1}^", + @"{CSI}{P*}`", + @"{CSI}{P*}a", + @"{CSI}{P1}b", + @"{CSI}{P1}c", + @"{CSI}={P1}c", + @"{CSI}>{P1}c", + @"{CSI}{P*}d", + @"{CSI}{P*}e", + @"{CSI}{P2}f", + @"{CSI}{P1}g", + @"{CSI}{P*}h", + @"{CSI}?{P*}h", + @"{CSI}{P*}i", + @"{CSI}?{P*}i", + @"{CSI}{P*}l", + @"{CSI}?{P*}l", + @"{CSI}{P*}m", + @"{CSI}>{P2}m", + @"{CSI}{P1}n", + @"{CSI}>{P1}n", + @"{CSI}?{P1}n", + @"{CSI}>{P1}p", + @"{CSI}!p", + @"{CSI}{P2}""p", + @"{CSI}{P1}$p", + @"{CSI}?{P1}$p", + @"{CSI}{P1}q", + @"{CSI}{P1}{SP}q", + @"{CSI}{P1}""q", + @"{CSI}{P2}r", + @"{CSI}?{P*}r", + @"{CSI}{P5}$r", + @"{CSI}s", + @"{CSI}{P2}s", + @"{CSI}?{P*}s", + @"{CSI}{P3}t", + @"{CSI}>{P2}t", + @"{CSI}{P1}{SP}t", + @"{CSI}{P5}$t", + @"{CSI}u", + @"{CSI}{P1}{SP}u", + @"{CSI}{P8}$v", + @"{CSI}{P1}$w", + @"{CSI}{P4}'w", + @"{CSI}{P1}x", + @"{CSI}{P1}*x", + @"{CSI}{P5}$x", + @"{CSI}{P1}#y", + @"{CSI}{P6}*y", + @"{CSI}{P2}'z", + @"{CSI}{P4}$z", + @"{CSI}{P*}'\{", + @"{CSI}#\{", + @"{CSI}{P2}#\{", + @"{CSI}{P4}$\{", + @"{CSI}{P4}#|", + @"{CSI}{P1}$|", + @"{CSI}{P1}'|", + @"{CSI}{P1}*|", + @"{CSI}#\}", + @"{CSI}{P*}'\}", + @"{CSI}{P*}'~", + @"{OSC}{P1};{Pt}{BEL}", + @"{OSC}{P1};{Pt}{ST}", + @"{PM}{Pt}{ST}", + @"{SOS}{Ps}{ST}", + }; + + [Test] + public void TestDfa() { + NfaManager nfa = new NfaManager(); + + EscapeSequenceContext lastCompletedContext = null; + + foreach (string pattern in XTERM_ESCAPE_SEQUENCE_PATTERNS) { + nfa.AddPattern(pattern, context => { + lastCompletedContext = context; + }); + } + + var sw = Stopwatch.StartNew(); + var dfaStateManager = nfa.CreateDfa(); + var elapsed = sw.ElapsedMilliseconds; + Console.WriteLine("CreateDfa : {0} ms", elapsed); + + DfaEngine dfa = new DfaEngine(dfaStateManager, new DummyEscapeSequenceExecutor()); + + byte[] origBytes = new byte[1]; + + foreach (string pattern in XTERM_ESCAPE_SEQUENCE_PATTERNS) { + foreach (DfaTestData testData in GenerateTestPatterns(pattern)) { +#if false + Debug.WriteLine("\"{0}\" => [{1}]", + pattern, + String.Join(" ", + testData.Bytes.Select(b => + (b >= 0x21 && b <= 0x7e) ? Char.ToString((Char)b) : ("<" + b.ToString("x2") + ">")))); +#endif + lastCompletedContext = null; + int index = 0; + try { + for (index = 0; index < testData.Bytes.Length; index++) { + byte b = testData.Bytes[index]; + origBytes[0] = b; + bool accepted = dfa.Process(b, origBytes); + + if (testData.IsFailurePattern && index == testData.Bytes.Length - 1) { + Assert.AreEqual(false, accepted); + } + else { + Assert.AreEqual(true, accepted); + } + + if (index < testData.Bytes.Length - 1 || testData.IsFailurePattern) { + Assert.IsNull(lastCompletedContext); + } + else { + Assert.AreEqual(pattern, lastCompletedContext.Pattern); + } + } + + if (!testData.IsFailurePattern) { + CollectionAssert.AreEqual( + testData.ExpectedNumericalParams, + lastCompletedContext.NumericalParams.Select(n => n.Value).ToArray()); + + Assert.AreEqual( + testData.ExpectedTextParam, + (lastCompletedContext.TextParam != null) ? lastCompletedContext.TextParam.Value : null); + } + } + catch (Exception e) { + Console.Out.WriteLine("pattern = \"{0}\"", pattern); + Console.Out.WriteLine("input = [{0}]", + String.Concat( + testData.Bytes.Select(b => + (b >= 0x21 && b <= 0x7e) ? Char.ToString((Char)b) : ("\\x" + b.ToString("x2"))))); + Console.Out.WriteLine("index = {0}", index); + Console.Out.WriteLine(e.StackTrace); + throw; + } + } + } + } + + /// + /// Test case data + /// + /// + /// Immutable object which retains conditions of a test case. + /// + private class DfaTestData { + /// + /// Bytes to input + /// + public readonly byte[] Bytes; + + /// + /// Whether this data must fail + /// + public readonly bool IsFailurePattern; + + /// + /// Expected numerical parameters + /// + public readonly uint?[] ExpectedNumericalParams; + + /// + /// Expected text parameter + /// + public readonly string ExpectedTextParam; + + public DfaTestData() + : this(new byte[0], false, new uint?[0], null) { + } + + private DfaTestData(byte[] bytes, bool isFailurePattern, uint?[] expectedNumericalParams, string expectedTextParam) { + this.Bytes = bytes; + this.IsFailurePattern = isFailurePattern; + this.ExpectedNumericalParams = expectedNumericalParams; + this.ExpectedTextParam = expectedTextParam; + } + + public DfaTestData AppendBytes(params byte[] bytes) { + return new DfaTestData( + ConcatArray(this.Bytes, bytes), + this.IsFailurePattern, + this.ExpectedNumericalParams, + this.ExpectedTextParam); + } + + public DfaTestData AppendBytesForFailure(params byte[] bytes) { + return new DfaTestData( + ConcatArray(this.Bytes, bytes), + true, + this.ExpectedNumericalParams, + this.ExpectedTextParam); + } + + public DfaTestData AppendNumericalParams(uint?[] paramValues, params byte[] bytes) { + return new DfaTestData( + ConcatArray(this.Bytes, bytes), + this.IsFailurePattern, + ConcatArray(this.ExpectedNumericalParams, paramValues), + this.ExpectedTextParam); + } + + public DfaTestData AppendTextParam(string paramValue, params byte[] bytes) { + return new DfaTestData( + ConcatArray(this.Bytes, bytes), + this.IsFailurePattern, + this.ExpectedNumericalParams, + paramValue); + } + + private T[] ConcatArray(T[] preceding, params T[] newItems) { + T[] newArray = new T[preceding.Length + newItems.Length]; + preceding.CopyTo(newArray, 0); + newItems.CopyTo(newArray, preceding.Length); + return newArray; + } + } + + private IEnumerable GenerateTestPatterns(string pattern) { + // Build linked generators. + // Each generator generates various test pattern for the pattern-element, + // and calls the next generator for each generated pattern. + // As a result, the first generator returns all combination of generated patterns. + + Func> generator = d => new DfaTestData[] { d }; // the last generator + + foreach (IPatternElement elem in new PatternParser().Parse(pattern).Reverse()) { + if (elem is CharacterSet) { + CharacterSet characterSet = elem as CharacterSet; + Func> next = generator; + generator = d => GenerateTestPatterns(d, characterSet, next); + } + else if (elem is ZeroOrMoreNumericalParams) { + ZeroOrMoreNumericalParams mparams = elem as ZeroOrMoreNumericalParams; + Func> next = generator; + generator = d => GenerateTestPatterns(d, mparams, next); + } + else if (elem is NNumericalParams) { + NNumericalParams nparams = elem as NNumericalParams; + Func> next = generator; + generator = d => GenerateTestPatterns(d, nparams, next); + } + else if (elem is TextParam) { + TextParam tparam = elem as TextParam; + Func> next = generator; + generator = d => GenerateTestPatterns(d, tparam, next); + } + else if (elem is AnyCharString) { + AnyCharString anyCharStr = elem as AnyCharString; + Func> next = generator; + generator = d => GenerateTestPatterns(d, anyCharStr, next); + } + else { + throw new Exception("unknown element type"); + } + } + + return generator(new DfaTestData()); + } + + /// + /// Generator for + /// + /// + /// + /// + /// + private IEnumerable GenerateTestPatterns( + DfaTestData testData, CharacterSet characterSet, Func> next) { + foreach (byte b in characterSet.Characters) { + var newTestData = testData.AppendBytes(b); + foreach (var r in next(newTestData)) { + yield return r; + } + } + + // failure cases + if (characterSet.Characters.Length == 1 && characterSet.Characters[0] == 0x9c /*ST*/) { + yield return testData.AppendBytesForFailure(0x98/*SOS*/); + } + else { + yield return testData.AppendBytesForFailure(0x00); + yield return testData.AppendBytesForFailure(0xff); + } + } + + private readonly string[] validNumericalParamValues = new string[] { + "", + "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", + "100", "101", "102", "103", "104", "105", "106", "107", "108", "109", + "000", "001", "002", "003", "004", "005", "006", "007", "008", "009", + }; + + /// + /// Generator for + /// + private IEnumerable GenerateTestPatterns( + DfaTestData testData, ZeroOrMoreNumericalParams p, Func> next) { + for (int testParamNum = 1; testParamNum < 10; testParamNum++) { + // generates parameter strings like: + // ;1;1 + // 1;;1 + // 1;1; + foreach (string paramValue in validNumericalParamValues) { + uint? paramIntValue = (paramValue.Length == 0) ? (uint?)null : UInt32.Parse(paramValue); + + for (int i = 0; i < testParamNum; i++) { + string paramStr = + String.Join(";", Enumerable.Range(0, testParamNum).Select(index => (index == i) ? paramValue : "1")); + + uint?[] expectedParamValues = + Enumerable.Range(0, testParamNum).Select(index => (index == i) ? paramIntValue : 1u).ToArray(); + + if (!expectedParamValues[expectedParamValues.Length - 1].HasValue) { + // last parameter will not be stored + expectedParamValues = expectedParamValues.Take(expectedParamValues.Length - 1).ToArray(); + } + + var newTestData = testData.AppendNumericalParams(expectedParamValues, Encoding.ASCII.GetBytes(paramStr)); + foreach (var r in next(newTestData)) { + yield return r; + } + } + } + + // generates ";;;...;;;" (all parameters are empty) + { + string paramStr = String.Concat(Enumerable.Repeat(";", testParamNum - 1)); + + // last parameter will not be stored + uint?[] expectedParamValues = Enumerable.Repeat((uint?)null, testParamNum - 1).ToArray(); + + var newTestData = testData.AppendNumericalParams(expectedParamValues, Encoding.ASCII.GetBytes(paramStr)); + foreach (var r in next(newTestData)) { + yield return r; + } + } + } + + // failure cases + for (int testParamNum = 1; testParamNum < 10; testParamNum++) { + string paramStr; + + paramStr = String.Concat(Enumerable.Repeat("1;", testParamNum - 1)) + "\u0000"; + yield return testData.AppendBytesForFailure(Encoding.ASCII.GetBytes(paramStr)); + + paramStr = String.Concat(Enumerable.Repeat("1;", testParamNum - 1)) + "1\u0000"; + yield return testData.AppendBytesForFailure(Encoding.ASCII.GetBytes(paramStr)); + } + } + + /// + /// Generator for + /// + private IEnumerable GenerateTestPatterns( + DfaTestData testData, NNumericalParams p, Func> next) { + foreach (int testParamNum in new int[] { p.Number, p.Number - 1 /* the last parameter was omitted */ }) { + if (testParamNum < 1) { + continue; + } + + // generates parameter strings like: + // ;1;1 + // 1;;1 + // 1;1; + foreach (string paramValue in validNumericalParamValues) { + if (paramValue.Length == 0 && testParamNum == p.Number - 1) { + // not include the case that the last two parameters were omitted + continue; + } + + uint? paramIntValue = (paramValue.Length == 0) ? (uint?)null : UInt32.Parse(paramValue); + + for (int i = 0; i < testParamNum; i++) { + + string paramStr = + String.Join(";", Enumerable.Range(0, testParamNum).Select(index => (index == i) ? paramValue : "1")); + + uint?[] expectedParamValues = + Enumerable.Range(0, testParamNum).Select(index => (index == i) ? paramIntValue : 1).ToArray(); + + if (!expectedParamValues[expectedParamValues.Length - 1].HasValue) { + // last parameter will not be stored + expectedParamValues = expectedParamValues.Take(expectedParamValues.Length - 1).ToArray(); + } + + var newTestData = testData.AppendNumericalParams(expectedParamValues, Encoding.ASCII.GetBytes(paramStr)); + foreach (var r in next(newTestData)) { + yield return r; + } + } + } + } + + // generates ";;;...;;;" (all parameters are empty) + { + string paramStr = String.Concat(Enumerable.Repeat(";", p.Number - 1)); + + // last parameter will not be stored + uint?[] expectedParamValues = Enumerable.Repeat((uint?)null, p.Number - 1).ToArray(); + + var newTestData = testData.AppendNumericalParams(expectedParamValues, Encoding.ASCII.GetBytes(paramStr)); + foreach (var r in next(newTestData)) { + yield return r; + } + } + + // failure cases + for (int testParamNum = 1; testParamNum < p.Number; testParamNum++) { + string paramStr; + + paramStr = String.Concat(Enumerable.Repeat("1;", testParamNum - 1)) + "\u0000"; + yield return testData.AppendBytesForFailure(Encoding.ASCII.GetBytes(paramStr)); + + paramStr = String.Concat(Enumerable.Repeat("1;", testParamNum - 1)) + "1\u0000"; + yield return testData.AppendBytesForFailure(Encoding.ASCII.GetBytes(paramStr)); + } + } + + /// + /// Generator for + /// + private IEnumerable GenerateTestPatterns(DfaTestData testData, TextParam p, Func> next) { + // empty + { + string expectedParamValue = null; + var newTestData = testData.AppendTextParam(expectedParamValue, new byte[] { }); + foreach (var r in next(newTestData)) { + yield return r; + } + } + + // one character + foreach (byte b in GeneratePrintable()) { + string expectedParamValue = Char.ToString((char)b); + var newTestData = testData.AppendTextParam(expectedParamValue, b); + foreach (var r in next(newTestData)) { + yield return r; + } + } + + // three characters + foreach (byte b in GeneratePrintable()) { + string s = Char.ToString((char)b); + string expectedParamValue = s + s + s; + var newTestData = testData.AppendTextParam(expectedParamValue, b, b, b); + foreach (var r in next(newTestData)) { + yield return r; + } + } + + // failure cases + { + yield return testData.AppendBytesForFailure(0x00); + yield return testData.AppendBytesForFailure(0x41/*A*/, 0x00); + } + } + + private IEnumerable GeneratePrintable() { + for (int c = 0x08; c <= 0x0d; c++) { + yield return (byte)c; + } + + for (int c = 0x20; c <= 0x7e; c++) { + yield return (byte)c; + } + } + + /// + /// Generator for + /// + private IEnumerable GenerateTestPatterns(DfaTestData testData, AnyCharString p, Func> next) { + // empty + { + string expectedParamValue = null; + var newTestData = testData.AppendTextParam(expectedParamValue, new byte[] { }); + foreach (var r in next(newTestData)) { + yield return r; + } + } + + // one character + foreach (byte b in GenerateStringChar()) { + string expectedParamValue = Char.ToString((char)b); + var newTestData = testData.AppendTextParam(expectedParamValue, b); + foreach (var r in next(newTestData)) { + yield return r; + } + } + + // three characters + foreach (byte b in GenerateStringChar()) { + string s = Char.ToString((char)b); + string expectedParamValue = s + s + s; + var newTestData = testData.AppendTextParam(expectedParamValue, b, b, b); + foreach (var r in next(newTestData)) { + yield return r; + } + } + + // failure cases + { + yield return testData.AppendBytesForFailure(0x98/*SOS*/); + yield return testData.AppendBytesForFailure(0x41, 0x98/*SOS*/); + } + } + + private IEnumerable GenerateStringChar() { + for (int c = 0; c <= 0xff; c++) { + if (c == 0x98 || c == 0x9c) { + continue; + } + yield return (byte)c; + } + } + } + + class DummyEscapeSequenceExecutor : IEscapeSequenceExecutor { + } + + [TestFixture] + class TestDfaLimits { + + private readonly string[] ESCAPE_SEQUENCE_PATTERNS = + { + "X{Pt}{ST}" + }; + + private bool finished; + private DfaEngine dfa; + + private readonly byte[] origBytes = new byte[1]; + + [SetUp] + public void SetUp() { + NfaManager nfa = new NfaManager(); + + foreach (string pattern in ESCAPE_SEQUENCE_PATTERNS) { + nfa.AddPattern(pattern, context => { + finished = true; + }); + } + + var dfaStateManager = nfa.CreateDfa(); + dfa = new DfaEngine(dfaStateManager, new DummyEscapeSequenceExecutor()); + finished = false; + } + + private bool Put(byte b) { + origBytes[0] = b; + return dfa.Process(b, origBytes); + } + + [Test] + public void FinishesBeforeLimit() { + bool accepted; + + accepted = Put((byte)'X'); + Assert.AreEqual(true, accepted); + Assert.AreEqual(false, finished); + + for (int i = 0; i < DfaEngine.MAX_SEQUENCE_LENGTH - 3; i++) { + accepted = Put((byte)'a'); + Assert.AreEqual(true, accepted); + Assert.AreEqual(false, finished); + } + + accepted = Put((byte)0x9c); + Assert.AreEqual(true, accepted); + Assert.AreEqual(true, finished); + } + + [Test] + public void FinishesJustAtLimit() { + bool accepted; + + accepted = Put((byte)'X'); + Assert.AreEqual(true, accepted); + Assert.AreEqual(false, finished); + + for (int i = 0; i < DfaEngine.MAX_SEQUENCE_LENGTH - 2; i++) { + accepted = Put((byte)'a'); + Assert.AreEqual(true, accepted); + Assert.AreEqual(false, finished); + } + + accepted = Put((byte)0x9c); + Assert.AreEqual(true, accepted); + Assert.AreEqual(true, finished); + } + + [Test] + public void NoFinish() { + bool accepted; + + accepted = Put((byte)'X'); + Assert.AreEqual(true, accepted); + Assert.AreEqual(false, finished); + + for (int i = 0; i < DfaEngine.MAX_SEQUENCE_LENGTH - 2; i++) { + accepted = Put((byte)'a'); + Assert.AreEqual(true, accepted); + Assert.AreEqual(false, finished); + } + + accepted = Put((byte)'a'); + Assert.AreEqual(false, accepted); // too long + Assert.AreEqual(false, finished); + } + } + + [TestFixture] + class DfaEngineFactoryTest { + + private class ExecResult { + public readonly string Method; + public readonly string Pattern; + public readonly byte[] Matched; + public ExecResult(string method, EscapeSequenceContext context) { + this.Method = method; + this.Pattern = context.Pattern; + this.Matched = context.Matched.ToArray(); + } + } + + private class EscapeSequenceExecutorBase : IEscapeSequenceExecutor { + + + public readonly List Results = new List(); + + [ESPattern("BA")] + public void BA(EscapeSequenceContext context) { + Results.Add(new ExecResult("Base:BA", context)); + } + + [ESPattern("BB")] + [ESPattern("BX")] + public virtual void BB(EscapeSequenceContext context) { + Results.Add(new ExecResult("Base:BB", context)); + } + + [ESPattern("BC")] + internal void BC(EscapeSequenceContext context) { + Results.Add(new ExecResult("Base:BC", context)); + } + + [ESPattern("BD")] + internal virtual void BD(EscapeSequenceContext context) { + Results.Add(new ExecResult("Base:BD", context)); + } + + [ESPattern("BE")] + private void BE(EscapeSequenceContext context) { + Results.Add(new ExecResult("Base:BE", context)); + } + } + + private class EscapeSequenceExecutor1 : EscapeSequenceExecutorBase { + + // [ESPattern("BB")] inherited + [ESPattern("BX")] // this pattern is also specified in the base method + [ESPattern("BY")] + public override void BB(EscapeSequenceContext context) { + Results.Add(new ExecResult("Override:BB", context)); + } + + [ESPattern("C1")] + [ESPattern("C2")] + public void C(EscapeSequenceContext context) { + Results.Add(new ExecResult("Exec:C", context)); + } + + [ESPattern("D1")] + [ESPattern("D2")] + internal void D(EscapeSequenceContext context) { + Results.Add(new ExecResult("Exec:D", context)); + } + + [ESPattern("E1")] + [ESPattern("E2")] + private void E(EscapeSequenceContext context) { + Results.Add(new ExecResult("Exec:E", context)); + } + } + + private class EscapeSequenceExecutor2 : EscapeSequenceExecutor1 { + + [ESPattern("F1")] + [ESPattern("F2")] + public void F(EscapeSequenceContext context) { + Results.Add(new ExecResult("Exec:F", context)); + } + + } + + [Test] + public void TestDfaEngineFactory_WithPreparation() { + DfaEngineFactory.Prepare(); + + var executor = new EscapeSequenceExecutor1(); + + DfaEngine engine = DfaEngineFactory.CreateDfaEngine(executor); + + foreach (string s in new string[] { + "BA", "BB", "BC", "BD", "BE", "BX", "BY", + "C1", "C2", "D1", "D2", "E1", "E2", "F1", "F2", + }) { + foreach (byte b in Encoding.ASCII.GetBytes(s)) { + engine.Process(b, new byte[] { b }); + } + } + + Assert.AreEqual(10, executor.Results.Count); + Check(executor.Results[0], "Base:BA", "BA", "BA"); + Check(executor.Results[1], "Override:BB", "BB", "BB"); + Check(executor.Results[2], "Base:BC", "BC", "BC"); + Check(executor.Results[3], "Base:BD", "BD", "BD"); + Check(executor.Results[4], "Override:BB", "BX", "BX"); + Check(executor.Results[5], "Override:BB", "BY", "BY"); + Check(executor.Results[6], "Exec:C", "C1", "C1"); + Check(executor.Results[7], "Exec:C", "C2", "C2"); + Check(executor.Results[8], "Exec:D", "D1", "D1"); + Check(executor.Results[9], "Exec:D", "D2", "D2"); + } + + private void Check(ExecResult result, string expectedMethod, string expectedPattern, string expectedMatched) { + Assert.AreEqual(expectedMethod, result.Method); + Assert.AreEqual(expectedPattern, result.Pattern); + CollectionAssert.AreEqual(Encoding.ASCII.GetBytes(expectedMatched), result.Matched); + } + + [Test] + public void CreateDfaEngine_CheckSharedDfaStateManager() { + var bag1 = new ConcurrentBag(); + var bag2 = new ConcurrentBag(); + Action action1 = () => { + var executor = new EscapeSequenceExecutor1(); + var dfaEngine = DfaEngineFactory.CreateDfaEngine(executor); + bag1.Add(dfaEngine); + }; + Action action2 = () => { + var executor = new EscapeSequenceExecutor2(); + var dfaEngine = DfaEngineFactory.CreateDfaEngine(executor); + bag2.Add(dfaEngine); + }; + var tasks = Enumerable.Range(0, 100).Select((n) => Task.Run((n % 2 == 0) ? action1 : action2)).ToArray(); + Task.WaitAll(tasks); + + Assert.AreEqual(50, bag1.Count); + Assert.AreEqual(50, bag2.Count); + + DfaStateManager stateManager1 = bag1.First().DfaStateManager; + DfaStateManager stateManager2 = bag2.First().DfaStateManager; + Assert.AreNotSame(stateManager1, stateManager2); + foreach (var dfaEngine in bag1) { + Assert.AreSame(stateManager1, dfaEngine.DfaStateManager); + } + foreach (var dfaEngine in bag2) { + Assert.AreSame(stateManager2, dfaEngine.DfaStateManager); + } + } + } +} +#endif diff --git a/TerminalEmulatorTest/EscapeSequenceEngine/EscapeSequenceProcessorTest.cs b/TerminalEmulatorTest/EscapeSequenceEngine/EscapeSequenceProcessorTest.cs new file mode 100644 index 00000000..35b5d6bf --- /dev/null +++ b/TerminalEmulatorTest/EscapeSequenceEngine/EscapeSequenceProcessorTest.cs @@ -0,0 +1,188 @@ +// Copyright 2019 The Poderosa Project. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#if UNITTEST +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Poderosa.Terminal.EscapeSequenceEngine { + + [TestFixture] + class EscapeSequenceProcessorTest { + + private TestEscapeSequenceExecutor executor; + private XTermEscapeSequenceProcessor processor; + private StringBuilder normalText; + private List unknownSequences; + + [SetUp] + public void SetUp() { + normalText = new StringBuilder(); + unknownSequences = new List(); + executor = new TestEscapeSequenceExecutor(); + + var dfaEngine = DfaEngineFactory.CreateDfaEngine(executor); + + processor = new XTermEscapeSequenceProcessor( + dfaEngine, + (ch) => normalText.Append(ch), + (seq) => unknownSequences.Add(new String(seq))); + } + + private void ClearResults() { + normalText.Clear(); + unknownSequences.Clear(); + executor.History.Clear(); + } + + private void Test_NormalAsciiText() { + processor.Process('A'); + processor.Process('B'); + processor.Process('C'); + + CollectionAssert.AreEqual(new string[0], executor.History); + Assert.AreEqual("ABC", normalText.ToString()); + CollectionAssert.AreEqual(new string[0], unknownSequences); + } + + private void Test_NormalNonAsciiText() { + processor.Process('\u677e'); + processor.Process('\u7af9'); + processor.Process('\u6885'); + + CollectionAssert.AreEqual(new string[0], executor.History); + Assert.AreEqual("\u677e\u7af9\u6885", normalText.ToString()); + CollectionAssert.AreEqual(new string[0], unknownSequences); + } + + private void Test_MatchSingleChar() { + processor.Process('\u0007'); // BEL + + CollectionAssert.AreEqual(new string[] { "BEL" }, executor.History); + Assert.AreEqual("", normalText.ToString()); + CollectionAssert.AreEqual(new string[0], unknownSequences); + } + + private void Test_NoC1Conversion_MatchSequence() { + processor.Process('\u001b'); + processor.Process('1'); // ESC 1 --> ESC 1 + + CollectionAssert.AreEqual(new string[] { "ESC_1" }, executor.History); + Assert.AreEqual("", normalText.ToString()); + CollectionAssert.AreEqual(new string[0], unknownSequences); + } + + private void Test_C1Code_MatchSequence() { + processor.Process('\u009b'); // CSI + processor.Process('S'); + + CollectionAssert.AreEqual(new string[] { "CSI_S" }, executor.History); + Assert.AreEqual("", normalText.ToString()); + CollectionAssert.AreEqual(new string[0], unknownSequences); + } + + private void Test_C1Conversion_MatchSequence() { + processor.Process('\u001b'); + processor.Process('['); // ESC [ --> CSI + processor.Process('S'); + + CollectionAssert.AreEqual(new string[] { "CSI_S" }, executor.History); + Assert.AreEqual("", normalText.ToString()); + CollectionAssert.AreEqual(new string[0], unknownSequences); + } + + private void Test_UnknownSequence() { + processor.Process('\u001b'); + processor.Process('2'); // ESC 2 --> unknwon + + CollectionAssert.AreEqual(new string[0], executor.History); + Assert.AreEqual("", normalText.ToString()); + CollectionAssert.AreEqual(new string[] { "\u001b2" }, unknownSequences); + } + + private void Test_UnknownSequence_NonAscii() { + processor.Process('\u001b'); + processor.Process('\u5b8c'); + + CollectionAssert.AreEqual(new string[0], executor.History); + Assert.AreEqual("", normalText.ToString()); + CollectionAssert.AreEqual(new string[] { "\u001b\u5b8c" }, unknownSequences); + } + + //-------------------------------------------------------------- + // Test combination of patterns + //-------------------------------------------------------------- + + [Test, Combinatorial] + public void TestCombinations([Range(1, 8)] int first, [Range(1, 8)] int second) { + DoTest(first); + ClearResults(); + DoTest(second); + } + + private void DoTest(int n) { + switch (n) { + case 1: + Test_NormalAsciiText(); + break; + case 2: + Test_NormalNonAsciiText(); + break; + case 3: + Test_MatchSingleChar(); + break; + case 4: + Test_NoC1Conversion_MatchSequence(); + break; + case 5: + Test_C1Code_MatchSequence(); + break; + case 6: + Test_C1Conversion_MatchSequence(); + break; + case 7: + Test_UnknownSequence(); + break; + case 8: + Test_UnknownSequence_NonAscii(); + break; + default: + Assert.Fail("unknown test number : {0}", n); + break; + } + } + + private class TestEscapeSequenceExecutor : IEscapeSequenceExecutor { + + public readonly List History = new List(); + + [ESPattern("{BEL}")] + public void BEL(EscapeSequenceContext context) { + History.Add("BEL"); + } + + [ESPattern("{ESC}1")] + public void ESC_1(EscapeSequenceContext context) { + History.Add("ESC_1"); + } + + [ESPattern("{CSI}S")] + public void CSI_S(EscapeSequenceContext context) { + History.Add("CSI_S"); + } + } + } +} +#endif \ No newline at end of file diff --git a/TerminalEmulatorTest/EscapeSequenceEngine/PatternTest.cs b/TerminalEmulatorTest/EscapeSequenceEngine/PatternTest.cs new file mode 100644 index 00000000..6ed33eaf --- /dev/null +++ b/TerminalEmulatorTest/EscapeSequenceEngine/PatternTest.cs @@ -0,0 +1,174 @@ +// Copyright 2019 The Poderosa Project. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +#if UNITTEST +using NUnit.Framework; +using System; +using System.Linq; + +namespace Poderosa.Terminal.EscapeSequenceEngine { + [TestFixture] + class PatternParserTest { + private PatternParser parser; + + [SetUp] + public void SetUp() { + parser = new PatternParser(); + } + + [Test] + public void EmptyPattern() { + Assert.Catch(() => parser.Parse("")); + } + + [Test] + public void NonEscapedCharacter() { + var str = "!\"#$%&'()*+,-./azAZ09:;<=>?@]^_`|}~\u0020"; + var elements = parser.Parse(str); + Assert.AreEqual(str.Length, elements.Count); + CollectionAssert.AllItemsAreInstancesOfType(elements, typeof(CharacterSet)); + CollectionAssert.AreEqual( + str.Select(c => new byte[] { (byte)c }), + elements.Select(s => ((CharacterSet)s).Characters)); + } + + [Test] + public void EscapedCharacter() { + var str = "!\"#$%&'()*+,-./azAZ09:;<=>?@[\\]^_`{|}~\u0020"; + var testStr = String.Join("", str.Select(c => new String(new char[] { '\\', c }))); + var elements = parser.Parse(testStr); + Assert.AreEqual(str.Length, elements.Count); + CollectionAssert.AllItemsAreInstancesOfType(elements, typeof(CharacterSet)); + CollectionAssert.AreEqual( + str.Select(c => new byte[] { (byte)c }), + elements.Select(s => ((CharacterSet)s).Characters)); + } + + [Test] + public void IncompleteEscape() { + Assert.Catch(() => parser.Parse("a\\")); + } + + [Test] + public void CharacterSet() { + var str = "[(\\\\)][\\[abcabc\\]][-a-c][a-c-][01c-a-e-gh][{CR}-{LF}{NUL}{ESC}][\\{CR}]"; + var elements = parser.Parse(str); + Assert.AreEqual(7, elements.Count); + CollectionAssert.AllItemsAreInstancesOfType(elements, typeof(CharacterSet)); + CollectionAssert.AreEqual( + new byte[] { 0x28, 0x29, 0x5c }, // ( ) \ + ((CharacterSet)elements[0]).Characters); + CollectionAssert.AreEqual( + new byte[] { 0x5b, 0x5d, 0x61, 0x62, 0x63 }, // [ ] a b c + ((CharacterSet)elements[1]).Characters); + CollectionAssert.AreEqual( + new byte[] { 0x2d, 0x61, 0x62, 0x63 }, // - a b c + ((CharacterSet)elements[2]).Characters); + CollectionAssert.AreEqual( + new byte[] { 0x2d, 0x61, 0x62, 0x63 }, // - a b c + ((CharacterSet)elements[3]).Characters); + CollectionAssert.AreEqual( + new byte[] { 0x2d, 0x30, 0x31, 0x61, 0x62, 0x63, 0x65, 0x66, 0x67, 0x68 }, // - 0 1 a b c e f g h + ((CharacterSet)elements[4]).Characters); + CollectionAssert.AreEqual( + new byte[] { 0x00, 0x0a, 0x0b, 0x0c, 0x0d, 0x1b }, // NUL LF VT FF CR ESC + ((CharacterSet)elements[5]).Characters); + CollectionAssert.AreEqual( + new byte[] { 0x43, 0x52, 0x7b, 0x7d }, // C R { } + ((CharacterSet)elements[6]).Characters); + } + + [Test] + public void EmptyCharacterSet() { + Assert.Catch(() => parser.Parse("a[]")); + } + + [Test] + public void IncompleteCharacterSet() { + Assert.Catch(() => parser.Parse("a[abc")); + } + + [Test] + public void NamedCharacter() { + var str = "{NUL}{ESC}{CR}{LF}{CSI}"; + var elements = parser.Parse(str); + Assert.AreEqual(5, elements.Count); + CollectionAssert.AllItemsAreInstancesOfType(elements, typeof(CharacterSet)); + CollectionAssert.AreEqual(new byte[] { 0x00 }, ((CharacterSet)elements[0]).Characters); + CollectionAssert.AreEqual(new byte[] { 0x1b }, ((CharacterSet)elements[1]).Characters); + CollectionAssert.AreEqual(new byte[] { 0x0d }, ((CharacterSet)elements[2]).Characters); + CollectionAssert.AreEqual(new byte[] { 0x0a }, ((CharacterSet)elements[3]).Characters); + CollectionAssert.AreEqual(new byte[] { 0x9b }, ((CharacterSet)elements[4]).Characters); + } + + [Test] + public void NumericalParameter() { + var str = "{P1}{P2}{P3}{P04}{P100}"; + var elements = parser.Parse(str); + Assert.AreEqual(5, elements.Count); + CollectionAssert.AllItemsAreInstancesOfType(elements, typeof(NNumericalParams)); + Assert.AreEqual(1, ((NNumericalParams)elements[0]).Number); + Assert.AreEqual(2, ((NNumericalParams)elements[1]).Number); + Assert.AreEqual(3, ((NNumericalParams)elements[2]).Number); + Assert.AreEqual(4, ((NNumericalParams)elements[3]).Number); + Assert.AreEqual(100, ((NNumericalParams)elements[4]).Number); + } + + [Test] + public void InvalidNumericalParameter() { + Assert.Catch(() => parser.Parse("{P-1}")); + Assert.Catch(() => parser.Parse("{P0}")); + Assert.Catch(() => parser.Parse("{P 1}")); + Assert.Catch(() => parser.Parse("{P1 }")); + Assert.Catch(() => parser.Parse("{P+1}")); + } + + [Test] + public void ZeroOrMoreNumericalParameters() { + var str = "{P*}"; + var elements = parser.Parse(str); + Assert.AreEqual(1, elements.Count); + CollectionAssert.AllItemsAreInstancesOfType(elements, typeof(ZeroOrMoreNumericalParams)); + } + + [Test] + public void TextParameter() { + var str = "{Pt}"; + var elements = parser.Parse(str); + Assert.AreEqual(1, elements.Count); + CollectionAssert.AllItemsAreInstancesOfType(elements, typeof(TextParam)); + } + + [Test] + public void StringParameter() { + var str = "{Ps}"; + var elements = parser.Parse(str); + Assert.AreEqual(1, elements.Count); + CollectionAssert.AllItemsAreInstancesOfType(elements, typeof(AnyCharString)); + } + + [Test] + public void UnknownName() { + Assert.Catch(() => parser.Parse("{Px}")); + Assert.Catch(() => parser.Parse("{aaa}")); + Assert.Catch(() => parser.Parse("{}")); + } + + [Test] + public void IncompleteNamedParam() { + Assert.Catch(() => parser.Parse("a{P")); + Assert.Catch(() => parser.Parse("a{ESC")); + } + } +} +#endif diff --git a/TerminalEmulatorTest/TerminalEmulatorTest.csproj b/TerminalEmulatorTest/TerminalEmulatorTest.csproj index 03f8b961..5e177c2c 100644 --- a/TerminalEmulatorTest/TerminalEmulatorTest.csproj +++ b/TerminalEmulatorTest/TerminalEmulatorTest.csproj @@ -1,7 +1,7 @@  + - Debug @@ -13,7 +13,7 @@ Poderosa.TerminalEmulatorTest v4.5 512 - d240a72f + 845a0f78 true @@ -50,6 +50,9 @@ + + + @@ -84,8 +87,8 @@ This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - +