using System; using System.Collections; using System.IO; using System.Reflection; using System.Text; using Gtk; using Pango; namespace MonoDevelop.Components.Diff { public class DiffWidget : VBox { Pango.FontDescription font = null; ScrolledWindow scroller; Table difftable; public double Position { get { return scroller.Vadjustment.Value; } set { scroller.Vadjustment.Value = value; } } public class Options { public string LeftName = null; public string RightName = null; public bool SideBySide = false; public bool LineWrap = false; public bool LineNumbers = true; public string Font = "Mono 9"; } Gdk.Color ColorChanged = new Gdk.Color (0xFF, 0x99, 0x99), ColorChangedHighlight = new Gdk.Color (0xFF, 0xBB, 0xBB), ColorAdded = new Gdk.Color (0xFF, 0xAA, 0x33), ColorAddedHighlight = new Gdk.Color (0xFF, 0xBB, 0x66), ColorRemoved = new Gdk.Color (0x33, 0xAA, 0xFF), ColorRemovedHighlight = new Gdk.Color (0x66, 0xBB, 0xFF), ColorDefault = new Gdk.Color (0xFF, 0xFF, 0xFF), ColorDefaultHighlight = new Gdk.Color (0xEE, 0xEE, 0xCC), ColorGrey = new Gdk.Color (0x99, 0x99, 0x99), ColorBlack = new Gdk.Color (0, 0, 0); public DiffWidget(Diff diff, Options options) : this(Hunkify(diff), options) { } public DiffWidget(Merge merge, Options options) : this(Hunkify(merge), options) { } protected override void OnDestroyed () { if (font != null) { font.Dispose (); font = null; } base.OnDestroyed (); } protected override void OnStyleSet (global::Gtk.Style previous_style) { ColorBlack = this.Style.Text (StateType.Normal); ColorGrey = this.Style.Text (StateType.Insensitive); ColorDefault = this.Style.Base (StateType.Normal); ColorDefaultHighlight = this.Style.Base (StateType.Prelight); base.OnStyleSet (previous_style); } private static Hunk[] Hunkify(IEnumerable e) { ArrayList a = new ArrayList(); foreach (Hunk x in e) a.Add(x); return (Hunk[])a.ToArray(typeof(Hunk)); } private DiffWidget(Hunk[] hunks, Options options) : base(false, 0) { if (hunks == null || hunks.Length == 0 || options == null) throw new ArgumentException(); if (options.SideBySide && options.LeftName != null && options.RightName != null) { HBox filetitles = new HBox(true, 2); PackStart(filetitles, false, false, 2); Label leftlabel = new Label(options.LeftName); Label rightlabel = new Label(options.RightName); filetitles.PackStart(leftlabel); filetitles.PackStart(rightlabel); } HBox centerpanel = new HBox(false, 0); PackStart(centerpanel); scroller = new ScrolledWindow(); centerpanel.PackStart (new OverviewRenderer (this, scroller, hunks, options.SideBySide), false, false, 0); Viewport textviewport = new Viewport(); centerpanel.PackStart(scroller); scroller.Add(textviewport); int nRows = 0; foreach (Hunk hunk in hunks) { if (options.SideBySide) { nRows += hunk.MaxLines(); } else { if (hunk.Same) { nRows += hunk.Original().Count; } else { for (int i = 0; i < hunk.ChangedLists; i++) nRows += hunk.Changes(i).Count; } } } uint nCols = 1 + (uint)hunks[0].ChangedLists; if (options.SideBySide) nCols += 2; if (options.LineNumbers) nCols++; VBox tablecontainer = new VBox(false, 0); textviewport.Add(tablecontainer); difftable = new Table((uint)nRows, (uint)nCols, false); tablecontainer.PackStart(difftable, false, false, 0); uint row = 0; if (options.Font != null) font = Pango.FontDescription.FromString(options.Font); Hunk lastHunk = hunks.Length > 0 ? hunks[hunks.Length - 1] : null; foreach (Hunk hunk in hunks) { char leftmode = hunk.Same ? ' ' : (hunk.ChangedLists == 1 && hunk.Changes(0).Count == 0) ? '-' : 'C'; uint inc = 0; if (options.SideBySide) { ComposeLines(hunk.Original(), leftmode, -1, difftable, row, false, 0, options.LineWrap, font, options.LineNumbers, lastHunk.Original ()); inc = (uint)hunk.Original().Count; } else { if (leftmode == 'C') leftmode = '-'; int altlines = -1; if (hunk.ChangedLists == 1 && hunk.Same) altlines = hunk.Changes(0).Start; ComposeLines(hunk.Original(), leftmode, altlines, difftable, row, true, 0, options.LineWrap, font, options.LineNumbers, lastHunk.Original ()); row += (uint)hunk.Original().Count; } for (int i = 0; i < hunk.ChangedLists; i++) { char rightmode = hunk.Same ? ' ' : hunk.Original().Count == 0 ? '+' : 'C'; if (options.SideBySide) { int colsper = 1 + (options.LineNumbers ? 1 : 0); ComposeLines(hunk.Changes(i), rightmode, -1, difftable, row, false, (uint)((i+1)*colsper), options.LineWrap, font, options.LineNumbers, lastHunk.Original ()); if (hunk.Changes(i).Count > inc) inc = (uint)hunk.Changes(i).Count; } else { if (rightmode == 'C') rightmode = '+'; if (!hunk.Same) ComposeLines(hunk.Changes(i), rightmode, -1, difftable, row, true, 0, options.LineWrap, font, options.LineNumbers, lastHunk.Original ()); if (!hunk.Same) row += (uint)hunk.Changes(i).Count; } } if (options.SideBySide) row += inc; } } void ComposeLines(MonoDevelop.Components.Diff.Range range, char style, int otherStart, Table table, uint startRow, bool axn, uint col, bool wrap, Pango.FontDescription font, bool lineNumbers, MonoDevelop.Components.Diff.Range lastRange) { if (range.Count == 0) return; StringBuilder text = new StringBuilder(); for (uint i = 0; i < range.Count; i++) { if (axn) { text.Append(style); text.Append(' '); } if (lineNumbers) { string lineNo = (range.Start + i + 1).ToString(); if (otherStart != -1 && range.Start != otherStart) lineNo += "/" + (otherStart + i + 1).ToString(); string max = lastRange.End + "/" + lastRange.End; text.Append(lineNo); for (int j = 0; j < max.Length - lineNo.Length + 1; j++) { text.Append(' '); } // text.Append('\t'); } text.Append((string)range[(int)i]); if (i < range.Count-1) text.Append('\n'); } RangeEventBox rangebox = new RangeEventBox(new RangeRenderer(this, text.ToString(), style, wrap, font)); table.Attach(rangebox, 0,1, startRow,(uint)(startRow+range.Count)); /* int off1 = lineNumbers ? 1 : 0; int off2 = off1 + (axn ? 1 : 0); for (uint i = 0; i < range.Count; i++) { if (lineNumbers) { string lineNo = (range.Start + i + 1).ToString(); if (otherStart != -1 && range.Start != otherStart) lineNo += "/" + (otherStart + i + 1).ToString(); Label label = new Label(lineNo); label.ModifyFont( font ); label.Yalign = 0; table.Attach(label, col,col+1, startRow+i,startRow+i+1, AttachOptions.Shrink, AttachOptions.Shrink, 1, 1); } if (axn) { Label actionlabel = new Label(style.ToString()); table.Attach(actionlabel, (uint)(col+off1),(uint)(col+off1+1), startRow+i,startRow+i+1, AttachOptions.Shrink, AttachOptions.Shrink, 1, 1); } RangeEventBox line = new RangeEventBox(new RangeRenderer((string)range[(int)i], style, wrap, font)); table.Attach(line, (uint)(col+off2),(uint)(col+off2+1), startRow+i,startRow+i+1); } */ } /*string GetRangeText(Algorithm.Diff.Range range) { string text = ""; foreach (string line in range) text += line + "\n"; return text; }*/ class RangeEventBox : EventBox { RangeRenderer renderer; public RangeEventBox(RangeRenderer r) { renderer = r; Add (r); this.EnterNotifyEvent += EnterNotifyEventHandler; this.LeaveNotifyEvent += LeaveNotifyEventHandler; } protected override void OnDestroyed () { this.EnterNotifyEvent -= EnterNotifyEventHandler; this.LeaveNotifyEvent -= LeaveNotifyEventHandler; if (renderer != null) renderer = null; base.OnDestroyed (); } void EnterNotifyEventHandler (object o, EnterNotifyEventArgs args) { renderer.Highlight(); } void LeaveNotifyEventHandler (object o, LeaveNotifyEventArgs args) { renderer.ClearHighlight(); } } class RangeRenderer : DrawingArea { Pango.Layout layout; Pango.FontDescription font; string text; bool wrap; char type; DiffWidget widget; public RangeRenderer(DiffWidget widget, string text, char type, bool wrap, Pango.FontDescription font) { this.widget = widget; this.text = text; this.wrap = wrap; this.font = font; this.type = type; this.Realized += OnRealized; this.SizeAllocated += SizeAllocatedHandler; ClearHighlight(); } protected override void OnDestroyed () { this.Realized -= OnRealized; this.SizeAllocated -= SizeAllocatedHandler; if (layout != null) { layout.Dispose (); layout = null; } base.OnDestroyed (); } Gdk.Color bg_color { get { switch (type) { case 'C': return widget.ColorChanged; case '+': return widget.ColorAdded; case '-': return widget.ColorRemoved; default: return widget.ColorDefault; } } } Gdk.Color highlight_bg_color { get { switch (type) { case 'C': return widget.ColorChangedHighlight; case '+': return widget.ColorAddedHighlight; case '-': return widget.ColorRemovedHighlight; default: return widget.ColorDefaultHighlight; } } } bool inStyleSet = false; protected override void OnStyleSet (global::Gtk.Style previous_style) { base.OnStyleSet (previous_style); if (!inStyleSet) { inStyleSet = true; if (highlight) { Highlight (); } else { ClearHighlight(); } QueueDraw (); inStyleSet = false; } } bool highlight = false; public void Highlight() { highlight = true; ModifyBg (StateType.Normal, highlight_bg_color); } public void ClearHighlight() { highlight = false; ModifyBg (StateType.Normal, bg_color); } protected override bool OnExposeEvent (Gdk.EventExpose evnt) { GdkWindow.DrawLayout (this.Style.TextGC (highlight ? StateType.Prelight : StateType.Normal), 1, 1, layout); return true; } void OnRealized (object o, EventArgs args) { layout = new Pango.Layout(PangoContext); layout.SingleParagraphMode = false; layout.FontDescription = font; layout.Indent = (int)(-Pango.Scale.PangoScale * 20); layout.SetText(text); Render(); } void SizeAllocatedHandler (object o, SizeAllocatedArgs args) { Render(); } void Render() { if (layout == null) return; if (wrap) layout.Width = (int)(Allocation.Width * Pango.Scale.PangoScale); else layout.Width = int.MaxValue; Rectangle ink, log; layout.GetPixelExtents(out ink, out log); HeightRequest = ink.Y + ink.Height + 3; if (!wrap) WidthRequest = ink.X + ink.Width + 3; } } class OverviewRenderer : EventBox { ScrolledWindow scroller; public OverviewRenderer(DiffWidget widget, ScrolledWindow scroller, Hunk[] hunks, bool sidebyside) { this.scroller = scroller; this.ButtonPressEvent += ButtonPressHandler; Add (new OverviewRenderer2 (widget, scroller, hunks, sidebyside)); } protected override void OnDestroyed () { scroller = null; this.ButtonPressEvent -= ButtonPressHandler; base.OnDestroyed (); } void ButtonPressHandler(object o, ButtonPressEventArgs args) { double position = ((double)args.Event.Y / Allocation.Height - (double)scroller.Allocation.Height/scroller.Vadjustment.Upper/2) * scroller.Vadjustment.Upper; if (position < 0) position = 0; if (position + scroller.Allocation.Height > scroller.Vadjustment.Upper) position = scroller.Vadjustment.Upper - scroller.Allocation.Height; scroller.Vadjustment.Value = position; } } class OverviewRenderer2 : DrawingArea { Hunk[] hunks; ScrolledWindow scroller; bool sidebyside; DiffWidget widget; public OverviewRenderer2(DiffWidget widget, ScrolledWindow scroller, Hunk[] hunks, bool sidebyside) { this.widget = widget; this.hunks = hunks; this.scroller = scroller; this.sidebyside = sidebyside; scroller.ExposeEvent += OnScroll; WidthRequest = 50; } protected override void OnDestroyed () { if (scroller != null) { scroller.ExposeEvent -= OnScroll; scroller = null; } base.OnDestroyed (); } void OnScroll (object o, ExposeEventArgs args) { QueueDrawArea(0, 0, Allocation.Width, Allocation.Height); } protected override bool OnExposeEvent (Gdk.EventExpose e) { Gdk.GC gc = new Gdk.GC (e.Window); int count = 0; foreach (Hunk h in hunks) { IncPos(h, ref count); } int start = 0; foreach (Hunk h in hunks) { int size = 0; IncPos(h, ref size); if (h.Same) gc.RgbFgColor = widget.ColorDefault; else if (h.Original().Count == 0) gc.RgbFgColor = widget.ColorAdded; else if (h.ChangedLists == 1 && h.Changes(0).Count == 0) gc.RgbFgColor = widget.ColorRemoved; else gc.RgbFgColor = widget.ColorChanged; GdkWindow.DrawRectangle(gc, true, 0, Allocation.Height*start/count, Allocation.Width, Allocation.Height*size/count); start += size; } gc.RgbFgColor = widget.ColorGrey; e.Window.DrawRectangle(gc, false, 1, (int)(Allocation.Height*scroller.Vadjustment.Value/scroller.Vadjustment.Upper) + 1, Allocation.Width-3, (int)(Allocation.Height*((double)scroller.Allocation.Height/scroller.Vadjustment.Upper))-2); gc.RgbFgColor = widget.ColorBlack; e.Window.DrawRectangle(gc, false, 0, (int)(Allocation.Height*scroller.Vadjustment.Value/scroller.Vadjustment.Upper), Allocation.Width-1, (int)(Allocation.Height*((double)scroller.Allocation.Height/scroller.Vadjustment.Upper))); gc.Dispose (); return true; } private void IncPos(Hunk h, ref int pos) { if (sidebyside) pos += h.MaxLines(); else if (h.Same) pos += h.Original().Count; else { pos += h.Original().Count; for (int i = 0; i < h.ChangedLists; i++) pos += h.Changes(i).Count; } } } } }