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