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