Tanoda
TextPic.cs
Go to the documentation of this file.
1
3
4using System;
5using System.Collections.Generic;
6using System.Text;
7using System.Text.RegularExpressions;
10
12 // Image according to the label inside the name attribute to load, read from the Resources directory. The size of the image is controlled by the size property.
13
14 // Use: Add Icon name and sprite to the icons list
15
16 [AddComponentMenu("UI/Extensions/TextPic")]
17
18 [ExecuteInEditMode] // Needed for culling images that are not used //
19 public class TextPic : Text, IPointerClickHandler, IPointerExitHandler, IPointerEnterHandler, ISelectHandler {
20 // Icon entry to replace text with
21 [Serializable]
22 public struct IconName {
23 public string name;
24 public Sprite sprite;
25 public Vector2 offset;
26 public Vector2 scale;
27 }
28
29 // Icons and text to replace
31
32 [Tooltip("Global scaling factor for all images")]
33 public float ImageScalingFactor = 1;
34
35 // Write the name or hex value of the hyperlink color
36 public string hyperlinkColor = "blue";
37
38 // Offset image by x, y
39 public Vector2 imageOffset = Vector2.zero;
40 public bool isCreating_m_HrefInfos = true;
41
42 [Serializable]
43 public class HrefClickEvent : UnityEvent<string> { }
44
45 [SerializeField]
46 private HrefClickEvent m_OnHrefClick = new HrefClickEvent();
47
52 get { return m_OnHrefClick; }
53 set { m_OnHrefClick = value; }
54 }
55
59 private readonly List<Image> m_ImagesPool = new List<Image>();
60 private readonly List<GameObject> culled_ImagesPool = new List<GameObject>();
61
62 // Used for check for culling images
63 private bool clearImages = false;
64
65 // Lock to ensure images get culled properly
66 private Object thisLock = new Object();
67
71 private readonly List<int> m_ImagesVertexIndex = new List<int>();
72
76 private static readonly Regex s_Regex =
77 new Regex(@"<quad name=(.+?) size=(\d*\.?\d+%?) width=(\d*\.?\d+%?) />", RegexOptions.Singleline);
78
82 private static readonly Regex s_HrefRegex =
83 new Regex(@"<a href=([^>\n\s]+)>(.*?)(</a>)", RegexOptions.Singleline);
84
85 // String to create quads
86 private string fixedString;
87
88 // Update the quad images when true
89 private bool updateQuad = false;
90
94 private string m_OutputText;
95
96 private Button button;
97
98 // Used for custom selection as a variable for other scripts
99 private bool selected = false;
100
101 public bool Selected
102 {
103 get { return selected; }
104 set { selected = value; }
105 }
106
107 // Positions of images for icon placement
108 private List<Vector2> positions = new List<Vector2>();
109
110 // Little hack to support multiple hrefs with same name
111 private string previousText = "";
112
116 [Serializable]
117 public class HrefInfo {
118 public int startIndex;
119
120 public int endIndex;
121
122 public string name;
123
124 public readonly List<Rect> boxes = new List<Rect>();
125 }
126
130 private readonly List<HrefInfo> m_HrefInfos = new List<HrefInfo>();
131
135 private static readonly StringBuilder s_TextBuilder = new StringBuilder();
136
137 // Matches for quad tags
138 private MatchCollection matches;
139
140 // Matches for quad tags
141 private MatchCollection href_matches;
142
143 // Matches for removing characters
144 private MatchCollection removeCharacters;
145
146 // Index of current pic
147 private int picIndex;
148
149 // Index of current pic vertex
150 private int vertIndex;
151
155
156 // There is no directive for incremented versions so will have to hack together
157 private bool usesNewRendering = false;
158
159 #if UNITY_2019_1_OR_NEWER
163 private static readonly Regex remove_Regex =
164 new Regex(@"<b>|</b>|<i>|</i>|<size=.*?>|</size>|<color=.*?>|</color>|<material=.*?>|</material>|<quad name=(.+?) size=(\d*\.?\d+%?) width=(\d*\.?\d+%?) />|<a href=([^>\n\s]+)>|</a>|\s", RegexOptions.Singleline);
165
166 // List of indexes that are compared against matches to remove quad tags
167 List<int> indexes = new List<int>();
168
169 // Characters to remove from string for finding the correct index for vertices for images
170 private int charactersRemoved = 0;
171
172 // Characters to remove from string for finding the correct start index for vertices for href bounds
173 private int startCharactersRemoved = 0;
174
175 // Characters to remove from string for finding the correct end index for vertices for href bounds
176 private int endCharactersRemoved = 0;
177 #endif
178
179 // Count of current href
180 private int count = 0;
181
182 // Index of current href
183 private int indexText = 0;
184
185 // Original text temporary variable holder
186 private string originalText;
187
188 // Vertex we are modifying
189 private UIVertex vert;
190
191 // Local Point for Href
192 private Vector2 lp;
193
194
196
197 public void ResetIconList() {
198 Reset_m_HrefInfos ();
199 base.Start();
200 }
201
202 protected void UpdateQuadImage() {
203#if UNITY_EDITOR && !UNITY_2018_3_OR_NEWER
204 if (UnityEditor.PrefabUtility.GetPrefabType(this) == UnityEditor.PrefabType.Prefab) {
205 return;
206 }
207#endif
208 m_OutputText = GetOutputText();
209
210 matches = s_Regex.Matches(m_OutputText);
211
212 if (matches != null && matches.Count > 0) {
213 for (int i = 0; i < matches.Count; i++) {
214 m_ImagesPool.RemoveAll(image => image == null);
215
216 if (m_ImagesPool.Count == 0) {
217 GetComponentsInChildren<Image>(true, m_ImagesPool);
218 }
219
220 if (matches.Count > m_ImagesPool.Count) {
221 DefaultControls.Resources resources = new DefaultControls.Resources();
222 GameObject go = DefaultControls.CreateImage(resources);
223
224 go.layer = gameObject.layer;
225
226 RectTransform rt = go.transform as RectTransform;
227
228 if (rt) {
229 rt.SetParent(rectTransform);
230 rt.anchoredPosition3D = Vector3.zero;
231 rt.localRotation = Quaternion.identity;
232 rt.localScale = Vector3.one;
233 }
234
235 m_ImagesPool.Add(go.GetComponent<Image>());
236 }
237
238 string spriteName = matches[i].Groups[1].Value;
239
240 Image img = m_ImagesPool[i];
241
242 Vector2 imgoffset = Vector2.zero;
243
244 if (img.sprite == null || img.sprite.name != spriteName) {
245 if (inspectorIconList != null && inspectorIconList.Length > 0) {
246 for (int s = 0; s < inspectorIconList.Length; s++) {
247 if (inspectorIconList[s].name == spriteName) {
248 img.sprite = inspectorIconList[s].sprite;
249 img.preserveAspect = true;
250 img.rectTransform.sizeDelta = new Vector2(fontSize * ImageScalingFactor * inspectorIconList[s].scale.x,
251 fontSize * ImageScalingFactor * inspectorIconList[s].scale.y);
252 imgoffset = inspectorIconList[s].offset;
253
254 break;
255 }
256 }
257 }
258 }
259
260 img.enabled = true;
261
262 if (positions.Count > 0 && i < positions.Count) {
263 img.rectTransform.anchoredPosition = positions[i] += imgoffset;
264 }
265 }
266 }
267 else {
268 // If there are no matches, remove the images from the pool
269 for (int i = m_ImagesPool.Count - 1; i > 0; i--) {
270 if (m_ImagesPool[i]) {
271 if (!culled_ImagesPool.Contains(m_ImagesPool[i].gameObject)) {
272 culled_ImagesPool.Add(m_ImagesPool[i].gameObject);
273 m_ImagesPool.Remove(m_ImagesPool[i]);
274 }
275 }
276 }
277 }
278
279 // Remove any images that are not being used
280 for (int i = m_ImagesPool.Count - 1; i >= matches.Count; i--) {
281 if (i >= 0 && m_ImagesPool.Count > 0) {
282 if (m_ImagesPool[i]) {
283 if (!culled_ImagesPool.Contains(m_ImagesPool[i].gameObject)) {
284 culled_ImagesPool.Add(m_ImagesPool[i].gameObject);
285 m_ImagesPool.Remove(m_ImagesPool[i]);
286 }
287 }
288 }
289 }
290
291 // Clear the images when it is safe to do so
292 if (culled_ImagesPool.Count > 0) {
293 clearImages = true;
294 }
295 }
296
297 // Reseting m_HrefInfos array if there is any change in text
298 void Reset_m_HrefInfos () {
299 previousText = text;
300
301 m_HrefInfos.Clear();
302
304 }
305
310 protected string GetOutputText() {
311 s_TextBuilder.Length = 0;
312
313 indexText = 0;
314
315 fixedString = this.text;
316
317 if (inspectorIconList != null && inspectorIconList.Length > 0) {
318 for (int i = 0; i < inspectorIconList.Length; i++) {
319 if (!string.IsNullOrEmpty(inspectorIconList[i].name)) {
320 fixedString = fixedString.Replace(inspectorIconList[i].name,
321 "<quad name=" + inspectorIconList[i].name + " size=" + fontSize + " width=1 />");
322 }
323 }
324 }
325
326 count = 0;
327
328 href_matches = s_HrefRegex.Matches(fixedString);
329
330 if (href_matches != null && href_matches.Count > 0) {
331 for (int i = 0; i < href_matches.Count; i++ ) {
332 s_TextBuilder.Append(fixedString.Substring(indexText, href_matches[i].Index - indexText));
333 s_TextBuilder.Append("<color=" + hyperlinkColor + ">"); // Hyperlink color
334
335 var group = href_matches[i].Groups[1];
336
338 HrefInfo hrefInfo = new HrefInfo {
339 // Hyperlinks in text starting index
340 startIndex = (usesNewRendering ? s_TextBuilder.Length : s_TextBuilder.Length * 4),
341 endIndex = (usesNewRendering ? (s_TextBuilder.Length + href_matches[i].Groups[2].Length - 1) : (s_TextBuilder.Length + href_matches[i].Groups[2].Length - 1) * 4 + 3),
342 name = group.Value
343 };
344
345 m_HrefInfos.Add(hrefInfo);
346 }
347 else {
348 if (count <= m_HrefInfos.Count - 1) {
349 // Hyperlinks in text starting index
350 m_HrefInfos[count].startIndex = (usesNewRendering ? s_TextBuilder.Length : s_TextBuilder.Length * 4);
351 m_HrefInfos[count].endIndex = (usesNewRendering ? (s_TextBuilder.Length + href_matches[i].Groups[2].Length - 1) : (s_TextBuilder.Length + href_matches[i].Groups[2].Length - 1) * 4 + 3);
352
353 count++;
354 }
355 }
356
357 s_TextBuilder.Append(href_matches[i].Groups[2].Value);
358 s_TextBuilder.Append("</color>");
359
360 indexText = href_matches[i].Index + href_matches[i].Length;
361 }
362 }
363
364 // we should create array only once or if there is any change in the text
367
368 s_TextBuilder.Append(fixedString.Substring(indexText, fixedString.Length - indexText));
369
370 m_OutputText = s_TextBuilder.ToString();
371
372 m_ImagesVertexIndex.Clear();
373
374 matches = s_Regex.Matches(m_OutputText);
375
376 #if UNITY_2019_1_OR_NEWER
377 href_matches = s_HrefRegex.Matches(m_OutputText);
378
379 indexes.Clear();
380
381 for (int r = 0; r < matches.Count; r++) {
382 indexes.Add(matches[r].Index);
383 }
384 #endif
385
386
387 if (matches != null && matches.Count > 0) {
388 for (int i = 0; i < matches.Count; i++) {
389 picIndex = matches[i].Index;
390
391 #if UNITY_2019_1_OR_NEWER
392 if (usesNewRendering) {
393 charactersRemoved = 0;
394
395 removeCharacters = remove_Regex.Matches(m_OutputText);
396
397 for (int r = 0; r < removeCharacters.Count; r++) {
398 if (removeCharacters[r].Index < picIndex && !indexes.Contains(removeCharacters[r].Index)) {
399 charactersRemoved += removeCharacters[r].Length;
400 }
401 }
402
403 for (int r = 0; r < i; r++) {
404 charactersRemoved += (matches[r].Length - 1);
405 }
406
407 picIndex -= charactersRemoved;
408 }
409 #endif
410
411 vertIndex = picIndex * 4 + 3;
412
413 m_ImagesVertexIndex.Add(vertIndex);
414 }
415 }
416
417 #if UNITY_2019_1_OR_NEWER
418 if (usesNewRendering) {
419 if (m_HrefInfos != null && m_HrefInfos.Count > 0) {
420 for (int i = 0; i < m_HrefInfos.Count; i++) {
421 startCharactersRemoved = 0;
422 endCharactersRemoved = 0;
423
424 removeCharacters = remove_Regex.Matches(m_OutputText);
425
426 for (int r = 0; r < removeCharacters.Count; r++) {
427 if (removeCharacters[r].Index < m_HrefInfos[i].startIndex && !indexes.Contains(removeCharacters[r].Index)) {
428 startCharactersRemoved += removeCharacters[r].Length;
429 }
430 else if (removeCharacters[r].Index < m_HrefInfos[i].startIndex && indexes.Contains(removeCharacters[r].Index)) {
431 startCharactersRemoved += removeCharacters[r].Length - 1;
432 }
433
434 if (removeCharacters[r].Index < m_HrefInfos[i].endIndex && !indexes.Contains(removeCharacters[r].Index)) {
435 endCharactersRemoved += removeCharacters[r].Length;
436 }
437 else if (removeCharacters[r].Index < m_HrefInfos[i].endIndex && indexes.Contains(removeCharacters[r].Index)) {
438 endCharactersRemoved += removeCharacters[r].Length - 1;
439 }
440 }
441
442 m_HrefInfos[i].startIndex -= startCharactersRemoved;
443
444 m_HrefInfos[i].startIndex = m_HrefInfos[i].startIndex * 4;
445
446 m_HrefInfos[i].endIndex -= endCharactersRemoved;
447
448 m_HrefInfos[i].endIndex = m_HrefInfos[i].endIndex * 4 + 3;
449 }
450 }
451 }
452 #endif
453
454 return m_OutputText;
455 }
456
457 // Process href links to open them as a url, can override this function for custom functionality
458 public virtual void OnHrefClick(string hrefName) {
459 Application.OpenURL(hrefName);
460
461 // Debug.Log(hrefName);
462 }
463
464
466
467 protected override void OnPopulateMesh(VertexHelper toFill) {
468 originalText = m_Text;
469 m_Text = GetOutputText();
470
471 base.OnPopulateMesh(toFill);
472
473 m_DisableFontTextureRebuiltCallback = true;
474
475 m_Text = originalText;
476
477 positions.Clear();
478
479 vert = new UIVertex();
480
481 for (int i = 0; i < m_ImagesVertexIndex.Count; i++) {
482 int endIndex = m_ImagesVertexIndex[i];
483
484 if (endIndex < toFill.currentVertCount) {
485 toFill.PopulateUIVertex(ref vert, endIndex);
486
487 positions.Add(new Vector2((vert.position.x + fontSize / 2), (vert.position.y + fontSize / 2)) + imageOffset);
488
489 // Erase the lower left corner of the black specks
490 toFill.PopulateUIVertex(ref vert, endIndex - 3);
491
492 Vector3 pos = vert.position;
493
494 for (int j = endIndex, m = endIndex - 3; j > m; j--) {
495 toFill.PopulateUIVertex(ref vert, endIndex);
496 vert.position = pos;
497 toFill.SetUIVertex(vert, j);
498 }
499 }
500 }
501
502 // Hyperlinks surround processing box
503 for (int h = 0; h < m_HrefInfos.Count; h++) {
504 m_HrefInfos[h].boxes.Clear();
505
506 if (m_HrefInfos[h].startIndex >= toFill.currentVertCount) {
507 continue;
508 }
509
510 // Hyperlink inside the text is added to surround the vertex index coordinate frame
511 toFill.PopulateUIVertex(ref vert, m_HrefInfos[h].startIndex);
512
513 Vector3 pos = vert.position;
514
515 Bounds bounds = new Bounds(pos, Vector3.zero);
516
517 for (int i = m_HrefInfos[h].startIndex, m = m_HrefInfos[h].endIndex; i < m; i++) {
518 if (i >= toFill.currentVertCount) {
519 break;
520 }
521
522 toFill.PopulateUIVertex(ref vert, i);
523
524 pos = vert.position;
525
526 // Wrap re-add surround frame
527 if (pos.x < bounds.min.x) {
528 m_HrefInfos[h].boxes.Add(new Rect(bounds.min, bounds.size));
529 bounds = new Bounds(pos, Vector3.zero);
530 }
531 else {
532 bounds.Encapsulate(pos); // Extended enclosed box
533 }
534 }
535
536 m_HrefInfos[h].boxes.Add(new Rect(bounds.min, bounds.size));
537 }
538
539 // Update the quad images
540 updateQuad = true;
541
542 m_DisableFontTextureRebuiltCallback = false;
543 }
544
549 public void OnPointerClick(PointerEventData eventData) {
550 RectTransformUtility.ScreenPointToLocalPointInRectangle(
551 rectTransform, eventData.position, eventData.pressEventCamera, out lp);
552
553 for (int h = 0; h < m_HrefInfos.Count; h++) {
554 for (int i = 0; i < m_HrefInfos[h].boxes.Count; ++i) {
555 if (m_HrefInfos[h].boxes[i].Contains(lp)) {
556 m_OnHrefClick.Invoke(m_HrefInfos[h].name);
557 return;
558 }
559 }
560 }
561 }
562
563 public void OnPointerEnter(PointerEventData eventData) {
564 //do your stuff when highlighted
565 selected = true;
566
567 if (m_ImagesPool.Count >= 1) {
568 for (int i = 0; i < m_ImagesPool.Count; i++) {
569 if (button != null && button.isActiveAndEnabled) {
570 m_ImagesPool[i].color = button.colors.highlightedColor;
571 }
572 }
573 }
574 }
575
576 public void OnPointerExit(PointerEventData eventData) {
577 //do your stuff when highlighted
578 selected = false;
579
580 if (m_ImagesPool.Count >= 1) {
581 for (int i = 0; i < m_ImagesPool.Count; i++) {
582 if (button != null && button.isActiveAndEnabled) {
583 m_ImagesPool[i].color = button.colors.normalColor;
584 }
585 else {
586 m_ImagesPool[i].color = color;
587 }
588 }
589 }
590 }
591
592 public void OnSelect(BaseEventData eventData) {
593 //do your stuff when selected
594 selected = true;
595
596 if (m_ImagesPool.Count >= 1) {
597 for (int i = 0; i < m_ImagesPool.Count; i++) {
598 if (button != null && button.isActiveAndEnabled) {
599 m_ImagesPool[i].color = button.colors.highlightedColor;
600 }
601 }
602 }
603 }
604
605 public void OnDeselect(BaseEventData eventData) {
606 //do your stuff when selected
607 selected = false;
608
609 if (m_ImagesPool.Count >= 1) {
610 for (int i = 0; i < m_ImagesPool.Count; i++) {
611 if (button != null && button.isActiveAndEnabled) {
612 m_ImagesPool[i].color = button.colors.normalColor;
613 }
614 }
615 }
616 }
617
618
619 public override void SetVerticesDirty() {
620 base.SetVerticesDirty();
621
622 // Update the quad images
623 updateQuad = true;
624 }
625
626#if UNITY_EDITOR
627 protected override void OnValidate() {
628 base.OnValidate();
629
630 // Update the quad images
631 updateQuad = true;
632
633 if (inspectorIconList != null) {
634 for (int i = 0; i < inspectorIconList.Length; i++) {
635 if (inspectorIconList[i].scale == Vector2.zero) {
636 inspectorIconList[i].scale = Vector2.one;
637 }
638 }
639 }
640 }
641#endif
642
643 protected override void OnEnable() {
644 #if UNITY_2019_1_OR_NEWER
645 // Here is the hack to see if Unity is using the new rendering system for text
646 usesNewRendering = false;
647
648 if (Application.unityVersion.StartsWith("2019.1.")) {
649 if (!Char.IsDigit(Application.unityVersion[8])) {
650 int number = Convert.ToInt32(Application.unityVersion[7].ToString());
651
652 if (number > 4) {
653 usesNewRendering = true;
654 }
655 }
656 else {
657 usesNewRendering = true;
658 }
659 }
660 else {
661 usesNewRendering = true;
662 }
663 #endif
664
665 base.OnEnable();
666
667 supportRichText = true;
668 alignByGeometry = true;
669
670 // Enable images on TextPic disable
671 if (m_ImagesPool.Count >= 1) {
672 for (int i = 0; i < m_ImagesPool.Count; i++) {
673 if(m_ImagesPool[i] != null) {
674 m_ImagesPool[i].enabled = true;
675 }
676 }
677 }
678
679 // Update the quads on re-enable
680 updateQuad = true;
681
682 this.onHrefClick.AddListener(OnHrefClick);
683 }
684
685 protected override void OnDisable() {
686 base.OnDisable();
687
688 // Disable images on TextPic disable
689 if (m_ImagesPool.Count >= 1) {
690 for (int i = 0; i < m_ImagesPool.Count; i++) {
691 if (m_ImagesPool[i] != null) {
692 m_ImagesPool[i].enabled = false;
693 }
694 }
695 }
696
697 this.onHrefClick.RemoveListener(OnHrefClick);
698 }
699
700 new void Start() {
701 button = GetComponent<Button>();
703 }
704
705 void LateUpdate() {
706 // Reset the hrefs if text is changed
707 if (previousText != text) {
708 Reset_m_HrefInfos();
709
710 // Update the quad on text change
711 updateQuad = true;
712 }
713
714 // Need to lock to remove images properly
715 lock (thisLock) {
716 // Can only update the images when it is not in a rebuild, this prevents the error
717 if (updateQuad) {
719 updateQuad = false;
720 }
721
722 // Destroy any images that are not in use
723 if (clearImages) {
724 for (int i = 0; i < culled_ImagesPool.Count; i++) {
725 DestroyImmediate(culled_ImagesPool[i]);
726 }
727
728 culled_ImagesPool.Clear();
729
730 clearImages = false;
731 }
732 }
733 }
734 }
735}
UnityEngine.UI.Button Button
Definition: Pointer.cs:7
System.Drawing.Image Image
Definition: TestScript.cs:37
void OnPointerClick(PointerEventData eventData)
Click event is detected whether to click a hyperlink text
Definition: TextPic.cs:549
void ResetIconList()
METHODS ///.
Definition: TextPic.cs:197
void OnPointerExit(PointerEventData eventData)
Definition: TextPic.cs:576
string GetOutputText()
Finally, the output text hyperlinks get parsed
Definition: TextPic.cs:310
override void OnPopulateMesh(VertexHelper toFill)
UNITY METHODS ///.
Definition: TextPic.cs:467
void OnDeselect(BaseEventData eventData)
Definition: TextPic.cs:605
override void SetVerticesDirty()
Definition: TextPic.cs:619
void OnSelect(BaseEventData eventData)
Definition: TextPic.cs:592
HrefClickEvent onHrefClick
Hyperlink Click Event
Definition: TextPic.cs:51
void OnPointerEnter(PointerEventData eventData)
Definition: TextPic.cs:563
virtual void OnHrefClick(string hrefName)
Definition: TextPic.cs:458
Credit Erdener Gonenc - @PixelEnvision.
UnityEngine.Object Object