/// <summary> /// Assigns words its width and height /// </summary> /// <param name="g"></param> internal void MeasureWordsSize(Graphics g) { // Check if measure white space if not yet done to measure once if (!_wordsSizeMeasured) { MeasureWordSpacing(g); if (HtmlTag != null && HtmlTag.Name == "img") { var image = CssValueParser.GetImage(GetAttribute("src"), HtmlContainer.Bridge); var word = new CssBoxWord(this, image); Words.Clear(); Words.Add(word); } else if (Words.Count > 0) { foreach (var boxWord in Words) { var sf = new StringFormat(); sf.SetMeasurableCharacterRanges(new[] { new CharacterRange(0, boxWord.Text.Length) }); var regions = g.MeasureCharacterRanges(boxWord.Text, ActualFont, new RectangleF(0, 0, float.MaxValue, float.MaxValue), sf); SizeF s = regions[0].GetBounds(g).Size; PointF p = regions[0].GetBounds(g).Location; boxWord.LastMeasureOffset = new PointF(p.X, p.Y); boxWord.Width = s.Width + ActualWordSpacing; boxWord.Height = s.Height; } } _wordsSizeMeasured = true; } }
/// <summary> /// Searches for the first word occourence inside the box, on the specified linebox /// </summary> /// <param name="b"></param> /// <param name="line"> </param> /// <returns></returns> internal CssBoxWord FirstWordOccourence(CssBox b, CssLineBox line) { if (b.Words.Count == 0 && b.Boxes.Count == 0) { return(null); } if (b.Words.Count > 0) { foreach (CssBoxWord word in b.Words) { if (line.Words.Contains(word)) { return(word); } } return(null); } else { foreach (CssBox bb in b.Boxes) { CssBoxWord w = FirstWordOccourence(bb, line); if (w != null) { return(w); } } return(null); } }
/// <summary> /// Applies right alignment to the text on the linebox /// </summary> /// <param name="g"></param> /// <param name="line"></param> private static void ApplyRightAlignment(Graphics g, CssLineBox line) { if (line.Words.Count == 0) { return; } CssBoxWord lastWord = line.Words[line.Words.Count - 1]; float right = line.OwnerBox.ActualRight - line.OwnerBox.ActualPaddingRight - line.OwnerBox.ActualBorderRightWidth; float diff = right - lastWord.Right - lastWord.LastMeasureOffset.X - lastWord.OwnerBox.ActualBorderRightWidth - lastWord.OwnerBox.ActualPaddingRight; if (diff <= 0) { return; } //if (line.OwnerBox.Direction == CssConstants.Rtl) //{ //} foreach (CssBoxWord word in line.Words) { word.Left += diff; } foreach (CssBox b in line.Rectangles.Keys) { RectangleF r = b.Rectangles[line]; b.Rectangles[line] = new RectangleF(r.X + diff, r.Y, r.Width, r.Height); } }
/// <summary> /// Sets the baseline of the words of the specified box to certain height /// </summary> /// <param name="g">Device info</param> /// <param name="b">box to check words</param> /// <param name="baseline">baseline</param> internal void SetBaseLine(Graphics g, CssBox b, float baseline) { //TODO: Aqui me quede, checar poniendo "by the" con un font-size de 3em List <CssBoxWord> ws = WordsOf(b); if (!Rectangles.ContainsKey(b)) { return; } RectangleF r = Rectangles[b]; //Save top of words related to the top of rectangle float gap = 0f; if (ws.Count > 0) { gap = ws[0].Top - r.Top; } else { CssBoxWord firstw = b.FirstWordOccourence(b, this); if (firstw != null) { gap = firstw.Top - r.Top; } } //New top that words will have //float newtop = baseline - (Height - OwnerBox.FontDescent - 3); //OLD float newtop = baseline;// -GetBaseLineHeight(b, g); //OLD if (b.ParentBox != null && b.ParentBox.Rectangles.ContainsKey(this) && r.Height < b.ParentBox.Rectangles[this].Height) { //Do this only if rectangle is shorter than parent's float recttop = newtop - gap; RectangleF newr = new RectangleF(r.X, recttop, r.Width, r.Height); Rectangles[b] = newr; b.OffsetRectangle(this, gap); } foreach (var word in ws) { if (!word.IsImage) { word.Top = newtop; } } }
/// <summary> /// Lets the linebox add the word an its box to their lists if necessary. /// </summary> /// <param name="word"></param> internal void ReportExistanceOf(CssBoxWord word) { if (!Words.Contains(word)) { Words.Add(word); word.LineBox = this; } if (!RelatedBoxes.Contains(word.OwnerBox)) { RelatedBoxes.Add(word.OwnerBox); } }
/// <summary> /// Gets the minimum width that the box can be. /// The box can be as thin as the longest word plus padding. /// The check is deep thru box tree. /// </summary> /// <returns></returns> internal float GetMinimumWidth() { float maxw = 0f; float padding = 0f; CssBoxWord word = null; GetMinimumWidth_LongestWord(this, ref maxw, ref word); if (word != null) { GetMinimumWidth_BubblePadding(word.OwnerBox, this, ref padding); } return(maxw + padding); }
/// <summary> /// Gets the longest word (in width) inside the box, deeply. /// </summary> /// <param name="b"></param> /// <param name="maxw"> </param> /// <param name="word"> </param> /// <returns></returns> private void GetMinimumWidth_LongestWord(CssBox b, ref float maxw, ref CssBoxWord word) { if (b.Words.Count > 0) { foreach (CssBoxWord w in b.Words) { if (w.FullWidth > maxw) { maxw = w.FullWidth; word = w; } } } else { foreach (CssBox bb in b.Boxes) { GetMinimumWidth_LongestWord(bb, ref maxw, ref word); } } }
/// <summary> /// The selection start index if the first selected word is partially selected (-1 if not selected or fully selected)<br/> /// if the given word is not starting or ending selection word -1 is returned as full word selection is in place. /// </summary> /// <remarks> /// Handles backward selecting by returning the selection end data instead of start. /// </remarks> /// <param name="word">the word to return the selection start index for</param> /// <returns>data value or -1 if not aplicable</returns> public int GetSelectingStartIndex(CssBoxWord word) { return word == (_backwardSelection ? _selectionEnd : _selectionStart) ? (_backwardSelection ? _selectionEndIndex : _selectionStartIndex) : -1; }
/// <summary> /// Select all the words that are between <paramref name="selectionStart"/> word and <paramref name="selectionEnd"/> word in the DOM hierarchy. /// </summary> /// <param name="box">the current traversal node</param> /// <param name="selectionStart">selection start word limit</param> /// <param name="selectionEnd">selection end word limit</param> /// <param name="inSelection">used to know the traversal is currently in selected range</param> /// <returns></returns> private bool SelectWordsInRange(CssBox box, CssBoxWord selectionStart, CssBoxWord selectionEnd, ref bool inSelection) { foreach (var boxWord in box.Words) { if (!inSelection && boxWord == selectionStart) { inSelection = true; } if (inSelection) { boxWord.Selection = this; if (selectionStart == selectionEnd || boxWord == selectionEnd) { return true; } } } foreach (var childBox in box.Boxes) { if (SelectWordsInRange(childBox, selectionStart, selectionEnd, ref inSelection)) { return true; } } return false; }
/// <summary> /// Select all the words that are between <paramref name="selectionStart"/> word and <paramref name="selectionEnd"/> word in the DOM hierarchy.<br/> /// </summary> /// <param name="root">the root of the DOM sub-tree the selection is in</param> /// <param name="selectionStart">selection start word limit</param> /// <param name="selectionEnd">selection end word limit</param> private void SelectWordsInRange(CssBox root, CssBoxWord selectionStart, CssBoxWord selectionEnd) { bool inSelection = false; SelectWordsInRange(root, selectionStart, selectionEnd, ref inSelection); }
/// <summary> /// Handle html text selection by mouse move over the html with left mouse button pressed.<br/> /// Calculate the words in the selected range and set their selected property. /// </summary> /// <param name="control">the control hosting the html to invalidate</param> /// <param name="loc">the mouse location</param> /// <param name="allowPartialSelect">true - partial word selection allowed, false - only full words selection</param> private void HandleSelection(Control control, Point loc, bool allowPartialSelect) { // get the line under the mouse or nearest from the top var lineBox = DomUtils.GetCssLineBox(_root, loc); if (lineBox != null) { // get the word under the mouse var word = DomUtils.GetCssBoxWord(lineBox, loc); // if no word found uner the mouse use the last or the first word in the line if (word == null && lineBox.Words.Count > 0) { if (loc.Y > lineBox.LineBottom) { // under the line word = lineBox.Words[lineBox.Words.Count - 1]; } else if (loc.X < lineBox.Words[0].Left) { // before the line word = lineBox.Words[0]; } else if (loc.X > lineBox.Words[lineBox.Words.Count - 1].Right) { // at the end of the line word = lineBox.Words[lineBox.Words.Count - 1]; } } // if there is matching word if (word != null) { if (_selectionStart == null) { // on start set the selection start word _selectionStartPoint = loc; _selectionStart = word; if (allowPartialSelect) CalculateWordCharIndexAndOffset(control, word, loc, true); } // always set selection end word _selectionEnd = word; if (allowPartialSelect) CalculateWordCharIndexAndOffset(control, word, loc, false); ClearSelection(_root); if (CheckNonEmptySelection(loc, allowPartialSelect)) { CheckSelectionDirection(); SelectWordsInRange(_root, _backwardSelection ? _selectionEnd : _selectionStart, _backwardSelection ? _selectionStart : _selectionEnd); } else { _selectionEnd = null; } control.Cursor = Cursors.IBeam; control.Invalidate(); } } }
/// <summary> /// Clear the current selection. /// </summary> private void ClearSelection() { // clear drag and drop _dragDropData = null; ClearSelection(_root); _selectionStartOffset = -1; _selectionStartIndex = -1; _selectionEndOffset = -1; _selectionEndIndex = -1; _selectionStartPoint = Point.Empty; _selectionStart = null; _selectionEnd = null; }
/// <summary> /// Calculate the charecter index and offset by charecters for the given word and given offset.<br/> /// <seealso cref="CalculateWordCharIndexAndOffset(Control, CssBoxWord, Point, bool, out int, out float)"/>. /// </summary> /// <param name="control">used to create graphics to measure string</param> /// <param name="word">the word to calculate its index and offset</param> /// <param name="loc">the location to calculate for</param> /// <param name="selectionStart">to set the starting or ending char and offset data</param> private void CalculateWordCharIndexAndOffset(Control control, CssBoxWord word, Point loc, bool selectionStart) { int selectionIndex; float selectionOffset; CalculateWordCharIndexAndOffset(control, word, loc, selectionStart, out selectionIndex, out selectionOffset); if(selectionStart) { _selectionStartIndex = selectionIndex; _selectionStartOffset = selectionOffset; } else { _selectionEndIndex = selectionIndex; _selectionEndOffset = selectionOffset; } }
/// <summary> /// Find the css line box that the given word is in. /// </summary> /// <param name="word">the word to search for it's line box</param> /// <returns>line box that the word is in</returns> public static CssLineBox GetCssLineBoxByWord(CssBoxWord word) { var box = word.OwnerBox; while (box != null && box.LineBoxes.Count == 0) { box = box.ParentBox; } if (box == null) return null; foreach (var lineBox in box.LineBoxes) { foreach (var lineWord in lineBox.Words) { if(lineWord == word) { return lineBox; } } } return box.LineBoxes[0]; }
/// <summary> /// The selection start offset if the first selected word is partially selected (-1 if not selected or fully selected)<br/> /// if the given word is not starting or ending selection word -1 is returned as full word selection is in place. /// </summary> /// <remarks> /// Handles backward selecting by returning the selection end data instead of start. /// </remarks> /// <param name="word">the word to return the selection start offset for</param> public float GetSelectedStartOffset(CssBoxWord word) { return word == (_backwardSelection ? _selectionEnd : _selectionStart) ? (_backwardSelection ? _selectionEndOffset : _selectionStartOffset) : -1; }
/// <summary> /// The selection end index if the last selected word is partially selected (-1 if not selected or fully selected)<br/> /// if the given word is not starting or ending selection word -1 is returned as full word selection is in place. /// </summary> /// <remarks> /// Handles backward selecting by returning the selection end data instead of start. /// </remarks> /// <param name="word">the word to return the selection end index for</param> public int GetSelectedEndIndexOffset(CssBoxWord word) { return word == (_backwardSelection ? _selectionStart : _selectionEnd) ? (_backwardSelection ? _selectionStartIndex : _selectionEndIndex) : -1; }
/// <summary> /// Gets the longest word (in width) inside the box, deeply. /// </summary> /// <param name="b"></param> /// <param name="maxw"> </param> /// <param name="word"> </param> /// <returns></returns> private void GetMinimumWidth_LongestWord(CssBox b, ref float maxw, ref CssBoxWord word) { if (b.Words.Count > 0) { foreach (CssBoxWord w in b.Words) { if (w.FullWidth > maxw) { maxw = w.FullWidth; word = w; } } } else { foreach(CssBox bb in b.Boxes) GetMinimumWidth_LongestWord(bb, ref maxw,ref word); } }
/// <summary> /// Assigns words its width and height /// </summary> /// <param name="g"></param> internal void MeasureWordsSize(Graphics g) { // Check if measure white space if not yet done to measure once if (!_wordsSizeMeasured) { MeasureWordSpacing(g); if (HtmlTag != null && HtmlTag.Name == "img") { var image = CssValueParser.GetImage(GetAttribute("src"), HtmlContainer.Bridge); var word = new CssBoxWord(this, image); Words.Clear(); Words.Add(word); } else if(Words.Count > 0) { foreach (var boxWord in Words) { var sf = new StringFormat(); sf.SetMeasurableCharacterRanges(new[] { new CharacterRange(0, boxWord.Text.Length) }); var regions = g.MeasureCharacterRanges(boxWord.Text, ActualFont, new RectangleF(0, 0, float.MaxValue, float.MaxValue), sf); SizeF s = regions[0].GetBounds(g).Size; PointF p = regions[0].GetBounds(g).Location; boxWord.LastMeasureOffset = new PointF(p.X, p.Y); boxWord.Width = s.Width + ActualWordSpacing; boxWord.Height = s.Height; } } _wordsSizeMeasured = true; } }
/// <summary> /// Get the selected word with respect to partial selected words. /// </summary> /// <param name="boxWord">the word to append</param> /// <param name="selectedText">is to get selected text or all the text in the word</param> private static string GetSelectedWord(CssBoxWord boxWord, bool selectedText) { if (selectedText && boxWord.SelectedStartIndex > -1 && boxWord.SelectedEndIndexOffset > -1) { return boxWord.Text.Substring(boxWord.SelectedStartIndex, boxWord.SelectedEndIndexOffset - boxWord.SelectedStartIndex); } else if (selectedText && boxWord.SelectedStartIndex > -1) { return boxWord.Text.Substring(boxWord.SelectedStartIndex) + " "; } else if (selectedText && boxWord.SelectedEndIndexOffset > -1) { return boxWord.Text.Substring(0, boxWord.SelectedEndIndexOffset); } else { return boxWord.Text + " "; } }
/// <summary> /// Select the word at the given location if found. /// </summary> /// <param name="control">the control hosting the html to invalidate</param> /// <param name="loc">the location to select word at</param> public void SelectWord(Control control, Point loc) { var word = DomUtils.GetCssBoxWord(_root, loc); if (word != null) { word.Selection = this; _selectionStartPoint = loc; _selectionStart = _selectionEnd = word; control.Invalidate(); } }
/// <summary> /// Calculate the charecter index and offset by charecters for the given word and given offset. /// If the location is below the word line then set the selection to the end.<br/> /// If the location is to the right of the word then set the selection to the end.<br/> /// If the offset is to the left of the word set the selection to the begining.<br/> /// Otherwise calculate the width of each substring to find the char the location is on. /// </summary> /// <param name="control">used to create graphics to measure string</param> /// <param name="word">the word to calculate its index and offset</param> /// <param name="loc">the location to calculate for</param> /// <param name="selectionIndex">return the index of the char under the location</param> /// <param name="selectionOffset">return the offset of the char under the location</param> /// <param name="inclusive">is to include the first charecter in the calculation</param> private static void CalculateWordCharIndexAndOffset(Control control, CssBoxWord word, Point loc, bool inclusive, out int selectionIndex, out float selectionOffset) { selectionIndex = 0; selectionOffset = 0.1f; var offset = loc.X - word.Left; if (word.Text == null) { // not a text word - set full selection selectionIndex = -1; selectionOffset = -1; } else if (offset > word.Width - word.OwnerBox.ActualWordSpacing || loc.Y > word.LineBox.LineBottom) { // mouse under the line, to the right of the word - set to the end of the word selectionIndex = word.Text.Length; selectionOffset = word.Width; } else if (offset > 0) { // calculate partial word selection selectionIndex = word.Text.Length; selectionOffset = word.OwnerBox.ActualWordSpacing/2f; using (var g = control.CreateGraphics()) { for (int i = 1; i <= word.Text.Length; i++) { var sf = new StringFormat(); sf.SetMeasurableCharacterRanges(new[] {new CharacterRange(0, i)}); var regions = g.MeasureCharacterRanges(word.Text, word.OwnerBox.ActualFont, new RectangleF(0, 0, float.MaxValue, float.MaxValue), sf); SizeF s = regions[0].GetBounds(g).Size; if(!inclusive) { selectionOffset = s.Width + (s.Width / i / 15f); } if (s.Width > offset) { selectionIndex = i - (inclusive ? 1 : 0); break; } selectionOffset = s.Width + (s.Width / i / 15f); } } } }