diff --git a/app/src/processing/app/Base.java b/app/src/processing/app/Base.java index ff6ac2621..1ae9bccfb 100644 --- a/app/src/processing/app/Base.java +++ b/app/src/processing/app/Base.java @@ -98,6 +98,7 @@ public class Base { ContributionManagerDialog libraryManagerFrame; ContributionManagerDialog toolManagerFrame; ContributionManagerDialog modeManagerFrame; + ContributionManagerDialog exampleManagerFrame; ContributionManagerDialog updateManagerFrame; // set to true after the first time the menu is built. @@ -123,6 +124,8 @@ public class Base { private Mode[] coreModes; //public List contribModes; protected ArrayList modeContribs; + + protected ArrayList exampleContribs; private JMenu sketchbookMenu; @@ -339,6 +342,19 @@ public class Base { } + /** + * Instantiates and adds new contributed modes to the contribModes list. + * Checks for duplicates so the same mode isn't instantiates twice. Does not + * remove modes because modes can't be removed once they are instantiated. + */ + void rebuildContribExamples() { + if (exampleContribs == null) { + exampleContribs = new ArrayList(); + } + ExamplesPackageContribution.loadMissing(this); + } + + public Base(String[] args) throws Exception { // // Get the sketchbook path, and make sure it's set properly // determineSketchbookFolder(); @@ -356,6 +372,8 @@ public class Base { ContributionManager.cleanup(this); buildCoreModes(); rebuildContribModes(); + + rebuildContribExamples(); // Needs to happen after the sketchbook folder has been located. // Also relies on the modes to be loaded so it knows what can be @@ -385,6 +403,8 @@ public class Base { new ContributionManagerDialog(ContributionType.TOOL); modeManagerFrame = new ContributionManagerDialog(ContributionType.MODE); + exampleManagerFrame = + new ContributionManagerDialog(ContributionType.EXAMPLES_PACKAGE); updateManagerFrame = new ContributionManagerDialog(null); @@ -658,6 +678,11 @@ public class Base { } + public ArrayList getExampleContribs() { + return exampleContribs; + } + + // Because of variations in native windowing systems, no guarantees about // changes to the focused and active Windows can be made. Developers must // never assume that this Window is the focused or active Window until this @@ -1483,7 +1508,7 @@ public class Base { JMenu submenu = new JMenu(name); // needs to be separate var otherwise would set ifound to false boolean anything = addSketches(submenu, subfolder, replaceExisting); - if (anything) { + if (anything && !name.equals("old")) { //Don't add old contributions menu.add(submenu); found = true; } @@ -1651,6 +1676,14 @@ public class Base { } + /** + * Show the examples installer window. + */ + public void handleOpenExampleManager() { + exampleManagerFrame.showFrame(activeEditor); + } + + public void handleShowUpdates() { updateManagerFrame.showFrame(activeEditor); } @@ -1923,6 +1956,7 @@ public class Base { getSketchbookLibrariesFolder().mkdir(); getSketchbookToolsFolder().mkdir(); getSketchbookModesFolder().mkdir(); + getSketchbookExamplesPackagesFolder().mkdir(); // System.err.println("sketchbook: " + sketchbookFolder); } @@ -1954,6 +1988,11 @@ public class Base { } + static public File getSketchbookExamplesPackagesFolder() { + return new File(sketchbookFolder, "examples-packages"); + } + + static protected File getDefaultSketchbookFolder() { File sketchbookFolder = null; try { diff --git a/app/src/processing/app/Mode.java b/app/src/processing/app/Mode.java index 3f669f643..b51e50438 100644 --- a/app/src/processing/app/Mode.java +++ b/app/src/processing/app/Mode.java @@ -29,11 +29,15 @@ import java.io.*; import java.util.*; import javax.swing.*; +import javax.swing.border.Border; import javax.swing.border.EmptyBorder; import javax.swing.event.TreeExpansionEvent; import javax.swing.event.TreeExpansionListener; +import javax.swing.plaf.basic.BasicTreeUI; import javax.swing.tree.*; +import processing.app.contrib.ContributionType; +import processing.app.contrib.ExamplesPackageContribution; import processing.app.syntax.*; import processing.core.PApplet; @@ -69,6 +73,8 @@ public abstract class Mode { protected File examplesFolder; protected File librariesFolder; protected File referenceFolder; + + protected File examplesContribFolder; public ArrayList coreLibraries; public ArrayList contribLibraries; @@ -100,6 +106,9 @@ public abstract class Mode { examplesFolder = new File(folder, "examples"); librariesFolder = new File(folder, "libraries"); referenceFolder = new File(folder, "reference"); + + // Get path to the contributed examples compatible with this mode + examplesContribFolder = Base.getSketchbookExamplesPackagesFolder(); // rebuildToolbarMenu(); rebuildLibraryList(); @@ -404,6 +413,14 @@ public abstract class Mode { } }); toolbarMenu.add(item); + + item = new JMenuItem("Add Examples..."); + item.addActionListener(new ActionListener() { + public void actionPerformed(ActionEvent e) { + base.handleOpenExampleManager(); + } + }); + toolbarMenu.add(item); // Add a list of all sketches and subfolders toolbarMenu.addSeparator(); @@ -588,10 +605,10 @@ public abstract class Mode { } - public JTree buildExamplesTree() { + public DefaultMutableTreeNode buildExamplesTree() { DefaultMutableTreeNode node = new DefaultMutableTreeNode("Examples"); - JTree examplesTree = new JTree(node); +// JTree examplesTree = new JTree(node); // rebuildExamplesTree(node); // } @@ -610,30 +627,28 @@ public abstract class Mode { // }); File[] subfolders = getExampleCategoryFolders(); -// DefaultMutableTreeNode examplesParent = new DefaultMutableTreeNode("Examples"); + DefaultMutableTreeNode modeExParent = new DefaultMutableTreeNode("Mode Examples"); + for (File sub : subfolders) { DefaultMutableTreeNode subNode = new DefaultMutableTreeNode(sub.getName()); if (base.addSketches(subNode, sub)) { // examplesParent.add(subNode); - node.add(subNode); + modeExParent.add(subNode); } } -// node.add(examplesParent); -// examplesTree.expandPath(new TreePath(examplesParent)); - + // get library examples boolean any = false; - DefaultMutableTreeNode libParent = new DefaultMutableTreeNode("Libraries"); for (Library lib : coreLibraries) { if (lib.hasExamples()) { DefaultMutableTreeNode libNode = new DefaultMutableTreeNode(lib.getName()); - any |= base.addSketches(libNode, lib.getExamplesFolder()); - libParent.add(libNode); + if (base.addSketches(libNode, lib.getExamplesFolder())) + modeExParent.add(libNode); } } - if (any) { - node.add(libParent); - } + + if (modeExParent.getChildCount() > 0) + node.add(modeExParent); // get contrib library examples any = false; @@ -644,7 +659,7 @@ public abstract class Mode { } if (any) { // menu.addSeparator(); - DefaultMutableTreeNode contribParent = new DefaultMutableTreeNode("Contributed Libraries"); + DefaultMutableTreeNode contribParent = new DefaultMutableTreeNode("Library Examples"); // Base.addDisabledItem(menu, "Contributed"); for (Library lib : contribLibraries) { if (lib.hasExamples()) { @@ -661,7 +676,46 @@ public abstract class Mode { } catch (IOException e) { e.printStackTrace(); } - return examplesTree; + + DefaultMutableTreeNode contribExampleNode = buildContributedExamplesTrees(); + if (contribExampleNode.getChildCount() > 0) + node.add(contribExampleNode); + return node; + } + + + public DefaultMutableTreeNode buildContributedExamplesTrees() { + DefaultMutableTreeNode node = new DefaultMutableTreeNode("Contributed Examples"); + + try { + File[] subfolders = ContributionType.EXAMPLES_PACKAGE.listCandidates(examplesContribFolder); + if (subfolders == null) { + subfolders = new File[0]; //empty array + } + for (File sub : subfolders) { + if (!ExamplesPackageContribution.isExamplesPackageCompatible(base, sub)) + continue; + DefaultMutableTreeNode subNode = new DefaultMutableTreeNode(sub.getName()); + if (base.addSketches(subNode, sub)) { + node.add(subNode); + int exampleNodeNumber = -1; + for (int y = 0; y < subNode.getChildCount(); y++) + if (subNode.getChildAt(y).toString().equals("examples-package")) + exampleNodeNumber = y; + if (exampleNodeNumber == -1) + continue; + TreeNode exampleNode = subNode.getChildAt(exampleNodeNumber); + subNode.remove(exampleNodeNumber); + int count = exampleNode.getChildCount(); + for (int x = 0; x < count; x++) { + subNode.add((DefaultMutableTreeNode) exampleNode.getChildAt(0)); + } + } + } + } catch (IOException e) { + e.printStackTrace(); + } + return node;//examplesTree; } @@ -682,6 +736,98 @@ public abstract class Mode { } + /** + * Function to give a JTree a pretty alternating gray-white colouring for + * its rows. + * + * @param tree + */ + private void colourizeTreeRows(JTree tree) { + // Code in this function adapted from: + // http://mateuszstankiewicz.eu/?p=263 + tree.setCellRenderer(new DefaultTreeCellRenderer() { + + @Override + public Component getTreeCellRendererComponent(JTree tree, Object value, + boolean sel, + boolean expanded, + boolean leaf, int row, + boolean hasFocus) { + JComponent c = (JComponent) super + .getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, + hasFocus); + + if (!tree.isRowSelected(row)) { + if (row % 2 == 0) { + + // Need to set this, else the gray from the odd + // rows colours this gray as well. + c.setBackground(new Color(255, 255, 255)); + + setBackgroundSelectionColor(new Color(0, 0, 255)); + setTextSelectionColor(Color.WHITE); + setBorderSelectionColor(new Color(0, 0, 255)); + } else { + + // Set background for entire component (including the image). + // Using transparency messes things up, probably since the + // transparent colour is not good friends with the images background colour. + c.setBackground(new Color(240, 240, 240)); + + // Can't use setBackgroundSelectionColor() directly, since then, the + // image's background isn't affected. + // The setUI() doesn't fix the image's background because the + // transparency likely interferes with its normal background, + // making its background lighter than the rest. +// setBackgroundNonSelectionColor(new Color(190, 190, 190)); + + setBackgroundSelectionColor(new Color(0, 0, 255)); + setTextSelectionColor(Color.WHITE); + setBorderSelectionColor(new Color(0, 0, 255)); + } + } else {// Transparent blue if selected + c.setBackground(new Color(127, 127, 255)); + } + + c.setOpaque(true); + return c; + } + + }); + + tree.setUI(new BasicTreeUI() { + + @Override + protected void paintRow(Graphics g, Rectangle clipBounds, Insets insets, + Rectangle bounds, TreePath path, int row, + boolean isExpanded, boolean hasBeenExpanded, + boolean isLeaf) { + Graphics g2 = g.create(); + + if (!tree.isRowSelected(row)) { + if (row % 2 == 0) { + // Need to set this, else the gray from the odd rows + // affects the even rows too. + g2.setColor(new Color(255, 255, 255, 128)); + } else { + // Transparent light-gray + g2.setColor(new Color(226, 226, 226, 128)); + } + } else + // Transparent blue if selected + g2.setColor(new Color(0, 0, 255, 128)); + + g2.fillRect(0, bounds.y, tree.getWidth(), bounds.height); + + g2.dispose(); + + super.paintRow(g, clipBounds, insets, bounds, path, row, isExpanded, + hasBeenExpanded, isLeaf); + } + }); + } + + public void showExamplesFrame() { if (examplesFrame == null) { examplesFrame = new JFrame(getTitle() + " " + Language.text("examples")); @@ -692,7 +838,49 @@ public abstract class Mode { } }); - final JTree tree = buildExamplesTree(); + JPanel examplesPanel = new JPanel(); + examplesPanel.setLayout(new BorderLayout()); + examplesPanel.setBackground(Color.WHITE); + + final JPanel openExamplesManagerPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); + JLabel openExamplesManagerLabel = new JLabel("Add Examples..."); +// openExamplesManagerLabel.setAlignmentX(Component.LEFT_ALIGNMENT); + openExamplesManagerPanel.add(openExamplesManagerLabel); + openExamplesManagerPanel.setOpaque(false); + Border lineBorder = BorderFactory.createMatteBorder(0, 0, 1, 0, Color.BLACK); + Border paddingBorder = BorderFactory.createEmptyBorder(3, 5, 1, 4); + openExamplesManagerPanel.setBorder(BorderFactory.createCompoundBorder(lineBorder, paddingBorder)); +// openExamplesManagerLabel.set + openExamplesManagerPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + openExamplesManagerPanel.setCursor(new Cursor(Cursor.HAND_CURSOR)); +// openExamplesManagerLabel.setForeground(new Color(0, 0, 238)); + openExamplesManagerPanel.addMouseListener(new MouseListener() { + + @Override + public void mouseReleased(MouseEvent e) {} + + @Override + public void mousePressed(MouseEvent e) {} + + @Override + public void mouseExited(MouseEvent e) {} + + @Override + public void mouseEntered(MouseEvent e) {} + + @Override + public void mouseClicked(MouseEvent e) { + base.handleOpenExampleManager(); +// openExamplesManagerLabel.setForeground(new Color(85, 26, 139)); + } + }); + + final JTree tree = new JTree(buildExamplesTree()); + + colourizeTreeRows(tree); + + tree.setOpaque(true); + tree.setAlignmentX(Component.LEFT_ALIGNMENT); tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); tree.setShowsRootHandles(true); @@ -754,16 +942,23 @@ public abstract class Mode { } }); - tree.setBorder(new EmptyBorder(5, 5, 5, 5)); + tree.setBorder(new EmptyBorder(0, 5, 5, 5)); if (Base.isMacOS()) { tree.setToggleClickCount(2); } else { tree.setToggleClickCount(1); } + JScrollPane treePane = new JScrollPane(tree); - treePane.setPreferredSize(new Dimension(250, 450)); - treePane.setBorder(new EmptyBorder(0, 0, 0, 0)); - examplesFrame.getContentPane().add(treePane); + treePane.setPreferredSize(new Dimension(250, 300)); + treePane.setBorder(new EmptyBorder(2, 0, 0, 0)); + treePane.setOpaque(true); + treePane.setBackground(Color.WHITE); + treePane.setAlignmentX(Component.LEFT_ALIGNMENT); + + examplesPanel.add(openExamplesManagerPanel,BorderLayout.PAGE_START); + examplesPanel.add(treePane, BorderLayout.CENTER); + examplesFrame.getContentPane().add(examplesPanel); examplesFrame.pack(); restoreExpanded(tree); diff --git a/app/src/processing/app/contrib/AvailableContribution.java b/app/src/processing/app/contrib/AvailableContribution.java index 1023f2a83..39b1cb1b0 100644 --- a/app/src/processing/app/contrib/AvailableContribution.java +++ b/app/src/processing/app/contrib/AvailableContribution.java @@ -1,4 +1,4 @@ -/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ +/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */ /* Part of the Processing project - http://processing.org @@ -268,6 +268,12 @@ class AvailableContribution extends Contribution { String prettyVersion = properties.get("prettyVersion"); if (prettyVersion == null || prettyVersion.isEmpty()) prettyVersion = getPrettyVersion(); + + String compatibleContribsList = null; + + if (getType() == ContributionType.EXAMPLES_PACKAGE) { + compatibleContribsList = properties.get("compatibleModesList"); + } long lastUpdated; try { @@ -294,6 +300,9 @@ class AvailableContribution extends Contribution { writer.println("version=" + version); writer.println("prettyVersion=" + prettyVersion); writer.println("lastUpdated=" + lastUpdated); + if (getType() == ContributionType.EXAMPLES_PACKAGE) { + writer.println("compatibleModesList=" + compatibleContribsList); + } writer.flush(); writer.close(); diff --git a/app/src/processing/app/contrib/ContributionManagerDialog.java b/app/src/processing/app/contrib/ContributionManagerDialog.java index 32dedb0ed..f9d11c1ff 100644 --- a/app/src/processing/app/contrib/ContributionManagerDialog.java +++ b/app/src/processing/app/contrib/ContributionManagerDialog.java @@ -392,7 +392,10 @@ public class ContributionManagerDialog { ArrayList modes = editor.getBase().getModeContribs(); contributions.addAll(modes); - + + ArrayList examples = editor.getBase().getExampleContribs(); + contributions.addAll(examples); + // ArrayList compilations = LibraryCompilation.list(libraries); // // // Remove libraries from the list that are part of a compilations diff --git a/app/src/processing/app/contrib/ContributionType.java b/app/src/processing/app/contrib/ContributionType.java index 8b882ee59..a2e661d98 100644 --- a/app/src/processing/app/contrib/ContributionType.java +++ b/app/src/processing/app/contrib/ContributionType.java @@ -31,7 +31,7 @@ import processing.app.Editor; import processing.app.Library; public enum ContributionType { - LIBRARY, TOOL, MODE; + LIBRARY, TOOL, MODE, EXAMPLES_PACKAGE; public String toString() { @@ -42,6 +42,8 @@ public enum ContributionType { return "tool"; case MODE: return "mode"; + case EXAMPLES_PACKAGE: + return "examples-package"; } return null; // should be unreachable }; @@ -53,7 +55,13 @@ public enum ContributionType { */ public String getTitle() { String s = toString(); - return Character.toUpperCase(s.charAt(0)) + s.substring(1); + if (this == EXAMPLES_PACKAGE) + return Character.toUpperCase(s.charAt(0)) + + s.substring(1, s.indexOf('-') + 1) + + Character.toUpperCase(s.charAt(s.indexOf('-') + 1)) + + s.substring(s.indexOf('-') + 2); + else + return Character.toUpperCase(s.charAt(0)) + s.substring(1); } @@ -65,6 +73,8 @@ public enum ContributionType { return "tools"; case MODE: return "modes"; + case EXAMPLES_PACKAGE: + return "examples-package"; } return null; // should be unreachable } @@ -106,6 +116,9 @@ public enum ContributionType { if ("mode".equalsIgnoreCase(s)) { return MODE; } + if ("examples-package".equalsIgnoreCase(s)) { + return EXAMPLES_PACKAGE; + } } return null; } @@ -119,6 +132,8 @@ public enum ContributionType { return Base.getSketchbookToolsFolder(); case MODE: return Base.getSketchbookModesFolder(); + case EXAMPLES_PACKAGE: + return Base.getSketchbookExamplesPackagesFolder(); } return null; } @@ -136,7 +151,7 @@ public enum ContributionType { * contribution type. For instance, a list of folders that have a 'mode' * subfolder if this is a ModeContribution. */ - File[] listCandidates(File folder) { + public File[] listCandidates(File folder) { return folder.listFiles(new FileFilter() { public boolean accept(File potential) { return isCandidate(potential); @@ -181,6 +196,8 @@ public enum ContributionType { return ToolContribution.load(folder); case MODE: return ModeContribution.load(base, folder); + case EXAMPLES_PACKAGE: + return ExamplesPackageContribution.load(folder); } return null; } @@ -198,6 +215,9 @@ public enum ContributionType { case MODE: contribs.addAll(editor.getBase().getModeContribs()); break; + case EXAMPLES_PACKAGE: + contribs.addAll(editor.getBase().getExampleContribs()); + break; } return contribs; } diff --git a/app/src/processing/app/contrib/ExamplesPackageContribution.java b/app/src/processing/app/contrib/ExamplesPackageContribution.java new file mode 100644 index 000000000..f94cc6669 --- /dev/null +++ b/app/src/processing/app/contrib/ExamplesPackageContribution.java @@ -0,0 +1,89 @@ +package processing.app.contrib; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; + +import processing.app.Base; +import processing.core.PApplet; + +public class ExamplesPackageContribution extends LocalContribution { + + private ArrayList compatibleModesList; + + static public ExamplesPackageContribution load(File folder) { + return new ExamplesPackageContribution(folder); + } + + private ExamplesPackageContribution(File folder) { + super(folder); + compatibleModesList = parseCompatibleModesList(properties + .get("compatibleModesList")); + } + + private static ArrayList parseCompatibleModesList(String unparsedModes) { + ArrayList modesList = new ArrayList(); + if (unparsedModes == null || unparsedModes.isEmpty()) + return modesList; + String[] splitStr = PApplet.trim(PApplet.split(unparsedModes, ','));//unparsedModes.split(","); + for (String mode : splitStr) + modesList.add(mode.trim()); + return modesList; + } + + /** + * Function to determine whether or not the example present in the + * exampleLocation directory is compatible with the present mode. + * + * @param base + * @param exampleLocationFolder + * @return true if the example is compatible with the mode of the currently + * active editor + */ + public static boolean isExamplesPackageCompatible(Base base, + File exampleLocationFolder) { + File propertiesFile = new File(exampleLocationFolder, + ContributionType.EXAMPLES_PACKAGE.toString() + + ".properties"); + if (propertiesFile.exists()) { + ArrayList compModesList = parseCompatibleModesList(Base + .readSettings(propertiesFile).get("compatibleModesList")); + for (String c : compModesList) { + if (c.equalsIgnoreCase(base.getActiveEditor().getMode().getIdentifier())) { + return true; + } + } + } + return false; + } + + static public void loadMissing(Base base) { + File examplesFolder = Base.getSketchbookExamplesPackagesFolder(); + ArrayList contribExamples = base.getExampleContribs(); + + HashMap existing = new HashMap(); + for (ExamplesPackageContribution contrib : contribExamples) { + existing.put(contrib.getFolder(), contrib); + } + File[] potential = ContributionType.EXAMPLES_PACKAGE.listCandidates(examplesFolder); + // If modesFolder does not exist or is inaccessible (folks might like to + // mess with folders then report it as a bug) 'potential' will be null. + if (potential != null) { + for (File folder : potential) { + if (!existing.containsKey(folder)) { + contribExamples.add(new ExamplesPackageContribution(folder)); + } + } + } + } + + @Override + public ContributionType getType() { + return ContributionType.EXAMPLES_PACKAGE; + } + + public ArrayList getCompatibleModesList() { + return compatibleModesList; + } + +}