// // Label.cs // // Author: // Jean-Philippe Bruyère // // Copyright (c) 2013-2017 Jean-Philippe Bruyère // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. using System; using System.Collections.Generic; using System.Linq; using System.Diagnostics; using Cairo; using System.Text.RegularExpressions; using System.Xml.Serialization; using System.ComponentModel; namespace Crow { public class Label : GraphicObject { #region CTOR public Label () : base(){} public Label(Interface iface) : base(iface) { } // public Label(string _text) // : base() // { // Text = _text; // } #endregion public event EventHandler TextChanged; public virtual void OnTextChanged(Object sender, TextChangeEventArgs e) { NotifyValueChanged ("Text", Text); TextChanged.Raise (this, e); } //TODO:change protected to private #region private and protected fields string _text = "label"; Alignment _textAlignment; bool horizontalStretch; bool verticalStretch; bool _selectable; bool _multiline; Color selBackground; Color selForeground; Point mouseLocalPos = -1;//mouse coord in widget space, filled only when clicked int _currentCol; //0 based cursor position in string int _currentLine; Point _selBegin = -1; //selection start (row,column) Point _selRelease = -1; //selection end (row,column) double textCursorPos; //cursor position in cairo units in widget client coord. double SelStartCursorPos = -1; double SelEndCursorPos = -1; bool SelectionInProgress = false; protected Rectangle rText; protected float widthRatio = 1f; protected float heightRatio = 1f; protected FontExtents fe; protected TextExtents te; #endregion [XmlAttributeAttribute][DefaultValue("BlueGray")] public virtual Color SelectionBackground { get { return selBackground; } set { if (value == selBackground) return; selBackground = value; NotifyValueChanged ("SelectionBackground", selBackground); RegisterForRedraw (); } } [XmlAttributeAttribute][DefaultValue("White")] public virtual Color SelectionForeground { get { return selForeground; } set { if (value == selForeground) return; selForeground = value; NotifyValueChanged ("SelectionForeground", selForeground); RegisterForRedraw (); } } [XmlAttributeAttribute][DefaultValue(Alignment.Left)] public Alignment TextAlignment { get { return _textAlignment; } set { if (value == _textAlignment) return; _textAlignment = value; RegisterForRedraw (); NotifyValueChanged ("TextAlignment", _textAlignment); } } [XmlAttributeAttribute][DefaultValue(false)] public virtual bool HorizontalStretch { get { return horizontalStretch; } set { if (horizontalStretch == value) return; horizontalStretch = value; RegisterForRedraw (); NotifyValueChanged ("HorizontalStretch", horizontalStretch); } } [XmlAttributeAttribute][DefaultValue(false)] public virtual bool VerticalStretch { get { return verticalStretch; } set { if (verticalStretch == value) return; verticalStretch = value; RegisterForRedraw (); NotifyValueChanged ("VerticalStretch", verticalStretch); } } [XmlAttributeAttribute][DefaultValue("label")] public string Text { get { return lines == null ? _text : lines.Aggregate((i, j) => i + Interface.LineBreak + j); } set { if (string.Equals (value, _text, StringComparison.Ordinal)) return; _text = value; if (string.IsNullOrEmpty(_text)) _text = ""; lines = getLines; OnTextChanged (this, new TextChangeEventArgs (Text)); RegisterForGraphicUpdate (); } } [XmlAttributeAttribute][DefaultValue(false)] public bool Selectable { get { return _selectable; } set { if (value == _selectable) return; _selectable = value; NotifyValueChanged ("Selectable", _selectable); SelBegin = -1; SelRelease = -1; RegisterForRedraw (); } } [XmlAttributeAttribute][DefaultValue(false)] public bool Multiline { get { return _multiline; } set { if (value == _multiline) return; _multiline = value; NotifyValueChanged ("Multiline", _multiline); RegisterForGraphicUpdate(); } } [XmlAttributeAttribute][DefaultValue(0)] public int CurrentColumn{ get { return _currentCol; } set { if (value == _currentCol) return; if (value < 0) _currentCol = 0; else if (value > lines [_currentLine].Length) _currentCol = lines [_currentLine].Length; else _currentCol = value; NotifyValueChanged ("CurrentColumn", _currentCol); } } [XmlAttributeAttribute][DefaultValue(0)] public int CurrentLine{ get { return _currentLine; } set { if (value == _currentLine) return; if (value >= lines.Count) _currentLine = lines.Count-1; else if (value < 0) _currentLine = 0; else _currentLine = value; //force recheck of currentCol for bounding int cc = _currentCol; _currentCol = 0; CurrentColumn = cc; NotifyValueChanged ("CurrentLine", _currentLine); } } [XmlIgnore]public Point CurrentPosition { get { return new Point(_currentCol, CurrentLine); } } //TODO:using HasFocus for drawing selection cause SelBegin and Release binding not to work /// /// Selection begin position in char units /// [XmlAttributeAttribute][DefaultValue("-1")] public Point SelBegin { get { return _selBegin; } set { if (value == _selBegin) return; _selBegin = value; NotifyValueChanged ("SelBegin", _selBegin); NotifyValueChanged ("SelectedText", SelectedText); } } [XmlAttributeAttribute][DefaultValue("-1")] public Point SelRelease { get { return _selRelease; } set { if (value == _selRelease) return; _selRelease = value; NotifyValueChanged ("SelRelease", _selRelease); NotifyValueChanged ("SelectedText", SelectedText); } } /// /// return char at CurrentLine, CurrentColumn /// [XmlIgnore]protected Char CurrentChar { get { return lines [CurrentLine] [CurrentColumn]; } } /// /// ordered selection start and end positions in char units /// [XmlIgnore]protected Point selectionStart { get { return SelRelease < 0 || SelBegin.Y < SelRelease.Y ? SelBegin : SelBegin.Y > SelRelease.Y ? SelRelease : SelBegin.X < SelRelease.X ? SelBegin : SelRelease; } } [XmlIgnore]public Point selectionEnd { get { return SelRelease < 0 || SelBegin.Y > SelRelease.Y ? SelBegin : SelBegin.Y < SelRelease.Y ? SelRelease : SelBegin.X > SelRelease.X ? SelBegin : SelRelease; } } [XmlIgnore]public string SelectedText { get { if (SelRelease < 0 || SelBegin < 0) return ""; if (selectionStart.Y == selectionEnd.Y) return lines [selectionStart.Y].Substring (selectionStart.X, selectionEnd.X - selectionStart.X); string tmp = ""; tmp = lines [selectionStart.Y].Substring (selectionStart.X); for (int l = selectionStart.Y + 1; l < selectionEnd.Y; l++) { tmp += Interface.LineBreak + lines [l]; } tmp += Interface.LineBreak + lines [selectionEnd.Y].Substring (0, selectionEnd.X); return tmp; } } [XmlIgnore]public bool selectionIsEmpty { get { return SelRelease < 0; } } List lines; List getLines { get { return _multiline ? Regex.Split (_text, "\r\n|\r|\n|\\\\n").ToList() : new List(new string[] { _text }); } } /// /// Moves cursor one char to the left. /// /// true if move succeed public bool MoveLeft(){ int tmp = _currentCol - 1; if (tmp < 0) { if (_currentLine == 0) return false; CurrentLine--; CurrentColumn = int.MaxValue; } else CurrentColumn = tmp; return true; } /// /// Moves cursor one char to the right. /// /// true if move succeed public bool MoveRight(){ int tmp = _currentCol + 1; if (tmp > lines [_currentLine].Length){ if (CurrentLine == lines.Count - 1) return false; CurrentLine++; CurrentColumn = 0; } else CurrentColumn = tmp; return true; } public void GotoWordStart(){ CurrentColumn--; //skip white spaces while (!char.IsLetterOrDigit (this.CurrentChar) && CurrentColumn > 0) CurrentColumn--; while (char.IsLetterOrDigit (lines [CurrentLine] [CurrentColumn]) && CurrentColumn > 0) CurrentColumn--; if (!char.IsLetterOrDigit (this.CurrentChar)) CurrentColumn++; } public void GotoWordEnd(){ //skip white spaces if (CurrentColumn >= lines [CurrentLine].Length - 1) return; while (!char.IsLetterOrDigit (this.CurrentChar) && CurrentColumn < lines [CurrentLine].Length-1) CurrentColumn++; while (char.IsLetterOrDigit (this.CurrentChar) && CurrentColumn < lines [CurrentLine].Length-1) CurrentColumn++; if (char.IsLetterOrDigit (this.CurrentChar)) CurrentColumn++; } public void DeleteChar() { if (selectionIsEmpty) { if (CurrentColumn == 0) { if (CurrentLine == 0 && lines.Count == 1) return; CurrentLine--; CurrentColumn = lines [CurrentLine].Length; lines [CurrentLine] += lines [CurrentLine + 1]; lines.RemoveAt (CurrentLine + 1); OnTextChanged (this, new TextChangeEventArgs (Text)); return; } CurrentColumn--; lines [CurrentLine] = lines [CurrentLine].Remove (CurrentColumn, 1); } else { int linesToRemove = selectionEnd.Y - selectionStart.Y + 1; int l = selectionStart.Y; if (linesToRemove > 0) { lines [l] = lines [l].Remove (selectionStart.X, lines [l].Length - selectionStart.X) + lines [selectionEnd.Y].Substring (selectionEnd.X, lines [selectionEnd.Y].Length - selectionEnd.X); l++; for (int c = 0; c < linesToRemove-1; c++) lines.RemoveAt (l); CurrentLine = selectionStart.Y; CurrentColumn = selectionStart.X; } else lines [l] = lines [l].Remove (selectionStart.X, selectionEnd.X - selectionStart.X); CurrentColumn = selectionStart.X; SelBegin = -1; SelRelease = -1; } OnTextChanged (this, new TextChangeEventArgs (Text)); } /// /// Insert new string at caret position, should be sure no line break is inside. /// /// String. protected void Insert(string str) { if (!selectionIsEmpty) this.DeleteChar (); if (_multiline) { string[] strLines = Regex.Split (str, "\r\n|\r|\n|" + @"\\n").ToArray(); lines [CurrentLine] = lines [CurrentLine].Insert (CurrentColumn, strLines[0]); CurrentColumn += strLines[0].Length; for (int i = 1; i < strLines.Length; i++) { InsertLineBreak (); lines [CurrentLine] = lines [CurrentLine].Insert (CurrentColumn, strLines[i]); CurrentColumn += strLines[i].Length; } } else { lines [CurrentLine] = lines [CurrentLine].Insert (CurrentColumn, str); CurrentColumn += str.Length; } OnTextChanged (this, new TextChangeEventArgs (Text)); } /// /// Insert a line break. /// protected void InsertLineBreak() { lines.Insert(CurrentLine + 1, lines[CurrentLine].Substring(CurrentColumn)); lines [CurrentLine] = lines [CurrentLine].Substring (0, CurrentColumn); CurrentLine++; CurrentColumn = 0; OnTextChanged (this, new TextChangeEventArgs (Text)); } #region GraphicObject overrides protected override int measureRawSize(LayoutingType lt) { if (lines == null) lines = getLines; using (ImageSurface img = new ImageSurface (Format.Argb32, 10, 10)) { using (Context gr = new Context (img)) { //Cairo.FontFace cf = gr.GetContextFontFace (); gr.SelectFontFace (Font.Name, Font.Slant, Font.Wheight); gr.SetFontSize (Font.Size); fe = gr.FontExtents; te = new TextExtents(); if (lt == LayoutingType.Height){ int lc = lines.Count; //ensure minimal height = text line height if (lc == 0) lc = 1; return (int)Math.Ceiling(fe.Height * lc) + Margin * 2; } try { foreach (string s in lines) { string l = s.Replace("\t", new String (' ', Interface.TabSize)); TextExtents tmp = gr.TextExtents (l); if (tmp.XAdvance > te.XAdvance) te = tmp; } } catch (Exception ex) { return -1; } return (int)Math.Ceiling (te.XAdvance) + Margin * 2; } } } protected override void onDraw (Context gr) { base.onDraw (gr); gr.SelectFontFace (Font.Name, Font.Slant, Font.Wheight); gr.SetFontSize (Font.Size); gr.FontOptions = Interface.FontRenderingOptions; gr.Antialias = Interface.Antialias; rText = new Rectangle(new Size( measureRawSize(LayoutingType.Width), measureRawSize(LayoutingType.Height))); rText.Width -= 2 * Margin; rText.Height -= 2 * Margin; widthRatio = 1f; heightRatio = 1f; Rectangle cb = ClientRectangle; rText.X = cb.X; rText.Y = cb.Y; if (horizontalStretch) { widthRatio = (float)cb.Width / (float)rText.Width; if (!verticalStretch) heightRatio = widthRatio; } if (verticalStretch) { heightRatio = (float)cb.Height / (float)rText.Height; if (!horizontalStretch) widthRatio = heightRatio; } rText.Width = (int)(widthRatio * (float)rText.Width); rText.Height = (int)(heightRatio * (float)rText.Height); switch (TextAlignment) { case Alignment.TopLeft: //ok rText.X = cb.X; rText.Y = cb.Y; break; case Alignment.Top: //ok rText.Y = cb.Y; rText.X = cb.X + cb.Width / 2 - rText.Width / 2; break; case Alignment.TopRight: //ok rText.Y = cb.Y; rText.X = cb.Right - rText.Width; break; case Alignment.Left://ok rText.X = cb.X; rText.Y = cb.Y + cb.Height / 2 - rText.Height / 2; break; case Alignment.Right://ok rText.X = cb.X + cb.Width - rText.Width; rText.Y = cb.Y + cb.Height / 2 - rText.Height / 2; break; case Alignment.Bottom://ok rText.X = cb.Width / 2 - rText.Width / 2; rText.Y = cb.Height - rText.Height; break; case Alignment.BottomLeft://ok rText.X = cb.X; rText.Y = cb.Bottom - rText.Height; break; case Alignment.BottomRight://ok rText.Y = cb.Bottom - rText.Height; rText.X = cb.Right - rText.Width; break; case Alignment.Center://ok rText.X = cb.X + cb.Width / 2 - rText.Width / 2; rText.Y = cb.Y + cb.Height / 2 - rText.Height / 2; break; } gr.FontMatrix = new Matrix(widthRatio * (float)Font.Size, 0, 0, heightRatio * (float)Font.Size, 0, 0); fe = gr.FontExtents; #region draw text cursor if (HasFocus && Selectable) { if (mouseLocalPos >= 0) { computeTextCursor(gr); if (SelectionInProgress) { if (SelBegin < 0){ SelBegin = new Point(CurrentColumn, CurrentLine); SelStartCursorPos = textCursorPos; SelRelease = -1; }else{ SelRelease = new Point(CurrentColumn, CurrentLine); if (SelRelease == SelBegin) SelRelease = -1; else SelEndCursorPos = textCursorPos; } }else computeTextCursorPosition(gr); }else computeTextCursorPosition(gr); Foreground.SetAsSource (gr); gr.LineWidth = 1.0; gr.MoveTo (0.5 + textCursorPos + rText.X, rText.Y + CurrentLine * fe.Height); gr.LineTo (0.5 + textCursorPos + rText.X, rText.Y + (CurrentLine + 1) * fe.Height); gr.Stroke(); } #endregion //****** debug selection ************* // if (SelRelease >= 0) { // new SolidColor(Color.DarkGreen).SetAsSource(gr); // Rectangle R = new Rectangle ( // rText.X + (int)SelEndCursorPos - 3, // rText.Y + (int)(SelRelease.Y * fe.Height), // 6, // (int)fe.Height); // gr.Rectangle (R); // gr.Fill (); // } // if (SelBegin >= 0) { // new SolidColor(Color.DarkRed).SetAsSource(gr); // Rectangle R = new Rectangle ( // rText.X + (int)SelStartCursorPos - 3, // rText.Y + (int)(SelBegin.Y * fe.Height), // 6, // (int)fe.Height); // gr.Rectangle (R); // gr.Fill (); // } //******************* for (int i = 0; i < lines.Count; i++) { string l = lines [i].Replace ("\t", new String (' ', Interface.TabSize)); int lineLength = (int)gr.TextExtents (l).XAdvance; Rectangle lineRect = new Rectangle ( rText.X, rText.Y + (int)Math.Ceiling(i * fe.Height), lineLength, (int)Math.Ceiling(fe.Height)); // if (TextAlignment == Alignment.Center || // TextAlignment == Alignment.Top || // TextAlignment == Alignment.Bottom) // lineRect.X += (rText.Width - lineLength) / 2; // else if (TextAlignment == Alignment.Right || // TextAlignment == Alignment.TopRight || // TextAlignment == Alignment.BottomRight) // lineRect.X += (rText.Width - lineLength); if (string.IsNullOrWhiteSpace (l)) continue; Foreground.SetAsSource (gr); gr.MoveTo (lineRect.X, rText.Y + fe.Ascent + fe.Height * i); gr.ShowText (l); gr.Fill (); if (Selectable) { if (SelRelease >= 0 && i >= selectionStart.Y && i <= selectionEnd.Y) { gr.SetSourceColor (selBackground); Rectangle selRect = lineRect; int cpStart = (int)SelStartCursorPos, cpEnd = (int)SelEndCursorPos; if (SelBegin.Y > SelRelease.Y) { cpStart = cpEnd; cpEnd = (int)SelStartCursorPos; } if (i == selectionStart.Y) { selRect.Width -= cpStart; selRect.Left += cpStart; } if (i == selectionEnd.Y) selRect.Width -= (lineLength - cpEnd); gr.Rectangle (selRect); gr.FillPreserve (); gr.Save (); gr.Clip (); gr.SetSourceColor (SelectionForeground); gr.MoveTo (lineRect.X, rText.Y + fe.Ascent + fe.Height * i); gr.ShowText (l); gr.Fill (); gr.Restore (); } } } } #endregion #region Mouse handling void updatemouseLocalPos(Point mpos){ mouseLocalPos = mpos - ScreenCoordinates(Slot).TopLeft - ClientRectangle.TopLeft; if (mouseLocalPos.X < 0) mouseLocalPos.X = 0; if (mouseLocalPos.Y < 0) mouseLocalPos.Y = 0; } public override void onMouseEnter (object sender, MouseMoveEventArgs e) { base.onMouseEnter (sender, e); if (Selectable) CurrentInterface.MouseCursor = XCursor.Text; } public override void onMouseLeave (object sender, MouseMoveEventArgs e) { base.onMouseLeave (sender, e); CurrentInterface.MouseCursor = XCursor.Default; } protected override void onFocused (object sender, EventArgs e) { base.onFocused (sender, e); if (!_selectable) return; SelBegin = new Point(0,0); SelRelease = new Point (lines.LastOrDefault ().Length, lines.Count-1); RegisterForRedraw (); } protected override void onUnfocused (object sender, EventArgs e) { base.onUnfocused (sender, e); SelBegin = -1; SelRelease = -1; RegisterForRedraw (); } public override void onMouseMove (object sender, MouseMoveEventArgs e) { base.onMouseMove (sender, e); if (!(SelectionInProgress && HasFocus && _selectable)) return; updatemouseLocalPos (e.Position); RegisterForRedraw(); } public override void onMouseDown (object sender, MouseButtonEventArgs e) { if (this.HasFocus && _selectable){ updatemouseLocalPos (e.Position); SelBegin = -1; SelRelease = -1; SelectionInProgress = true; RegisterForRedraw();//TODO:should put it in properties } //done at the end to set 'hasFocus' value after testing it base.onMouseDown (sender, e); } public override void onMouseUp (object sender, MouseButtonEventArgs e) { base.onMouseUp (sender, e); if (!SelectionInProgress) return; updatemouseLocalPos (e.Position); SelectionInProgress = false; RegisterForRedraw (); } public override void onMouseDoubleClick (object sender, MouseButtonEventArgs e) { base.onMouseDoubleClick (sender, e); GotoWordStart (); SelBegin = CurrentPosition; GotoWordEnd (); SelRelease = CurrentPosition; SelectionInProgress = false; RegisterForRedraw (); } #endregion /// /// Update Current Column, line and TextCursorPos /// from mouseLocalPos /// void computeTextCursor(Context gr) { TextExtents te; double cPos = 0f; CurrentLine = (int)(mouseLocalPos.Y / fe.Height); //fix cu if (CurrentLine >= lines.Count) CurrentLine = lines.Count - 1; switch (TextAlignment) { case Alignment.Center: case Alignment.Top: case Alignment.Bottom: cPos+= ClientRectangle.Width - gr.TextExtents(lines [CurrentLine]).Width/2.0; break; case Alignment.Right: case Alignment.TopRight: case Alignment.BottomRight: cPos += ClientRectangle.Width - gr.TextExtents(lines [CurrentLine]).Width; break; } for (int i = 0; i < lines[CurrentLine].Length; i++) { string c = lines [CurrentLine].Substring (i, 1); if (c == "\t") c = new string (' ', Interface.TabSize); te = gr.TextExtents(c); double halfWidth = te.XAdvance / 2; if (mouseLocalPos.X <= cPos + halfWidth) { CurrentColumn = i; textCursorPos = cPos; mouseLocalPos = -1; return; } cPos += te.XAdvance; } CurrentColumn = lines[CurrentLine].Length; textCursorPos = cPos; //reset mouseLocalPos mouseLocalPos = -1; } /// Computes offsets in cairo units void computeTextCursorPosition(Context gr) { if (SelBegin >= 0) SelStartCursorPos = GetXFromTextPointer (gr, SelBegin); if (SelRelease >= 0) SelEndCursorPos = GetXFromTextPointer (gr, SelRelease); textCursorPos = GetXFromTextPointer (gr, new Point(CurrentColumn, CurrentLine)); } /// Compute x offset in cairo unit from text position double GetXFromTextPointer(Context gr, Point pos) { try { string l = lines [pos.Y].Substring (0, pos.X). Replace ("\t", new String (' ', Interface.TabSize)); return gr.TextExtents (l).XAdvance; } catch{ return -1; } } } }