1 /*
2  *  "GEDKeeper", the personal genealogical database editor.
3  *  Copyright (C) 2011-2021 by Sergey V. Zhdanovskih.
4  *
5  *  This file is part of "GEDKeeper".
6  *
7  *  This program is free software: you can redistribute it and/or modify
8  *  it under the terms of the GNU General Public License as published by
9  *  the Free Software Foundation, either version 3 of the License, or
10  *  (at your option) any later version.
11  *
12  *  This program is distributed in the hope that it will be useful,
13  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
14  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  *  GNU General Public License for more details.
16  *
17  *  You should have received a copy of the GNU General Public License
18  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
19  */
20 
21 using System;
22 using System.Collections.Generic;
23 using System.Drawing;
24 using System.Windows.Forms;
25 using BSLib;
26 using BSLib.Design.Handlers;
27 using GKCore;
28 using GKCore.BBText;
29 using GKCore.MVP.Controls;
30 
31 namespace GKUI.Components
32 {
33     using SDFontStyle = System.Drawing.FontStyle;
34 
LinkEventHandler(object sender, string linkName)35     public delegate void LinkEventHandler(object sender, string linkName);
36 
37     /// <summary>
38     ///
39     /// </summary>
40     public class HyperView : ScrollablePanel, IHyperView
41     {
42         private readonly List<BBTextChunk> fChunks;
43         private readonly List<int> fHeights;
44         private readonly StringList fLines;
45         private readonly StringFormat fStrFormat;
46 
47         private bool fAcceptFontChange;
48         private int fBorderWidth;
49         private BBTextChunk fCurrentLink;
50         private Color fLinkColor;
51         private ExtSize fTextSize;
52         private bool fWordWrap;
53 
54         private static readonly object EventLink;
55 
HyperView()56         static HyperView()
57         {
58             EventLink = new object();
59         }
60 
61         public event LinkEventHandler OnLink
62         {
63             add { Events.AddHandler(EventLink, value); }
64             remove { Events.RemoveHandler(EventLink, value); }
65         }
66 
67         public int BorderWidth
68         {
69             get { return fBorderWidth; }
70             set {
71                 if (fBorderWidth != value) {
72                     fBorderWidth = value;
73                     Invalidate();
74                 }
75             }
76         }
77 
78         public StringList Lines
79         {
80             get { return fLines; }
81         }
82 
83         public Color LinkColor
84         {
85             get { return fLinkColor; }
86             set {
87                 if (fLinkColor != value) {
88                     fLinkColor = value;
89                     Invalidate();
90                 }
91             }
92         }
93 
94         public bool WordWrap
95         {
96             get { return fWordWrap; }
97             set { fWordWrap = value; }
98         }
99 
100 
HyperView()101         public HyperView()
102         {
103             SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint |
104                      ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true);
105             UpdateStyles();
106 
107             BorderStyle = BorderStyle.Fixed3D;
108             DoubleBuffered = true;
109             TabStop = true;
110 
111             fAcceptFontChange = true;
112             fChunks = new List<BBTextChunk>();
113             fCurrentLink = null;
114             fHeights = new List<int>();
115             fLines = new StringList();
116             fLines.OnChange += LinesChanged;
117             fLinkColor = Color.Blue;
118             fTextSize = ExtSize.Empty;
119             fStrFormat = new StringFormat(StringFormatFlags.MeasureTrailingSpaces | StringFormatFlags.NoWrap | StringFormatFlags.NoClip);
120             fWordWrap = true;
121         }
122 
Dispose(bool disposing)123         protected override void Dispose(bool disposing)
124         {
125             if (disposing) {
126                 fChunks.Clear();
127                 fHeights.Clear();
128                 fLines.Dispose();
129                 fStrFormat.Dispose();
130             }
131             base.Dispose(disposing);
132         }
133 
Activate()134         public void Activate()
135         {
136             Select();
137         }
138 
LinesChanged(object sender)139         private void LinesChanged(object sender)
140         {
141             UpdateScrollPosition(0, 0);
142             ArrangeText();
143         }
144 
ArrangeText()145         private void ArrangeText()
146         {
147             try {
148                 SuspendLayout();
149 
150                 fAcceptFontChange = false;
151                 fHeights.Clear();
152 
153                 Graphics gfx = CreateGraphics();
154                 //gfx.TextRenderingHint = TextRenderingHint.AntiAlias;
155                 try {
156                     int xPos = 0;
157                     int yPos = 0;
158                     int xMax = 0;
159                     int lineHeight = 0;
160 
161                     string text = fLines.Text;
162                     Font defFont = this.Font;
163                     float maxWidth = this.ClientSize.Width - (2 * fBorderWidth);
164                     SizeF zerosz = new SizeF(0f, 0f);
165 
166                     text = SysUtils.StripHTML(text);
167 
168                     var parser = new BBTextParser(AppHost.GfxProvider, defFont.SizeInPoints,
169                                                   new ColorHandler(fLinkColor), new ColorHandler(ForeColor));
170 
171                     parser.ParseText(fChunks, text);
172 
173                     int line = -1;
174                     int chunksCount = fChunks.Count;
175                     int k = 0;
176                     while (k < chunksCount) {
177                         BBTextChunk chunk = fChunks[k];
178                         bool recalcChunk = false;
179 
180                         if (line != chunk.Line) {
181                             line = chunk.Line;
182 
183                             if (line > 0) {
184                                 yPos += lineHeight;
185                                 fHeights.Add(lineHeight);
186                             }
187 
188                             xPos = 0;
189                             lineHeight = 0;
190                         }
191 
192                         int prevX = xPos;
193                         int prevY = yPos;
194 
195                         string chunkStr = chunk.Text;
196                         if (!string.IsNullOrEmpty(chunkStr)) {
197                             using (var font = new Font(defFont.Name, chunk.Size, (SDFontStyle)chunk.Style, defFont.Unit)) {
198                                 SizeF strSize = gfx.MeasureString(chunkStr, font, zerosz, fStrFormat);
199 
200                                 if (fWordWrap && xPos + strSize.Width > maxWidth) {
201                                     int lastPos = 0, prevPos = 0;
202                                     string tempStr, prevStr = string.Empty;
203                                     int sliceType = -1;
204                                     while (true) {
205                                         tempStr = GetSlice(chunkStr, ref lastPos, ref sliceType);
206                                         strSize = gfx.MeasureString(tempStr, font, zerosz, fStrFormat);
207                                         if (xPos + strSize.Width <= maxWidth) {
208                                             prevStr = tempStr;
209                                             prevPos = lastPos;
210                                         } else {
211                                             if (sliceType == 0) {
212                                                 // first word
213                                                 if (xPos == 0) {
214                                                     string tail = chunkStr.Substring(lastPos);
215                                                     SplitChunk(chunk, k, tempStr, tail, ref chunksCount);
216                                                 } else {
217                                                     ShiftChunks(k, chunksCount);
218                                                     recalcChunk = true;
219                                                 }
220                                                 break;
221                                             } else if (sliceType == 1 || sliceType == 2) {
222                                                 // middle or tail word
223                                                 string tail = chunkStr.Substring(prevPos);
224                                                 SplitChunk(chunk, k, prevStr, tail, ref chunksCount);
225                                                 break;
226                                             } else if (sliceType == 3) {
227                                                 // one first and last word, nothing to do
228                                                 break;
229                                             }
230                                         }
231                                     }
232                                 }
233 
234                                 strSize = gfx.MeasureString(chunk.Text, font, zerosz, fStrFormat);
235                                 chunk.Width = (int)strSize.Width;
236 
237                                 xPos += chunk.Width;
238                                 if (xMax < xPos) xMax = xPos;
239 
240                                 int h = (int)strSize.Height;
241                                 if (lineHeight < h) lineHeight = h;
242                             }
243 
244                             if (!string.IsNullOrEmpty(chunk.URL)) {
245                                 chunk.LinkRect = ExtRect.CreateBounds(prevX, prevY, xPos - prevX, lineHeight);
246                             }
247                         }
248 
249                         if (!recalcChunk) {
250                             k++;
251                         }
252                     }
253 
254                     fTextSize = new ExtSize(xMax + 2 * fBorderWidth, yPos + 2 * fBorderWidth);
255                 } finally {
256                     gfx.Dispose();
257                     fAcceptFontChange = true;
258                     SetImageSize(fTextSize);
259 
260                     ResumeLayout(true);
261                 }
262             } catch (Exception ex) {
263                 Logger.WriteError("HyperView.ArrangeText()", ex);
264             }
265         }
266 
GetSlice(string str, ref int lastPos, ref int type)267         private static string GetSlice(string str, ref int lastPos, ref int type)
268         {
269             // type: -1 initial none, 0 first word, 1 any middle, 2 last word, 3 only one word in str
270             int pos = str.IndexOf(' ', lastPos);
271             string result;
272             if (pos >= 0) {
273                 result = str.Substring(0, pos);
274                 lastPos = pos + 1;
275                 type = (type == -1) ? 0 : 1;
276             } else {
277                 result = str;
278                 if (lastPos > 0) {
279                     lastPos = -1;
280                     type = 2;
281                 } else {
282                     lastPos = -1;
283                     type = 3;
284                 }
285             }
286             return result;
287         }
288 
SplitChunk(BBTextChunk chunk, int index, string head, string tail, ref int chunksCount)289         private void SplitChunk(BBTextChunk chunk, int index, string head, string tail, ref int chunksCount)
290         {
291             chunk.Text = head;
292 
293             if (!string.IsNullOrEmpty(tail)) {
294                 var newChunk = chunk.Clone();
295                 newChunk.Text = tail;
296                 fChunks.Insert(index + 1, newChunk);
297                 chunksCount += 1;
298 
299                 ShiftChunks(index + 1, chunksCount);
300             }
301         }
302 
ShiftChunks(int startIndex, int chunksCount)303         private void ShiftChunks(int startIndex, int chunksCount)
304         {
305             for (int m = startIndex; m < chunksCount; m++) {
306                 fChunks[m].Line += 1;
307             }
308         }
309 
DoPaint(Graphics gfx)310         private void DoPaint(Graphics gfx)
311         {
312             try {
313                 //gfx.TextRenderingHint = TextRenderingHint.AntiAlias;
314                 fAcceptFontChange = false;
315                 SolidBrush brush = new SolidBrush(this.ForeColor);
316                 Font font = null;
317                 try {
318                     Rectangle clientRect = ClientRectangle;
319                     gfx.FillRectangle(new SolidBrush(BackColor), clientRect);
320 
321                     var scrollPos = AutoScrollPosition;
322                     int xOffset = fBorderWidth + scrollPos.X;
323                     int yOffset = fBorderWidth + scrollPos.Y;
324                     int lineHeight = 0;
325 
326                     int line = -1;
327                     int chunksCount = fChunks.Count;
328                     for (int k = 0; k < chunksCount; k++) {
329                         BBTextChunk chunk = fChunks[k];
330 
331                         if (line != chunk.Line) {
332                             line = chunk.Line;
333 
334                             xOffset = fBorderWidth + scrollPos.X;
335                             yOffset += lineHeight;
336 
337                             // this condition is dirty hack
338                             if (line >= 0 && line < fHeights.Count) {
339                                 lineHeight = fHeights[line];
340                             }
341                         }
342 
343                         string ct = chunk.Text;
344                         if (!string.IsNullOrEmpty(ct)) {
345                             Color chunkColor = (chunk.Color == null) ? ForeColor : ((ColorHandler)chunk.Color).Handle;
346                             brush.Color = chunkColor;
347                             font = ProcessFont(font, chunk.Size, (SDFontStyle)chunk.Style);
348                             gfx.DrawString(ct, font, brush, xOffset, yOffset, fStrFormat);
349 
350                             xOffset += chunk.Width;
351                         }
352                     }
353                 } finally {
354                     fAcceptFontChange = true;
355                     if (brush != null) brush.Dispose();
356                     if (font != null) font.Dispose();
357                 }
358             } catch (Exception ex) {
359                 Logger.WriteError("HyperView.DoPaint()", ex);
360             }
361         }
362 
ProcessFont(Font prevFont, float emSize, FontStyle style)363         private Font ProcessFont(Font prevFont, float emSize, FontStyle style)
364         {
365             Font result;
366             if (prevFont == null) {
367                 var defFont = this.Font;
368                 result = new Font(defFont.Name, emSize, style, defFont.Unit);
369             } else {
370                 if (prevFont.Size == emSize && prevFont.Style == style) {
371                     result = prevFont;
372                 } else {
373                     result = new Font(prevFont.Name, emSize, style, prevFont.Unit);
374                     prevFont.Dispose();
375                 }
376             }
377             return result;
378         }
379 
DoLink(string linkName)380         private void DoLink(string linkName)
381         {
382             LinkEventHandler eventHandler = (LinkEventHandler)Events[EventLink];
383             if (eventHandler != null) eventHandler(this, linkName);
384         }
385 
386         #region Protected methods
387 
OnFontChanged(EventArgs e)388         protected override void OnFontChanged(EventArgs e)
389         {
390             if (fAcceptFontChange) {
391                 ArrangeText();
392             }
393 
394             base.OnFontChanged(e);
395         }
396 
OnResize(EventArgs e)397         protected override void OnResize(EventArgs e)
398         {
399             ArrangeText();
400 
401             base.OnResize(e);
402         }
403 
OnKeyDown(KeyEventArgs e)404         protected override void OnKeyDown(KeyEventArgs e)
405         {
406             switch (e.KeyCode)
407             {
408                 case Keys.Prior:
409                     AdjustScroll(0, -VerticalScroll.LargeChange);
410                     break;
411 
412                 case Keys.Next:
413                     AdjustScroll(0, VerticalScroll.LargeChange);
414                     break;
415 
416                 case Keys.Home:
417                     AdjustScroll(-HorizontalScroll.Maximum, -VerticalScroll.Maximum);
418                     break;
419 
420                 case Keys.End:
421                     AdjustScroll(-HorizontalScroll.Maximum, VerticalScroll.Maximum);
422                     break;
423 
424                 case Keys.Left:
425                     AdjustScroll(-(e.Modifiers == Keys.None ? HorizontalScroll.SmallChange : HorizontalScroll.LargeChange), 0);
426                     break;
427 
428                 case Keys.Right:
429                     AdjustScroll(e.Modifiers == Keys.None ? HorizontalScroll.SmallChange : HorizontalScroll.LargeChange, 0);
430                     break;
431 
432                 case Keys.Up:
433                     AdjustScroll(0, -(e.Modifiers == Keys.None ? VerticalScroll.SmallChange : VerticalScroll.LargeChange));
434                     break;
435 
436                 case Keys.Down:
437                     AdjustScroll(0, e.Modifiers == Keys.None ? VerticalScroll.SmallChange : VerticalScroll.LargeChange);
438                     break;
439             }
440 
441             base.OnKeyDown(e);
442         }
443 
IsInputKey(Keys keyData)444         protected override bool IsInputKey(Keys keyData)
445         {
446             bool result;
447 
448             if ((keyData & Keys.Right) == Keys.Right || (keyData & Keys.Left) == Keys.Left ||
449                 (keyData & Keys.Up) == Keys.Up || (keyData & Keys.Down) == Keys.Down ||
450                 (keyData & Keys.Prior) == Keys.Prior || (keyData & Keys.Next) == Keys.Next ||
451                 (keyData & Keys.End) == Keys.End || (keyData & Keys.Home) == Keys.Home)
452                 result = true;
453             else
454                 result = base.IsInputKey(keyData);
455 
456             return result;
457         }
458 
OnMouseDown(MouseEventArgs e)459         protected override void OnMouseDown(MouseEventArgs e)
460         {
461             base.OnMouseDown(e);
462 
463             if (fCurrentLink != null) DoLink(fCurrentLink.URL);
464         }
465 
OnMouseMove(MouseEventArgs e)466         protected override void OnMouseMove(MouseEventArgs e)
467         {
468             base.OnMouseMove(e);
469 
470             Point mpt = GetImageRelativeLocation(e.Location);
471             mpt.Offset(-fBorderWidth, -fBorderWidth);
472             fCurrentLink = null;
473 
474             int num = fChunks.Count;
475             for (int i = 0; i < num; i++) {
476                 BBTextChunk chunk = fChunks[i];
477                 if (string.IsNullOrEmpty(chunk.URL)) continue;
478 
479                 if (chunk.HasCoord(mpt.X, mpt.Y)) {
480                     fCurrentLink = chunk;
481                     break;
482                 }
483             }
484 
485             Cursor = (fCurrentLink == null) ? Cursors.Default : Cursors.Hand;
486         }
487 
OnPaint(PaintEventArgs e)488         protected override void OnPaint(PaintEventArgs e)
489         {
490             DoPaint(e.Graphics);
491             base.OnPaint(e);
492         }
493 
494         #endregion
495     }
496 }
497