/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */
/*
Part of the Processing project - http://processing.org
Copyright (c) 2013-15 The Processing Foundation
Copyright (c) 2010-13 Ben Fry and Casey Reas
This program 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package processing.app;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import java.util.List;
import javax.swing.*;
import processing.app.contrib.ContributionManager;
import processing.app.syntax.*;
import processing.app.ui.Editor;
import processing.app.ui.EditorException;
import processing.app.ui.EditorState;
import processing.app.ui.ExamplesFrame;
import processing.app.ui.Recent;
import processing.app.ui.Toolkit;
import processing.core.PApplet;
import processing.utils.SketchException;
public abstract class Mode {
protected Base base;
protected File folder;
protected TokenMarker tokenMarker;
protected Map keywordToReference = new HashMap();
protected Settings theme;
// maps imported packages to their library folder
protected Map importToLibraryTable;
// these menus are shared so that they needn't be rebuilt for all windows
// each time a sketch is created, renamed, or moved.
// protected JMenu examplesMenu; // this is for the menubar, not the toolbar
protected JMenu importMenu;
protected ExamplesFrame examplesFrame;
// popup menu used for the toolbar
protected JMenu toolbarMenu;
protected File examplesFolder;
protected File librariesFolder;
protected File referenceFolder;
// protected File examplesContribFolder;
public List coreLibraries;
public List contribLibraries;
// Initialize to empty b/c these may not exist for contributed Mode classes.
public List foundationLibraries = new ArrayList();
/** Library folder for core. (Used for OpenGL in particular.) */
protected Library coreLibrary;
/**
* ClassLoader used to retrieve classes for this mode. Useful if you want
* to grab any additional classes that subclass what's in the mode folder.
*/
protected ClassLoader classLoader;
public Mode(Base base, File folder) {
this.base = base;
this.folder = folder;
tokenMarker = createTokenMarker();
// Get paths for the libraries and examples in the mode folder
examplesFolder = new File(folder, "examples");
librariesFolder = new File(folder, "libraries");
referenceFolder = new File(folder, "reference");
// rebuildToolbarMenu();
rebuildLibraryList();
// rebuildExamplesMenu();
try {
for (File file : getKeywordFiles()) {
loadKeywords(file);
}
} catch (IOException e) {
Messages.showWarning("Problem loading keywords",
"Could not load keywords file for " + getTitle() + " mode.", e);
}
}
/**
* To add additional keywords, or to grab them from another mode, override
* this function. If your mode has no keywords, return a zero length array.
*/
public File[] getKeywordFiles() {
return new File[] { new File(folder, "keywords.txt") };
}
protected void loadKeywords(File keywordFile) throws IOException {
// overridden for Python, where # is an actual keyword
loadKeywords(keywordFile, "#");
}
@SuppressWarnings("SameParameterValue")
protected void loadKeywords(File keywordFile,
String commentPrefix) throws IOException {
String[] lines = PApplet.loadStrings(keywordFile);
if (lines != null) {
for (String line : lines) {
if (!line.trim().startsWith(commentPrefix)) {
// Was difficult to make sure that mode authors were properly doing
// tab-separated values. By definition, there can't be additional
// spaces inside a keyword (or filename), so just splitting on tokens.
String[] pieces = PApplet.splitTokens(line);
if (pieces.length >= 2) {
String keyword = pieces[0];
String coloring = pieces[1];
if (coloring.length() > 0) {
tokenMarker.addColoring(keyword, coloring);
}
if (pieces.length == 3) {
String htmlFilename = pieces[2];
if (htmlFilename.length() > 0) {
// if the file is for the version with parens,
// add a paren to the keyword
if (htmlFilename.endsWith("_")) {
keyword += "_";
}
// Allow the bare size() command to override the lookup
// for StringList.size() and others, but not vice-versa.
// https://github.com/processing/processing/issues/4224
boolean seen = keywordToReference.containsKey(keyword);
if (!seen || keyword.equals(htmlFilename)) {
keywordToReference.put(keyword, htmlFilename);
}
}
}
}
}
}
} else {
System.err.println("Could not read " + keywordFile);
}
}
public void setClassLoader(ClassLoader loader) {
this.classLoader = loader;
}
public ClassLoader getClassLoader() {
return classLoader;
}
public File getContentFile(String path) {
return new File(folder, path);
}
@SuppressWarnings("unused")
public InputStream getContentStream(String path) throws FileNotFoundException {
return new FileInputStream(getContentFile(path));
}
/**
* Add files to a folder to create an empty sketch. This can be overridden
* to add template files to a sketch for Modes that need them.
*
* @param sketchFolder the directory where the new sketch should live
* @param sketchName the name of the new sketch
* @return the main file for the sketch to be opened via handleOpen()
* @throws IOException if the file somehow already exists
*/
public File addTemplateFiles(File sketchFolder,
String sketchName) throws IOException {
// Make an empty .pde file
File newbieFile =
new File(sketchFolder, sketchName + "." + getDefaultExtension());
try {
// First see if the user has overridden the default template
File templateFolder = checkSketchbookTemplate();
// Next see if the Mode has its own template
if (templateFolder == null) {
templateFolder = getTemplateFolder();
}
if (templateFolder.exists()) {
Util.copyDir(templateFolder, sketchFolder);
File templateFile =
new File(sketchFolder, "sketch." + getDefaultExtension());
if (!templateFile.renameTo(newbieFile)) {
System.err.println("Error while assigning the sketch template.");
}
} else {
if (!newbieFile.createNewFile()) {
System.err.println(newbieFile + " already exists.");
}
}
} catch (Exception e) {
// just spew out this error and try to recover below
e.printStackTrace();
}
return newbieFile;
}
/**
* See if the user has their own template for this Mode. If the default
* extension is "pde", this will look for a file called sketch.pde to use
* as the template for all sketches.
*/
protected File checkSketchbookTemplate() {
File user = new File(Base.getSketchbookTemplatesFolder(), getTitle());
if (user.exists()) {
File template = new File(user, "sketch." + getDefaultExtension());
if (template.exists() && template.canRead()) {
return user;
}
}
return null;
}
public File getTemplateFolder() {
return getContentFile("template");
}
/**
* Return the pretty/printable/menu name for this mode. This is separate from
* the single word name of the folder that contains this mode. It could even
* have spaces, though that might result in sheer madness or total mayhem.
*/
abstract public String getTitle();
/**
* Get an identifier that can be used to resurrect this mode and connect it
* to a sketch. Using this instead of getTitle() because there might be name
* clashes with the titles, but there should not be once the actual package,
* et al. is included.
* @return full name (package + class name) for this mode.
*/
public String getIdentifier() {
return getClass().getCanonicalName();
}
/**
* Create a new editor associated with this mode.
*/
abstract public Editor createEditor(Base base, String path,
EditorState state) throws EditorException;
/**
* Get the folder where this mode is stored.
* @since 3.0a3
*/
public File getFolder() {
return folder;
}
public File getExamplesFolder() {
return examplesFolder;
}
public File getLibrariesFolder() {
return librariesFolder;
}
public File getReferenceFolder() {
return referenceFolder;
}
public void rebuildLibraryList() {
// reset the table mapping imports to libraries
Map newTable = new HashMap();
Library core = getCoreLibrary();
if (core != null) {
core.addPackageList(newTable);
}
coreLibraries = Library.list(librariesFolder);
File contribLibrariesFolder = Base.getSketchbookLibrariesFolder();
contribLibraries = Library.list(contribLibrariesFolder);
// Check to see if video and sound are installed and move them
// from the contributed list to the core list.
foundationLibraries = new ArrayList();
for (Library lib : contribLibraries) {
if (lib.isFoundation()) {
foundationLibraries.add(lib);
}
}
coreLibraries.addAll(foundationLibraries);
contribLibraries.removeAll(foundationLibraries);
for (Library lib : coreLibraries) {
lib.addPackageList(newTable);
}
for (Library lib : contribLibraries) {
lib.addPackageList(newTable);
}
// Make this Map thread-safe
importToLibraryTable = Collections.unmodifiableMap(newTable);
if (base != null) {
base.getEditors().forEach(Editor::librariesChanged);
}
}
public Library getCoreLibrary() {
return null;
}
public Library getLibrary(String pkgName) throws SketchException {
List libraries = importToLibraryTable.get(pkgName);
if (libraries == null) {
return null;
} else if (libraries.size() > 1) {
String primary = "More than one library is competing for this sketch.";
String secondary = "The import " + pkgName + " points to multiple libraries:
";
for (Library library : libraries) {
String location = library.getPath();
if (location.startsWith(getLibrariesFolder().getAbsolutePath())) {
location = "part of Processing";
}
secondary += "
" + library.getName() + " (" + location + ")
";
}
secondary += "Extra libraries need to be removed before this sketch can be used.";
Messages.showWarningTiered("Duplicate Library Problem", primary, secondary, null);
throw new SketchException("Duplicate libraries found for " + pkgName + ".");
} else {
return libraries.get(0);
}
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
// abstract public EditorToolbar createToolbar(Editor editor);
@SuppressWarnings("unused")
public JMenu getToolbarMenu() {
if (toolbarMenu == null) {
rebuildToolbarMenu();
}
return toolbarMenu;
}
public void insertToolbarRecentMenu() {
if (toolbarMenu == null) {
rebuildToolbarMenu();
} else {
toolbarMenu.insert(Recent.getToolbarMenu(), 1);
}
}
public void removeToolbarRecentMenu() {
toolbarMenu.remove(Recent.getToolbarMenu());
}
protected void rebuildToolbarMenu() { //JMenu menu) {
JMenuItem item;
if (toolbarMenu == null) {
toolbarMenu = new JMenu();
} else {
toolbarMenu.removeAll();
}
//System.out.println("rebuilding toolbar menu");
// Add the single "Open" item
item = Toolkit.newJMenuItem("Open...", 'O');
item.addActionListener(e -> base.handleOpenPrompt());
toolbarMenu.add(item);
insertToolbarRecentMenu();
item = Toolkit.newJMenuItemShift("Examples...", 'O');
item.addActionListener(e -> showExamplesFrame());
toolbarMenu.add(item);
item = new JMenuItem(Language.text("examples.add_examples"));
item.addActionListener(e -> ContributionManager.openExamples());
toolbarMenu.add(item);
// Add a list of all sketches and subfolders
toolbarMenu.addSeparator();
base.populateSketchbookMenu(toolbarMenu);
// boolean found = false;
// try {
// found = base.addSketches(toolbarMenu, base.getSketchbookFolder(), true);
// } catch (IOException e) {
// Base.showWarning("Sketchbook Toolbar Error",
// "An error occurred while trying to list the sketchbook.", e);
// }
// if (!found) {
// JMenuItem empty = new JMenuItem("(empty)");
// empty.setEnabled(false);
// toolbarMenu.add(empty);
// }
}
protected int importMenuIndex = -1;
/**
* Rather than re-building the library menu for every open sketch (very slow
* and prone to bugs when updating libs, particularly with the contribs mgr),
* share a single instance across all windows.
* @since 3.0a6
* @param sketchMenu the Sketch menu that's currently active
*/
public void removeImportMenu(JMenu sketchMenu) {
JMenu importMenu = getImportMenu();
//importMenuIndex = sketchMenu.getComponentZOrder(importMenu);
importMenuIndex = Toolkit.getMenuItemIndex(sketchMenu, importMenu);
sketchMenu.remove(importMenu);
}
/**
* Re-insert the Import Library menu. Added function so that other modes
* need not have an 'import' menu.
* @since 3.0a6
* @param sketchMenu the Sketch menu that's currently active
*/
public void insertImportMenu(JMenu sketchMenu) {
// hard-coded as 4 in 3.0a5, change to 5 for 3.0a6, but... yuck
//sketchMenu.insert(mode.getImportMenu(), 4);
// This is -1 on when the editor window is first shown, but that's fine
// because the import menu has just been added in the Editor constructor.
if (importMenuIndex != -1) {
sketchMenu.insert(getImportMenu(), importMenuIndex);
}
}
public JMenu getImportMenu() {
if (importMenu == null) {
rebuildImportMenu();
}
return importMenu;
}
public void rebuildImportMenu() { //JMenu importMenu) {
if (importMenu == null) {
importMenu = new JMenu(Language.text("menu.library"));
} else {
//System.out.println("rebuilding import menu");
importMenu.removeAll();
}
JMenuItem manageLibs = new JMenuItem(Language.text("menu.library.manage_libraries"));
manageLibs.addActionListener(e -> ContributionManager.openLibraries());
importMenu.add(manageLibs);
importMenu.addSeparator();
rebuildLibraryList();
ActionListener listener = e -> base.activeEditor.handleImportLibrary(e.getActionCommand());
// try {
// pw = new PrintWriter(new FileWriter(System.getProperty("user.home") + "/Desktop/libs.csv"));
// } catch (IOException e1) {
// e1.printStackTrace();
// }
if (coreLibraries.size() == 0) {
JMenuItem item = new JMenuItem(getTitle() + " " + Language.text("menu.library.no_core_libraries"));
item.setEnabled(false);
importMenu.add(item);
} else {
for (Library library : coreLibraries) {
JMenuItem item = new JMenuItem(library.getName());
item.addActionListener(listener);
// changed to library-name to facilitate specification of imports from properties file
item.setActionCommand(library.getName());
importMenu.add(item);
}
}
if (contribLibraries.size() != 0) {
importMenu.addSeparator();
JMenuItem contrib = new JMenuItem(Language.text("menu.library.contributed"));
contrib.setEnabled(false);
importMenu.add(contrib);
Map subfolders = new HashMap();
for (Library library : contribLibraries) {
JMenuItem item = new JMenuItem(library.getName());
item.addActionListener(listener);
// changed to library-name to facilitate specification if imports from properties file
item.setActionCommand(library.getName());
String group = library.getGroup();
if (group != null) {
JMenu subMenu = subfolders.get(group);
if (subMenu == null) {
subMenu = new JMenu(group);
importMenu.add(subMenu);
subfolders.put(group, subMenu);
}
subMenu.add(item);
} else {
importMenu.add(item);
}
}
}
}
/**
* Require examples to explicitly state that they're compatible with this
* Mode before they're included. Helpful for Modes like p5js or Python
* where the .java examples cannot be used.
* @since 3.2
* @return true if an examples package must list this Mode's identifier
*/
public boolean requireExampleCompatibility() {
return false;
}
/**
* Override this to control the order of the first set of example folders
* and how they appear in the examples window.
*/
public File[] getExampleCategoryFolders() {
return examplesFolder.listFiles((dir, name) -> dir.isDirectory() && name.charAt(0) != '.');
}
public void rebuildExamplesFrame() {
if (examplesFrame != null) {
boolean visible = examplesFrame.isVisible();
Rectangle bounds = null;
if (visible) {
bounds = examplesFrame.getBounds();
examplesFrame.setVisible(false);
examplesFrame.dispose();
}
examplesFrame = null;
if (visible) {
showExamplesFrame();
examplesFrame.setBounds(bounds);
}
}
}
public void showExamplesFrame() {
if (examplesFrame == null) {
examplesFrame = new ExamplesFrame(base, this);
}
examplesFrame.setVisible();
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
/**
* Get an ImageIcon object from the Mode folder.
* Or when prefixed with /lib, load it from the main /lib folder.
* @since 3.0a6
*/
public ImageIcon loadIcon(String filename) {
if (filename.startsWith("/lib/")) {
return Toolkit.getLibIcon(filename.substring(5));
}
File file = new File(folder, filename);
if (!file.exists()) {
return null;
}
return new ImageIcon(file.getAbsolutePath());
}
/**
* Get an image object from the mode folder.
* Or when prefixed with /lib, load it from the main /lib folder.
*/
public Image loadImage(String filename) {
ImageIcon icon = loadIcon(filename);
if (icon != null) {
return icon.getImage();
}
return null;
}
public Image loadImageX(String filename) {
return loadImage(filename + "-" + Toolkit.highResMultiplier() + "x.png");
}
public String loadString(String filename) {
File file;
if (filename.startsWith("/lib/")) {
// remove the slash from the front
file = Platform.getContentFile(filename.substring(1));
} else {
file = new File(folder, filename);
}
return Util.loadFile(file);
}
/**
* Returns the HTML filename (including path prefix if necessary)
* for this keyword, or null if it doesn't exist.
*/
public String lookupReference(String keyword) {
return keywordToReference.get(keyword);
}
/**
* Specialized version of getTokenMarker() that can be overridden to
* provide different TokenMarker objects for different file types.
* @since 3.2
* @param code the code for which we need a TokenMarker
*/
@SuppressWarnings("unused")
public TokenMarker getTokenMarker(SketchCode code) {
return getTokenMarker();
}
public TokenMarker getTokenMarker() {
return tokenMarker;
}
protected TokenMarker createTokenMarker() {
return new PdeTokenMarker();
}
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
// Breaking out extension types in order to clean up the code, and make it
// easier for other environments (like Arduino) to incorporate changes.
/**
* True if the specified extension should be hidden when shown on a tab.
* For Processing, this is true for .pde files. (Broken out for subclasses.)
* You can override this in your Mode subclass to handle it differently.
*/
public boolean hideExtension(String ext) {
return ext.equals(getDefaultExtension());
}
/**
* True if the specified code has the default file extension.
*/
public boolean isDefaultExtension(SketchCode code) {
return code.getExtension().equals(getDefaultExtension());
}
/**
* True if the specified extension is the default file extension.
*/
public boolean isDefaultExtension(String ext) {
return ext.equals(getDefaultExtension());
}
/**
* True if this Mode can edit this file (usually meaning that
* its extension matches one that is supported by the Mode).
*/
public boolean canEdit(final File file) {
final int dot = file.getName().lastIndexOf('.');
if (dot < 0) {
return false;
}
return validExtension(file.getName().substring(dot + 1));
}
public boolean canEdit(Sketch sketch) {
for (final SketchCode code : sketch.getCode()) {
if (!validExtension(code.getExtension())) {
return false;
}
}
return true;
}
/**
* Check this extension (no dots, please) against the list of valid
* extensions.
*/
public boolean validExtension(String what) {
String[] ext = getExtensions();
for (String s : ext) {
if (s.equals(what)) return true;
}
return false;
}
/**
* Returns the default extension for this editor setup.
*/
abstract public String getDefaultExtension();
/**
* Returns the appropriate file extension to use for auxiliary source
* files in a sketch. For example, in a Java-mode sketch, auxiliary files
* can be named "Foo.java"; in Python mode, they should be named "foo.py".
*
* Modes that do not override this function will get the
* default behavior of returning the default extension.
*/
public String getModuleExtension() {
return getDefaultExtension();
}
/**
* Returns a String[] array of proper extensions.
*/
abstract public String[] getExtensions();
/**
* Get array of file/directory names that needn't be copied during "Save As".
*/
abstract public String[] getIgnorable();
// . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
/**
* Checks coreLibraries and contribLibraries for a library with the specified name
* @param libName the name of the library to find
* @return the Library or null if not found
*/
public Library findLibraryByName(String libName) {
for (Library lib : this.coreLibraries) {
if (libName.equals(lib.getName()))
return lib;
}
for (Library lib : this.contribLibraries) {
if (libName.equals(lib.getName()))
return lib;
}
return null;
}
/**
* Create a fresh application folder if the 'delete target folder'
* pref has been set in the preferences.
*/
public void prepareExportFolder(File targetFolder) {
if (targetFolder != null) {
// Nuke the old application folder because it can cause trouble
if (Preferences.getBoolean("export.delete_target_folder")) {
if (targetFolder.exists()) {
try {
Platform.deleteFile(targetFolder);
} catch (IOException e) {
// ignore errors/continue; likely to be ok
e.printStackTrace();
}
}
}
// Create a fresh output folder (needed before preproc is run next)
if (!targetFolder.exists()) {
if (!targetFolder.mkdirs()) {
Messages.err("Could not create " + targetFolder);
}
}
}
}
@Override
public String toString() {
return getTitle();
}
}