diff --git a/app/src/processing/app/contrib/AvailableContribution.java b/app/src/processing/app/contrib/AvailableContribution.java index 9c8dd3357..2cef2c9e0 100644 --- a/app/src/processing/app/contrib/AvailableContribution.java +++ b/app/src/processing/app/contrib/AvailableContribution.java @@ -33,7 +33,7 @@ import processing.core.PApplet; /** * A class to hold information about a Contribution that can be downloaded. */ -class AvailableContribution extends Contribution { +public class AvailableContribution extends Contribution { protected final ContributionType type; // Library, tool, etc. protected final String link; // Direct link to download the file @@ -44,6 +44,7 @@ class AvailableContribution extends Contribution { //category = ContributionListing.getCategory(params.get("category")); categories = parseCategories(params.get("category")); + specifiedImports = parseImports(params.get("imports")); name = params.get("name"); authorList = params.get("authorList"); url = params.get("url"); @@ -251,6 +252,20 @@ class AvailableContribution extends Contribution { category = sb.toString(); } + String specifiedImport = ""; + List importsList = parseImports(properties.get("imports")); + if (importsList == null || importsList.isEmpty()) { + specifiedImport = getImportStr(); + } else { + StringBuilder sbImport = new StringBuilder(); + for (String it : specifiedImports) { + sbImport.append(it); + sbImport.append(','); + } + sbImport.deleteCharAt(sbImport.length() - 1); + specifiedImport = sbImport.toString(); + } + String authorList = properties.get("authorList"); if (authorList == null || authorList.isEmpty()) { authorList = getAuthorList(); @@ -336,6 +351,9 @@ class AvailableContribution extends Contribution { writer.println("lastUpdated=" + lastUpdated); writer.println("minRevision=" + minRev); writer.println("maxRevision=" + maxRev); + if (getType() == ContributionType.LIBRARY) { + writer.println("imports=" + specifiedImport); + } if (getType() == ContributionType.EXAMPLES) { writer.println("compatibleModesList=" + compatibleContribsList); } diff --git a/app/src/processing/app/contrib/Contribution.java b/app/src/processing/app/contrib/Contribution.java index 183d7ffd2..6438768c6 100644 --- a/app/src/processing/app/contrib/Contribution.java +++ b/app/src/processing/app/contrib/Contribution.java @@ -47,6 +47,7 @@ abstract public class Contribution { protected long lastUpdated; // 1402805757 protected int minRevision; // 0 protected int maxRevision; // 227 + protected List specifiedImports; // pdf.export.*,pdf.convert.common.* // "Sound" @@ -81,6 +82,37 @@ abstract public class Contribution { } } return false; + } + + + // pdf.export.*,pdf.convert.common.* + protected List getImports() { + return specifiedImports; + } + + + protected String getImportStr() { + if (specifiedImports == null || specifiedImports.isEmpty()) { + return ""; + } + StringBuilder sb = new StringBuilder(); + for (String importName : specifiedImports) { + sb.append(importName); + sb.append(','); + } + sb.deleteCharAt(sb.length() - 1); // delete last comma + return sb.toString(); + } + + protected boolean hasImport(String importName) { + if (specifiedImports != null && importName != null) { + for (String c : specifiedImports) { + if (importName.equalsIgnoreCase(c)) { + return true; + } + } + } + return false; } @@ -228,4 +260,20 @@ abstract public class Contribution { } return outgoing; } + + + /** + * @return the list of imports that this contribution (library) contains. + */ + static List parseImports(String importStr) { + List outgoing = new ArrayList(); + + if (importStr != null) { + String[] importList = PApplet.trim(PApplet.split(importStr, ',')); + for (String importName : importList) { + outgoing.add(importName); + } + } + return (outgoing.size() > 0) ? outgoing : null; + } } diff --git a/app/src/processing/app/contrib/ContributionListing.java b/app/src/processing/app/contrib/ContributionListing.java index 25763bcf8..1e0915c78 100644 --- a/app/src/processing/app/contrib/ContributionListing.java +++ b/app/src/processing/app/contrib/ContributionListing.java @@ -43,6 +43,7 @@ public class ContributionListing { ArrayList listeners; ArrayList advertisedContributions; Map> librariesByCategory; + public Map librariesByImportHeader; ArrayList allContributions; boolean hasDownloadedLatestList; boolean hasListDownloadFailed; @@ -53,6 +54,7 @@ public class ContributionListing { listeners = new ArrayList(); advertisedContributions = new ArrayList(); librariesByCategory = new HashMap>(); + librariesByImportHeader = new HashMap(); allContributions = new ArrayList(); downloadingListingLock = new ReentrantLock(); @@ -64,7 +66,7 @@ public class ContributionListing { } - static ContributionListing getInstance() { + public static ContributionListing getInstance() { if (singleInstance == null) { synchronized (ContributionListing.class) { if (singleInstance == null) { @@ -119,6 +121,12 @@ public class ContributionListing { } } + if (oldLib.getImports() != null) { + for (String importName : oldLib.getImports()) { + librariesByImportHeader.replace(importName, newLib); + } + } + for (int i = 0; i < allContributions.size(); i++) { if (allContributions.get(i) == oldLib) { allContributions.set(i, newLib); @@ -131,6 +139,11 @@ public class ContributionListing { private void addContribution(Contribution contribution) { + if (contribution.getImports() != null) { + for (String importName : contribution.getImports()) { + librariesByImportHeader.put(importName, contribution); + } + } for (String category : contribution.getCategories()) { if (librariesByCategory.containsKey(category)) { List list = librariesByCategory.get(category); @@ -155,6 +168,11 @@ public class ContributionListing { librariesByCategory.get(category).remove(contribution); } } + if (contribution.getImports() != null) { + for (String importName : contribution.getImports()) { + librariesByImportHeader.remove(importName); + } + } allContributions.remove(contribution); notifyRemove(contribution); } diff --git a/app/src/processing/app/contrib/ContributionManager.java b/app/src/processing/app/contrib/ContributionManager.java index 1e00eb2f6..b32228e56 100644 --- a/app/src/processing/app/contrib/ContributionManager.java +++ b/app/src/processing/app/contrib/ContributionManager.java @@ -110,8 +110,9 @@ public class ContributionManager { // Hiding stack trace. An error has been shown where needed. // ioe.printStackTrace(); } - if (progress != null) + if (progress != null) { progress.finished(); + } return success; } @@ -154,8 +155,9 @@ public class ContributionManager { contribListing.replaceContribution(ad, contribution); if (contribution.getType() == ContributionType.MODE) { ArrayList contribModes = editor.getBase().getModeContribs(); - if (!contribModes.contains(contribution)) + if (!contribModes.contains(contribution)) { contribModes.add((ModeContribution) contribution); + } } refreshInstalled(editor); } @@ -197,7 +199,10 @@ public class ContributionManager { /** * Non-blocking call to download and install a contribution in a new thread. - * + * Used when information about the progress of the download and install + * procedure is not of importance, such as if a contribution has to be + * installed at startup time. + * * @param url * Direct link to the contribution. * @param ad @@ -229,8 +234,9 @@ public class ContributionManager { contribModes.add((ModeContribution) contribution); } } - if (base.getActiveEditor() != null) + if (base.getActiveEditor() != null) { refreshInstalled(base.getActiveEditor()); + } } contribZip.delete(); @@ -240,13 +246,12 @@ public class ContributionManager { } catch (Exception e) { // Chuck the stack trace. The user might have no idea why it is appearing, or what (s)he did wrong... // e.printStackTrace(); - System.out.println("Error during download and install of " - + ad.getName()); + String arg = "contrib.startup.errors.download_install"; + System.err.println(Language.interpolate(arg, ad.getName())); } } catch (IOException e) { - System.err - .println("Could not write to temporary directory during download and install of " - + ad.getName()); + String arg = "contrib.startup.errors.temp_dir"; + System.err.println(Language.interpolate(arg, ad.getName())); } } }, "Contribution Installer").start(); @@ -285,13 +290,112 @@ public class ContributionManager { } catch (IOException e) { // Again, forget about the stack trace. The user ain't done wrong // e.printStackTrace(); - System.err.println("The unupdated contribution marker seems to not like " - + ac.getName() + ". You may have to install it manually to update..."); + String arg = "contrib.startup.errors.new_marker"; + System.err.println(Language.interpolate(arg, ac.getName())); } } + /** + * Blocking call to download and install a set of libraries. Used when a list + * of libraries have to be installed while forcing the user to not modify + * anything and providing feedback via the console status area, such as when + * the user tries to run a sketch that imports uninstaled libraries. + * + * @param aList + * The list of AvailableContributions to be downloaded and installed. + */ + public static void downloadAndInstallOnImport(final Base base, + final ArrayList aList) { + + // To avoid the user from modifying stuff, since this function is only called + // during pre-processing + base.getActiveEditor().getTextArea().setEditable(false); + base.getActiveEditor().getConsole().clear(); + + ArrayList installedLibList = new ArrayList(); + + // boolean variable to check if previous lib was installed successfully, + // to give the user an idea about progress being made. + boolean isPrevDone = false; + + for (AvailableContribution ad : aList) { + if (ad.getType() != ContributionType.LIBRARY) { + continue; + } + try { + URL url = new URL(ad.link); + String filename = url.getFile(); + filename = filename.substring(filename.lastIndexOf('/') + 1); + try { + + File contribZip = File.createTempFile("download", filename); + contribZip.setWritable(true); + + try { + // Use the console to let the user know what's happening + // The slightly complex if-else is required to let the user know when + // one install is completed and the next download has begun without + // interfereing with occur status messages that may arise in the meanwhile + String statusMsg = base.getActiveEditor().getStatusMessage(); + if (isPrevDone) { + String status = statusMsg + " " + + Language.interpolate("contrib.import.progress.download", ad.name); + base.getActiveEditor().statusNotice(status); + } + else { + String arg = "contrib.import.progress.download"; + String status = Language.interpolate(arg, ad.name); + base.getActiveEditor().statusNotice(status); + } + + isPrevDone = false; + + download(url, contribZip, null); + + String arg = "contrib.import.progress.install"; + base.getActiveEditor().statusNotice(Language.interpolate(arg,ad.name)); + LocalContribution contribution = ad.install(base, contribZip, + false, null); + + if (contribution != null) { + contribListing.replaceContribution(ad, contribution); + if (base.getActiveEditor() != null) { + refreshInstalled(base.getActiveEditor()); + } + } + + contribZip.delete(); + + installedLibList.add(ad.name); + isPrevDone = true; + + arg = "contrib.import.progress.done"; + base.getActiveEditor().statusNotice(Language.interpolate(arg,ad.name)); + + } catch (Exception e) { + String arg = "contrib.startup.errors.download_install"; + System.err.println(Language.interpolate(arg, ad.getName())); + } + } catch (IOException e) { + String arg = "contrib.startup.errors.temp_dir"; + System.err.println(Language.interpolate(arg,ad.getName())); + } + } catch (MalformedURLException e1) { + System.err.println(Language.interpolate("contrib.import.errors.link", + ad.getName())); + } + } + base.getActiveEditor().getTextArea().setEditable(true); + base.getActiveEditor().statusEmpty(); + System.out.println(Language.text("contrib.import.progress.final_list")); + for (String l : installedLibList) { + System.out.println(" * " + l); + } + } + + static public void refreshInstalled(Editor e) { Iterator iter = e.getBase().getEditors().iterator(); diff --git a/app/src/processing/app/contrib/LocalContribution.java b/app/src/processing/app/contrib/LocalContribution.java index 908a64bd9..cd80acd2f 100644 --- a/app/src/processing/app/contrib/LocalContribution.java +++ b/app/src/processing/app/contrib/LocalContribution.java @@ -48,7 +48,6 @@ public abstract class LocalContribution extends Contribution { protected File folder; protected Map properties; protected ClassLoader loader; - protected List specifiedImports; // mylib,mylib.util; public LocalContribution(File folder) { this.folder = folder; diff --git a/build/shared/lib/languages/PDE.properties b/build/shared/lib/languages/PDE.properties index 3ffa6804c..034c04d85 100644 --- a/build/shared/lib/languages/PDE.properties +++ b/build/shared/lib/languages/PDE.properties @@ -338,8 +338,9 @@ delete.messages.is_read_only.description = Some files are marked \"read-only\", # --------------------------------------- -# Contribution Panel +# Contributions +# Contribution Panel contrib = Contribution Manager contrib.manager_title.update = Update Manager contrib.manager_title.mode = Mode Manager @@ -384,6 +385,20 @@ contrib.progress.downloading = Downloading contrib.download_error = An error occured while downloading the contribution. contrib.unsupported_operating_system = Your operating system does not appear to be supported. You should visit the %s\'s library for more info. +# Install on Startup +contrib.startup.errors.download_install = Error during download and install of %s +contrib.startup.errors.temp_dir = Could not write to temporary directory during download and install of %s +contrib.startup.errors.new_marker = The unupdated contribution marker seems to not like %s. You may have to install it manually to update... + +# Install on Import +contrib.import.dialog.title = Missing Libraries Available +contrib.import.dialog.primary_text = The following imported libraries are available for download, but have not been installed. +contrib.import.dialog.secondary_text = Would you like to install them now? +contrib.import.progress.download = Downloading %s... +contrib.import.progress.install = Installing %s... +contrib.import.progress.done = %s has been installed. +contrib.import.progress.final_list = The following libraries have been installed: +contrib.import.errors.link = Error: The library %s has a strange looking download link. # --------------------------------------- # Warnings diff --git a/java/src/processing/mode/java/JavaEditor.java b/java/src/processing/mode/java/JavaEditor.java index 086422309..fe68eec34 100644 --- a/java/src/processing/mode/java/JavaEditor.java +++ b/java/src/processing/mode/java/JavaEditor.java @@ -8,6 +8,7 @@ import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; @@ -22,6 +23,10 @@ import org.eclipse.jdt.core.compiler.IProblem; import processing.app.*; import processing.app.Toolkit; +import processing.app.contrib.AvailableContribution; +import processing.app.contrib.Contribution; +import processing.app.contrib.ContributionListing; +import processing.app.contrib.ContributionManager; import processing.app.contrib.ToolContribution; import processing.app.syntax.JEditTextArea; import processing.app.syntax.PdeTextAreaDefaults; @@ -32,6 +37,7 @@ import processing.mode.java.debug.LineID; import processing.mode.java.pdex.ErrorCheckerService; import processing.mode.java.pdex.ErrorMarker; import processing.mode.java.pdex.ErrorMessageSimplifier; +import processing.mode.java.pdex.ImportStatement; import processing.mode.java.pdex.JavaTextArea; import processing.mode.java.pdex.Problem; import processing.mode.java.pdex.XQConsoleToggle; @@ -1845,8 +1851,93 @@ public class JavaEditor extends Editor { public void prepareRun() { autoSave(); super.prepareRun(); + downloadImports(); } + + /** + * Downloads libraries that have been imported, that aren't available as a + * LocalContribution, but that have an AvailableContribution associated with + * them. + */ + protected void downloadImports() { + String importRegex = errorCheckerService.importRegexp; + String tabCode; + for (SketchCode sc : sketch.getCode()) { + if (sc.isExtension("pde")) { + tabCode = sc.getProgram(); + + String[][] pieces = PApplet.matchAll(tabCode, importRegex); + + if (pieces != null) { + ArrayList importHeaders = new ArrayList(); + for (String[] importStatement : pieces) { + importHeaders.add(importStatement[2]); + } + ArrayList installLibsHeaders = getNotInstalledAvailableLibs(importHeaders); + if (!installLibsHeaders.isEmpty()) { + StringBuilder libList = new StringBuilder("Would you like to install them now?"); + for (AvailableContribution ac : installLibsHeaders) { + libList.append("\n • " + ac.getName()); + } + int option = Base.showYesNoQuestion(this, + Language.text("contrib.import.dialog.title"), + Language.text("contrib.import.dialog.primary_text"), + libList.toString()); + + if (option == JOptionPane.YES_OPTION) { + ContributionManager.downloadAndInstallOnImport(base, + installLibsHeaders); + } + } + } + } + } + } + + + /** + * Returns a list of AvailableContributions of those libraries that the user + * wants imported, but that are not installed. + * + * @param importHeaders + */ + private ArrayList getNotInstalledAvailableLibs(ArrayList importHeadersList) { + Map importMap = ContributionListing.getInstance().librariesByImportHeader; + ArrayList libList = new ArrayList(); + for (String importHeaders : importHeadersList) { + int dot = importHeaders.lastIndexOf('.'); + String entry = (dot == -1) ? importHeaders : importHeaders.substring(0, + dot); + + if (entry.startsWith("java.") || entry.startsWith("javax.") + || entry.startsWith("processing.")) { + continue;// null; + } + + Library library = null; + try { + library = this.getMode().getLibrary(entry); + if (library == null) { + Contribution c = importMap.get(importHeaders); + if (c != null && c instanceof AvailableContribution) { + libList.add((AvailableContribution) c);// System.out.println(importHeaders + // + "not found"); + } + } + } catch (Exception e) { + // Not gonna happen (hopefully) + Contribution c = importMap.get(importHeaders); + if (c != null && c instanceof AvailableContribution) { + libList.add((AvailableContribution) c);// System.out.println(importHeaders + // + "not found"); + } + } + } + return libList; + } + + /** * Displays a JDialog prompting the user to save when the user hits * run/present/etc.