diff --git a/app/src/cc/arduino/contributions/libraries/ui/ContributedLibraryTableCellJPanel.java b/app/src/cc/arduino/contributions/libraries/ui/ContributedLibraryTableCellJPanel.java index 99d4b66d23e..d4382cf5147 100644 --- a/app/src/cc/arduino/contributions/libraries/ui/ContributedLibraryTableCellJPanel.java +++ b/app/src/cc/arduino/contributions/libraries/ui/ContributedLibraryTableCellJPanel.java @@ -213,6 +213,7 @@ public ContributedLibraryTableCellJPanel(JTable parentTable, Object value, } } + // TODO Make this a method of Theme private JTextPane makeNewDescription() { if (getComponentCount() > 0) { remove(0); diff --git a/app/src/cc/arduino/contributions/libraries/ui/LibraryManagerUI.java b/app/src/cc/arduino/contributions/libraries/ui/LibraryManagerUI.java index 7ff1925878c..66ad0e44ba5 100644 --- a/app/src/cc/arduino/contributions/libraries/ui/LibraryManagerUI.java +++ b/app/src/cc/arduino/contributions/libraries/ui/LibraryManagerUI.java @@ -52,6 +52,7 @@ import cc.arduino.contributions.libraries.ContributedLibraryReleases; import cc.arduino.contributions.libraries.LibraryInstaller; import cc.arduino.contributions.libraries.LibraryTypeComparator; +import cc.arduino.contributions.libraries.ui.MultiLibraryInstallDialog.Result; import cc.arduino.contributions.ui.DropdownItem; import cc.arduino.contributions.ui.FilteredAbstractTableModel; import cc.arduino.contributions.ui.InstallerJDialog; @@ -85,7 +86,7 @@ protected void onInstall(ContributedLibrary selectedLibrary, Optional mayReplaced) { + public void onInstallPressed(final ContributedLibrary lib) { + List deps = BaseNoGui.librariesIndexer.getIndex().resolveDependeciesOf(lib); + boolean depsInstalled = deps.stream().allMatch(l -> l.getInstalledLibrary().isPresent() || l.getName().equals(lib.getName())); + Result installDeps; + if (!depsInstalled) { + MultiLibraryInstallDialog dialog; + dialog = new MultiLibraryInstallDialog(this, lib, deps); + dialog.setLocationRelativeTo(this); + dialog.setVisible(true); + installDeps = dialog.getInstallDepsResult(); + if (installDeps == Result.CANCEL) + return; + } else { + installDeps = Result.NONE; + } clearErrorMessage(); installerThread = new Thread(() -> { try { setProgressVisible(true, tr("Installing...")); - installer.install(lib, mayReplaced, this::setProgress); + if (installDeps == Result.ALL) { + installer.install(deps, this::setProgress); + } else { + installer.install(lib, this::setProgress); + } // TODO: Do a better job in refreshing only the needed element if (contribTable.getCellEditor() != null) { contribTable.getCellEditor().stopCellEditing(); diff --git a/app/src/cc/arduino/contributions/libraries/ui/MultiLibraryInstallDialog.java b/app/src/cc/arduino/contributions/libraries/ui/MultiLibraryInstallDialog.java new file mode 100644 index 00000000000..75f7703f430 --- /dev/null +++ b/app/src/cc/arduino/contributions/libraries/ui/MultiLibraryInstallDialog.java @@ -0,0 +1,177 @@ +/* + * This file is part of Arduino. + * + * Copyright 2017 Arduino LLC (http://www.arduino.cc/) + * + * Arduino is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + * As a special exception, you may use this file as part of a free software + * library without restriction. Specifically, if other files instantiate + * templates or use macros or inline functions from this file, or you compile + * this file and link it with other files to produce an executable, this + * file does not by itself cause the resulting executable to be covered by + * the GNU General Public License. This exception does not however + * invalidate any other reasons why the executable file might be covered by + * the GNU General Public License. + */ + +package cc.arduino.contributions.libraries.ui; + +import static processing.app.I18n.format; +import static processing.app.I18n.tr; + +import java.awt.BorderLayout; +import java.awt.Container; +import java.awt.Insets; +import java.awt.Window; +import java.awt.event.WindowEvent; +import java.util.List; + +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JDialog; +import javax.swing.JPanel; +import javax.swing.JTextPane; +import javax.swing.WindowConstants; +import javax.swing.border.EmptyBorder; +import javax.swing.text.Document; +import javax.swing.text.html.HTMLDocument; +import javax.swing.text.html.StyleSheet; + +import cc.arduino.contributions.libraries.ContributedLibrary; +import cc.arduino.contributions.libraries.UnavailableContributedLibrary; +import processing.app.Base; +import processing.app.Theme; + +public class MultiLibraryInstallDialog extends JDialog { + + enum Result { + ALL, NONE, CANCEL + } + + private Result result = Result.CANCEL; + + public MultiLibraryInstallDialog(Window parent, ContributedLibrary lib, + List dependencies) { + super(parent, format(tr("Dependencies for library {0}:{1}"), lib.getName(), + lib.getParsedVersion()), + ModalityType.APPLICATION_MODAL); + Container pane = getContentPane(); + pane.setLayout(new BorderLayout()); + + pane.add(Box.createHorizontalStrut(10), BorderLayout.WEST); + pane.add(Box.createHorizontalStrut(10), BorderLayout.EAST); + + { + JButton cancel = new JButton(tr("Cancel")); + cancel.addActionListener(ev -> { + result = Result.CANCEL; + setVisible(false); + }); + + JButton all = new JButton(tr("Install all")); + all.addActionListener(ev -> { + result = Result.ALL; + setVisible(false); + }); + + JButton none = new JButton(format(tr("Install '{0}' only"), lib.getName())); + none.addActionListener(ev -> { + result = Result.NONE; + setVisible(false); + }); + + Box buttonsBox = Box.createHorizontalBox(); + buttonsBox.add(all); + buttonsBox.add(Box.createHorizontalStrut(5)); + buttonsBox.add(none); + buttonsBox.add(Box.createHorizontalStrut(5)); + buttonsBox.add(cancel); + + JPanel buttonsPanel = new JPanel(); + buttonsPanel.setBorder(new EmptyBorder(7, 10, 7, 10)); + buttonsPanel.setLayout(new BoxLayout(buttonsPanel, BoxLayout.Y_AXIS)); + buttonsPanel.add(buttonsBox); + + pane.add(buttonsPanel, BorderLayout.SOUTH); + } + + { + String libName = format("{0}:{1}", lib.getName(), + lib.getParsedVersion()); + String desc = format(tr("The library {0} needs some other library
dependencies currently not installed:"), + libName); + desc += "

