1 //
2 // PathBar.cs
3 //
4 // Author:
5 //       Michael Hutchinson <mhutchinson@novell.com>
6 //
7 // Copyright (c) 2010 Novell, Inc. (http://www.novell.com)
8 //
9 // Permission is hereby granted, free of charge, to any person obtaining a copy
10 // of this software and associated documentation files (the "Software"), to deal
11 // in the Software without restriction, including without limitation the rights
12 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 // copies of the Software, and to permit persons to whom the Software is
14 // furnished to do so, subject to the following conditions:
15 //
16 // The above copyright notice and this permission notice shall be included in
17 // all copies or substantial portions of the Software.
18 //
19 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 // THE SOFTWARE.
26 
27 using System;
28 using System.Collections.Generic;
29 using System.Linq;
30 
31 using Gtk;
32 using Gdk;
33 using Pinta.Docking;
34 using Pinta.Docking.Gui;
35 
36 namespace MonoDevelop.Components
37 {
38 	enum EntryPosition
39 	{
40 		Left,
41 		Right
42 	}
43 
44 	class PathEntry
45 	{
46 		Gdk.Pixbuf darkIcon;
47 
48 		public Gdk.Pixbuf Icon {
49 			get;
50 			private set;
51 		}
52 
53 		public string Markup {
54 			get;
55 			private set;
56 		}
57 
58 		public object Tag {
59 			get;
60 			set;
61 		}
62 
63 		public bool IsPathEnd {
64 			get;
65 			set;
66 		}
67 
68 		public EntryPosition Position {
69 			get;
70 			set;
71 		}
72 
PathEntry(Gdk.Pixbuf icon, string markup)73         public PathEntry (Gdk.Pixbuf icon, string markup)
74 		{
75 			this.Icon = icon;
76 			this.Markup = markup;
77 		}
78 
PathEntry(string markup)79 		public PathEntry (string markup)
80 		{
81 			this.Markup = markup;
82 		}
83 
Equals(object obj)84 		public override bool Equals (object obj)
85 		{
86 			if (obj == null)
87 				return false;
88 			if (ReferenceEquals (this, obj))
89 				return true;
90 			if (obj.GetType () != typeof(PathEntry))
91 				return false;
92 			MonoDevelop.Components.PathEntry other = (MonoDevelop.Components.PathEntry)obj;
93 			return Icon == other.Icon && Markup == other.Markup;
94 		}
95 
GetHashCode()96 		public override int GetHashCode ()
97 		{
98 			unchecked {
99 				return (Icon != null ? Icon.GetHashCode () : 0) ^ (Markup != null ? Markup.GetHashCode () : 0);
100 			}
101 		}
102 
103 		internal Gdk.Pixbuf DarkIcon {
104 			get {
105 				if (darkIcon == null && Icon != null) {
106 					darkIcon = Icon;
107 /*					if (Styles.BreadcrumbGreyscaleIcons)
108 						darkIcon = ImageService.MakeGrayscale (darkIcon);
109 					if (Styles.BreadcrumbInvertedIcons)
110 						darkIcon = ImageService.MakeInverted (darkIcon);*/
111 				}
112 				return darkIcon;
113 			}
114 		}
115 	}
116 
117 	class PathBar : Gtk.DrawingArea
118 	{
119 		PathEntry[] leftPath  = new PathEntry[0];
120 		PathEntry[] rightPath = new PathEntry[0];
121 		Pango.Layout layout;
122 		Pango.AttrList boldAtts = new Pango.AttrList ();
123 
124         const char CR = (char)0x0D;
125         const char LF = (char)0x0A;
126 
127 		//HACK: a surrogate widget object to pass to style calls instead of "this" when using "button" hint.
128 		// This avoids GTK-Criticals in themes which try to cast the widget object to a button.
129 		Gtk.Button styleButton = new Gtk.Button ();
130 
131 		// The widths array contains the widths of the items at the left and the right
132 		int[] widths;
133 
134 		int height;
135 		int textHeight;
136 
137 		bool pressed, hovering, menuVisible;
138 		int hoverIndex = -1;
139 		int activeIndex = -1;
140 
141 		const int leftPadding = 6;
142 		const int rightPadding = 6;
143 		const int topPadding = 2;
144 		const int bottomPadding = 4;
145 		const int iconSpacing = 4;
146 		const int padding = 3;
147 		const int buttonPadding = 2;
148 		const int arrowLeftPadding = 10;
149 		const int arrowRightPadding = 10;
150 		const int arrowSize = 6;
151 		const int spacing = arrowLeftPadding + arrowRightPadding + arrowSize;
152 		const int minRegionSelectorWidth = 30;
153 
154 		Func<int, Widget> createMenuForItem;
155 		Widget menuWidget;
156 
PathBar(Func<int, Widget> createMenuForItem)157 		public PathBar (Func<int, Widget> createMenuForItem)
158 		{
159 			this.Events =  EventMask.ExposureMask |
160 				           EventMask.EnterNotifyMask |
161 				           EventMask.LeaveNotifyMask |
162 				           EventMask.ButtonPressMask |
163 				           EventMask.ButtonReleaseMask |
164 				           EventMask.KeyPressMask |
165 					       EventMask.PointerMotionMask;
166 			boldAtts.Insert (new Pango.AttrWeight (Pango.Weight.Bold));
167 			this.createMenuForItem = createMenuForItem;
168 			EnsureLayout ();
169 		}
170 
GetFirstLineFromMarkup(string markup)171 		internal static string GetFirstLineFromMarkup (string markup)
172 		{
173 			var idx = markup.IndexOfAny (new [] { CR, LF });
174 			if (idx >= 0)
175 				return markup.Substring (0, idx);
176 			return markup;
177 		}
178 
179 		public new PathEntry[] Path { get; private set; }
180 		public int ActiveIndex { get { return activeIndex; } }
181 
SetPath(PathEntry[] path)182 		public void SetPath (PathEntry[] path)
183 		{
184 			if (ArrSame (this.leftPath, path))
185 				return;
186 
187 			HideMenu ();
188 
189 			this.Path = path ?? new PathEntry[0];
190 			this.leftPath = Path.Where (p => p.Position == EntryPosition.Left).ToArray ();
191 			this.rightPath = Path.Where (p => p.Position == EntryPosition.Right).ToArray ();
192 
193 			activeIndex = -1;
194 			widths = null;
195 			EnsureWidths ();
196 			QueueResize ();
197 		}
198 
ArrSame(PathEntry[] a, PathEntry[] b)199 		bool ArrSame (PathEntry[] a, PathEntry[] b)
200 		{
201 			if ((a == null || b == null) && a != b)
202 				return false;
203 			if (a.Length != b.Length)
204 				return false;
205 			for (int i = 0; i < a.Length; i++)
206 				if (!a[i].Equals(b[i]))
207 					return false;
208 			return true;
209 		}
210 
SetActive(int index)211 		public void SetActive (int index)
212 		{
213 			if (index >= leftPath.Length)
214 				throw new IndexOutOfRangeException ();
215 
216 			if (activeIndex != index) {
217 				activeIndex = index;
218 				widths = null;
219 				QueueResize ();
220 			}
221 		}
222 
OnSizeRequested(ref Requisition requisition)223 		protected override void OnSizeRequested (ref Requisition requisition)
224 		{
225 			EnsureWidths ();
226 			requisition.Width = Math.Max (WidthRequest, 0);
227 			requisition.Height = height + topPadding + bottomPadding;
228 		}
229 
GetCurrentWidths(out bool widthReduced)230 		int[] GetCurrentWidths (out bool widthReduced)
231 		{
232 			int totalWidth = widths.Sum ();
233 			totalWidth += leftPadding + (arrowSize + arrowRightPadding) * leftPath.Length - 1;
234 			totalWidth += rightPadding + arrowSize * rightPath.Length - 1;
235 			int[] currentWidths = widths;
236 			widthReduced = false;
237 			int overflow = totalWidth - Allocation.Width;
238 			if (overflow > 0) {
239 				currentWidths = ReduceWidths (overflow);
240 				widthReduced = true;
241 			}
242 			return currentWidths;
243 		}
244 
OnExposeEvent(EventExpose evnt)245 		protected override bool OnExposeEvent (EventExpose evnt)
246 		{
247 			using (var ctx = Gdk.CairoHelper.Create (GdkWindow)) {
248 
249 				ctx.Rectangle (0, 0, Allocation.Width, Allocation.Height);
250 				using (var g = new Cairo.LinearGradient (0, 0, 0, Allocation.Height)) {
251 					g.AddColorStop (0, Styles.BreadcrumbBackgroundColor);
252 					g.AddColorStop (1, Styles.BreadcrumbGradientEndColor);
253 					ctx.SetSource (g);
254 				}
255 				ctx.Fill ();
256 
257 				if (widths == null)
258 					return true;
259 
260 				// Calculate the total required with, and the reduction to be applied in case it doesn't fit the available space
261 
262 				bool widthReduced;
263 				var currentWidths = GetCurrentWidths (out widthReduced);
264 
265 				// Render the paths
266 
267 				int textTopPadding = topPadding + (height - textHeight) / 2;
268 				int xpos = leftPadding, ypos = topPadding;
269 
270 				for (int i = 0; i < leftPath.Length; i++) {
271 					bool last = i == leftPath.Length - 1;
272 
273 					// Reduce the item size when required
274 					int itemWidth = currentWidths [i];
275 					int x = xpos;
276 					xpos += itemWidth;
277 
278 					if (hoverIndex >= 0 && hoverIndex < Path.Length && leftPath [i] == Path [hoverIndex] && (menuVisible || pressed || hovering))
279 						DrawButtonBorder (ctx, x - padding, itemWidth + padding + padding);
280 
281 					int textOffset = 0;
282 					if (leftPath [i].DarkIcon != null) {
283 						int iy = (height - (int)leftPath [i].DarkIcon.Height) / 2 + topPadding;
284 						ctx.DrawImage (this, leftPath [i].DarkIcon, x, iy);
285 						textOffset += (int) leftPath [i].DarkIcon.Width + iconSpacing;
286 					}
287 
288 					layout.Attributes = (i == activeIndex) ? boldAtts : null;
289 					layout.SetMarkup (GetFirstLineFromMarkup (leftPath [i].Markup));
290 
291 					ctx.Save ();
292 
293 					// If size is being reduced, ellipsize it
294 					bool showText = true;
295 					if (widthReduced) {
296 						int w = itemWidth - textOffset;
297 						if (w > 0) {
298 							ctx.Rectangle (x + textOffset, textTopPadding, w, height);
299 							ctx.Clip ();
300 						} else
301 							showText = false;
302 					} else
303 						layout.Width = -1;
304 
305 					if (showText) {
306 						// Text
307 						ctx.SetSourceColor (Styles.BreadcrumbTextColor.ToCairoColor ());
308 						ctx.MoveTo (x + textOffset, textTopPadding);
309 						Pango.CairoHelper.ShowLayout (ctx, layout);
310 					}
311 					ctx.Restore ();
312 
313 					if (!last) {
314 						xpos += arrowLeftPadding;
315 						if (leftPath [i].IsPathEnd) {
316 							Style.PaintVline (Style, GdkWindow, State, evnt.Area, this, "", ypos, ypos + height, xpos - arrowSize / 2);
317 						} else {
318 							int arrowH = Math.Min (height, arrowSize);
319 							int arrowY = ypos + (height - arrowH) / 2;
320 							DrawPathSeparator (ctx, xpos, arrowY, arrowH);
321 						}
322 						xpos += arrowSize + arrowRightPadding;
323 					}
324 				}
325 
326 				int xposRight = Allocation.Width - rightPadding;
327 				for (int i = 0; i < rightPath.Length; i++) {
328 					//				bool last = i == rightPath.Length - 1;
329 
330 					// Reduce the item size when required
331 					int itemWidth = currentWidths [i + leftPath.Length];
332 					xposRight -= itemWidth;
333 					xposRight -= arrowSize;
334 
335 					int x = xposRight;
336 
337 					if (hoverIndex >= 0 && hoverIndex < Path.Length && rightPath [i] == Path [hoverIndex] && (menuVisible || pressed || hovering))
338 						DrawButtonBorder (ctx, x - padding, itemWidth + padding + padding);
339 
340 					int textOffset = 0;
341 					if (rightPath [i].DarkIcon != null) {
342 						ctx.DrawImage (this, rightPath [i].DarkIcon, x, ypos);
343 						textOffset += (int) rightPath [i].DarkIcon.Width + padding;
344 					}
345 
346 					layout.Attributes = (i == activeIndex) ? boldAtts : null;
347 					layout.SetMarkup (GetFirstLineFromMarkup (rightPath [i].Markup));
348 
349 					ctx.Save ();
350 
351 					// If size is being reduced, ellipsize it
352 					bool showText = true;
353 					if (widthReduced) {
354 						int w = itemWidth - textOffset;
355 						if (w > 0) {
356 							ctx.Rectangle (x + textOffset, textTopPadding, w, height);
357 							ctx.Clip ();
358 						} else
359 							showText = false;
360 					} else
361 						layout.Width = -1;
362 
363 					if (showText) {
364 						// Text
365 						ctx.SetSourceColor (Styles.BreadcrumbTextColor.ToCairoColor ());
366 						ctx.MoveTo (x + textOffset, textTopPadding);
367 						Pango.CairoHelper.ShowLayout (ctx, layout);
368 					}
369 
370 					ctx.Restore ();
371 				}
372 
373 				ctx.MoveTo (0, Allocation.Height - 0.5);
374 				ctx.RelLineTo (Allocation.Width, 0);
375 				ctx.SetSourceColor (Styles.BreadcrumbBottomBorderColor);
376 				ctx.LineWidth = 1;
377 				ctx.Stroke ();
378 			}
379 
380 			return true;
381 		}
382 
DrawPathSeparator(Cairo.Context ctx, double x, double y, double size)383 		void DrawPathSeparator (Cairo.Context ctx, double x, double y, double size)
384 		{
385 			ctx.MoveTo (x, y);
386 			ctx.LineTo (x + arrowSize, y + size / 2);
387 			ctx.LineTo (x, y + size);
388 			ctx.ClosePath ();
389 			ctx.SetSourceColor (CairoExtensions.ColorShade (Style.Dark (State).ToCairoColor (), 0.6));
390 			ctx.Fill ();
391 		}
392 
DrawButtonBorder(Cairo.Context ctx, double x, double width)393 		void DrawButtonBorder (Cairo.Context ctx, double x, double width)
394 		{
395 			x -= buttonPadding;
396 			width += buttonPadding;
397 			double y = topPadding - buttonPadding;
398 			double height = Allocation.Height - topPadding - bottomPadding + buttonPadding * 2;
399 
400 			ctx.Rectangle (x, y, width, height);
401 			ctx.SetSourceColor (Styles.BreadcrumbButtonFillColor);
402 			ctx.Fill ();
403 
404 			ctx.Rectangle (x + 0.5, y + 0.5, width - 1, height - 1);
405 			ctx.SetSourceColor (Styles.BreadcrumbButtonBorderColor);
406 			ctx.LineWidth = 1;
407 			ctx.Stroke ();
408 		}
409 
ReduceWidths(int overflow)410 		int[] ReduceWidths (int overflow)
411 		{
412 			int minItemWidth = 30;
413 			int[] currentWidths = new int[widths.Length];
414 			Array.Copy (widths, currentWidths, widths.Length);
415 			int itemsToShrink = widths.Count (i => i > minItemWidth);
416 			while (overflow > 0 && itemsToShrink > 0) {
417 				int itemSizeReduction = overflow / itemsToShrink;
418 				if (itemSizeReduction == 0)
419 					itemSizeReduction = 1;
420 				int reduced = 0;
421 				for (int n = 0; n < widths.Length && reduced < overflow; n++) {
422 					if (currentWidths [n] > minItemWidth) {
423 						var nw = currentWidths [n] - itemSizeReduction;
424 						if (nw <= minItemWidth) {
425 							nw = minItemWidth;
426 							itemsToShrink--;
427 						}
428 						reduced += currentWidths [n] - nw;
429 						currentWidths [n] = nw;
430 					}
431 				}
432 				overflow -= reduced;
433 			}
434 			return currentWidths;
435 		}
436 
OnButtonPressEvent(EventButton evnt)437 		protected override bool OnButtonPressEvent (EventButton evnt)
438 		{
439 			HideMenu ();
440 			if (hovering) {
441 				pressed = true;
442 				QueueDraw ();
443 			}
444 			return true;
445 		}
446 
OnButtonReleaseEvent(EventButton evnt)447 		protected override bool OnButtonReleaseEvent (EventButton evnt)
448 		{
449 			pressed = false;
450 			if (hovering) {
451 				QueueDraw ();
452 				ShowMenu ();
453 			}
454 			return true;
455 		}
456 
ShowMenu()457 		void ShowMenu ()
458 		{
459 			if (hoverIndex < 0)
460 				return;
461 
462 			HideMenu ();
463 
464 			menuWidget = createMenuForItem (hoverIndex);
465 			if (menuWidget == null)
466 				return;
467 			menuWidget.Hidden += delegate {
468 
469 				menuVisible = false;
470 				QueueDraw ();
471 
472 				//FIXME: for some reason the menu's children don't get activated if we destroy
473 				//directly here, so use a timeout to delay it
474 				GLib.Timeout.Add (100, delegate {
475 					HideMenu ();
476 					return false;
477 				});
478 			};
479 			menuVisible = true;
480 			if (menuWidget is Menu) {
481 				((Menu)menuWidget).Popup (null, null, PositionFunc, 0, Gtk.Global.CurrentEventTime);
482 			} else {
483 				PositionWidget (menuWidget);
484 				menuWidget.ShowAll ();
485 			}
486 		}
487 
HideMenu()488 		public void HideMenu ()
489 		{
490 			if (menuWidget != null) {
491 				menuWidget.Destroy ();
492 				menuWidget = null;
493 			}
494 		}
495 
GetHoverXPosition(out int w)496 		public int GetHoverXPosition (out int w)
497 		{
498 			bool widthReduced;
499 			int[] currentWidths = GetCurrentWidths (out widthReduced);
500 
501 			if (Path[hoverIndex].Position == EntryPosition.Left) {
502 				int idx = leftPath.TakeWhile (p => p != Path[hoverIndex]).Count ();
503 
504 				if (idx >= 0) {
505 					w = currentWidths[idx];
506 					return currentWidths.Take (idx).Sum () + idx * spacing;
507 				}
508 			} else {
509 				int idx = rightPath.TakeWhile (p => p != Path[hoverIndex]).Count ();
510 				if (idx >= 0) {
511 					w = currentWidths[idx + leftPath.Length];
512 					return Allocation.Width - padding - currentWidths[idx + leftPath.Length] - spacing;
513 				}
514 			}
515 			w = Allocation.Width;
516 			return 0;
517 		}
518 
PositionWidget(Gtk.Widget widget)519 		void PositionWidget (Gtk.Widget widget)
520 		{
521 			if (!(widget is Gtk.Window))
522 				return;
523 			int ox, oy;
524 			ParentWindow.GetOrigin (out ox, out oy);
525 			int w;
526 			int itemXPosition = GetHoverXPosition (out w);
527 			int dx = ox + this.Allocation.X + itemXPosition;
528 			int dy = oy + this.Allocation.Bottom;
529 
530 			var req = widget.SizeRequest ();
531 
532 			Gdk.Rectangle geometry = GtkWorkarounds.GetUsableMonitorGeometry (Screen, Screen.GetMonitorAtPoint (dx, dy));
533 			int width = System.Math.Max (req.Width, w);
534 			if (width >= geometry.Width - spacing * 2) {
535 				width = geometry.Width - spacing * 2;
536 				dx = geometry.Left + spacing;
537 			}
538 			widget.WidthRequest = width;
539 			if (dy + req.Height > geometry.Bottom)
540 				dy = oy + this.Allocation.Y - req.Height;
541 			if (dx + width > geometry.Right)
542 				dx = geometry.Right - width;
543 			(widget as Gtk.Window).Move (dx, dy);
544 			(widget as Gtk.Window).Resize (width, req.Height);
545 			widget.GrabFocus ();
546 		}
547 
548 
549 
PositionFunc(Menu mn, out int x, out int y, out bool push_in)550 		void PositionFunc (Menu mn, out int x, out int y, out bool push_in)
551 		{
552 			this.GdkWindow.GetOrigin (out x, out y);
553 			int w;
554 			var rect = this.Allocation;
555 			y += rect.Height;
556 			x += GetHoverXPosition (out w);
557 			//if the menu would be off the bottom of the screen, "drop" it upwards
558 			if (y + mn.Requisition.Height > this.Screen.Height) {
559 				y -= mn.Requisition.Height;
560 				y -= rect.Height;
561 			}
562 
563 			//let GTK reposition the button if it still doesn't fit on the screen
564 			push_in = true;
565 		}
566 
OnMotionNotifyEvent(EventMotion evnt)567 		protected override bool OnMotionNotifyEvent (EventMotion evnt)
568 		{
569 			SetHover (GetItemAt ((int)evnt.X, (int)evnt.Y));
570 			return true;
571 		}
572 
OnLeaveNotifyEvent(EventCrossing evnt)573 		protected override bool OnLeaveNotifyEvent (EventCrossing evnt)
574 		{
575 			pressed = false;
576 			SetHover (-1);
577 			return true;
578 		}
579 
OnEnterNotifyEvent(EventCrossing evnt)580 		protected override bool OnEnterNotifyEvent (EventCrossing evnt)
581 		{
582 			SetHover (GetItemAt ((int)evnt.X, (int)evnt.Y));
583 			return true;
584 		}
585 
SetHover(int i)586 		void SetHover (int i)
587 		{
588 			bool oldHovering = hovering;
589 			hovering = i > -1;
590 
591 			if (hoverIndex != i || oldHovering != hovering) {
592 				if (hovering)
593 					hoverIndex = i;
594 				QueueDraw ();
595 			}
596 		}
597 
IndexOf(PathEntry entry)598 		public int IndexOf (PathEntry entry)
599 		{
600 			return Path.TakeWhile (p => p != entry).Count ();
601 		}
602 
GetItemAt(int x, int y)603 		int GetItemAt (int x, int y)
604 		{
605 			int xpos = padding, xposRight = Allocation.Width - padding;
606 			if (widths == null || x < xpos || x > xposRight)
607 				return -1;
608 
609 			bool widthReduced;
610 			int[] currentWidths = GetCurrentWidths (out widthReduced);
611 
612 			for (int i = 0; i < rightPath.Length; i++) {
613 				xposRight -= currentWidths[i + leftPath.Length] + spacing;
614 				if (x > xposRight)
615 					return IndexOf (rightPath[i]);
616 			}
617 
618 			for (int i = 0; i < leftPath.Length; i++) {
619 				xpos += currentWidths[i] + spacing;
620 				if (x < xpos)
621 					return IndexOf (leftPath[i]);
622 			}
623 			return -1;
624 		}
625 
EnsureLayout()626 		void EnsureLayout ()
627 		{
628 			if (layout != null)
629 				layout.Dispose ();
630 			layout = new Pango.Layout (PangoContext);
631 		}
632 
CreateWidthArray(int[] result, int index, PathEntry[] path)633 		void CreateWidthArray (int[] result, int index, PathEntry[] path)
634 		{
635 			// Assume that there will be icons of at least 16 pixels. This avoids
636 			// annoying path bar height changes when switching between empty and full paths
637 			int maxIconHeight = 16;
638 
639 			for (int i = 0; i < path.Length; i++) {
640 				layout.Attributes = (i == activeIndex)? boldAtts : null;
641 				layout.SetMarkup (GetFirstLineFromMarkup (path[i].Markup));
642 				layout.Width = -1;
643 				int w, h;
644 				layout.GetPixelSize (out w, out h);
645 				textHeight = Math.Max (h, textHeight);
646 				if (path[i].DarkIcon != null) {
647 					maxIconHeight = Math.Max ((int)path[i].DarkIcon.Height, maxIconHeight);
648 					w += (int)path[i].DarkIcon.Width + iconSpacing;
649 				}
650 				result[i + index] = w;
651 			}
652 			height = Math.Max (height, maxIconHeight);
653 			height = Math.Max (height, textHeight);
654 		}
655 
EnsureWidths()656 		void EnsureWidths ()
657 		{
658 			if (widths != null)
659 				return;
660 
661 			layout.SetText ("#");
662 			int w;
663 			layout.GetPixelSize (out w, out this.height);
664 			textHeight = height;
665 
666 			widths = new int [leftPath.Length + rightPath.Length];
667 			CreateWidthArray (widths, 0, leftPath);
668 			CreateWidthArray (widths, leftPath.Length, rightPath);
669 		}
670 
OnStyleSet(Style previous)671 		protected override void OnStyleSet (Style previous)
672 		{
673 			base.OnStyleSet (previous);
674 			KillLayout ();
675 			EnsureLayout ();
676 		}
677 
KillLayout()678 		void KillLayout ()
679 		{
680 			if (layout == null)
681 				return;
682 			layout.Dispose ();
683 			layout = null;
684 			boldAtts.Dispose ();
685 
686 			widths = null;
687 		}
688 
Destroy()689 		public override void Destroy ()
690 		{
691 			base.Destroy ();
692 			styleButton.Destroy ();
693 			KillLayout ();
694 			this.boldAtts.Dispose ();
695 		}
696 	}
697 }
698