2008-12-06 Ivan N. Zlatev <contact@i-nz.net>
[mono.git] / mcs / class / Managed.Windows.Forms / System.Windows.Forms / LineTag.cs
1 // Permission is hereby granted, free of charge, to any person obtaining\r
2 // a copy of this software and associated documentation files (the\r
3 // "Software"), to deal in the Software without restriction, including\r
4 // without limitation the rights to use, copy, modify, merge, publish,\r
5 // distribute, sublicense, and/or sell copies of the Software, and to\r
6 // permit persons to whom the Software is furnished to do so, subject to\r
7 // the following conditions:\r
8 // \r
9 // The above copyright notice and this permission notice shall be\r
10 // included in all copies or substantial portions of the Software.\r
11 // \r
12 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,\r
13 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\r
14 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\r
15 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\r
16 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\r
17 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\r
18 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\r
19 //\r
20 // Copyright (c) 2004-2006 Novell, Inc. (http://www.novell.com)\r
21 //\r
22 // Authors:\r
23 //      Peter Bartok    pbartok@novell.com\r
24 //\r
25 //\r
26 \r
27 using System;\r
28 using System.Collections;\r
29 using System.Drawing;\r
30 using System.Drawing.Text;\r
31 using System.Text;\r
32 \r
33 namespace System.Windows.Forms\r
34 {\r
35         internal class LineTag\r
36         {\r
37                 #region Local Variables\r
38                 // Formatting\r
39                 private Font            font;           // System.Drawing.Font object for this tag\r
40                 private Color           color;          // The font color for this tag\r
41                 private Color           back_color;     // In 2.0 tags can have background colours.\r
42                 private Font            link_font;      // Cached font used for link if IsLink\r
43                 private bool            is_link;        // Whether this tag is a link\r
44                 private string          link_text;      // The full link text e.g. this might be \r
45                                                         // word-wrapped to "w" but this would be\r
46                                                         // "www.mono-project.com"\r
47 \r
48                 // Payload; text\r
49                 private int             start;          // start, in chars; index into Line.text\r
50                                                         // 1 based!!\r
51 \r
52                 // Drawing support\r
53                 private int             height;         // Height in pixels of the text this tag describes\r
54                 private int             ascent;         // Ascent of the font for this tag\r
55                 private int             descent;        // Descent of the font for this tag\r
56                 private int             shift;          // Shift down for this tag, to stay on baseline\r
57 \r
58                 // Administrative\r
59                 private Line            line;           // The line we're on\r
60                 private LineTag         next;           // Next tag on the same line\r
61                 private LineTag         previous;       // Previous tag on the same line\r
62                 #endregion\r
63 \r
64                 #region Constructors\r
65                 public LineTag (Line line, int start)\r
66                 {\r
67                         this.line = line;\r
68                         Start = start;\r
69                         link_font = null;\r
70                         is_link = false;\r
71                         link_text = null;\r
72                 }\r
73                 #endregion      // Constructors\r
74 \r
75                 #region Public Properties\r
76                 public int Ascent {\r
77                         get { return ascent; }\r
78                 }\r
79                 \r
80                 public Color BackColor {\r
81                         get { return back_color; }\r
82                         set { back_color = value; }\r
83                 }\r
84 \r
85                 public Color ColorToDisplay {\r
86                         get {\r
87                                 if (IsLink == true)\r
88                                         return Color.Blue;\r
89 \r
90                                 return color;\r
91                         }\r
92                 }\r
93 \r
94                 public Color Color {\r
95                         get { return color; }\r
96                         set { color = value; }\r
97                 }\r
98                 \r
99                 public int Descent {\r
100                         get { return descent; }\r
101                 }\r
102 \r
103                 public int End {\r
104                         get { return start + Length; }\r
105                 }\r
106 \r
107                 public Font FontToDisplay {\r
108                         get {\r
109                                 if (IsLink) {\r
110                                         if (link_font == null)\r
111                                                 link_font = new Font (font.FontFamily, font.Size, font.Style | FontStyle.Underline);\r
112 \r
113                                         return link_font;\r
114                                 }\r
115 \r
116                                 return font;\r
117                         }\r
118                 }\r
119 \r
120                 public Font Font {\r
121                         get { return font; }\r
122                         set { \r
123                                 if (font != value) {\r
124                                         link_font = null;\r
125                                         font = value;\r
126         \r
127                                         height = Font.Height;\r
128                                         XplatUI.GetFontMetrics (Hwnd.GraphicsContext, Font, out ascent, out descent);\r
129                                         line.recalc = true;\r
130                                 }\r
131                         }\r
132                 }\r
133 \r
134                 public int Height {\r
135                         get { return height; }\r
136                         set { height = value; }\r
137                 }\r
138 \r
139                 public virtual bool IsTextTag {\r
140                         get { return true; }\r
141                 }\r
142 \r
143                 public int Length {\r
144                         get {\r
145                                 int res = 0;\r
146                                 if (next != null)\r
147                                         res = next.start - start;\r
148                                 else\r
149                                         res = line.text.Length - (start - 1);\r
150 \r
151                                 return res > 0 ? res : 0;\r
152                         }\r
153                 }\r
154 \r
155                 public Line Line {\r
156                         get { return line; }\r
157                         set { line = value; }\r
158                 }\r
159 \r
160                 public LineTag Next {\r
161                         get { return next; }\r
162                         set { next = value; }\r
163                 }\r
164 \r
165                 public LineTag Previous {\r
166                         get { return previous; }\r
167                         set { previous = value; }\r
168                 }\r
169 \r
170                 public int Shift {\r
171                         get { return shift; }\r
172                         set { shift = value; }\r
173                 }\r
174 \r
175                 public int Start {\r
176                         get { return start; }\r
177                         set {\r
178 #if DEBUG\r
179                                 if (value <= 0)\r
180                                         throw new Exception("Start of tag must be 1 or higher!");\r
181 \r
182                                 if (this.Previous != null) {\r
183                                         if  (this.Previous.Start == value)\r
184                                                 System.Console.Write("Creating empty tag");\r
185                                         if  (this.Previous.Start > value)\r
186                                                 throw new Exception("New tag makes an insane tag");\r
187                                 }\r
188 #endif\r
189                                 start = value; \r
190                         }\r
191                 }\r
192 \r
193                 public int TextEnd {\r
194                         get { return start + TextLength; }\r
195                 }\r
196 \r
197                 public int TextLength {\r
198                         get {\r
199                                 int res = 0;\r
200                                 if (next != null)\r
201                                         res = next.start - start;\r
202                                 else\r
203                                         res = line.TextLengthWithoutEnding () - (start - 1);\r
204 \r
205                                 return res > 0 ? res : 0;\r
206                         }\r
207                 }\r
208 \r
209                 public float Width {\r
210                         get {\r
211                                 if (Length == 0)\r
212                                         return 0;\r
213                                 return line.widths [start + Length - 1] - (start != 0 ? line.widths [start - 1] : 0);\r
214                         }\r
215                 }\r
216 \r
217                 public float X {\r
218                         get {\r
219                                 if (start == 0)\r
220                                         return line.X;\r
221                                 return line.X + line.widths [start - 1];\r
222                         }\r
223                 }\r
224 \r
225                 public bool IsLink {\r
226                         get { return is_link; }\r
227                         set { is_link = value; }\r
228                 }\r
229 \r
230                 public string LinkText {\r
231                         get { return link_text; }\r
232                         set { link_text = value; }\r
233                 }\r
234                 #endregion\r
235                 \r
236                 #region Public Methods\r
237                 ///<summary>Break a tag into two with identical attributes; pos is 1-based; returns tag starting at &gt;pos&lt; or null if end-of-line</summary>\r
238                 public LineTag Break (int pos)\r
239                 {\r
240                         LineTag new_tag;\r
241 \r
242 #if DEBUG\r
243                         // Sanity\r
244                         if (pos < this.Start)\r
245                                 throw new Exception ("Breaking at a negative point");\r
246 #endif\r
247 \r
248 #if DEBUG\r
249                         if (pos > End)\r
250                                 throw new Exception ("Breaking past the end of a line");\r
251 #endif\r
252 \r
253                         new_tag = new LineTag(line, pos);\r
254                         new_tag.CopyFormattingFrom (this);\r
255 \r
256                         new_tag.next = this.next;\r
257                         this.next = new_tag;\r
258                         new_tag.previous = this;\r
259 \r
260                         if (new_tag.next != null)\r
261                                 new_tag.next.previous = new_tag;\r
262 \r
263                         return new_tag;\r
264                 }\r
265 \r
266                 /// <summary>Combines 'this' tag with 'other' tag</summary>\r
267                 public bool Combine (LineTag other)\r
268                 {\r
269                         if (!this.Equals (other))\r
270                                 return false;\r
271 \r
272                         this.next = other.next;\r
273                         \r
274                         if (this.next != null)\r
275                                 this.next.previous = this;\r
276 \r
277                         return true;\r
278                 }\r
279 \r
280                 public void CopyFormattingFrom (LineTag other)\r
281                 {\r
282                         Font = other.font;\r
283                         color = other.color;\r
284                         back_color = other.back_color;\r
285                 }\r
286 \r
287                 public void Delete ()\r
288                 {\r
289                         // If we are the only tag, we can't be deleted\r
290                         if (previous == null && next == null)\r
291                                 return;\r
292                                 \r
293                         // If we are the last tag, deletion is easy\r
294                         if (next == null) {\r
295                                 previous.next = null;\r
296                                 return;\r
297                         }\r
298                         \r
299                         // Easy cases gone, little tougher, delete ourself\r
300                         // Update links, and start\r
301                         next.previous = null;\r
302                         \r
303                         LineTag loop = next;\r
304                         \r
305                         while (loop != null) {\r
306                                 loop.Start -= Length;\r
307                                 loop = loop.next;                       \r
308                         }\r
309                         \r
310                         return;\r
311                 }\r
312                 \r
313                 public virtual void Draw (Graphics dc, Color color, float x, float y, int start, int end)\r
314                 {\r
315                         TextBoxTextRenderer.DrawText (dc, line.text.ToString (start, end).Replace ("\r", string.Empty), FontToDisplay, color, x, y, false);\r
316                 }\r
317                 \r
318                 public virtual void Draw (Graphics dc, Color color, float xoff, float y, int start, int end, string text)\r
319                 {\r
320                         Rectangle measured_text;\r
321                         Draw (dc, color, xoff, y, start, end, text, out measured_text, false);\r
322                 }\r
323 \r
324                 /// <summary>\r
325                 /// \r
326                 /// </summary>\r
327                 /// <param name="drawStart">0 based start index</param>\r
328                 public virtual void Draw (Graphics dc, Color color, float xoff, float y, int drawStart, int drawEnd,\r
329                                           string text, out Rectangle measuredText, bool measureText)\r
330                 {\r
331                         if (measureText) {\r
332                                 int xstart = (int)line.widths [drawStart] + (int)xoff;\r
333                                 int xend = (int)line.widths [drawEnd] - (int)line.widths [drawStart];\r
334                                 int ystart = (int)y;\r
335                                 int yend = (int)TextBoxTextRenderer.MeasureText (dc, Text (), FontToDisplay).Height;\r
336 \r
337                                 measuredText = new Rectangle (xstart, ystart, xend, yend);\r
338                         } else {\r
339                                 measuredText = new Rectangle ();\r
340                         }\r
341 \r
342                         while (drawStart < drawEnd) {\r
343                                 int tab_index = text.IndexOf ("\t", drawStart);\r
344                                 \r
345                                 if (tab_index == -1)\r
346                                         tab_index = drawEnd;\r
347 \r
348                                 TextBoxTextRenderer.DrawText (dc, text.Substring (drawStart, tab_index - drawStart).Replace ("\r", string.Empty), FontToDisplay, color, xoff + line.widths [drawStart], y, false);\r
349 \r
350                                 // non multilines get the unknown char \r
351                                 if (!line.document.multiline && tab_index != drawEnd)\r
352                                         TextBoxTextRenderer.DrawText (dc, "\u0013", FontToDisplay, color, xoff + line.widths [tab_index], y, true);\r
353 \r
354                                 drawStart = tab_index + 1;\r
355                         }\r
356                 }\r
357 \r
358                 /// <summary>Checks if 'this' tag describes the same formatting options as 'obj'</summary>\r
359                 public override bool Equals (object obj)\r
360                 {\r
361                         LineTag other;\r
362 \r
363                         if (obj == null)\r
364                                 return false;\r
365 \r
366                         if (!(obj is LineTag))\r
367                                 return false;\r
368 \r
369                         if (obj == this)\r
370                                 return true;\r
371 \r
372                         other = (LineTag)obj;\r
373 \r
374                         if (other.IsTextTag != IsTextTag)\r
375                                 return false;\r
376 \r
377                         if (this.IsLink != other.IsLink)\r
378                                 return false;\r
379 \r
380                         if (this.LinkText != other.LinkText)\r
381                                 return false;\r
382 \r
383                         if (this.font.Equals (other.font) && this.color.Equals (other.color))\r
384                                 return true;\r
385 \r
386                         return false;\r
387                 }\r
388 \r
389                 /// <summary>Finds the tag that describes the character at position 'pos' (0 based) on 'line'</summary>\r
390                 public static LineTag FindTag (Line line, int pos)\r
391                 {\r
392                         LineTag tag = line.tags;\r
393 \r
394                         // Beginning of line is a bit special\r
395                         if (pos == 0)\r
396                                 return tag;     // Not sure if we should get the final tag here\r
397 \r
398                         while (tag != null) {\r
399                                 // [H  e][l][l  o  _  W][o  r]  Text\r
400                                 // [1  2][3][4  5  6  7][8  9]  Start\r
401                                 //     3  4           8     10  End\r
402                                 // 0 1  2  3  4  5  6  7  8  9   Pos\r
403                                 if ((tag.start <= pos) && (pos < tag.End))\r
404                                         return GetFinalTag (tag);\r
405 \r
406                                 tag = tag.next;\r
407                         }\r
408 \r
409                         return null;\r
410                 }\r
411 \r
412                 /// <summary>Applies 'font' and 'brush' to characters starting at 'start' for 'length' chars; \r
413                 /// Removes any previous tags overlapping the same area; \r
414                 /// returns true if lineheight has changed</summary>\r
415                 /// <param name="formatStart">1-based character position on line</param>\r
416                 public static bool FormatText (Line line, int formatStart, int length, Font font, Color color, Color backColor, FormatSpecified specified)\r
417                 {\r
418                         LineTag tag;\r
419                         LineTag start_tag;\r
420                         LineTag end_tag;\r
421                         int end;\r
422                         bool retval = false;            // Assume line-height doesn't change\r
423 \r
424                         // Too simple?\r
425                         if (((FormatSpecified.Font & specified) == FormatSpecified.Font) && font.Height != line.height)\r
426                                 retval = true;\r
427 \r
428                         line.recalc = true;             // This forces recalculation of the line in RecalculateDocument\r
429 \r
430                         // A little sanity, not sure if it's needed, might be able to remove for speed\r
431                         if (length > line.text.Length)\r
432                                 length = line.text.Length;\r
433 \r
434                         tag = line.tags;\r
435                         end = formatStart + length;\r
436 \r
437                         // Common special case\r
438                         if ((formatStart == 1) && (length == tag.Length)) {\r
439                                 SetFormat (tag, font, color, backColor, specified);\r
440                                 return retval;\r
441                         }\r
442 \r
443                         // empty selection style at begining of line means\r
444                         // we only need one new tag\r
445                         if  (formatStart == 1 && length == 0) {\r
446                                 line.tags.Break (1);\r
447                                 SetFormat (line.tags, font, color, backColor, specified);\r
448                                 return retval;\r
449                         }\r
450 \r
451                         start_tag = FindTag (line, formatStart - 1);\r
452 \r
453                         // we are at an empty tag already!\r
454                         // e.g. [Tag 0 - "He"][Tag 1 = 0 length][Tag 2 "llo world"]\r
455                         // Find Tag will return tag 0 at position 3, but we should just\r
456                         // use the empty tag after..\r
457                         if (start_tag.End == formatStart && length == 0 && start_tag.Next != null && start_tag.Next.Length == 0) {\r
458                                 SetFormat (start_tag.Next, font, color, backColor, specified);\r
459                                 return retval;\r
460                         }\r
461 \r
462                         // if we are at the end of a tag, we want to move to the next tag\r
463                         while (start_tag.End == formatStart && start_tag.Next != null)\r
464                                 start_tag = start_tag.Next;\r
465 \r
466                         tag = start_tag.Break (formatStart);\r
467 \r
468                         // empty selection style at end of line - its the only situation\r
469                         // where the rest of the tag would be empty, since we moved to the\r
470                         // begining of next non empty tag\r
471                         if (tag.Length == 0) {\r
472                                 SetFormat (tag, font, color, backColor, specified);\r
473                                 return retval;\r
474                         }\r
475 \r
476                         // empty - so we just create another tag for\r
477                         // after our new (now) empty one..\r
478                         if (length == 0) {\r
479                                 tag.Break (formatStart);\r
480                                 SetFormat (tag, font, color, backColor, specified);\r
481                                 return retval;\r
482                         }\r
483 \r
484                         while (tag != null && tag.End <= end) {\r
485                                 SetFormat (tag, font, color, backColor, specified);\r
486                                 tag = tag.next;\r
487                         }\r
488 \r
489                         // did the last tag conveniently fit?\r
490                         if (tag != null && tag.End == end)\r
491                                 return retval;\r
492 \r
493                         /// Now do the last tag\r
494                         end_tag = FindTag (line, end-1);\r
495 \r
496                         if (end_tag != null) {\r
497                                 end_tag.Break (end);\r
498                                 SetFormat (end_tag, font, color, backColor, specified);\r
499                         }\r
500 \r
501                         return retval;\r
502                 }\r
503 \r
504                 // Gets the character at the x-coordinate.  Index is based from the\r
505                 // line, not the start of the tag.\r
506                 // returns 0 based index (0 means before character at 1, 1 means at character 1)\r
507                 public int GetCharIndex (int x)\r
508                 {\r
509                         int low = start;\r
510                         int high = low + Length;\r
511                         int length_no_ending = line.TextLengthWithoutEnding ();\r
512 \r
513                         if (Length == 0)\r
514                                 return low-1;\r
515 \r
516                         if (length_no_ending == 0)\r
517                                 return 0;\r
518 \r
519                         if (x < line.widths [low]) {\r
520                                 if (low == 1 && x > (line.widths [1] / 2))\r
521                                         return low;\r
522                                 return low - 1;\r
523                         }\r
524 \r
525                         if (x > line.widths[length_no_ending])\r
526                                 return length_no_ending;\r
527                                 \r
528                         while (low < high - 1) {\r
529                                 int mid = (high + low) / 2;\r
530                                 float width = line.widths[mid];\r
531 \r
532                                 if (width < x)\r
533                                         low = mid;\r
534                                 else\r
535                                         high = mid;\r
536                         }\r
537 \r
538                         float char_width = line.widths[high] - line.widths[low];\r
539 \r
540                         if ((x - line.widths[low]) >= (char_width / 2))\r
541                                 return high;\r
542                         else\r
543                                 return low;     \r
544                 }\r
545                 \r
546                 // There can be multiple tags at the same position, we want to make\r
547                 // sure we are using the very last tag at the given position\r
548                 // Empty tags are necessary if style is set at a position with\r
549                 // no length.\r
550                 public static LineTag GetFinalTag (LineTag tag)\r
551                 {\r
552                         LineTag res = tag;\r
553 \r
554                         while (res.Length == 0 && res.next != null && res.next.Length == 0)\r
555                                 res = res.next;\r
556 \r
557                         return res;\r
558                 }\r
559 \r
560                 public override int GetHashCode ()\r
561                 {\r
562                         return base.GetHashCode ();\r
563                 }\r
564                 \r
565                 internal virtual int MaxHeight ()\r
566                 {\r
567                         return font.Height;\r
568                 }\r
569 \r
570                 private static void SetFormat (LineTag tag, Font font, Color color, Color back_color, FormatSpecified specified)\r
571                 {\r
572                         if ((FormatSpecified.Font & specified) == FormatSpecified.Font) {\r
573                                 tag.Font = font;\r
574                         }\r
575                         if ((FormatSpecified.Color & specified) == FormatSpecified.Color)\r
576                                 tag.color = color;\r
577                         if ((FormatSpecified.BackColor & specified) == FormatSpecified.BackColor) {\r
578                                 tag.back_color = back_color;\r
579                         }\r
580                         // Console.WriteLine ("setting format:   {0}  {1}   new color {2}", color.Color, specified, tag.color.Color);\r
581                 }\r
582 \r
583                 public virtual SizeF SizeOfPosition (Graphics dc, int pos)\r
584                 {\r
585                         if (pos >= line.TextLengthWithoutEnding () && line.document.multiline)\r
586                                 return SizeF.Empty;\r
587 \r
588                         string text = line.text.ToString (pos, 1);\r
589                         switch ((int) text [0]) {\r
590                         case '\t':\r
591                                 if (!line.document.multiline)\r
592                                         goto case 10;\r
593                                 SizeF res = TextBoxTextRenderer.MeasureText (dc, " ", font); \r
594                                 res.Width *= 8.0F;\r
595                                 return res;\r
596                         case 10:\r
597                         case 13:\r
598                                 return TextBoxTextRenderer.MeasureText (dc, "\u000D", font);\r
599                         }\r
600                         \r
601                         return TextBoxTextRenderer.MeasureText (dc, text, font);\r
602                 }\r
603 \r
604                 public virtual string Text ()\r
605                 {\r
606                         return line.text.ToString (start - 1, Length);\r
607                 }\r
608 \r
609                 public override string ToString ()\r
610                 {\r
611                         if (Length > 0)\r
612                                 return string.Format ("{0} Tag starts at index: {1}, length: {2}, text: {3}, font: {4}", GetType (), start, Length, Text (), font.ToString ());\r
613                                 \r
614                         return string.Format ("Zero Length tag at index: {0}", start);\r
615                 }\r
616                 #endregion      // Internal Methods\r
617         }\r
618 }\r