"; + for (ContributedLibrary l : dependencies) { + if (l.getName().equals(lib.getName())) + continue; + if (l.getInstalledLibrary().isPresent()) + continue; + if (l instanceof UnavailableContributedLibrary) + continue; + desc += format("- {0}
", l.getName()); + } + desc += "
"; + desc += tr("Would you like to install also all the missing dependencies?"); + + JTextPane textArea = makeNewDescription(); + textArea.setContentType("text/html"); + textArea.setText(desc); + + JPanel libsList = new JPanel(); + libsList.setLayout(new BoxLayout(libsList, BoxLayout.Y_AXIS)); + libsList.add(textArea); + libsList.setBorder(new EmptyBorder(7, 7, 7, 7)); + pane.add(libsList, BorderLayout.NORTH); + } + + pack(); + setResizable(false); + setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + + WindowEvent closing = new WindowEvent(this, WindowEvent.WINDOW_CLOSING); + Base.registerWindowCloseKeys(getRootPane(), e -> dispatchEvent(closing)); + } + + // TODO Make this a method of Theme + private JTextPane makeNewDescription() { + JTextPane description = new JTextPane(); + description.setInheritsPopupMenu(true); + Insets margin = description.getMargin(); + margin.bottom = 0; + description.setMargin(margin); + description.setContentType("text/html"); + Document doc = description.getDocument(); + if (doc instanceof HTMLDocument) { + HTMLDocument html = (HTMLDocument) doc; + StyleSheet s = html.getStyleSheet(); + s.addRule("body { margin: 0; padding: 0;" + + "font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;" + + "color: black;" + "font-size: " + 15 * Theme.getScale() / 100 + + "; }"); + } + description.setOpaque(false); + description.setBorder(new EmptyBorder(4, 7, 7, 7)); + description.setHighlighter(null); + description.setEditable(false); + add(description, 0); + return description; + } + + public Result getInstallDepsResult() { + return result; + } +} diff --git a/app/src/processing/app/Base.java b/app/src/processing/app/Base.java index ef4d9b30a4e..c8591cc2007 100644 --- a/app/src/processing/app/Base.java +++ b/app/src/processing/app/Base.java @@ -384,7 +384,7 @@ public Base(String[] args) throws Exception { library, mayInstalled.get().getParsedVersion()))); libraryInstaller.remove(mayInstalled.get(), progressListener); } else { - libraryInstaller.install(selected, mayInstalled, progressListener); + libraryInstaller.install(selected, progressListener); } } diff --git a/arduino-core/src/cc/arduino/contributions/VersionHelper.java b/arduino-core/src/cc/arduino/contributions/VersionHelper.java index caf98c8f120..bead8d46ebf 100644 --- a/arduino-core/src/cc/arduino/contributions/VersionHelper.java +++ b/arduino-core/src/cc/arduino/contributions/VersionHelper.java @@ -65,4 +65,7 @@ public static Optional valueOf(String ver) { } } + public static int compare(String a, String b) { + return valueOf(a).get().compareTo(valueOf(b).get()); + } } diff --git a/arduino-core/src/cc/arduino/contributions/libraries/ContributedLibrary.java b/arduino-core/src/cc/arduino/contributions/libraries/ContributedLibrary.java index 3aa1198882c..692a3dfb20b 100644 --- a/arduino-core/src/cc/arduino/contributions/libraries/ContributedLibrary.java +++ b/arduino-core/src/cc/arduino/contributions/libraries/ContributedLibrary.java @@ -32,12 +32,13 @@ import cc.arduino.contributions.DownloadableContribution; import processing.app.I18n; import processing.app.packages.UserLibrary; +import static processing.app.I18n.tr; import java.util.Comparator; import java.util.List; import java.util.Optional; -import static processing.app.I18n.tr; +import cc.arduino.contributions.VersionHelper; public abstract class ContributedLibrary extends DownloadableContribution { @@ -63,7 +64,7 @@ public abstract class ContributedLibrary extends DownloadableContribution { public abstract List getTypes(); - public abstract List getRequires(); + public abstract List getDependencies(); public abstract List getProvidesIncludes(); @@ -145,8 +146,8 @@ public String info() { } res += "\n"; res += " requires :\n"; - if (getRequires() != null) - for (ContributedLibraryReference r : getRequires()) { + if (getDependencies() != null) + for (ContributedLibraryDependency r : getDependencies()) { res += " " + r; } res += "\n"; @@ -166,7 +167,7 @@ public boolean equals(Object obj) { String thisVersion = getParsedVersion(); String otherVersion = other.getParsedVersion(); - boolean versionEquals = (thisVersion != null && otherVersion != null + boolean versionEquals = (thisVersion != null && thisVersion.equals(otherVersion)); // Important: for legacy libs, versions are null. Two legacy libs must @@ -176,9 +177,18 @@ public boolean equals(Object obj) { String thisName = getName(); String otherName = other.getName(); - - boolean nameEquals = thisName == null || otherName == null || thisName.equals(otherName); + boolean nameEquals = thisName != null && thisName.equals(otherName); return versionEquals && nameEquals; } + + public boolean isBefore(ContributedLibrary other) { + return VersionHelper.compare(getVersion(), other.getVersion()) < 0; + } + + @Override + public int hashCode() { + String hashingData = "CONTRIBUTEDLIB" + getName() + getVersion(); + return hashingData.hashCode(); + } } diff --git a/arduino-core/src/cc/arduino/contributions/libraries/ContributedLibraryReference.java b/arduino-core/src/cc/arduino/contributions/libraries/ContributedLibraryDependency.java similarity index 90% rename from arduino-core/src/cc/arduino/contributions/libraries/ContributedLibraryReference.java rename to arduino-core/src/cc/arduino/contributions/libraries/ContributedLibraryDependency.java index f4edd57327f..e8e500dd585 100644 --- a/arduino-core/src/cc/arduino/contributions/libraries/ContributedLibraryReference.java +++ b/arduino-core/src/cc/arduino/contributions/libraries/ContributedLibraryDependency.java @@ -29,16 +29,14 @@ package cc.arduino.contributions.libraries; -public abstract class ContributedLibraryReference { +public abstract class ContributedLibraryDependency { public abstract String getName(); - public abstract String getMaintainer(); - public abstract String getVersion(); @Override public String toString() { - return getName() + " " + getVersion() + " (" + getMaintainer() + ")"; + return getName() + " " + getVersion(); } } diff --git a/arduino-core/src/cc/arduino/contributions/libraries/LibrariesIndex.java b/arduino-core/src/cc/arduino/contributions/libraries/LibrariesIndex.java index 7998525a152..d66a31fd3ce 100644 --- a/arduino-core/src/cc/arduino/contributions/libraries/LibrariesIndex.java +++ b/arduino-core/src/cc/arduino/contributions/libraries/LibrariesIndex.java @@ -29,6 +29,7 @@ package cc.arduino.contributions.libraries; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -37,6 +38,8 @@ import java.util.Optional; import java.util.stream.Collectors; +import cc.arduino.contributions.VersionComparator; + public abstract class LibrariesIndex { public abstract List getLibraries(); @@ -98,4 +101,78 @@ public Optional getInstalled(String name) { ContributedLibraryReleases rel = new ContributedLibraryReleases(find(name)); return rel.getInstalled(); } + + public List resolveDependeciesOf(ContributedLibrary library) { + List solution = new ArrayList<>(); + solution.add(library); + if (resolveDependeciesOf(solution, library)) { + return solution; + } else { + return null; + } + } + + public boolean resolveDependeciesOf(List solution, + ContributedLibrary library) { + List requirements = library.getDependencies(); + if (requirements == null) { + // No deps for this library, great! + return true; + } + + for (ContributedLibraryDependency dep : requirements) { + + // If the current solution already contains this dependency, skip over + boolean alreadyInSolution = solution.stream() + .anyMatch(l -> l.getName().equals(dep.getName())); + if (alreadyInSolution) + continue; + + // Generate possible matching dependencies + List possibleDeps = findMatchingDependencies(dep); + + // If there are no dependencies available add as "missing" lib + if (possibleDeps.isEmpty()) { + solution.add(new UnavailableContributedLibrary(dep)); + continue; + } + + // Pick the installed version if available + ContributedLibrary selected; + Optional installed = possibleDeps.stream() + .filter(l -> l.getInstalledLibrary().isPresent()).findAny(); + if (installed.isPresent()) { + selected = installed.get(); + } else { + // otherwise pick the latest version + selected = possibleDeps.stream().reduce(VersionComparator::max).get(); + } + + // Add dependency to the solution and process recursively + solution.add(selected); + if (!resolveDependeciesOf(solution, selected)) { + return false; + } + } + return true; + } + + private List findMatchingDependencies(ContributedLibraryDependency dep) { + List available = find(dep.getName()); + if (dep.getVersion() == null || dep.getVersion().isEmpty()) + return available; + + // XXX: The following part is actually never reached. The use of version + // constraints requires a much complex backtracking algorithm, the following + // is just a draft placeholder. + +// List match = available.stream() +// // TODO: add more complex version comparators (> >= < <= ~ 1.0.* 1.*...) +// .filter(candidate -> candidate.getParsedVersion() +// .equals(dep.getVersionRequired())) +// .collect(Collectors.toList()); +// return match; + + return available; + } } diff --git a/arduino-core/src/cc/arduino/contributions/libraries/LibraryInstaller.java b/arduino-core/src/cc/arduino/contributions/libraries/LibraryInstaller.java index 4b4fb7f7dbb..de91c049090 100644 --- a/arduino-core/src/cc/arduino/contributions/libraries/LibraryInstaller.java +++ b/arduino-core/src/cc/arduino/contributions/libraries/LibraryInstaller.java @@ -43,6 +43,8 @@ import java.io.File; import java.io.IOException; import java.net.URL; +import java.util.ArrayList; +import java.util.List; import java.util.Optional; import static processing.app.I18n.tr; @@ -86,15 +88,44 @@ public synchronized void updateIndex(ProgressListener progressListener) throws E rescanLibraryIndex(progress, progressListener); } - public synchronized void install(ContributedLibrary lib, Optional mayReplacedLib, ProgressListener progressListener) throws Exception { + public void install(ContributedLibrary lib, ProgressListener progressListener) throws Exception { + ArrayList libs = new ArrayList<>(); + libs.add(lib); + install(libs, progressListener); + } + + public synchronized void install(List libs, ProgressListener progressListener) throws Exception { + MultiStepProgress progress = new MultiStepProgress(3 * libs.size() + 1); + + for (ContributedLibrary lib : libs) { + // Do install library (3 steps) + performInstall(lib, progressListener, progress); + } + + // Rescan index (1 step) + rescanLibraryIndex(progress, progressListener); + } + + private void performInstall(ContributedLibrary lib, ProgressListener progressListener, MultiStepProgress progress) throws Exception { if (lib.isLibraryInstalled()) { System.out.println(I18n.format(tr("Library is already installed: {0}:{1}"), lib.getName(), lib.getParsedVersion())); return; } - DownloadableContributionsDownloader downloader = new DownloadableContributionsDownloader(BaseNoGui.librariesIndexer.getStagingFolder()); + File libsFolder = BaseNoGui.getSketchbookLibrariesFolder().folder; + File destFolder = new File(libsFolder, lib.getName().replaceAll(" ", "_")); - final MultiStepProgress progress = new MultiStepProgress(3); + // Check if we are replacing an already installed lib + LibrariesIndex index = BaseNoGui.librariesIndexer.getIndex(); + Optional replacedLib = index.find(lib.getName()).stream() // + .filter(l -> l.getInstalledLibrary().isPresent()) // + .filter(l -> l.getInstalledLibrary().get().getInstalledFolder().equals(destFolder)) // + .findAny(); + if (!replacedLib.isPresent() && destFolder.exists()) { + System.out.println(I18n.format(tr("Library {0} is already installed in: {1}"), lib.getName(), destFolder)); + return; + } + DownloadableContributionsDownloader downloader = new DownloadableContributionsDownloader(BaseNoGui.librariesIndexer.getStagingFolder()); // Step 1: Download library try { @@ -103,6 +134,7 @@ public synchronized void install(ContributedLibrary lib, Optional getArchitectures() { + return new ArrayList<>(); + } + + @Override + public List getTypes() { + return new ArrayList<>(); + } + + @Override + public List getDependencies() { + return new ArrayList<>(); + } + + @Override + public String getUrl() { + return ""; + } + + @Override + public String getVersion() { + return version; + } + + @Override + public String getChecksum() { + return ""; + } + + @Override + public long getSize() { + return 0; + } + + @Override + public String getArchiveFileName() { + return ""; + } + + @Override + public String toString() { + return "!" + super.toString(); + } + + @Override + public List getProvidesIncludes() { + return new ArrayList<>(); + } +} diff --git a/arduino-core/src/processing/app/packages/UserLibrary.java b/arduino-core/src/processing/app/packages/UserLibrary.java index e22ae36eb29..c1625b88a01 100644 --- a/arduino-core/src/processing/app/packages/UserLibrary.java +++ b/arduino-core/src/processing/app/packages/UserLibrary.java @@ -44,7 +44,7 @@ import cc.arduino.Constants; import cc.arduino.contributions.VersionHelper; -import cc.arduino.contributions.libraries.ContributedLibraryReference; +import cc.arduino.contributions.libraries.ContributedLibraryDependency; import processing.app.helpers.PreferencesMap; import processing.app.packages.UserLibraryFolder.Location; @@ -230,7 +230,7 @@ public String getMaintainer() { return maintainer; } - public List getRequires() { + public List getRequires() { return null; }