From 9d020ce355ee58faffcf736e0fa86f2123daf43d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Kr=C3=BCger?= Date: Fri, 4 Oct 2019 10:24:13 +0200 Subject: [PATCH 1/5] Fixes VSTS Bug 810342: Accessibility: Version Control - Log: Voice Over was taking the code getting displayed after expanding the row as a row and reading it as empty it as row with empty content. https://devdiv.visualstudio.com/DevDiv/_workitems/edit/810342 --- ...oDevelop.VersionControl.Views.LogWidget.cs | 13 +- .../CellRendererDiff.cs | 3 +- .../LogWidget.cs | 230 ++++++++++-------- .../StatusView.cs | 5 +- 4 files changed, 138 insertions(+), 113 deletions(-) diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl/Gui/MonoDevelop.VersionControl.Views.LogWidget.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl/Gui/MonoDevelop.VersionControl.Views.LogWidget.cs index 79b6481d875..699d7c7f5d2 100644 --- a/main/src/addins/VersionControl/MonoDevelop.VersionControl/Gui/MonoDevelop.VersionControl.Views.LogWidget.cs +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl/Gui/MonoDevelop.VersionControl.Views.LogWidget.cs @@ -1,5 +1,7 @@ #pragma warning disable 436 // This file has been generated by the GUI designer. Do not modify. +using Gtk; + namespace MonoDevelop.VersionControl.Views { public partial class LogWidget @@ -25,7 +27,10 @@ public partial class LogWidget private global::Gtk.ScrolledWindow scrolledwindow1; private global::Gtk.TextView textviewDetails; private global::Gtk.ScrolledWindow scrolledwindowFiles; + private global::Gtk.ScrolledWindow scrolledwindowFileContents = new ScrolledWindow (); + private HPaned fileHPaned; + protected virtual void Build () { MonoDevelop.Components.Gui.Initialize (this); @@ -178,7 +183,13 @@ protected virtual void Build () this.scrolledwindowFiles = new global::Gtk.ScrolledWindow (); this.scrolledwindowFiles.CanFocus = true; this.scrolledwindowFiles.Name = "scrolledwindowFiles"; - this.vpaned1.Add (this.scrolledwindowFiles); + this.fileHPaned = new HPaned (); + this.fileHPaned.Position = 333; + + fileHPaned.Add (this.scrolledwindowFiles); + fileHPaned.Add (this.scrolledwindowFileContents); + + this.vpaned1.Add (fileHPaned); this.vbox1.Add (this.vpaned1); global::Gtk.Box.BoxChild w23 = ((global::Gtk.Box.BoxChild)(this.vbox1 [this.vpaned1])); w23.Position = 0; diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/CellRendererDiff.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/CellRendererDiff.cs index 2c41dd248df..026e5498c48 100644 --- a/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/CellRendererDiff.cs +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/CellRendererDiff.cs @@ -112,7 +112,7 @@ static string ProcessLine (string line) protected override void Render (Drawable window, Widget widget, Gdk.Rectangle background_area, Gdk.Rectangle cell_area, Gdk.Rectangle expose_area, CellRendererState flags) { - if (isDisposed) + if (isDisposed || layout == null) return; if (diffMode) { @@ -129,7 +129,6 @@ protected override void Render (Drawable window, Widget widget, Gdk.Rectangle ba } var treeview = widget as FileTreeView; var p = treeview != null? treeview.CursorLocation : null; - cell_area.Width -= RightPadding; window.DrawRectangle (widget.Style.BaseGC (Gtk.StateType.Normal), true, cell_area.X, cell_area.Y, cell_area.Width - 1, cell_area.Height); diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/LogWidget.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/LogWidget.cs index 6930b0b0dca..dc61e720006 100644 --- a/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/LogWidget.cs +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/LogWidget.cs @@ -58,8 +58,10 @@ public Revision[] History { } ListStore logstore = new ListStore (typeof (Revision), typeof(string)); - FileTreeView treeviewFiles; + TreeView treeviewFiles; + FileTreeView treeviewFileContents; TreeStore changedpathstore; + TreeStore contentspathstore; DocumentToolButton revertButton, revertToButton, refreshButton; SearchEntry searchEntry; string currentFilter; @@ -204,10 +206,15 @@ public LogWidget (VersionControlDocumentInfo info) treeviewLog.Selection.Changed += TreeSelectionChanged; treeviewFiles = new FileTreeView (); - treeviewFiles.DiffLineActivated += HandleTreeviewFilesDiffLineActivated; + scrolledwindowFiles.Child = treeviewFiles; scrolledwindowFiles.ShowAll (); + treeviewFileContents = new FileTreeView (); + treeviewFileContents.DiffLineActivated += HandleTreeviewFilesDiffLineActivated; + scrolledwindowFileContents.Child = treeviewFileContents; + scrolledwindowFileContents.ShowAll (); + changedpathstore = new TreeStore (typeof(Xwt.Drawing.Image), typeof (string), // icon/file name typeof(Xwt.Drawing.Image), typeof (string), // icon/operation typeof (string), // path @@ -215,6 +222,10 @@ public LogWidget (VersionControlDocumentInfo info) typeof (string []) // diff ); + contentspathstore = new TreeStore (typeof(bool), // diff mode + typeof (string []) // diff + ); + TreeViewColumn colChangedFile = new TreeViewColumn (); var crp = new CellRendererImage (); var crt = new CellRendererText (); @@ -239,10 +250,14 @@ public LogWidget (VersionControlDocumentInfo info) diffRenderer.DrawLeft = true; colChangedPath.PackStart (diffRenderer, true); colChangedPath.SetCellDataFunc (diffRenderer, SetDiffCellData); - treeviewFiles.AppendColumn (colChangedPath); + treeviewFileContents.HeadersVisible = false; + treeviewFileContents.AppendColumn (colChangedPath); + treeviewFileContents.Model = contentspathstore; + treeviewFileContents.Events |= Gdk.EventMask.PointerMotionMask; + + treeviewFiles.Model = changedpathstore; - treeviewFiles.TestExpandRow += HandleTreeviewFilesTestExpandRow; - treeviewFiles.Events |= Gdk.EventMask.PointerMotionMask; + treeviewFiles.Selection.Changed += SetDiff; textviewDetails.WrapMode = Gtk.WrapMode.Word; textviewDetails.AddEvents ((int)Gdk.EventMask.ButtonPressMask); @@ -472,119 +487,121 @@ async void HandleTreeviewFilesDiffLineActivated (object sender, EventArgs e) const int colPath = 5; const int colDiff = 6; - void HandleTreeviewFilesTestExpandRow (object o, TestExpandRowArgs args) + + void SetDiff (object o, EventArgs args) { - TreeIter iter; - if (changedpathstore.IterChildren (out iter, args.Iter)) { - string[] diff = changedpathstore.GetValue (iter, colDiff) as string[]; - if (diff != null) - return; + this.contentspathstore.Clear (); + if (!this.treeviewFiles.Selection.GetSelected (out var model, out var iter)) + return; - string path = (string)changedpathstore.GetValue (args.Iter, colPath); - - changedpathstore.SetValue (iter, colDiff, new string[] { GettextCatalog.GetString ("Loading data...") }); - var rev = SelectedRevision; - Task.Run (async delegate { - string text = ""; - try { - text = await info.Repository.GetTextAtRevisionAsync (path, rev); - } catch (Exception e) { - Application.Invoke ((o2, a2) => { - LoggingService.LogError ("Error while getting revision text", e); - MessageService.ShowError ( - GettextCatalog.GetString ("Error while getting revision text."), - GettextCatalog.GetString ("The file may not be part of the working copy.") - ); - }); - return; - } - Revision prevRev = null; - try { - prevRev = await rev.GetPreviousAsync (); - } catch (Exception e) { - Application.Invoke ((o2, a2) => { - MessageService.ShowError (GettextCatalog.GetString ("Error while getting previous revision."), e); - }); - return; - } - string[] lines; - // Indicator that the file was binary - if (text == null) { - lines = new [] { GettextCatalog.GetString (" Binary files differ") }; + var newIter = this.contentspathstore.AppendValues (false, new string [] { GettextCatalog.GetString ("Loading data…") }); + this.treeviewFileContents.ExpandAll (); + string path = (string)changedpathstore.GetValue (iter, colPath); + var rev = SelectedRevision; + Task.Run (async delegate { + string text = ""; + try { + text = await info.Repository.GetTextAtRevisionAsync (path, rev); + } catch (Exception e) { + await Runtime.RunInMainThread (delegate { + LoggingService.LogError ("Error while getting revision text", e); + MessageService.ShowError ( + GettextCatalog.GetString ("Error while getting revision text."), + GettextCatalog.GetString ("The file may not be part of the working copy.") + ); + }); + return; + } + Revision prevRev = null; + try { + prevRev = await rev.GetPreviousAsync (); + } catch (Exception e) { + await Runtime.RunInMainThread (delegate { + MessageService.ShowError (GettextCatalog.GetString ("Error while getting previous revision."), e); + }); + return; + } + string[] lines; + // Indicator that the file was binary + if (text == null) { + lines = new [] { GettextCatalog.GetString (" Binary files differ") }; + } else { + var changedDocument = Mono.TextEditor.TextDocument.CreateImmutableDocument (text); + if (prevRev == null) { + lines = new string [changedDocument.LineCount]; + for (int i = 0; i < changedDocument.LineCount; i++) { + lines[i] = "+ " + changedDocument.GetLineText (i + 1).TrimEnd ('\r','\n'); + } } else { - var changedDocument = Mono.TextEditor.TextDocument.CreateImmutableDocument (text); - if (prevRev == null) { + string prevRevisionText = ""; + try { + prevRevisionText = await info.Repository.GetTextAtRevisionAsync (path, prevRev); + } catch (Exception e) { + Application.Invoke ((o2, a2) => { + LoggingService.LogError ("Error while getting revision text", e); + MessageService.ShowError ( + GettextCatalog.GetString ("Error while getting revision text."), + GettextCatalog.GetString ("The file may not be part of the working copy.") + ); + }); + return; + } + + if (string.IsNullOrEmpty (text) && !string.IsNullOrEmpty (prevRevisionText)) { lines = new string [changedDocument.LineCount]; for (int i = 0; i < changedDocument.LineCount; i++) { - lines[i] = "+ " + changedDocument.GetLineText (i + 1).TrimEnd ('\r','\n'); - } - } else { - string prevRevisionText = ""; - try { - prevRevisionText = await info.Repository.GetTextAtRevisionAsync (path, prevRev); - } catch (Exception e) { - Application.Invoke ((o2, a2) => { - LoggingService.LogError ("Error while getting revision text", e); - MessageService.ShowError ( - GettextCatalog.GetString ("Error while getting revision text."), - GettextCatalog.GetString ("The file may not be part of the working copy.") - ); - }); - return; - } - - if (String.IsNullOrEmpty (text)) { - if (!String.IsNullOrEmpty (prevRevisionText)) { - lines = new string [changedDocument.LineCount]; - for (int i = 0; i < changedDocument.LineCount; i++) { - lines [i] = "- " + changedDocument.GetLineText (i + 1).TrimEnd ('\r','\n'); - } - } + lines [i] = "- " + changedDocument.GetLineText (i + 1).TrimEnd ('\r', '\n'); } - - var originalDocument = Mono.TextEditor.TextDocument.CreateImmutableDocument (prevRevisionText); - originalDocument.FileName = GettextCatalog.GetString ("Revision {0}", prevRev); - changedDocument.FileName = GettextCatalog.GetString ("Revision {0}", rev); - lines = Mono.TextEditor.Utils.Diff.GetDiffString (originalDocument, changedDocument).Split ('\n'); } + + var originalDocument = Mono.TextEditor.TextDocument.CreateImmutableDocument (prevRevisionText); + originalDocument.FileName = GettextCatalog.GetString ("Revision {0}", prevRev); + changedDocument.FileName = GettextCatalog.GetString ("Revision {0}", rev); + lines = Mono.TextEditor.Utils.Diff.GetDiffString (originalDocument, changedDocument).Split ('\n'); } - Application.Invoke ((o2, a2) => { - changedpathstore.SetValue (iter, colDiff, lines); - }); + } + await Runtime.RunInMainThread (delegate { + this.contentspathstore.Clear (); + this.contentspathstore.AppendValues (true, lines); + changedpathstore.SetValue (iter, colDiff, lines); }); - } + }); } -/* void FileSelectionChanged (object sender, EventArgs e) - { - Revision rev = SelectedRevision; - if (rev == null) { - diffWidget.ComparisonWidget.OriginalEditor.Text = ""; - diffWidget.ComparisonWidget.DiffEditor.Text = ""; - return; - } - TreeIter iter; - if (!treeviewFiles.Selection.GetSelected (out iter)) - return; - string path = (string)changedpathstore.GetValue (iter, colPath); - ThreadPool.QueueUserWorkItem (delegate { - string text = info.Repository.GetTextAtRevision (path, rev); - string prevRevision = text; // info.Repository.GetTextAtRevision (path, rev.GetPrevious ()); - - Application.Invoke (delegate { - diffWidget.ComparisonWidget.MimeType = IdeServices.DesktopService.GetMimeTypeForUri (path); - diffWidget.ComparisonWidget.OriginalEditor.Text = prevRevision; - diffWidget.ComparisonWidget.DiffEditor.Text = text; - diffWidget.ComparisonWidget.CreateDiff (); - }); - }); - }*/ + /* void FileSelectionChanged (object sender, EventArgs e) + { + Revision rev = SelectedRevision; + if (rev == null) { + diffWidget.ComparisonWidget.OriginalEditor.Text = ""; + diffWidget.ComparisonWidget.DiffEditor.Text = ""; + return; + } + TreeIter iter; + if (!treeviewFiles.Selection.GetSelected (out iter)) + return; + string path = (string)changedpathstore.GetValue (iter, colPath); + ThreadPool.QueueUserWorkItem (delegate { + string text = info.Repository.GetTextAtRevision (path, rev); + string prevRevision = text; // info.Repository.GetTextAtRevision (path, rev.GetPrevious ()); + + Application.Invoke (delegate { + diffWidget.ComparisonWidget.MimeType = IdeServices.DesktopService.GetMimeTypeForUri (path); + diffWidget.ComparisonWidget.OriginalEditor.Text = prevRevision; + diffWidget.ComparisonWidget.DiffEditor.Text = text; + diffWidget.ComparisonWidget.CreateDiff (); + }); + }); + }*/ protected override void OnDestroyed () { IsDestroyed = true; selectionCancellationTokenSource.Cancel (); + treeviewFiles.Selection.Changed -= SetDiff; + + treeviewFileContents.DiffLineActivated -= HandleTreeviewFilesDiffLineActivated; + textviewDetails.ButtonPressEvent -= TextviewDetails_ButtonPressEvent; labelDate.ButtonPressEvent -= LabelDate_ButtonPressEvent; @@ -712,12 +729,11 @@ static void RevisionFunc (Gtk.TreeViewColumn tree_column, Gtk.CellRenderer cell, static void SetDiffCellData (Gtk.TreeViewColumn tree_column, Gtk.CellRenderer cell, Gtk.TreeModel model, Gtk.TreeIter iter) { - CellRendererDiff rc = (CellRendererDiff)cell; - string[] lines = (string[])model.GetValue (iter, colDiff); - if (lines == null) - lines = new string[] { (string)model.GetValue (iter, colOperation) }; + var rc = (CellRendererDiff)cell; + var diffMode = (bool)model.GetValue (iter, 0); + var lines = (string[])model.GetValue (iter, 1) ?? new string [] { "" }; - rc.InitCell (tree_column.TreeView, ((TreeStore)model).IterDepth (iter) != 0, lines, model.GetPath (iter)); + rc.InitCell (tree_column.TreeView, diffMode, lines, model.GetPath (iter)); } protected override void OnSizeAllocated (Gdk.Rectangle allocation) @@ -760,6 +776,7 @@ void TreeSelectionChanged (object o, EventArgs args) { Revision d = SelectedRevision; changedpathstore.Clear (); + contentspathstore.Clear (); textviewDetails.Buffer.Clear (); if (d == null) return; @@ -797,7 +814,6 @@ void TreeSelectionChanged (object o, EventArgs args) } Xwt.Drawing.Image fileIcon = IdeServices.DesktopService.GetIconForFile (rp.Path, Gtk.IconSize.Menu); var iter = changedpathstore.AppendValues (actionIcon, action, fileIcon, System.IO.Path.GetFileName (rp.Path), System.IO.Path.GetDirectoryName (rp.Path), rp.Path, null); - changedpathstore.AppendValues (iter, null, null, null, null, null, rp.Path, null); if (rp.Path == preselectFile) { selectIter = iter; select = true; diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/StatusView.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/StatusView.cs index afbafe0e844..efdbfd9b2f9 100644 --- a/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/StatusView.cs +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/StatusView.cs @@ -1266,7 +1266,7 @@ protected override bool OnButtonPressEvent (Gdk.EventButton evnt) if (!ctxMenu) { TreePath path; GetPathAtPos ((int)evnt.X, (int)evnt.Y, out path); - if (path != null && path.Depth == 2) { + if (path != null && path.Depth >= 1) { vpos = Vadjustment.Value; keepPos = true; if (Selection.PathIsSelected (path) && Selection.GetSelectedRows ().Length == 1 && evnt.Button == 1) { @@ -1316,9 +1316,8 @@ protected override bool OnMotionNotifyEvent (Gdk.EventMotion evnt) { TreePath path; GetPathAtPos ((int)evnt.X, (int)evnt.Y, out path); - // Diff cells need to be redrawn so they can show the updated selected line - if (path != null && path.Depth == 2) { + if (path != null && path.Depth >= 1) { CursorLocation = new Gdk.Point ((int)evnt.X, (int)evnt.Y); //FIXME: we should optimize these draws QueueDraw (); From 4b1db1fc44a0bcb69cf449ad9b360e3ba596895a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Kr=C3=BCger?= Date: Thu, 17 Oct 2019 12:47:14 +0200 Subject: [PATCH 2/5] [VersionControl] Use a custom diff renderer widget instead of a treeview/cell renderer to draw the diff. Mostly copied the cell renderer diff code & altered it a bit to work in a drawing area. --- .../DiffRendererWidget.cs | 541 ++++++++++++++++++ .../LogWidget.cs | 37 +- .../MonoDevelop.VersionControl.csproj | 1 + 3 files changed, 553 insertions(+), 26 deletions(-) create mode 100644 main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/DiffRendererWidget.cs diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/DiffRendererWidget.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/DiffRendererWidget.cs new file mode 100644 index 00000000000..7b97b273252 --- /dev/null +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/DiffRendererWidget.cs @@ -0,0 +1,541 @@ +// +// DiffRendererWidget.cs +// +// Author: +// Mike Krüger +// +// Copyright (c) 2019 Microsoft Corporation. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +using System; +using System.Collections.Generic; +using Gtk; +using Gdk; +using MonoDevelop.Ide; +using MonoDevelop.Components; +using System.Text; +using MonoDevelop.Ide.Fonts; + +namespace MonoDevelop.VersionControl.Views +{ + class DiffRendererWidget : Gtk.DrawingArea + { + Pango.Layout layout; + + int width, height, lineHeight; + string [] lines; + int selectedLine = -1; + int RightPadding = 4; + + int RoundedSectionRadius = 4; + int LeftPaddingBlock = 19; + + public string[] Lines { + get => lines; + set { + lines = value; + InitCell (); + QueueDraw (); + } + } + + public int SelectedLine => selectedLine; + + public event EventHandler DiffLineActivated; + + public DiffRendererWidget () + { + Events |= EventMask.PointerMotionMask | EventMask.LeaveNotifyMask | EventMask.ButtonPressMask; + } + + void DisposeLayout () + { + if (layout != null) { + layout.Dispose (); + layout = null; + } + } + + bool isDisposed = false; + + protected override void OnDestroyed () + { + isDisposed = true; + DisposeLayout (); + base.OnDestroyed (); + } + + public void InitCell () + { + if (isDisposed) + return; + if (lines != null && lines.Length > 0) { + int maxlen = -1; + int maxlin = -1; + for (int n = 0; n < lines.Length; n++) { + string line = ProcessLine (lines [n]); + if (line == null) + throw new Exception ("Line " + n + " from diff was null."); + if (line.Length > maxlen) { + maxlen = lines [n].Length; + maxlin = n; + } + } + DisposeLayout (); + layout = CreateLayout (lines [maxlin]); + layout.GetPixelSize (out width, out lineHeight); + height = lineHeight * lines.Length; + width += LeftPaddingBlock + RightPadding; + } else + width = height = 0; + QueueResize (); + } + + protected override void OnSizeRequested (ref Requisition requisition) + { + base.OnSizeRequested (ref requisition); + requisition.Width = width; + requisition.Height = height; + } + + int px, py; + protected override bool OnMotionNotifyEvent (EventMotion evnt) + { + px = (int)evnt.X; + py = (int)evnt.Y; + QueueDraw (); + return base.OnMotionNotifyEvent (evnt); + } + + protected override bool OnLeaveNotifyEvent (EventCrossing evnt) + { + px = py = -1; + QueueDraw (); + return base.OnLeaveNotifyEvent (evnt); + } + + protected override bool OnButtonPressEvent (EventButton evnt) + { + if (selectedLine >= 0) { + if (evnt.Type == Gdk.EventType.TwoButtonPress) + DiffLineActivated?.Invoke (this, EventArgs.Empty); + } + return base.OnButtonPressEvent (evnt); + } + + Pango.Layout CreateLayout (string text) + { + var layout = new Pango.Layout (PangoContext); + layout.SingleParagraphMode = false; + layout.FontDescription = IdeServices.FontService.MonospaceFont; + layout.SetText (text); + return layout; + } + + protected override bool OnExposeEvent (Gdk.EventExpose e) + { + var window = e.Window; + var widget = this; + using (Cairo.Context cr = Gdk.CairoHelper.Create (e.Window)) { + int w, maxy; + window.GetSize (out w, out maxy); + var cell_area = Allocation; +// if (DrawLeft) { +// cell_area.Width += cell_area.X - leftSpace; +// cell_area.X = leftSpace; +// } + + cell_area.Width -= RightPadding; + + window.DrawRectangle (widget.Style.BaseGC (Gtk.StateType.Normal), true, cell_area.X, cell_area.Y, cell_area.Width - 1, cell_area.Height); + if (lines == null) + return true; + Gdk.GC normalGC = widget.Style.TextGC (StateType.Normal); + Gdk.GC removedGC = new Gdk.GC (window); + removedGC.Copy (normalGC); + removedGC.RgbFgColor = Styles.LogView.DiffRemoveBackgroundColor.AddLight (-0.3).ToGdkColor (); + Gdk.GC addedGC = new Gdk.GC (window); + addedGC.Copy (normalGC); + addedGC.RgbFgColor = Styles.LogView.DiffAddBackgroundColor.AddLight (-0.3).ToGdkColor (); + Gdk.GC infoGC = new Gdk.GC (window); + infoGC.Copy (normalGC); + infoGC.RgbFgColor = widget.Style.Text (StateType.Normal).AddLight (0.2); + + Cairo.Context ctx = CairoHelper.Create (window); + + // Rendering is done in two steps: + // 1) Get a list of blocks to render + // 2) render the blocks + + var blocks = CalculateBlocks (maxy, cell_area.Y + 2); + + // Now render the blocks + + // The y position of the highlighted line + int selectedLineRowTop = -1; + + BlockInfo lastCodeSegmentStart = null; + BlockInfo lastCodeSegmentEnd = null; + + foreach (BlockInfo block in blocks) { + if (block.Type == BlockType.Info) { + // Finished drawing the content of a code segment. Now draw the segment border and label. + if (lastCodeSegmentStart != null) + DrawCodeSegmentBorder (infoGC, ctx, cell_area.X, cell_area.Width, lastCodeSegmentStart, lastCodeSegmentEnd, lines, widget, window); + lastCodeSegmentStart = block; + } + + lastCodeSegmentEnd = block; + + if (block.YEnd < 0) + continue; + + // Draw the block background + DrawBlockBg (ctx, cell_area.X + 1, cell_area.Width - 2, block); + + // Get all text for the current block + StringBuilder sb = new StringBuilder (); + for (int n = block.FirstLine; n <= block.LastLine; n++) { + string s = ProcessLine (lines [n]); + if (n > block.FirstLine) + sb.Append ('\n'); + if ((block.Type == BlockType.Added || block.Type == BlockType.Removed) && s.Length > 0) { + sb.Append (' '); + sb.Append (s, 1, s.Length - 1); + } else + sb.Append (s); + } + + // Draw a special background for the selected line + if (px < 0) { + selectedLine = selectedLineRowTop = -1; + } else if (block.Type != BlockType.Info && px >= cell_area.X && py <= cell_area.Right && py >= block.YStart && py <= block.YEnd) { + int row = (py - block.YStart) / lineHeight; + double yrow = block.YStart + lineHeight * row; + double xrow = cell_area.X + LeftPaddingBlock; + int wrow = cell_area.Width - 1 - LeftPaddingBlock; + if (block.Type == BlockType.Added) + ctx.SetSourceColor (Styles.LogView.DiffAddBackgroundColor.AddLight (0.1).ToCairoColor ()); + else if (block.Type == BlockType.Removed) + ctx.SetSourceColor (Styles.LogView.DiffRemoveBackgroundColor.AddLight (0.1).ToCairoColor ()); + else { + ctx.SetSourceColor (Styles.LogView.DiffHighlightColor.ToCairoColor ()); + xrow -= LeftPaddingBlock; + wrow += LeftPaddingBlock; + } + ctx.Rectangle (xrow, yrow, wrow, lineHeight); + ctx.Fill (); + selectedLine = block.SourceLineStart + row; + // selctedPath = path; + selectedLineRowTop = (int)yrow; + } + + // Draw the line text. Ignore header blocks, since they are drawn as labels in DrawCodeSegmentBorder + + if (block.Type != BlockType.Info) { + layout.SetMarkup (""); + layout.SetText (sb.ToString ()); + Gdk.GC gc; + switch (block.Type) { + case BlockType.Removed: gc = removedGC; break; + case BlockType.Added: gc = addedGC; break; + case BlockType.Info: gc = infoGC; break; + default: gc = normalGC; break; + } + window.DrawLayout (gc, cell_area.X + 2 + LeftPaddingBlock, block.YStart, layout); + } + + // Finally draw the change symbol at the left margin + + DrawChangeSymbol (ctx, widget, cell_area.X + 1, cell_area.Width - 2, block); + } + + // Finish the drawing of the code segment + if (lastCodeSegmentStart != null) + DrawCodeSegmentBorder (infoGC, ctx, cell_area.X, cell_area.Width, lastCodeSegmentStart, lastCodeSegmentEnd, lines, widget, window); + + // Draw the source line number at the current selected line. It must be done at the end because it must + // be drawn over the source code text and segment borders. + if (selectedLineRowTop != -1) + DrawLineBox (normalGC, ctx, Allocation.Right - 4, selectedLineRowTop, selectedLine, widget, window); + + ((IDisposable)ctx).Dispose (); + removedGC.Dispose (); + addedGC.Dispose (); + infoGC.Dispose (); + } + return true; + } + + static string ProcessLine (string line) + { + if (line == null) + return null; + return line.Replace ("\t", " "); + } + + List CalculateBlocks (int maxy, int y) + { + // cline keeps track of the current source code line (the one to jump to when double clicking) + int cline = 1; + + BlockInfo currentBlock = null; + + var result = new List (); + if (lines == null) + return result; + int removedLines = 0; + for (int n = 0; n < lines.Length; n++, y += lineHeight) { + + string line = lines [n]; + if (line.Length == 0) { + currentBlock = null; + y -= lineHeight; + continue; + } + + char tag = line [0]; + + if (line.StartsWith ("---", StringComparison.Ordinal) || + line.StartsWith ("+++", StringComparison.Ordinal)) { + // Ignore this part of the header. + currentBlock = null; + y -= lineHeight; + continue; + } + if (tag == '@') { + int l = ParseCurrentLine (line); + if (l != -1) cline = l - 1; + } else + cline++; + + BlockType type; + switch (tag) { + case '-': + type = BlockType.Removed; + removedLines++; + break; + case '+': type = BlockType.Added; break; + case '@': type = BlockType.Info; break; + default: type = BlockType.Unchanged; break; + } + + if (type != BlockType.Removed && removedLines > 0) { + cline -= removedLines; + removedLines = 0; + } + + if (currentBlock == null || type != currentBlock.Type) { + if (y > maxy) + break; + + // Starting a new block. Mark section ends between a change block and a normal code block + if (currentBlock != null && IsChangeBlock (currentBlock.Type) && !IsChangeBlock (type)) + currentBlock.SectionEnd = true; + + currentBlock = new BlockInfo { + YStart = y, + FirstLine = n, + Type = type, + SourceLineStart = cline, + SectionStart = (result.Count == 0 || !IsChangeBlock (result [result.Count - 1].Type)) && IsChangeBlock (type) + }; + result.Add (currentBlock); + } + // Include the line in the current block + currentBlock.YEnd = y + lineHeight; + currentBlock.LastLine = n; + } + + return result; + } + + static bool IsChangeBlock (BlockType t) + { + return t == BlockType.Added || t == BlockType.Removed; + } + + class BlockInfo + { + public BlockType Type; + public int YEnd; + public int YStart; + public int FirstLine; + public int LastLine; + public bool SectionStart; + public bool SectionEnd; + public int SourceLineStart; + } + + enum BlockType + { + Info, + Added, + Removed, + Unchanged + } + + void DrawCodeSegmentBorder (Gdk.GC gc, Cairo.Context ctx, double x, int width, BlockInfo firstBlock, BlockInfo lastBlock, string [] lines, Gtk.Widget widget, Gdk.Drawable window) + { + int shadowSize = 2; + int spacing = 4; + int bottomSpacing = (lineHeight - spacing) / 2; + + ctx.Rectangle (x + shadowSize + 0.5, firstBlock.YStart + bottomSpacing + spacing - shadowSize + 0.5, width - shadowSize * 2, shadowSize); + ctx.SetSourceColor (Styles.LogView.DiffBoxSplitterColor.ToCairoColor ()); + ctx.LineWidth = 1; + ctx.Fill (); + + ctx.Rectangle (x + shadowSize + 0.5, lastBlock.YEnd + bottomSpacing + 0.5, width - shadowSize * 2, shadowSize); + ctx.SetSourceColor (Styles.LogView.DiffBoxSplitterColor.ToCairoColor ()); + ctx.Fill (); + + ctx.Rectangle (x + 0.5, firstBlock.YStart + bottomSpacing + spacing + 0.5, width, lastBlock.YEnd - firstBlock.YStart - spacing); + ctx.SetSourceColor (Styles.LogView.DiffBoxBorderColor.ToCairoColor ()); + ctx.Stroke (); + + string text = lines [firstBlock.FirstLine].Replace ("@", "").Replace ("-", ""); + text = "" + text.Replace ("+", " ") + ""; + + layout.SetText (""); + layout.SetMarkup (text); + int tw, th; + layout.GetPixelSize (out tw, out th); + th--; + + int dy = (lineHeight - th) / 2; + + ctx.Rectangle (x + 2 + LeftPaddingBlock - 1 + 0.5, firstBlock.YStart + dy - 1 + 0.5, tw + 2, th + 2); + ctx.LineWidth = 1; + ctx.SetSourceColor (widget.Style.Base (StateType.Normal).ToCairoColor ()); + ctx.FillPreserve (); + ctx.SetSourceColor (Styles.LogView.DiffBoxBorderColor.ToCairoColor ()); + ctx.Stroke (); + + window.DrawLayout (gc, (int)(x + 2 + LeftPaddingBlock), firstBlock.YStart + dy, layout); + } + + void DrawLineBox (Gdk.GC gc, Cairo.Context ctx, int right, int top, int line, Gtk.Widget widget, Gdk.Drawable window) + { + layout.SetText (""); + layout.SetMarkup ("" + line.ToString () + ""); + int tw, th; + layout.GetPixelSize (out tw, out th); + th--; + + int dy = (lineHeight - th) / 2; + + ctx.Rectangle (right - tw - 2 + 0.5, top + dy - 1 + 0.5, tw + 2, th + 2); + ctx.LineWidth = 1; + ctx.SetSourceColor (widget.Style.Base (Gtk.StateType.Normal).ToCairoColor ()); + ctx.FillPreserve (); + ctx.SetSourceColor (Styles.LogView.DiffBoxBorderColor.ToCairoColor ()); + ctx.Stroke (); + + window.DrawLayout (gc, right - tw - 1, top + dy, layout); + } + + void DrawBlockBg (Cairo.Context ctx, double x, int width, BlockInfo block) + { + if (!IsChangeBlock (block.Type)) + return; + + var color = block.Type == BlockType.Added ? Styles.LogView.DiffAddBackgroundColor : Styles.LogView.DiffRemoveBackgroundColor; + double y = block.YStart; + int height = block.YEnd - block.YStart; + + double markerx = x + LeftPaddingBlock; + double rd = RoundedSectionRadius; + if (block.SectionStart) { + ctx.Arc (x + rd, y + rd, rd, 180 * (Math.PI / 180), 270 * (Math.PI / 180)); + ctx.LineTo (markerx, y); + } else { + ctx.MoveTo (markerx, y); + } + + ctx.LineTo (markerx, y + height); + + if (block.SectionEnd) { + ctx.LineTo (x + rd, y + height); + ctx.Arc (x + rd, y + height - rd, rd, 90 * (Math.PI / 180), 180 * (Math.PI / 180)); + } else { + ctx.LineTo (x, y + height); + } + if (block.SectionStart) { + ctx.LineTo (x, y + rd); + } else { + ctx.LineTo (x, y); + } + ctx.SetSourceColor (color.AddLight (0.1).ToCairoColor ()); + ctx.Fill (); + + ctx.Rectangle (markerx, y, width - markerx, height); + + // FIXME: VV: Remove gradient features + using (Cairo.Gradient pat = new Cairo.LinearGradient (x, y, x + width, y)) { + pat.AddColorStop (0, color.AddLight (0.21).ToCairoColor ()); + pat.AddColorStop (1, color.AddLight (0.3).ToCairoColor ()); + ctx.SetSource (pat); + ctx.Fill (); + } + } + + static Xwt.Drawing.Image gutterAdded = Xwt.Drawing.Image.FromResource ("gutter-added-15.png"); + static Xwt.Drawing.Image gutterRemoved = Xwt.Drawing.Image.FromResource ("gutter-removed-15.png"); + + void DrawChangeSymbol (Cairo.Context ctx, Widget widget, double x, int width, BlockInfo block) + { + if (!IsChangeBlock (block.Type)) + return; + + if (block.Type == BlockType.Added) { + var ix = x + (LeftPaddingBlock / 2) - (gutterAdded.Width / 2); + var iy = block.YStart + ((block.YEnd - block.YStart) / 2 - gutterAdded.Height / 2); + ctx.DrawImage (widget, gutterAdded, ix, iy); + } else { + var ix = x + (LeftPaddingBlock / 2) - (gutterRemoved.Width / 2); + var iy = block.YStart + ((block.YEnd - block.YStart) / 2 - gutterRemoved.Height / 2); + ctx.DrawImage (widget, gutterRemoved, ix, iy); + } + } + + static StateType GetState (Gtk.Widget widget, CellRendererState flags) + { + if ((flags & CellRendererState.Selected) != 0) + return widget.HasFocus ? StateType.Selected : StateType.Active; + else + return StateType.Normal; + } + + static int ParseCurrentLine (string line) + { + int i = line.IndexOf ('+'); + if (i == -1) return -1; + i++; + int j = line.IndexOf (',', i); + if (j == -1) return -1; + int cline; + if (!int.TryParse (line.Substring (i, j - i), out cline)) + return -1; + return cline; + } + + } +} diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/LogWidget.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/LogWidget.cs index dc61e720006..ed2e43ba4d6 100644 --- a/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/LogWidget.cs +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/LogWidget.cs @@ -59,16 +59,13 @@ public Revision[] History { ListStore logstore = new ListStore (typeof (Revision), typeof(string)); TreeView treeviewFiles; - FileTreeView treeviewFileContents; TreeStore changedpathstore; - TreeStore contentspathstore; DocumentToolButton revertButton, revertToButton, refreshButton; SearchEntry searchEntry; string currentFilter; VersionControlDocumentInfo info; string preselectFile; - CellRendererDiff diffRenderer = new CellRendererDiff (); CellRendererText messageRenderer = new CellRendererText (); CellRendererText textRenderer = new CellRendererText (); CellRendererImage pixRenderer = new CellRendererImage (); @@ -76,6 +73,7 @@ public Revision[] History { bool currentRevisionShortened; Xwt.Menu popupMenu; + DiffRendererWidget diffRenderer; class RevisionGraphCellRenderer : Gtk.CellRenderer { @@ -210,9 +208,9 @@ public LogWidget (VersionControlDocumentInfo info) scrolledwindowFiles.Child = treeviewFiles; scrolledwindowFiles.ShowAll (); - treeviewFileContents = new FileTreeView (); - treeviewFileContents.DiffLineActivated += HandleTreeviewFilesDiffLineActivated; - scrolledwindowFileContents.Child = treeviewFileContents; + diffRenderer = new DiffRendererWidget (); + diffRenderer.DiffLineActivated += HandleTreeviewFilesDiffLineActivated; + scrolledwindowFileContents.AddWithViewport (diffRenderer); scrolledwindowFileContents.ShowAll (); changedpathstore = new TreeStore (typeof(Xwt.Drawing.Image), typeof (string), // icon/file name @@ -222,10 +220,6 @@ public LogWidget (VersionControlDocumentInfo info) typeof (string []) // diff ); - contentspathstore = new TreeStore (typeof(bool), // diff mode - typeof (string []) // diff - ); - TreeViewColumn colChangedFile = new TreeViewColumn (); var crp = new CellRendererImage (); var crt = new CellRendererText (); @@ -247,13 +241,6 @@ public LogWidget (VersionControlDocumentInfo info) TreeViewColumn colChangedPath = new TreeViewColumn (); colChangedPath.Title = GettextCatalog.GetString ("Path"); - diffRenderer.DrawLeft = true; - colChangedPath.PackStart (diffRenderer, true); - colChangedPath.SetCellDataFunc (diffRenderer, SetDiffCellData); - treeviewFileContents.HeadersVisible = false; - treeviewFileContents.AppendColumn (colChangedPath); - treeviewFileContents.Model = contentspathstore; - treeviewFileContents.Events |= Gdk.EventMask.PointerMotionMask; treeviewFiles.Model = changedpathstore; @@ -472,7 +459,7 @@ async void HandleTreeviewFilesDiffLineActivated (object sender, EventArgs e) changedpathstore.GetIter (out iter, paths[0]); string fileName = (string)changedpathstore.GetValue (iter, colPath); - int line = diffRenderer.GetSelectedLine (paths[0]); + int line = diffRenderer.SelectedLine; if (line == -1) line = 1; @@ -480,7 +467,7 @@ async void HandleTreeviewFilesDiffLineActivated (object sender, EventArgs e) var doc = await IdeApp.Workbench.OpenDocument (fileName, proj, line, 0, OpenDocumentOptions.Default | OpenDocumentOptions.OnlyInternalViewer); doc?.GetContent ()?.ShowDiffView (await SelectedRevision.GetPreviousAsync (), SelectedRevision, line); } - + const int colFile = 3; const int colOperation = 4; const int colOperationText = 1; @@ -490,12 +477,11 @@ async void HandleTreeviewFilesDiffLineActivated (object sender, EventArgs e) void SetDiff (object o, EventArgs args) { - this.contentspathstore.Clear (); + this.diffRenderer.Lines = null; if (!this.treeviewFiles.Selection.GetSelected (out var model, out var iter)) return; - var newIter = this.contentspathstore.AppendValues (false, new string [] { GettextCatalog.GetString ("Loading data…") }); - this.treeviewFileContents.ExpandAll (); + this.diffRenderer.Lines = new string [] { GettextCatalog.GetString ("Loading data…") }; string path = (string)changedpathstore.GetValue (iter, colPath); var rev = SelectedRevision; Task.Run (async delegate { @@ -561,8 +547,7 @@ await Runtime.RunInMainThread (delegate { } } await Runtime.RunInMainThread (delegate { - this.contentspathstore.Clear (); - this.contentspathstore.AppendValues (true, lines); + this.diffRenderer.Lines = lines; changedpathstore.SetValue (iter, colDiff, lines); }); }); @@ -600,7 +585,7 @@ protected override void OnDestroyed () treeviewFiles.Selection.Changed -= SetDiff; - treeviewFileContents.DiffLineActivated -= HandleTreeviewFilesDiffLineActivated; + diffRenderer.DiffLineActivated -= HandleTreeviewFilesDiffLineActivated; textviewDetails.ButtonPressEvent -= TextviewDetails_ButtonPressEvent; labelDate.ButtonPressEvent -= LabelDate_ButtonPressEvent; @@ -776,7 +761,7 @@ void TreeSelectionChanged (object o, EventArgs args) { Revision d = SelectedRevision; changedpathstore.Clear (); - contentspathstore.Clear (); + diffRenderer.Lines = null; textviewDetails.Buffer.Clear (); if (d == null) return; diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.csproj b/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.csproj index a37d7f073ab..7961134cc34 100644 --- a/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.csproj +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.csproj @@ -524,6 +524,7 @@ + From 6b89aad9bd3ec34cc34bd27f9eeed173442107ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Kr=C3=BCger?= Date: Thu, 17 Oct 2019 16:42:10 +0200 Subject: [PATCH 3/5] [VersionControl] Added accessibility groups for the diff renderer widget. Individual lines can now be selected and activated. --- .../DiffRendererWidget.cs | 109 +++++++++++++++++- .../LogWidget.cs | 6 +- 2 files changed, 107 insertions(+), 8 deletions(-) diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/DiffRendererWidget.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/DiffRendererWidget.cs index 7b97b273252..b59e986c2e0 100644 --- a/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/DiffRendererWidget.cs +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/DiffRendererWidget.cs @@ -32,6 +32,10 @@ using MonoDevelop.Components; using System.Text; using MonoDevelop.Ide.Fonts; +using MonoDevelop.Core; +using MonoDevelop.Components; +using MonoDevelop.Components.AtkCocoaHelper; +using System.Linq; namespace MonoDevelop.VersionControl.Views { @@ -58,7 +62,12 @@ public string[] Lines { public int SelectedLine => selectedLine; - public event EventHandler DiffLineActivated; + public event EventHandler DiffLineActivated; + + protected virtual void OnDiffLineActivated (int line) + { + DiffLineActivated?.Invoke (this, line); + } public DiffRendererWidget () { @@ -79,6 +88,7 @@ protected override void OnDestroyed () { isDisposed = true; DisposeLayout (); + ClearAccessibleLines (); base.OnDestroyed (); } @@ -135,7 +145,7 @@ protected override bool OnButtonPressEvent (EventButton evnt) { if (selectedLine >= 0) { if (evnt.Type == Gdk.EventType.TwoButtonPress) - DiffLineActivated?.Invoke (this, EventArgs.Empty); + OnDiffLineActivated (selectedLine); } return base.OnButtonPressEvent (evnt); } @@ -153,6 +163,7 @@ protected override bool OnExposeEvent (Gdk.EventExpose e) { var window = e.Window; var widget = this; + ClearAccessibleLines (); using (Cairo.Context cr = Gdk.CairoHelper.Create (e.Window)) { int w, maxy; window.GetSize (out w, out maxy); @@ -211,7 +222,9 @@ protected override bool OnExposeEvent (Gdk.EventExpose e) DrawBlockBg (ctx, cell_area.X + 1, cell_area.Width - 2, block); // Get all text for the current block - StringBuilder sb = new StringBuilder (); + var sb = new StringBuilder (); + bool replaceFirst = false; + int subLine = 0; for (int n = block.FirstLine; n <= block.LastLine; n++) { string s = ProcessLine (lines [n]); if (n > block.FirstLine) @@ -219,8 +232,21 @@ protected override bool OnExposeEvent (Gdk.EventExpose e) if ((block.Type == BlockType.Added || block.Type == BlockType.Removed) && s.Length > 0) { sb.Append (' '); sb.Append (s, 1, s.Length - 1); + replaceFirst = true; } else sb.Append (s); + int idx = 0, curIdx = 0; + while ((idx = s.IndexOf ('\n')) >= 0) { + var y1 = block.YStart + subLine * lineHeight; + if (y1 < cell_area.Bottom && y1 + lineHeight >= e.Area.Y && y1 < e.Area.Bottom) + AddAccessibleLine (cell_area.X + 2 + LeftPaddingBlock, y1, block.Type, block.FirstLine + n + subLine, ref replaceFirst, s.Substring (curIdx, idx)); + subLine++; + curIdx = idx; + } + var y2 = block.YStart + subLine * lineHeight; + if (y2 < cell_area.Bottom && y2 + lineHeight >= e.Area.Y && y2 < e.Area.Bottom) + AddAccessibleLine (cell_area.X + 2 + LeftPaddingBlock, y2, block.Type, block.FirstLine + n + subLine, ref replaceFirst, curIdx > 0 ? s.Substring (curIdx) : s); + subLine++; } // Draw a special background for the selected line @@ -281,9 +307,19 @@ protected override bool OnExposeEvent (Gdk.EventExpose e) addedGC.Dispose (); infoGC.Dispose (); } + Accessible.SetAccessibleChildren (accessibleLines.Select (l => l.Accessible).ToArray ()); return true; } + void AddAccessibleLine (int x, int y, BlockType blockType, int lineNumber, ref bool replaceFirst, string text) + { + if (replaceFirst) { + text = ' ' + text.Substring (1); + replaceFirst = false; + } + this.accessibleLines.Add (new DiffLineAccessible (this, x, y, blockType, lineNumber, text)); + } + static string ProcessLine (string line) { if (line == null) @@ -537,5 +573,72 @@ static int ParseCurrentLine (string line) return cline; } + List accessibleLines = new List (); + + void ClearAccessibleLines () + { + foreach (var button in accessibleLines) { + button.Dispose (); + } + accessibleLines.Clear (); + Accessible.SetAccessibleChildren (Array.Empty ()); + } + + class DiffLineAccessible : IDisposable + { + DiffRendererWidget widget; + int line; + + public AccessibilityElementProxy Accessible { get; private set; } + public bool Visible { get; internal set; } + + public DiffLineAccessible (DiffRendererWidget widget, int x, int y, BlockType blockType, int line, string text) + { + this.widget = widget; + this.line = line; + + Accessible = AccessibilityElementProxy.ButtonElementProxy (); + Accessible.GtkParent = widget; + Accessible.PerformPress += PerformPress; + string msg; + switch (blockType) { + case BlockType.Added: + msg = GettextCatalog.GetString ("Added line"); + break; + case BlockType.Removed: + msg = GettextCatalog.GetString ("Removed line"); + break; + default: + msg = GettextCatalog.GetString ("Unchanged line"); + break; + } + Accessible.SetRole (AtkCocoa.Roles.AXButton, msg); + + Accessible.Label = GettextCatalog.GetString ("Line {0}, Text {1}", line, text); + + SetBounds (x, y, widget.Allocation.Width, widget.lineHeight); + } + + public void SetBounds (int x, int y, int w, int h) + { + Accessible.FrameInGtkParent = new Rectangle (x, y, w, h); + var cocoaY = widget.Allocation.Height - y - h; + Accessible.FrameInParent = new Rectangle (x, cocoaY, w, h); + } + + void PerformPress (object sender, EventArgs e) + { + widget.OnDiffLineActivated (this.line); + } + + public void Dispose () + { + if (Accessible == null) + return; + Accessible.PerformPress -= PerformPress; + Accessible = null; + widget = null; + } + } } } diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/LogWidget.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/LogWidget.cs index ed2e43ba4d6..6400b80388f 100644 --- a/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/LogWidget.cs +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/LogWidget.cs @@ -448,7 +448,7 @@ void RefreshClicked (object src, EventArgs args) revertButton.GetNativeWidget ().Sensitive = revertToButton.GetNativeWidget ().Sensitive = false; } - async void HandleTreeviewFilesDiffLineActivated (object sender, EventArgs e) + async void HandleTreeviewFilesDiffLineActivated (object sender, int line) { TreePath[] paths = treeviewFiles.Selection.GetSelectedRows (); @@ -459,10 +459,6 @@ async void HandleTreeviewFilesDiffLineActivated (object sender, EventArgs e) changedpathstore.GetIter (out iter, paths[0]); string fileName = (string)changedpathstore.GetValue (iter, colPath); - int line = diffRenderer.SelectedLine; - if (line == -1) - line = 1; - var proj = IdeApp.Workspace.GetProjectsContainingFile (fileName).FirstOrDefault (); var doc = await IdeApp.Workbench.OpenDocument (fileName, proj, line, 0, OpenDocumentOptions.Default | OpenDocumentOptions.OnlyInternalViewer); doc?.GetContent ()?.ShowDiffView (await SelectedRevision.GetPreviousAsync (), SelectedRevision, line); From 6b6eaacb5e6527f7e4f988a8cbe7ce855ac91fbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Kr=C3=BCger?= Date: Mon, 21 Oct 2019 11:17:44 +0200 Subject: [PATCH 4/5] [VersionControl] Improved DiffRendererWidget accessiblity. --- .../MonoDevelop.VersionControl.Views/DiffRendererWidget.cs | 7 ++++++- .../MonoDevelop.VersionControl.Views/LogWidget.cs | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/DiffRendererWidget.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/DiffRendererWidget.cs index b59e986c2e0..6c3656a8d65 100644 --- a/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/DiffRendererWidget.cs +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/DiffRendererWidget.cs @@ -72,6 +72,7 @@ protected virtual void OnDiffLineActivated (int line) public DiffRendererWidget () { Events |= EventMask.PointerMotionMask | EventMask.LeaveNotifyMask | EventMask.ButtonPressMask; + Accessible?.SetRole (AtkCocoa.Roles.AXGroup, GettextCatalog.GetString ("Diff View")); } void DisposeLayout () @@ -307,12 +308,14 @@ protected override bool OnExposeEvent (Gdk.EventExpose e) addedGC.Dispose (); infoGC.Dispose (); } - Accessible.SetAccessibleChildren (accessibleLines.Select (l => l.Accessible).ToArray ()); + Accessible?.SetAccessibleChildren (accessibleLines.Select (l => l.Accessible).ToArray ()); return true; } void AddAccessibleLine (int x, int y, BlockType blockType, int lineNumber, ref bool replaceFirst, string text) { + if (Accessible == null) + return; if (replaceFirst) { text = ' ' + text.Substring (1); replaceFirst = false; @@ -577,6 +580,8 @@ static int ParseCurrentLine (string line) void ClearAccessibleLines () { + if (Accessible == null) + return; foreach (var button in accessibleLines) { button.Dispose (); } diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/LogWidget.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/LogWidget.cs index 6400b80388f..cae03b99fa6 100644 --- a/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/LogWidget.cs +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/LogWidget.cs @@ -474,6 +474,8 @@ async void HandleTreeviewFilesDiffLineActivated (object sender, int line) void SetDiff (object o, EventArgs args) { this.diffRenderer.Lines = null; + this.scrolledwindowFileContents.Accessible.Description = GettextCatalog.GetString ("empty"); + if (!this.treeviewFiles.Selection.GetSelected (out var model, out var iter)) return; @@ -544,6 +546,7 @@ await Runtime.RunInMainThread (delegate { } await Runtime.RunInMainThread (delegate { this.diffRenderer.Lines = lines; + this.scrolledwindowFileContents.Accessible.Description = GettextCatalog.GetString ("file {0}", path); changedpathstore.SetValue (iter, colDiff, lines); }); }); From 48f8a1d03e2216ffa89b062ad1d1ab80a95c5cb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Kr=C3=BCger?= Date: Mon, 28 Oct 2019 10:48:51 +0100 Subject: [PATCH 5/5] [VersionControl] Added full path as label inside the log view. --- .../Gui/MonoDevelop.VersionControl.Views.LogWidget.cs | 10 ++++++++-- .../MonoDevelop.VersionControl.Views/LogWidget.cs | 10 +++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl/Gui/MonoDevelop.VersionControl.Views.LogWidget.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl/Gui/MonoDevelop.VersionControl.Views.LogWidget.cs index 699d7c7f5d2..9ec28b3985b 100644 --- a/main/src/addins/VersionControl/MonoDevelop.VersionControl/Gui/MonoDevelop.VersionControl.Views.LogWidget.cs +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl/Gui/MonoDevelop.VersionControl.Views.LogWidget.cs @@ -28,8 +28,10 @@ public partial class LogWidget private global::Gtk.TextView textviewDetails; private global::Gtk.ScrolledWindow scrolledwindowFiles; private global::Gtk.ScrolledWindow scrolledwindowFileContents = new ScrolledWindow (); - + Label labelFilePathName = new Gtk.Label (); + private HPaned fileHPaned; + private VBox vboxFileContents; protected virtual void Build () { @@ -187,7 +189,11 @@ protected virtual void Build () this.fileHPaned.Position = 333; fileHPaned.Add (this.scrolledwindowFiles); - fileHPaned.Add (this.scrolledwindowFileContents); + vboxFileContents = new VBox (); + labelFilePathName.Xalign = 0f; + vboxFileContents.PackStart (labelFilePathName, false, true, 6); + vboxFileContents.PackStart (scrolledwindowFileContents, true, true, 0); + fileHPaned.Add (vboxFileContents); this.vpaned1.Add (fileHPaned); this.vbox1.Add (this.vpaned1); diff --git a/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/LogWidget.cs b/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/LogWidget.cs index cae03b99fa6..fa484450d4f 100644 --- a/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/LogWidget.cs +++ b/main/src/addins/VersionControl/MonoDevelop.VersionControl/MonoDevelop.VersionControl.Views/LogWidget.cs @@ -476,11 +476,15 @@ void SetDiff (object o, EventArgs args) this.diffRenderer.Lines = null; this.scrolledwindowFileContents.Accessible.Description = GettextCatalog.GetString ("empty"); - if (!this.treeviewFiles.Selection.GetSelected (out var model, out var iter)) + if (!this.treeviewFiles.Selection.GetSelected (out var model, out var iter)) { + labelFilePathName.Text = ""; return; - + } this.diffRenderer.Lines = new string [] { GettextCatalog.GetString ("Loading data…") }; - string path = (string)changedpathstore.GetValue (iter, colPath); + FilePath path = (string)changedpathstore.GetValue (iter, colPath); + FilePath personal = Environment.GetFolderPath (Environment.SpecialFolder.Personal); + + labelFilePathName.Text = path.IsChildPathOf (personal) ? "~/" + path.ToRelative (personal) : path.ToString (); var rev = SelectedRevision; Task.Run (async delegate { string text = "";