diff --git a/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement.Gui/LicenseAcceptanceDialog.cs b/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement.Gui/LicenseAcceptanceDialog.cs index 852aa83080d..0e1a16751ea 100644 --- a/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement.Gui/LicenseAcceptanceDialog.cs +++ b/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement.Gui/LicenseAcceptanceDialog.cs @@ -28,9 +28,11 @@ // using System; +using System.Collections.Generic; using System.Linq; using MonoDevelop.Core; -using MonoDevelop.Ide; +using MonoDevelop.Ide; +using NuGet.PackageManagement.UI; using Xwt; namespace MonoDevelop.PackageManagement @@ -46,7 +48,6 @@ internal class LicenseAcceptanceDialog : Dialog public LicenseAcceptanceDialog (LicenseAcceptanceViewModel viewModel) { Height = 350; - Resizable = false; Padding = 0; Title = GettextCatalog.GetString ("License Acceptance"); this.viewModel = viewModel; @@ -126,21 +127,20 @@ protected override void OnShown () void AddPackage (PackageLicenseViewModel package) { + var label = new Label { + Markup = string.Format ("{0} – {1}", package.Id, package.Author), + }; var titleBox = new VBox (); titleBox.Spacing = 0; titleBox.MarginBottom = 4; - titleBox.PackStart (new Label { - Markup = string.Format ("{0} – {1}", package.Id, package.Author), - }); - var licenseLabel = new LinkLabel (GettextCatalog.GetString ("View License")); - licenseLabel.Uri = package.LicenseUrl; - licenseLabel.LinkClicked += (sender, e) => IdeServices.DesktopService.ShowUrl (e.Target.AbsoluteUri); - titleBox.PackStart (licenseLabel); + titleBox.PackStart (label); + AddLicenseInfo (package, titleBox); var rowBox = new HBox (); rowBox.Margin = rowMargin; var icon = new ImageView (ImageService.GetIcon ("md-package", Gtk.IconSize.Dnd)); + icon.Accessible.LabelWidget = label; if (package.IconUrl != null && !string.IsNullOrEmpty (package.IconUrl.AbsoluteUri)) imageLoader.LoadFrom (package.IconUrl, icon); @@ -151,6 +151,108 @@ void AddPackage (PackageLicenseViewModel package) packagesList.PackStart (rowBox); } + static void AddLicenseInfo (PackageLicenseViewModel package, VBox parentVBox) + { + if (package.LicenseLinks.Any ()) { + if (package.LicenseLinks.Count == 1) { + IText textLink = package.LicenseLinks [0]; + if (textLink is LicenseText licenseText) { + AddLicenseLinkLabel (licenseText.Link, licenseText.Text, parentVBox); + return; + } else if (textLink is LicenseFileText licenseFileText) { + AddFileLicenseLinkLabel (licenseFileText, parentVBox); + return; + } else { + // Warning or free text fallback to showing an expression which will handle this. + } + } + AddLicenseExpressionLabel (package, parentVBox); + } else { + AddLicenseLinkLabel (package.LicenseUrl, GettextCatalog.GetString ("View License"), parentVBox); + } + } + + static void AddLicenseExpressionLabel (PackageLicenseViewModel package, VBox parentVBox) + { + // Using the HBox as an accessibility group since the license will have multiple parts. + // Cannot use a single label with markup for the license expression since it is not accessible, + // also when using the native toolkit the hyperlinks cannot be clicked. + var hbox = new HBox (); + hbox.Spacing = 0; + hbox.Accessible.Role = Xwt.Accessibility.Role.Group; + hbox.Accessible.Label = GettextCatalog.GetString ("License Expression"); + + // Get any warnings. + List warnings = null; + foreach (IText textLink in package.LicenseLinks) { + if (textLink is WarningText warning) { + warnings ??= new List (); + warnings.Add (warning); + } + } + + if (warnings?.Any () == true) { + var warningTextBuilder = StringBuilderCache.Allocate (); + foreach (WarningText warning in warnings) { + warningTextBuilder.Append (warning.Text); + warningTextBuilder.Append (' '); + } + + var warningWidget = new MonoDevelop.Components.InformationPopoverWidget (); + warningWidget.Severity = Ide.Tasks.TaskSeverity.Warning; + warningWidget.Message = StringBuilderCache.ReturnAndFree (warningTextBuilder).TrimEnd (); + warningWidget.MarginRight = 6; + + // Disable focus. With focus enabled the info popup ends up being displayed and focused + // if it is the first package license on opening the dialog. Unable to set focus to a + // dialog button. + warningWidget.CanGetFocus = false; + + hbox.PackStart (warningWidget); + } + + foreach (IText textLink in package.LicenseLinks) { + if (textLink is LicenseText licenseText) { + var label = new LinkLabel (licenseText.Text); + label.TextAlignment = Alignment.Start; + label.Uri = licenseText.Link; + label.LinkClicked += (sender, e) => IdeServices.DesktopService.ShowUrl (e.Target.AbsoluteUri); + hbox.PackStart (label, false, false); + } else if (textLink is WarningText warning) { + // Ignore. + } else if (textLink is LicenseFileText licenseFileText) { + // Should not happen. A license expression should not contain a license file. + LoggingService.LogError ("Unexpected LicenseFileText when building licence expression {0}", licenseFileText.Text); + } else { + var label = new Label (textLink.Text); + label.TextAlignment = Alignment.Start; + hbox.PackStart (label, false, false); + } + } + + parentVBox.PackStart (hbox, false, false); + } + + static void AddFileLicenseLinkLabel (LicenseFileText licenseFileText, VBox parentVBox) + { + var licenseLabel = new LinkLabel (GettextCatalog.GetString ("View License")); + licenseLabel.Uri = licenseFileText.CreateLicenseFileUri (); + licenseLabel.Tag = licenseFileText; + licenseLabel.NavigateToUrl += (sender, e) => { + e.SetHandled (); + ShowFileDialog ((LinkLabel)sender); + }; + parentVBox.PackStart (licenseLabel); + } + + static void AddLicenseLinkLabel (Uri licenseUrl, string labelText, VBox parentVBox) + { + var licenseLabel = new LinkLabel (labelText); + licenseLabel.Uri = licenseUrl; + licenseLabel.LinkClicked += (sender, e) => IdeServices.DesktopService.ShowUrl (e.Target.AbsoluteUri); + parentVBox.PackStart (licenseLabel); + } + public new bool Run (WindowFrame parentWindow) { return base.Run (parentWindow) == Command.Ok; @@ -162,6 +264,16 @@ protected override void Dispose (bool disposing) imageLoader.Dispose (); base.Dispose (disposing); } + + static void ShowFileDialog (LinkLabel label) + { + var licenseFileText = (LicenseFileText)label.Tag; + Xwt.Toolkit.NativeEngine.Invoke (delegate { + using (var dialog = new LicenseFileDialog (licenseFileText)) { + dialog.Run (label.ParentWindow); + } + }); + } } } diff --git a/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement.Gui/LicenseFileDialog.cs b/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement.Gui/LicenseFileDialog.cs new file mode 100644 index 00000000000..4ed7da4312b --- /dev/null +++ b/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement.Gui/LicenseFileDialog.cs @@ -0,0 +1,91 @@ +// +// LicenseFileDialog.cs +// +// Author: +// Matt Ward +// +// Copyright (c) 2019 Microsoft Corporation +// +// 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.ComponentModel; +using NuGet.PackageManagement.UI; +using Xwt; +using Xwt.Formats; + +namespace MonoDevelop.PackageManagement +{ + sealed class LicenseFileDialog : Dialog + { + RichTextView textView; + LicenseFileText licenseFileText; + + public LicenseFileDialog (LicenseFileText licenseFileText) + { + this.licenseFileText = licenseFileText; + + Build (); + LoadText (); + + licenseFileText.PropertyChanged += LicenseFileTextPropertyChanged; + licenseFileText.LoadLicenseFile (); + } + + void Build () + { + Height = 450; + Width = 450; + Title = licenseFileText.LicenseHeader; + + var scrollView = new ScrollView (); + scrollView.HorizontalScrollPolicy = ScrollPolicy.Never; + Content = scrollView; + + textView = new RichTextView (); + textView.ReadOnly = true; + scrollView.Content = textView; + + var okCommand = new Command ("Ok", Core.GettextCatalog.GetString ("OK")); + Buttons.Add (); + DefaultCommand = okCommand; + } + + void LicenseFileTextPropertyChanged (object sender, PropertyChangedEventArgs e) + { + LoadText (); + + // Need to refresh the dialog after the license text has been loaded. Otherwise when using the + // native toolkit the vertical scrollbar is not enabled unless you re-size the dialog. + OnReallocate (); + } + + void LoadText () + { + textView.LoadText (licenseFileText.LicenseText, TextFormat.Plain); + } + + protected override void Dispose (bool disposing) + { + base.Dispose (disposing); + if (disposing) { + licenseFileText.PropertyChanged -= LicenseFileTextPropertyChanged; + } + } + } +} diff --git a/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement.Gui/LicenseFileTextExtensions.cs b/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement.Gui/LicenseFileTextExtensions.cs new file mode 100644 index 00000000000..154255e3ce0 --- /dev/null +++ b/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement.Gui/LicenseFileTextExtensions.cs @@ -0,0 +1,39 @@ +// +// LicenseFileTextExtensions.cs +// +// Author: +// Matt Ward +// +// Copyright (c) 2019 Microsoft Corporation +// +// 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 NuGet.PackageManagement.UI; + +namespace MonoDevelop.PackageManagement +{ + static class LicenseFileTextExtensions + { + public static Uri CreateLicenseFileUri (this LicenseFileText licenseFileText) + { + return new Uri ($"file:///{licenseFileText.Text}"); + } + } +} diff --git a/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement.Gui/LicenseLinkMarkupBuilder.cs b/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement.Gui/LicenseLinkMarkupBuilder.cs new file mode 100644 index 00000000000..3a6149526d1 --- /dev/null +++ b/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement.Gui/LicenseLinkMarkupBuilder.cs @@ -0,0 +1,73 @@ +// +// LicenseLinkMarkupBuilder.cs +// +// Author: +// Matt Ward +// +// Copyright (c) 2019 Microsoft Corporation +// +// 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 System.Linq; +using System.Security; +using MonoDevelop.Core; +using NuGet.PackageManagement.UI; + +namespace MonoDevelop.PackageManagement +{ + sealed class LicenseLinkMarkupBuilder + { + List warnings; + + public string GetMarkup (IReadOnlyList textLinks) + { + var markupBuilder = StringBuilderCache.Allocate (); + + foreach (IText textLink in textLinks) { + if (textLink is LicenseText licenseText) { + markupBuilder.Append (GetUriMarkup (licenseText.Link, licenseText.Text)); + } else if (textLink is LicenseFileText licenseFileText) { + // Should not happen. Building an expression should not contain a license file. + LoggingService.LogError ("Unexpected LicenseFileText when building markup {0}", licenseFileText.Text); + } else if (textLink is WarningText warning) { + warnings ??= new List (); + warnings.Add (warning); + } else { + markupBuilder.Append (textLink.Text); + } + } + + return StringBuilderCache.ReturnAndFree (markupBuilder); + } + + public IEnumerable Warnings { + get { return warnings ?? Enumerable.Empty (); } + } + + static string GetUriMarkup (Uri uri, string text) + { + return string.Format ( + "{1}", + uri != null ? SecurityElement.Escape (uri.ToString ()) : string.Empty, + SecurityElement.Escape (text)); + } + } +} diff --git a/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement.Gui/ManagePackagesDialog.UI.cs b/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement.Gui/ManagePackagesDialog.UI.cs index 5a23a14c4d9..8c594e2a57d 100644 --- a/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement.Gui/ManagePackagesDialog.UI.cs +++ b/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement.Gui/ManagePackagesDialog.UI.cs @@ -47,7 +47,10 @@ internal partial class ManagePackagesDialog : ExtendedTitleBarDialog Label packageAuthor; Label packagePublishedDate; Label packageDownloads; + Label packageLicenseLabel; LinkLabel packageLicenseLink; + InformationPopoverWidget packageLicenseMetadataWarningInfoPopoverWidget; + Label packageLicenseMetadataLinkLabel; LinkLabel packageProjectPageLink; Label packageDependenciesList; HBox packageDependenciesHBox; @@ -346,15 +349,27 @@ void Build () var packageLicenseHBox = new HBox (); packageInfoVBox.PackStart (packageLicenseHBox); - var packageLicenseLabel = new Label (); + packageLicenseLabel = new Label (); packageLicenseLabel.Text = GettextCatalog.GetString ("License"); packageLicenseLabel.Font = packageInfoBoldFont; - packageLicenseHBox.PackStart (packageLicenseLabel); + packageLicenseHBox.PackStart (packageLicenseLabel, vpos: WidgetPlacement.Start); packageLicenseLink = new LinkLabel (); packageLicenseLink.Text = GettextCatalog.GetString ("View License"); packageLicenseLink.Font = packageInfoSmallFont; - packageLicenseHBox.PackEnd (packageLicenseLink); + packageLicenseLink.TextAlignment = Alignment.End; + packageLicenseHBox.PackStart (packageLicenseLink, true); + + packageLicenseMetadataLinkLabel = new Label (); + packageLicenseMetadataLinkLabel.Wrap = WrapMode.Word; + packageLicenseMetadataLinkLabel.Font = packageInfoSmallFont; + packageLicenseMetadataLinkLabel.Accessible.LabelWidget = packageLicenseLabel; + packageLicenseMetadataLinkLabel.TextAlignment = Alignment.End; + packageLicenseHBox.PackStart (packageLicenseMetadataLinkLabel, true, vpos: WidgetPlacement.Start); + + packageLicenseMetadataWarningInfoPopoverWidget = new InformationPopoverWidget (); + packageLicenseMetadataWarningInfoPopoverWidget.Severity = Ide.Tasks.TaskSeverity.Warning; + packageLicenseHBox.PackStart (packageLicenseMetadataWarningInfoPopoverWidget, vpos: WidgetPlacement.Start, hpos: WidgetPlacement.End); // Package project page. var packageProjectPageHBox = new HBox (); diff --git a/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement.Gui/ManagePackagesDialog.cs b/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement.Gui/ManagePackagesDialog.cs index b5b1fac718b..af960723bbb 100644 --- a/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement.Gui/ManagePackagesDialog.cs +++ b/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement.Gui/ManagePackagesDialog.cs @@ -27,10 +27,12 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Security; using MonoDevelop.Components.AtkCocoaHelper; using MonoDevelop.Core; using MonoDevelop.Ide; using MonoDevelop.Projects; +using NuGet.PackageManagement.UI; using NuGet.Versioning; using Xwt; using Xwt.Drawing; @@ -98,6 +100,7 @@ public ManagePackagesDialog ( LoadViewModel (initialSearch); closeButton.Clicked += CloseButtonClicked; + packageLicenseLink.NavigateToUrl += PackageLicenseNavigateToUrl; showPrereleaseCheckBox.Clicked += ShowPrereleaseCheckBoxClicked; packageSourceComboBox.SelectionChanged += PackageSourceChanged; addPackagesButton.Clicked += AddPackagesButtonClicked; @@ -147,6 +150,7 @@ void UpdateTabAccessibility () protected override void Dispose (bool disposing) { closeButton.Clicked -= CloseButtonClicked; + packageLicenseLink.NavigateToUrl -= PackageLicenseNavigateToUrl; currentPackageVersionLabel.BoundsChanged -= PackageVersionLabelBoundsChanged; showPrereleaseCheckBox.Clicked -= ShowPrereleaseCheckBoxClicked; @@ -477,6 +481,10 @@ void ShowPackageInformation (ManagePackagesSearchResultViewModel packageViewMode { bool consolidate = viewModel.IsConsolidatePageSelected; + foreach (Widget child in packageInfoVBox.Children) { + child.Visible = !consolidate; + } + if (consolidate) { projectsListViewLabel.Text = GettextCatalog.GetString ("Select projects and a version for a consolidation."); } else { @@ -491,7 +499,7 @@ void ShowPackageInformation (ManagePackagesSearchResultViewModel packageViewMode this.packageId.Visible = packageViewModel.HasNoGalleryUrl; ShowUri (this.packageIdLink, packageViewModel.GalleryUrl, packageViewModel.Id); ShowUri (this.packageProjectPageLink, packageViewModel.ProjectUrl); - ShowUri (this.packageLicenseLink, packageViewModel.LicenseUrl); + ShowLicense (packageViewModel); PopulatePackageDependencies (packageViewModel); } @@ -511,10 +519,6 @@ void ShowPackageInformation (ManagePackagesSearchResultViewModel packageViewMode packageVersionsHBox.Visible = true; } - foreach (Widget child in packageInfoVBox.Children) { - child.Visible = !consolidate; - } - if (consolidate) { PopulateProjectList (); } else { @@ -1042,6 +1046,7 @@ void SelectedPackageViewModelChanged (object sender, PropertyChangedEventArgs e) } else { if (!viewModel.IsConsolidatePageSelected) { packagePublishedDate.Text = viewModel.SelectedPackage.GetLastPublishedDisplayText (); + ShowLicense (viewModel.SelectedPackage); PopulatePackageDependencies (viewModel.SelectedPackage); } } @@ -1284,5 +1289,82 @@ void ProjectCheckBoxCellViewToggled (object sender, WidgetEventArgs e) UpdateAddPackagesButton (); } + + void ShowLicense (ManagePackagesSearchResultViewModel packageViewModel) + { + packageLicenseLink.Tag = null; + if (packageViewModel.HasLicenseMetadata) { + ShowLicenseMetadata (packageViewModel); + } else { + packageLicenseMetadataLinkLabel.Visible = false; + ShowUri (packageLicenseLink, packageViewModel.LicenseUrl, GettextCatalog.GetString ("View License")); + } + } + + void PackageLicenseNavigateToUrl (object sender, NavigateToUrlEventArgs e) + { + var licenseFileText = packageLicenseLink.Tag as LicenseFileText; + if (licenseFileText != null) { + e.SetHandled (); + Toolkit.NativeEngine.Invoke (delegate { + using (var dialog = new LicenseFileDialog (licenseFileText)) { + dialog.Run (this); + } + }); + } + } + + void ShowLicenseMetadata (ManagePackagesSearchResultViewModel packageViewModel) + { + packageLicenseLink.Visible = false; + packageLicenseMetadataLinkLabel.Visible = false; + packageLicenseMetadataWarningInfoPopoverWidget.Visible = false; + + var textLinks = packageViewModel.GetLicenseLinks (); + if (textLinks.Count == 0) { + return; + } + + // Single link - show this on the same line as the License label. + if (textLinks.Count == 1) { + packageLicenseLink.Visible = true; + + IText textLink = textLinks [0]; + if (textLink is LicenseText licenseText) { + packageLicenseLink.Text = licenseText.Text; + packageLicenseLink.Uri = licenseText.Link; + return; + } else if (textLink is LicenseFileText licenseFileText) { + packageLicenseLink.Text = GettextCatalog.GetString ("View License"); + packageLicenseLink.Uri = licenseFileText.CreateLicenseFileUri (); + packageLicenseLink.Tag = licenseFileText; + return; + } else { + // Warning or plain text - handled below. + } + } + + // Multiple text links. We need to allow these to wrap so show these below the license label. + var markupBuilder = new LicenseLinkMarkupBuilder (); + packageLicenseMetadataLinkLabel.Markup = markupBuilder.GetMarkup (textLinks); + packageLicenseMetadataLinkLabel.Visible = true; + + if (markupBuilder.Warnings.Any ()) { + AddWarnings (markupBuilder.Warnings); + } else { + packageLicenseMetadataWarningInfoPopoverWidget.Visible = false; + } + } + + void AddWarnings (IEnumerable warnings) + { + var warningTextBuilder = StringBuilderCache.Allocate (); + foreach (WarningText warning in warnings) { + warningTextBuilder.Append (warning.Text); + warningTextBuilder.Append (' '); + } + packageLicenseMetadataWarningInfoPopoverWidget.Message = StringBuilderCache.ReturnAndFree (warningTextBuilder).TrimEnd (); + packageLicenseMetadataWarningInfoPopoverWidget.Visible = true; + } } } \ No newline at end of file diff --git a/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement.csproj b/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement.csproj index 0cee2f7ffa7..50f0d17b01f 100644 --- a/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement.csproj +++ b/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement.csproj @@ -369,6 +369,15 @@ + + + + + + + + + diff --git a/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement/ManagePackagesSearchResultViewModel.cs b/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement/ManagePackagesSearchResultViewModel.cs index 6aa0d2c97a1..8234f3ddf5d 100644 --- a/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement/ManagePackagesSearchResultViewModel.cs +++ b/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement/ManagePackagesSearchResultViewModel.cs @@ -47,6 +47,7 @@ internal class ManagePackagesSearchResultViewModel : ViewModelBase licenseLinks; string summary; bool isChecked; @@ -383,6 +384,7 @@ void OnPackageMetadataLoaded (Task task) if (metadata != null) { viewModel.Published = metadata.Published; dependencies = GetCompatibleDependencies ().ToArray (); + licenseLinks = PackageLicenseUtilities.GenerateLicenseLinks (metadata); OnPropertyChanged ("Dependencies"); } } @@ -467,6 +469,16 @@ public string GetCurrentPackageVersionAdditionalText () { return parent.GetCurrentPackageVersionAdditionalText (Id); } + + public bool HasLicenseMetadata { + get { return packageDetailModel?.PackageMetadata?.LicenseMetadata != null; } + } + + public IReadOnlyList GetLicenseLinks () + { + licenseLinks ??= new List (); + return licenseLinks; + } } } diff --git a/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement/NuGetPackageLicense.cs b/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement/NuGetPackageLicense.cs index 3bbb43c9e9e..f4db7130cb7 100644 --- a/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement/NuGetPackageLicense.cs +++ b/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement/NuGetPackageLicense.cs @@ -25,6 +25,8 @@ // THE SOFTWARE. using System; +using System.Collections.Generic; +using NuGet.PackageManagement.UI; using NuGet.Packaging.Core; using NuGet.Protocol.Core.Types; @@ -40,6 +42,7 @@ public NuGetPackageLicense (IPackageSearchMetadata metadata) PackageAuthor = metadata.Authors; LicenseUrl = metadata.LicenseUrl; IconUrl = metadata.IconUrl; + LicenseLinks = PackageLicenseUtilities.GenerateLicenseLinks (metadata); } public PackageIdentity PackageIdentity { get; private set; } @@ -48,6 +51,7 @@ public NuGetPackageLicense (IPackageSearchMetadata metadata) public string PackageAuthor { get; private set; } public Uri LicenseUrl { get; private set; } public Uri IconUrl { get; private set; } + public IReadOnlyList LicenseLinks { get; } } } diff --git a/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement/PackageLicenseViewModel.cs b/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement/PackageLicenseViewModel.cs index cad8322ad45..ec72fb70db0 100644 --- a/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement/PackageLicenseViewModel.cs +++ b/main/src/addins/MonoDevelop.PackageManagement/MonoDevelop.PackageManagement/PackageLicenseViewModel.cs @@ -27,6 +27,9 @@ // using System; +using System.Collections.Generic; +using System.Linq; +using NuGet.PackageManagement.UI; namespace MonoDevelop.PackageManagement { @@ -54,5 +57,9 @@ public string Author { public Uri IconUrl { get { return packageLicense.IconUrl; } } + + public IReadOnlyList LicenseLinks { + get { return packageLicense.LicenseLinks; } + } } } diff --git a/main/src/addins/MonoDevelop.PackageManagement/NuGet.PackageManagement.UI/DetailedPackageMetadata.cs b/main/src/addins/MonoDevelop.PackageManagement/NuGet.PackageManagement.UI/DetailedPackageMetadata.cs index e16eddcd532..91ab22e8090 100644 --- a/main/src/addins/MonoDevelop.PackageManagement/NuGet.PackageManagement.UI/DetailedPackageMetadata.cs +++ b/main/src/addins/MonoDevelop.PackageManagement/NuGet.PackageManagement.UI/DetailedPackageMetadata.cs @@ -4,6 +4,8 @@ using System; using System.Collections.Generic; using System.Linq; +using NuGet.Packaging; +using NuGet.Protocol; using NuGet.Protocol.Core.Types; using NuGet.Versioning; @@ -17,6 +19,7 @@ public DetailedPackageMetadata() public DetailedPackageMetadata(IPackageSearchMetadata serverData, long? downloadCount) { + Id = serverData.Identity.Id; Version = serverData.Identity.Version; Summary = serverData.Summary; Description = serverData.Description; @@ -34,9 +37,16 @@ public DetailedPackageMetadata(IPackageSearchMetadata serverData, long? download ?? new PackageDependencySetMetadata[] { }; HasDependencies = DependencySets.Any( dependencySet => dependencySet.Dependencies != null && dependencySet.Dependencies.Count > 0); + LicenseMetadata = serverData.LicenseMetadata; + localMetadata = serverData as LocalPackageSearchMetadata; } + readonly LocalPackageSearchMetadata localMetadata; + + public string Id { get; set; } + public NuGetVersion Version { get; set; } + public string Summary { get; set; } public string Description { get; set; } @@ -63,5 +73,17 @@ public DetailedPackageMetadata(IPackageSearchMetadata serverData, long? download // This property is used by data binding to display text "No dependencies" public bool HasDependencies { get; set; } + + public LicenseMetadata LicenseMetadata { get; set; } + + public IReadOnlyList LicenseLinks => PackageLicenseUtilities.GenerateLicenseLinks (this); + + public string LoadFileAsText (string path) + { + if (localMetadata != null) { + return localMetadata.LoadFileAsText (path); + } + return null; + } } } \ No newline at end of file diff --git a/main/src/addins/MonoDevelop.PackageManagement/NuGet.PackageManagement.UI/Models/FreeText.cs b/main/src/addins/MonoDevelop.PackageManagement/NuGet.PackageManagement.UI/Models/FreeText.cs new file mode 100644 index 00000000000..80508374086 --- /dev/null +++ b/main/src/addins/MonoDevelop.PackageManagement/NuGet.PackageManagement.UI/Models/FreeText.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace NuGet.PackageManagement.UI +{ + internal class FreeText : IText + { + public FreeText (string text) + { + Text = text; + } + + public string Text { get; } + } +} \ No newline at end of file diff --git a/main/src/addins/MonoDevelop.PackageManagement/NuGet.PackageManagement.UI/Models/IText.cs b/main/src/addins/MonoDevelop.PackageManagement/NuGet.PackageManagement.UI/Models/IText.cs new file mode 100644 index 00000000000..afbd4ddc043 --- /dev/null +++ b/main/src/addins/MonoDevelop.PackageManagement/NuGet.PackageManagement.UI/Models/IText.cs @@ -0,0 +1,10 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace NuGet.PackageManagement.UI +{ + public interface IText + { + string Text { get; } + } +} \ No newline at end of file diff --git a/main/src/addins/MonoDevelop.PackageManagement/NuGet.PackageManagement.UI/Models/LicenseFileText.cs b/main/src/addins/MonoDevelop.PackageManagement/NuGet.PackageManagement.UI/Models/LicenseFileText.cs new file mode 100644 index 00000000000..43f53cc98d9 --- /dev/null +++ b/main/src/addins/MonoDevelop.PackageManagement/NuGet.PackageManagement.UI/Models/LicenseFileText.cs @@ -0,0 +1,78 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.ComponentModel; +using System.Threading; +using System.Threading.Tasks; +using MonoDevelop.Core; + +namespace NuGet.PackageManagement.UI +{ + internal class LicenseFileText : IText, INotifyPropertyChanged + { + string _text; + string _licenseText; + string _licenseHeader; + readonly string _licenseFileLocation; + private Func _loadFileFromPackage; + + int _initialized; + + internal LicenseFileText (string text, string licenseFileHeader, Func loadFileFromPackage, string licenseFileLocation) + { + _text = text; + _licenseHeader = licenseFileHeader; + _licenseText = GettextCatalog.GetString ("Loading license file…"); + _loadFileFromPackage = loadFileFromPackage; + _licenseFileLocation = licenseFileLocation; + } + + internal void LoadLicenseFile () + { + if (Interlocked.CompareExchange (ref _initialized, 1, 0) == 0) { + if (_loadFileFromPackage != null) { + Task.Run (async () => { + string content = _loadFileFromPackage (_licenseFileLocation); + await Runtime.RunInMainThread (() => { + LicenseText = content; + }); + }).Ignore (); + } + } + } + + public string LicenseHeader { + get => _licenseHeader; + set { + _licenseHeader = value; + OnPropertyChanged ("LicenseHeader"); + } + } + + public string Text { + get => _text; + set { + _text = value; + OnPropertyChanged ("Text"); + } + } + + public string LicenseFileLocation => _licenseFileLocation; + + public string LicenseText { + get => _licenseText; + set { + _licenseText = value; + OnPropertyChanged ("LicenseText"); + } + } + + public event PropertyChangedEventHandler PropertyChanged; + + protected void OnPropertyChanged (string name) + { + PropertyChanged?.Invoke (this, new System.ComponentModel.PropertyChangedEventArgs (name)); + } + } +} \ No newline at end of file diff --git a/main/src/addins/MonoDevelop.PackageManagement/NuGet.PackageManagement.UI/Models/LicenseText.cs b/main/src/addins/MonoDevelop.PackageManagement/NuGet.PackageManagement.UI/Models/LicenseText.cs new file mode 100644 index 00000000000..41b470657cf --- /dev/null +++ b/main/src/addins/MonoDevelop.PackageManagement/NuGet.PackageManagement.UI/Models/LicenseText.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace NuGet.PackageManagement.UI +{ + internal class LicenseText : IText + { + public LicenseText (string text, Uri link) + { + Text = text; + Link = link; + } + + public string Text { get; set; } + public Uri Link { get; set; } + } +} \ No newline at end of file diff --git a/main/src/addins/MonoDevelop.PackageManagement/NuGet.PackageManagement.UI/Models/WarningText.cs b/main/src/addins/MonoDevelop.PackageManagement/NuGet.PackageManagement.UI/Models/WarningText.cs new file mode 100644 index 00000000000..dff77a14aa0 --- /dev/null +++ b/main/src/addins/MonoDevelop.PackageManagement/NuGet.PackageManagement.UI/Models/WarningText.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace NuGet.PackageManagement.UI +{ + internal class WarningText : IText + { + public WarningText (string text) + { + Text = text; + } + + public string Text { get; } + } +} \ No newline at end of file diff --git a/main/src/addins/MonoDevelop.PackageManagement/NuGet.PackageManagement.UI/PackageLicenseUtilities.cs b/main/src/addins/MonoDevelop.PackageManagement/NuGet.PackageManagement.UI/PackageLicenseUtilities.cs new file mode 100644 index 00000000000..30a0ad015d4 --- /dev/null +++ b/main/src/addins/MonoDevelop.PackageManagement/NuGet.PackageManagement.UI/PackageLicenseUtilities.cs @@ -0,0 +1,134 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Globalization; +using MonoDevelop.Core; +using NuGet.Packaging; +using NuGet.Packaging.Licenses; +using NuGet.Protocol; +using NuGet.Protocol.Core.Types; + +namespace NuGet.PackageManagement.UI +{ + internal class PackageLicenseUtilities + { + internal static IReadOnlyList GenerateLicenseLinks (DetailedPackageMetadata metadata) + { + return GenerateLicenseLinks (metadata.LicenseMetadata, metadata.LicenseUrl, GettextCatalog.GetString ("{0} License", metadata.Id), metadata.LoadFileAsText); + } + + internal static IReadOnlyList GenerateLicenseLinks (IPackageSearchMetadata metadata) + { + if (metadata is LocalPackageSearchMetadata localMetadata) { + return GenerateLicenseLinks (metadata.LicenseMetadata, metadata.LicenseUrl, GettextCatalog.GetString ("{0} License", metadata.Identity.Id), localMetadata.LoadFileAsText); + } + return GenerateLicenseLinks (metadata.LicenseMetadata, metadata.LicenseUrl, metadata.Identity.Id, null); + } + + internal static IReadOnlyList GenerateLicenseLinks (LicenseMetadata licenseMetadata, Uri licenseUrl, string licenseFileHeader, Func loadFile) + { + if (licenseMetadata != null) { + return GenerateLicenseLinks (licenseMetadata, licenseFileHeader, loadFile); + } else if (licenseUrl != null) { + return new List () { new LicenseText (GettextCatalog.GetString ("View License"), licenseUrl) }; + } + return new List (); + } + + //internal static Paragraph[] GenerateParagraphs (string licenseContent) + //{ + // var textParagraphs = licenseContent.Split ( + // new[] { "\n\n", "\r\n\r\n" }, // Take care of paragraphs regardless of the name ending. It's a best effort, so weird line ending combinations might not work too well. + // StringSplitOptions.None); + + // var paragraphs = new Paragraph[textParagraphs.Length]; + // for (var i = 0; i < textParagraphs.Length; i++) { + // paragraphs[i] = new Paragraph (new Run (textParagraphs[i])); + // } + // return paragraphs; + //} + + // Internal for testing purposes. + internal static IReadOnlyList GenerateLicenseLinks (LicenseMetadata metadata, string licenseFileHeader, Func loadFile) + { + var list = new List (); + + if (metadata.WarningsAndErrors != null) { + list.Add (new WarningText (string.Join (Environment.NewLine, metadata.WarningsAndErrors))); + } + + switch (metadata.Type) { + case LicenseType.Expression: + + if (metadata.LicenseExpression != null && !metadata.LicenseExpression.IsUnlicensed ()) { + var identifiers = new List (); + PopulateLicenseIdentifiers (metadata.LicenseExpression, identifiers); + + var licenseToBeProcessed = metadata.License; + + foreach (var identifier in identifiers) { + var licenseStart = licenseToBeProcessed.IndexOf (identifier); + if (licenseStart != 0) { + list.Add (new FreeText (licenseToBeProcessed.Substring (0, licenseStart))); + } + var license = licenseToBeProcessed.Substring (licenseStart, identifier.Length); + list.Add (new LicenseText (license, new Uri (string.Format (LicenseMetadata.LicenseServiceLinkTemplate, license)))); + licenseToBeProcessed = licenseToBeProcessed.Substring (licenseStart + identifier.Length); + } + + if (licenseToBeProcessed.Length != 0) { + list.Add (new FreeText (licenseToBeProcessed)); + } + } else { + list.Add (new FreeText (metadata.License)); + } + + break; + + case LicenseType.File: + list.Add (new LicenseFileText (metadata.License, licenseFileHeader, loadFile, metadata.License)); + break; + + default: + break; + } + + return list; + } + + private static void PopulateLicenseIdentifiers (NuGetLicenseExpression expression, IList identifiers) + { + switch (expression.Type) { + case LicenseExpressionType.License: + var license = (NuGetLicense)expression; + identifiers.Add (license.Identifier); + break; + + case LicenseExpressionType.Operator: + var licenseOperator = (LicenseOperator)expression; + switch (licenseOperator.OperatorType) { + case LicenseOperatorType.LogicalOperator: + var logicalOperator = (LogicalOperator)licenseOperator; + PopulateLicenseIdentifiers (logicalOperator.Left, identifiers); + PopulateLicenseIdentifiers (logicalOperator.Right, identifiers); + break; + + case LicenseOperatorType.WithOperator: + var withOperator = (WithOperator)licenseOperator; + identifiers.Add (withOperator.License.Identifier); + identifiers.Add (withOperator.Exception.Identifier); + break; + + default: + break; + } + break; + + default: + break; + } + } + } +} \ No newline at end of file