[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/botbit/processing/master/app/src/processing/app/Editor.java [Back]  [Original]

/* -*- mode: java; c-basic-offset: 2; indent-tabs-mode: nil -*- */

/*
  Part of the Processing project - http://processing.org

  Copyright (c) 2012-14 The Processing Foundation
  Copyright (c) 2004-12 Ben Fry and Casey Reas
  Copyright (c) 2001-04 Massachusetts Institute of Technology

  This program is free software; you can redistribute it and/or modify
  it under the terms of the GNU General Public License version 2
  as published by the Free Software Foundation.

  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 processing.app.contrib.ToolContribution;
import processing.app.syntax.*;
import processing.app.tools.*;
import processing.core.*;

import java.awt.*;
import java.awt.datatransfer.*;
import java.awt.event.*;
import java.awt.print.*;
import java.io.*;
import java.util.*;
import java.util.List;
import java.util.Timer;

import javax.swing.*;
import javax.swing.event.*;
import javax.swing.plaf.basic.*;
import javax.swing.text.*;
import javax.swing.undo.*;

/**
 * Main editor panel for the Processing Development Environment.
 */
public abstract class Editor extends JFrame implements RunnerListener {
  protected Base base;
  protected EditorState state;
  protected Mode mode;

  static public final int LEFT_GUTTER = 44;
  static public final int RIGHT_GUTTER = 20;
  static public final int GUTTER_MARGIN = 3;


  // Otherwise, if the window is resized with the message label
  // set to blank, its preferredSize() will be fuckered
  static protected final String EMPTY =
    "                                                                     " +
    "                                                                     " +
    "                                                                     ";

  /**
   * true if this file has not yet been given a name by the user
   */
//  private boolean untitled;

  private PageFormat pageFormat;
  private PrinterJob printerJob;

  // File and sketch menus for re-inserting items
  private JMenu fileMenu;
//  private JMenuItem saveMenuItem;
//  private JMenuItem saveAsMenuItem;

  private JMenu sketchMenu;

  protected EditorHeader header;
  protected EditorToolbar toolbar;
  protected JEditTextArea textarea;
  protected EditorStatus status;
  protected JSplitPane splitPane;
  protected JPanel consolePanel;
  protected EditorConsole console;
  protected EditorLineStatus lineStatus;

  // currently opened program
  protected Sketch sketch;

  // runtime information and window placement
  private Point sketchWindowLocation;

  // undo fellers
  private JMenuItem undoItem, redoItem, copyItems, cutItems;
  protected UndoAction undoAction;
  protected RedoAction redoAction;
  /** the currently selected tab's undo manager */
  protected CopyAction copyAction;
  protected CutAction cutAction;
  private UndoManager undo;
  // used internally for every edit. Groups hotkey-event text manipulations and
  // groups  multi-character inputs into a single undos.
  private CompoundEdit compoundEdit;
  // timer to decide when to group characters into an undo
  private Timer timer;
  private TimerTask endUndoEvent;
  // true if inserting text, false if removing text
  private boolean isInserting;
  // maintain caret position during undo operations
  private final Stack caretUndoStack = new Stack();
  private final Stack caretRedoStack = new Stack();

  private FindReplace find;
  JMenu toolsMenu;
  JMenu modeMenu;

  ArrayList coreTools;
  public ArrayList contribTools;

  Image backgroundGradient;


//  protected Editor(final Base base, String path, int[] location, final Mode mode) {
  protected Editor(final Base base, String path, EditorState state, final Mode mode) {
    super("Processing", state.checkConfig());
    this.base = base;
    this.state = state;
    this.mode = mode;

    Toolkit.setIcon(this);  // TODO should this be per-mode?

    // Install default actions for Run, Present, etc.
//    resetHandlers();

    // add listener to handle window close box hit event
    addWindowListener(new WindowAdapter() {
        public void windowClosing(WindowEvent e) {
          base.handleClose(Editor.this, false);
        }
      });
    // don't close the window when clicked, the app will take care
    // of that via the handleQuitInternal() methods
    // http://dev.processing.org/bugs/show_bug.cgi?id=440
    setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE);

    // When bringing a window to front, let the Base know
    addWindowListener(new WindowAdapter() {
//      int importIndex;

        public void windowActivated(WindowEvent e) {
          base.handleActivated(Editor.this);
          fileMenu.insert(base.getRecentMenu(), 2);
          Toolkit.setMenuMnemsInside(fileMenu);

          //sketchMenu.insert(mode.getImportMenu(), 5);
          mode.insertImportMenu(sketchMenu);
          //sketchMenu.insert(mode.getImportMenu(), importIndex);
          Toolkit.setMenuMnemsInside(sketchMenu);
          mode.insertToolbarRecentMenu();
        }

        public void windowDeactivated(WindowEvent e) {
          fileMenu.remove(base.getRecentMenu());
//          JMenu importMenu = mode.getImportMenu();
//          importIndex = sketchMenu.getComponentZOrder(mode.getImportMenu());
//          sketchMenu.remove(mode.getImportMenu());
          mode.removeImportMenu(sketchMenu);
          mode.removeToolbarRecentMenu();
        }
      });

    timer = new Timer();

    buildMenuBar();

    /*
    //backgroundGradient = Toolkit.getLibImage("vertical-gradient.png");
    backgroundGradient = mode.getGradient("editor", 400, 400);
    JPanel contentPain = new JPanel() {
      @Override
      public void paintComponent(Graphics g) {
//        super.paintComponent(g);
        Dimension dim = getSize();
        g.drawImage(backgroundGradient, 0, 0, dim.width, dim.height, this);
//        g.setColor(Color.RED);
//        g.fillRect(0, 0, dim.width, dim.height);
      }
    };
    */
    //contentPain.setBorder(new EmptyBorder(0, 0, 0, 0));
    //System.out.println(contentPain.getBorder());
    JPanel contentPain = new JPanel();

//    JFrame f = new JFrame();
//    f.setContentPane(new JPanel() {
//      @Override
//      public void paintComponent(Graphics g) {
////        super.paintComponent(g);
//        Dimension dim = getSize();
//        g.drawImage(backgroundGradient, 0, 0, dim.width, dim.height, this);
////        g.setColor(Color.RED);
////        g.fillRect(0, 0, dim.width, dim.height);
//      }
//    });
//    f.setResizable(true);
//    f.setVisible(true);

    //Container contentPain = getContentPane();
    setContentPane(contentPain);
    contentPain.setLayout(new BorderLayout());
//    JPanel pain = new JPanel();
//    pain.setOpaque(false);
//    pain.setLayout(new BorderLayout());
//    contentPain.add(pain, BorderLayout.CENTER);
//    contentPain.setBorder(new EmptyBorder(10, 10, 10, 10));

    Box box = Box.createVerticalBox();
    Box upper = Box.createVerticalBox();
//    upper.setOpaque(false);
//    box.setOpaque(false);

    initModeMenu();
    toolbar = createToolbar();
    upper.add(toolbar);

    header = new EditorHeader(this);
    upper.add(header);

    textarea = createTextArea();
    textarea.setRightClickPopup(new TextAreaPopup());
    textarea.setHorizontalOffset(JEditTextArea.leftHandGutter);

    // assemble console panel, consisting of status area and the console itself
    consolePanel = new JPanel();
    consolePanel.setLayout(new BorderLayout());

    status = new EditorStatus(this);
    consolePanel.add(status, BorderLayout.NORTH);

    console = new EditorConsole(this);
    // windows puts an ugly border on this guy
    console.setBorder(null);
    consolePanel.add(console, BorderLayout.CENTER);

    lineStatus = new EditorLineStatus(this);
    consolePanel.add(lineStatus, BorderLayout.SOUTH);

    upper.add(textarea);

    // alternate spot for status, but ugly
//    status = new EditorStatus(this);
//    upper.add(status);

    splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, upper, consolePanel);

    // disable this because it hides the message area, which is essential (issue #745)
    splitPane.setOneTouchExpandable(false);
    // repaint child panes while resizing
    splitPane.setContinuousLayout(true);
    // if window increases in size, give all of increase to
    // the textarea in the upper pane
    splitPane.setResizeWeight(1D);
    // remove any ugly borders added by PLAFs
    splitPane.setBorder(null);
    // necessary to let the gradient show through
//    splitPane.setOpaque(false);

    // remove an ugly border around anything in a SplitPane !$*&!%
    UIManager.getDefaults().put("SplitPane.border", BorderFactory.createEmptyBorder());
    // override the look of the SplitPane so that it's identical across OSes
    splitPane.setUI(new BasicSplitPaneUI() {
      public BasicSplitPaneDivider createDefaultDivider() {
        return new BasicSplitPaneDivider(this) {
          final Color dividerColor = mode.getColor("divider.color"); //new Color(204, 204, 204);
          final Color dotColor = mode.getColor("divider.dot.color"); //new Color(80, 80, 80);
          int dotSize = mode.getInteger("divider.dot.diameter"); //3;

          @Override
          public void paint(Graphics g) {
            Graphics2D g2 = (Graphics2D) g;
            g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                                RenderingHints.VALUE_ANTIALIAS_ON);

            final int w = getSize().width;
            final int h = getSize().height;
            g.setColor(dividerColor);
            g.fillRect(0, 0, w, h);

            g.setColor(dotColor);
            int x = w/2 - dotSize/2;
            int y = h/2 - dotSize/2;
            g.fillOval(x, y, dotSize, dotSize);
          }
        };
      }
    });

//    EditorConsole.systemOut.println("divider default size is " + splitPane.getDividerSize());
//    // the default size on windows is too small and kinda ugly
//    int dividerSize = Preferences.getInteger("editor.divider.size");
//    if (dividerSize != 0) {
//      splitPane.setDividerSize(dividerSize);
//    }
    splitPane.setDividerSize(mode.getInteger("divider.height"));

    box.add(splitPane);

    contentPain.add(box);

    // get shift down/up events so we can show the alt version of toolbar buttons
    //textarea.addKeyListener(toolbar);

    // end an undo-chunk any time the caret moves unless it's when text is edited
    textarea.addCaretListener(new CaretListener() {
      String lastText = textarea.getText();
      public void caretUpdate(CaretEvent e) {
        String newText = textarea.getText();
        if (lastText.equals(newText) && isDirectEdit()) {
          endTextEditHistory();
        }
        lastText = newText;
      }
    });

    contentPain.setTransferHandler(new FileDropHandler());

    // Finish preparing Editor (formerly found in Base)
    pack();

    // Set the window bounds and the divider location before setting it visible
    state.apply(this);

    // Set the minimum size for the editor window
    setMinimumSize(new Dimension(Preferences.getInteger("editor.window.width.min"),
                                 Preferences.getInteger("editor.window.height.min")));

    // Bring back the general options for the editor
    applyPreferences();

    // Make textField get the focus whenever frame is activated.
    // http://download.oracle.com/javase/tutorial/uiswing/misc/focus.html
    // May not be necessary, but helps avoid random situations with
    // the editor not being able to request its own focus.
    addWindowFocusListener(new WindowAdapter() {
      public void windowGainedFocus(WindowEvent e) {
        textarea.requestFocusInWindow();
      }

//      public void windowLostFocus(WindowEvent e) {
//        System.out.println("lost focus, should we tell the text area?");
//      }
    });

    // Open the document that was passed in
    boolean loaded = handleOpenInternal(path);
    if (!loaded) {
      sketch = null;
    }

    // Add a window listener to watch for changes to the files in the sketch
    addWindowFocusListener(new ChangeDetector(this));
  }


  protected ArrayList getCoreTools() {
    return coreTools;
  }


  /**
   * Broken out to get modes working for GSOC, but this needs a longer-term
   * solution where the listeners are handled properly.
   */
  protected JEditTextArea createTextArea() {
    return new JEditTextArea(new PdeTextAreaDefaults(mode),
                             new PdeInputHandler());
    /*
    return new JEditTextArea(new PdeTextAreaDefaults(mode), new PdeInputHandler()) {
      // this is a kludge that needs to be removed [fry 150120]
      public void processKeyEvent(KeyEvent evt) {
        // this had to be added in Processing 007X, because the menu key
        // events weren't making it up to the frame.
        super.processKeyEvent(evt);

        if (inputHandler != null) {
          // Prevent sketch being marked modified cia ctrl-, on Windows/Linux
          if ((evt.getModifiers() & InputEvent.CTRL_MASK) != 0 &&
              evt.getKeyChar() == ',') {
            System.out.println("skipping " + evt);
            return;
          }
          switch (evt.getID()) {
          case KeyEvent.KEY_TYPED:
            inputHandler.keyTyped(evt);
            break;
          case KeyEvent.KEY_PRESSED:
            inputHandler.keyPressed(evt);
            break;
          case KeyEvent.KEY_RELEASED:
            inputHandler.keyReleased(evt);
            break;
          }
        }
      }
    };
    */
  }


  public EditorState getEditorState() {
    return state;
  }


  public void removeRecent() {
    base.removeRecent(this);
  }


  public void addRecent() {
    base.handleRecent(this);
  }


  /**
   * Handles files dragged & dropped from the desktop and into the editor
   * window. Dragging files into the editor window is the same as using
   * "Sketch → Add File" for each file.
   */
  class FileDropHandler extends TransferHandler {
    public boolean canImport(TransferHandler.TransferSupport support) {
      return !sketch.isReadOnly();
    }

    @SuppressWarnings("unchecked")
    public boolean importData(TransferHandler.TransferSupport support) {
      int successful = 0;

      if (!canImport(support)) {
        return false;
      }

      try {
        Transferable transferable = support.getTransferable();
        DataFlavor uriListFlavor =
          new DataFlavor("text/uri-list;class=java.lang.String");

        if (transferable.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
          java.util.List list = (java.util.List)
            transferable.getTransferData(DataFlavor.javaFileListFlavor);
          for (int i = 0; i < list.size(); i++) {
            File file = (File) list.get(i);
            if (sketch.addFile(file)) {
              successful++;
            }
          }
        } else if (transferable.isDataFlavorSupported(uriListFlavor)) {
          // Some platforms (Mac OS X and Linux, when this began) preferred
          // this method of moving files.
          String data = (String)transferable.getTransferData(uriListFlavor);
          String[] pieces = PApplet.splitTokens(data, "\r\n");
          for (int i = 0; i < pieces.length; i++) {
            if (pieces[i].startsWith("#")) continue;

            String path = null;
            if (pieces[i].startsWith("file:///")) {
              path = pieces[i].substring(7);
            } else if (pieces[i].startsWith("file:/")) {
              path = pieces[i].substring(5);
            }
            if (sketch.addFile(new File(path))) {
              successful++;
            }
          }
        }
      } catch (Exception e) {
        Base.showWarning("Drag & Drop Problem",
                         "An error occurred while trying to add files to the sketch.", e);
        return false;
      }

      statusNotice(Language.pluralize("editor.status.drag_and_drop.files_added", successful));

      return true;
    }
  }


  public Base getBase() {
    return base;
  }


  public Mode getMode() {
    return mode;
  }


  protected void initModeMenu() {
    modeMenu = new JMenu();
    ButtonGroup modeGroup = new ButtonGroup();
    for (final Mode m : base.getModeList()) {
      JRadioButtonMenuItem item = new JRadioButtonMenuItem(m.getTitle());
      item.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          if (!sketch.isModified()) {
            base.changeMode(m);
          } else {
            Base.showWarning("Save",
                             "Please save the sketch before changing the mode.",
                             null);

            // Re-select the old checkbox, because it was automatically
            // updated by Java, even though the Mode could not be changed.
            // https://github.com/processing/processing/issues/2615
            for (Component c : modeMenu.getPopupMenu().getComponents()) {
              if (c instanceof JRadioButtonMenuItem) {
                if (((JRadioButtonMenuItem)c).getText() == mode.getTitle()) {
                  ((JRadioButtonMenuItem)c).setSelected(true);
                  break;
                }
              }
            }
          }
        }
      });
      modeMenu.add(item);
      modeGroup.add(item);
      if (mode == m) {
        item.setSelected(true);
      }
    }

    modeMenu.addSeparator();
    JMenuItem addLib = new JMenuItem(Language.text("toolbar.add_mode"));
    addLib.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        base.handleOpenModeManager();
      }
    });
    modeMenu.add(addLib);

    Toolkit.setMenuMnemsInside(modeMenu);
  }


  public void rebuildModeMenu() {
    initModeMenu();
  }


  public JMenu getModeMenu() {
    return modeMenu;
  }


  public EditorConsole getConsole() {
    return console;
  }


//  public Settings getTheme() {
//    return mode.getTheme();
//  }


  abstract public EditorToolbar createToolbar();


  abstract public Formatter createFormatter();


//  protected void setPlacement(int[] location) {
//    setBounds(location[0], location[1], location[2], location[3]);
//    if (location[4] != 0) {
//      splitPane.setDividerLocation(location[4]);
//    }
//  }
//
//
//  protected int[] getPlacement() {
//    int[] location = new int[5];
//
//    // Get the dimensions of the Frame
//    Rectangle bounds = getBounds();
//    location[0] = bounds.x;
//    location[1] = bounds.y;
//    location[2] = bounds.width;
//    location[3] = bounds.height;
//
//    // Get the current placement of the divider
//    location[4] = splitPane.getDividerLocation();
//
//    return location;
//  }


  protected void setDividerLocation(int pos) {
    splitPane.setDividerLocation(pos);
  }


  protected int getDividerLocation() {
    return splitPane.getDividerLocation();
  }


  // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .


  /**
   * Read and apply new values from the preferences, either because
   * the app is just starting up, or the user just finished messing
   * with things in the Preferences window.
   */
  protected void applyPreferences() {
    // Update fonts and other items controllable from the prefs
    textarea.getPainter().updateAppearance();
    textarea.repaint();

    console.updateAppearance();

    // All of this code was specific to using an external editor.
    /*
//    // apply the setting for 'use external editor'
//    boolean external = Preferences.getBoolean("editor.external");
//    textarea.setEditable(!external);
//    saveMenuItem.setEnabled(!external);
//    saveAsMenuItem.setEnabled(!external);

    TextAreaPainter painter = textarea.getPainter();
//    if (external) {
//      // disable line highlight and turn off the caret when disabling
//      Color color = mode.getColor("editor.external.bgcolor");
//      painter.setBackground(color);
//      painter.setLineHighlightEnabled(false);
//      textarea.setCaretVisible(false);
//    } else {
    Color color = mode.getColor("editor.bgcolor");
    painter.setBackground(color);
    boolean highlight = Preferences.getBoolean("editor.linehighlight");
    painter.setLineHighlightEnabled(highlight);
    textarea.setCaretVisible(true);
//    }

    // apply changes to the font size for the editor
//    painter.setFont(Preferences.getFont("editor.font"));

    // in case tab expansion stuff has changed
    // removing this, just checking prefs directly instead
//    listener.applyPreferences();

    // in case moved to a new location
    // For 0125, changing to async version (to be implemented later)
    //sketchbook.rebuildMenus();
    // For 0126, moved into Base, which will notify all editors.
    //base.rebuildMenusAsync();
     */
  }


  // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .


  protected void buildMenuBar() {
    JMenuBar menubar = new JMenuBar();
    fileMenu = buildFileMenu();
    menubar.add(fileMenu);
    menubar.add(buildEditMenu());
    menubar.add(buildSketchMenu());

    // For 3.0a4 move mode menu to the left of the Tool menu
    JMenu modeMenu = buildModeMenu();
    if (modeMenu != null) {
      menubar.add(modeMenu);
    }

    rebuildToolMenu();
    menubar.add(getToolMenu());

    menubar.add(buildHelpMenu());
    Toolkit.setMenuMnemonics(menubar);
    setJMenuBar(menubar);
  }


  abstract public JMenu buildFileMenu();


//  public JMenu buildFileMenu(Editor editor) {
//    return buildFileMenu(editor, null);
//  }
//
//
//  // most of these items are per-mode
//  protected JMenu buildFileMenu(Editor editor, JMenuItem[] exportItems) {


  protected JMenu buildFileMenu(JMenuItem[] exportItems) {
    JMenuItem item;
    JMenu fileMenu = new JMenu(Language.text("menu.file"));

    item = Toolkit.newJMenuItem(Language.text("menu.file.new"), 'N');
    item.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          base.handleNew();
        }
      });
    fileMenu.add(item);

    item = Toolkit.newJMenuItem(Language.text("menu.file.open"), 'O');
    item.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          base.handleOpenPrompt();
        }
      });
    fileMenu.add(item);

//    fileMenu.add(base.getSketchbookMenu());

    item = Toolkit.newJMenuItemShift(Language.text("menu.file.sketchbook"), 'K');
    item.addActionListener(new ActionListener() {
      @Override
      public void actionPerformed(ActionEvent e) {
        mode.showSketchbookFrame();
      }
    });
    fileMenu.add(item);

    item = Toolkit.newJMenuItemShift(Language.text("menu.file.examples"), 'O');
    item.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        mode.showExamplesFrame();
      }
    });
    fileMenu.add(item);

    item = Toolkit.newJMenuItem(Language.text("menu.file.close"), 'W');
    item.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        base.handleClose(Editor.this, false);
      }
    });
    fileMenu.add(item);

    item = Toolkit.newJMenuItem(Language.text("menu.file.save"), 'S');
    item.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        handleSave(false);
      }
    });
//    saveMenuItem = item;
    fileMenu.add(item);

    item = Toolkit.newJMenuItemShift(Language.text("menu.file.save_as"), 'S');
    item.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        handleSaveAs();
      }
    });
//    saveAsMenuItem = item;
    fileMenu.add(item);

    if (exportItems != null) {
      for (JMenuItem ei : exportItems) {
        fileMenu.add(ei);
      }
    }
    fileMenu.addSeparator();

    item = Toolkit.newJMenuItemShift(Language.text("menu.file.page_setup"), 'P');
    item.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        handlePageSetup();
      }
    });
    fileMenu.add(item);

    item = Toolkit.newJMenuItem(Language.text("menu.file.print"), 'P');
    item.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        handlePrint();
      }
    });
    fileMenu.add(item);

    // Mac OS X already has its own preferences and quit menu.
    // That's right! Think different, b*tches!
    if (!Base.isMacOS()) {
      fileMenu.addSeparator();

      item = Toolkit.newJMenuItem(Language.text("menu.file.preferences"), ',');
      item.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          base.handlePrefs();
        }
      });
      fileMenu.add(item);

      fileMenu.addSeparator();

      item = Toolkit.newJMenuItem(Language.text("menu.file.quit"), 'Q');
      item.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          base.handleQuit();
        }
      });
      fileMenu.add(item);
    }
    return fileMenu;
  }


//  public void setSaveItem(JMenuItem item) {
//    saveMenuItem = item;
//  }


//  public void setSaveAsItem(JMenuItem item) {
//    saveAsMenuItem = item;
//  }


  protected JMenu buildEditMenu() {
    JMenu menu = new JMenu(Language.text("menu.edit"));
    JMenuItem item;

    undoItem = Toolkit.newJMenuItem(Language.text("menu.edit.undo"), 'Z');
    undoItem.addActionListener(undoAction = new UndoAction());
    menu.add(undoItem);

    // Gotta follow them interface guidelines
    // http://code.google.com/p/processing/issues/detail?id=363
    if (Base.isWindows()) {
      redoItem = Toolkit.newJMenuItem(Language.text("menu.edit.redo"), 'Y');
    } else {  // Linux and OS X
      redoItem = Toolkit.newJMenuItemShift(Language.text("menu.edit.redo"), 'Z');
    }
    redoItem.addActionListener(redoAction = new RedoAction());
    menu.add(redoItem);

    menu.addSeparator();

    cutItems = Toolkit.newJMenuItem(Language.text("menu.edit.cut"), 'X');
    cutItems.addActionListener(cutAction = new CutAction());
    menu.add(cutItems);

    copyItems = Toolkit.newJMenuItem(Language.text("menu.edit.copy"), 'C');
    copyItems.addActionListener(copyAction = new CopyAction());
    menu.add(copyItems);

    item = Toolkit.newJMenuItemShift(Language.text("menu.edit.copy_as_html"), 'C');
    item.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          handleCopyAsHTML();
        }
      });
    menu.add(item);

    item = Toolkit.newJMenuItem(Language.text("menu.edit.paste"), 'V');
    item.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          textarea.paste();
          sketch.setModified(true);
        }
      });
    menu.add(item);

    item = Toolkit.newJMenuItem(Language.text("menu.edit.select_all"), 'A');
    item.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          textarea.selectAll();
        }
      });
    menu.add(item);

    /*
    menu.addSeparator();

    item = Toolkit.newJMenuItem("Delete Selected Lines", 'D');
    item.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        handleDeleteLines();
      }
    });
    menu.add(item);

    item = new JMenuItem("Move Selected Lines Up");
    item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_UP, Event.ALT_MASK));
    item.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          handleMoveLines(true);
        }
      });
    menu.add(item);

    item = new JMenuItem("Move Selected Lines Down");
    item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, Event.ALT_MASK));
    item.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          handleMoveLines(false);
        }
      });
    menu.add(item);
     */

    menu.addSeparator();

    item = Toolkit.newJMenuItem(Language.text("menu.edit.auto_format"), 'T');
    item.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          handleAutoFormat();
        }
    });
    menu.add(item);

    item = Toolkit.newJMenuItem(Language.text("menu.edit.comment_uncomment"), '/');
    item.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          handleCommentUncomment();
        }
    });
    menu.add(item);

    item = Toolkit.newJMenuItem("\u2192 "+Language.text("menu.edit.increase_indent"), ']');
    item.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          handleIndentOutdent(true);
        }
    });
    menu.add(item);

    item = Toolkit.newJMenuItem("\u2190 "+Language.text("menu.edit.decrease_indent"), '[');
    item.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          handleIndentOutdent(false);
        }
    });
    menu.add(item);

    menu.addSeparator();

    item = Toolkit.newJMenuItem(Language.text("menu.edit.find"), 'F');
    item.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          if (find == null) {
            find = new FindReplace(Editor.this);
          }
          //new FindReplace(Editor.this).show();
          find.setVisible(true);
        }
      });
    menu.add(item);

    // TODO find next should only be enabled after a
    // search has actually taken place
    item = Toolkit.newJMenuItem(Language.text("menu.edit.find_next"), 'G');
    item.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          if (find != null) {
            find.findNext();
          }
        }
      });
    menu.add(item);

    item = Toolkit.newJMenuItemShift(Language.text("menu.edit.find_previous"), 'G');
    item.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          if (find != null) {
            find.findPrevious();
          }
        }
      });
    menu.add(item);

    // For Arduino and Mac, this should be command-E, but that currently conflicts with Export Applet
    //item = Toolkit.newJMenuItemAlt(Language.text("menu.edit.use_selection_for_find"), 'F');
    item = Toolkit.newJMenuItem(Language.text("menu.edit.use_selection_for_find"), 'E');
    item.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          if (find == null) {
            find = new FindReplace(Editor.this);
          }
          find.setFindText(getSelectedText());
        }
      });
    menu.add(item);

    // Update copy/cut state on selection/de-selection
    menu.addMenuListener(new MenuListener() {

      @Override
      public void menuCanceled(MenuEvent e) {
        copyItems.setEnabled(true);
        cutItems.setEnabled(true);
      }

      @Override
      public void menuDeselected(MenuEvent e) {
        copyItems.setEnabled(true);
        cutItems.setEnabled(true);
      }

      @Override
      public void menuSelected(MenuEvent e) {
        copyAction.updateCopyState();
        cutAction.updateCutState();
      }
    });
    return menu;
  }


  abstract public JMenu buildSketchMenu();


  protected JMenu buildSketchMenu(JMenuItem[] runItems) {
    JMenuItem item;
    sketchMenu = new JMenu(Language.text("menu.sketch"));

    for (JMenuItem mi : runItems) {
      sketchMenu.add(mi);
    }

    sketchMenu.addSeparator();

    sketchMenu.add(mode.getImportMenu());

    item = Toolkit.newJMenuItem(Language.text("menu.sketch.show_sketch_folder"), 'K');
    item.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          Base.openFolder(sketch.getFolder());
        }
      });
    sketchMenu.add(item);
    item.setEnabled(Base.openFolderAvailable());

    item = new JMenuItem(Language.text("menu.sketch.add_file"));
    item.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          sketch.handleAddFile();
        }
      });
    sketchMenu.add(item);

    if (runItems != null && runItems.length != 0) {
      sketchMenu.addSeparator();
    }

//    final Editor editorName = this;

    sketchMenu.addMenuListener(new MenuListener() {
      // Menu Listener that populates the menu only when the menu is opened
      List menuList = new ArrayList();

      @Override
      public void menuSelected(MenuEvent event) {
        JMenuItem item;
        for (final Editor editor : base.getEditors()) {
          //if (Editor.this.getSketch().getName().trim().contains(editor2.getSketch().getName().trim()))
          if (getSketch().getMainFilePath().equals(editor.getSketch().getMainFilePath())) {
            item = new JCheckBoxMenuItem(editor.getSketch().getName());
            item.setSelected(true);
          } else {
            item = new JMenuItem(editor.getSketch().getName());
          }
          item.setText(editor.getSketch().getName() +
                       " (" + editor.getMode().getTitle() + ")");

          // Action listener to bring the appropriate sketch in front
          item.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
              editor.setState(Frame.NORMAL);
              editor.setVisible(true);
              editor.toFront();
            }
          });
          sketchMenu.add(item);
          menuList.add(item);
          Toolkit.setMenuMnemsInside(sketchMenu);
        }
      }

      @Override
      public void menuDeselected(MenuEvent event) {
        for (JMenuItem item : menuList) {
          sketchMenu.remove(item);
        }
        menuList.clear();
      }

      @Override
      public void menuCanceled(MenuEvent event) {
        menuDeselected(event);
      }
    });

    return sketchMenu;
  }


  abstract public void handleImportLibrary(String name);


  public JMenu getToolMenu() {
    if (toolsMenu == null) {
      rebuildToolMenu();
    }
    return toolsMenu;
  }


//  protected void rebuildToolList() {
//    coreTools = ToolContribution.list(Base.getToolsFolder(), true);
//    contribTools = ToolContribution.list(Base.getSketchbookToolsFolder(), true);
//  }


  public void rebuildToolMenu() {
    if (toolsMenu == null) {
      toolsMenu = new JMenu(Language.text("menu.tools"));
    } else {
      toolsMenu.removeAll();
    }

//    rebuildToolList();
    coreTools = ToolContribution.loadAll(Base.getToolsFolder());
    contribTools = ToolContribution.loadAll(Base.getSketchbookToolsFolder());

    addInternalTools(toolsMenu);
    addTools(toolsMenu, coreTools);
    addTools(toolsMenu, contribTools);

    toolsMenu.addSeparator();
    JMenuItem item = new JMenuItem(Language.text("menu.tools.add_tool"));
    item.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        base.handleOpenToolManager();
      }
    });
    toolsMenu.add(item);
  }


  public void clearToolMenu() {
    toolsMenu.removeAll();
    System.gc();
  }


  public void removeTool() {
    rebuildToolMenu();
  }


//  /**
//   * Attempt to init or run a Tool from the safety of a try/catch block that
//   * will report errors to the user.
//   * @param tool The Tool object to be inited or run
//   * @param item null to call init(), or the existing JMenuItem for run()
//   * @return
//   */
//  protected boolean safeTool(Tool tool, JMenuItem item) {
//    try {
//      if (item == null) {
//        tool.init(Editor.this);
//      } else {
//        tool.run();
//      }
//      return true;
//
//    } catch (NoSuchMethodError nsme) {
//      System.out.println("tool is " + tool + " ");
//      statusError("\"" + tool.getMenuTitle() + "\" " +
//                  "is not compatible with this version of Processing");
//      nsme.printStackTrace();
//
//    } catch (Exception ex) {
//      statusError("An error occurred inside \"" + tool.getMenuTitle() + "\"");
//      ex.printStackTrace();
//    }
//    if (item != null) {
//      item.setEnabled(false);  // don't you try that again
//    }
//    return false;
//  }


  void addToolItem(final Tool tool, HashMap toolItems) {
    String title = tool.getMenuTitle();
    final JMenuItem item = new JMenuItem(title);
    item.addActionListener(new ActionListener() {

      public void actionPerformed(ActionEvent e) {
        try {
          tool.run();

        } catch (NoSuchMethodError nsme) {
          statusError("\"" + tool.getMenuTitle() + "\" is not" +
                      "compatible with this version of Processing");
          //nsme.printStackTrace();
          Base.loge("Incompatible tool found during tool.run()", nsme);
          item.setEnabled(false);

        } catch (Exception ex) {
          statusError("An error occurred inside \"" + tool.getMenuTitle() + "\"");
          ex.printStackTrace();
          item.setEnabled(false);
        }
      }
    });
    //menu.add(item);
    toolItems.put(title, item);
  }


  protected void addTools(JMenu menu, ArrayList tools) {
    HashMap toolItems = new HashMap();

    for (final ToolContribution tool : tools) {
      try {
        tool.init(Editor.this);
        // If init() fails, the item won't be added to the menu
        addToolItem(tool, toolItems);

        // With the exceptions, we can't call statusError because the window
        // isn't completely set up yet. Also not gonna pop up a warning because
        // people may still be running different versions of Processing.
        // TODO Once the dust settles on 2.x, change this to Base.showError()
        // and open the Tools folder instead of showing System.err.println().

      } catch (VerifyError ve) {
        System.err.println("\"" + tool.getMenuTitle() + "\" is not " +
                           "compatible with this version of Processing");

      } catch (NoSuchMethodError nsme) {
        System.err.println("\"" + tool.getMenuTitle() + "\" is not " +
                           "compatible with this version of Processing");
        System.err.println("The " + nsme.getMessage() + " method no longer exists.");
        Base.loge("Incompatible Tool found during tool.init()", nsme);

      } catch (NoClassDefFoundError ncdfe) {
        System.err.println("\"" + tool.getMenuTitle() + "\" is not " +
                           "compatible with this version of Processing");
        System.err.println("The " + ncdfe.getMessage() + " class is no longer available.");
        Base.loge("Incompatible Tool found during tool.init()", ncdfe);

      } catch (Error err) {
        System.err.println("An error occurred inside \"" + tool.getMenuTitle() + "\"");
        err.printStackTrace();

      } catch (Exception ex) {
        System.err.println("An exception occurred inside \"" + tool.getMenuTitle() + "\"");
        ex.printStackTrace();
      }
    }

    ArrayList toolList = new ArrayList(toolItems.keySet());
    if (toolList.size() > 0) {
      menu.addSeparator();
      Collections.sort(toolList);
      for (String title : toolList) {
        menu.add(toolItems.get(title));
      }
    }
  }


  /**
   * Override this if you want a special menu for your particular 'mode'.
   */
  public JMenu buildModeMenu() {
    return null;
  }


  protected void addToolMenuItem(JMenu menu, String className) {
    try {
      Class toolClass = Class.forName(className);
      final Tool tool = (Tool) toolClass.newInstance();

      JMenuItem item = new JMenuItem(tool.getMenuTitle());

      tool.init(Editor.this);

      item.addActionListener(new ActionListener() {
        public void actionPerformed(ActionEvent e) {
          EventQueue.invokeLater(tool);
        }
      });
      menu.add(item);
//      return item;

    } catch (Exception e) {
      e.printStackTrace();
//      return null;
    }
  }


  protected JMenu addInternalTools(JMenu menu) {
//    JMenuItem item;
//    item = createToolMenuItem("processing.app.tools.AutoFormatTool");
//    int modifiers = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
//    item.setAccelerator(KeyStroke.getKeyStroke('T', modifiers));
//    menu.add(item);

    addToolMenuItem(menu, "processing.app.tools.CreateFont");
    addToolMenuItem(menu, "processing.app.tools.ColorSelector");
    addToolMenuItem(menu, "processing.app.tools.Archiver");

    if (Base.isMacOS()) {
      addToolMenuItem(menu, "processing.app.tools.InstallCommander");
    }

    // I think this is no longer needed... It was Mac OS X specific,
    // and they gave up on MacRoman a long time ago.
//    addToolMenuItem(menu, "processing.app.tools.FixEncoding");

    // currently commented out
//    if (Base.DEBUG) {
//      addToolMenuItem(menu, "processing.app.tools.ExportExamples");
//    }

//    // These are temporary entries while Android mode is being worked out.
//    // The mode will not be in the tools menu, and won't involve a cmd-key
//    if (!Base.RELEASE) {
//      item = createToolMenuItem("processing.app.tools.android.AndroidTool");
//      item.setAccelerator(KeyStroke.getKeyStroke('D', modifiers));
//      menu.add(item);
//      menu.add(createToolMenuItem("processing.app.tools.android.Permissions"));
//      menu.add(createToolMenuItem("processing.app.tools.android.Reset"));
//    }

    return menu;
  }


  /*
  // testing internal web server to serve up docs from a zip file
  item = new JMenuItem("Web Server Test");
  item.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        //WebServer ws = new WebServer();
        SwingUtilities.invokeLater(new Runnable() {
          public void run() {
            try {
              int port = WebServer.launch("/Users/fry/coconut/processing/build/shared/reference.zip");
              Base.openURL("http://127.0.0.1:" + port + "/reference/setup_.html");

            } catch (IOException e1) {
              e1.printStackTrace();
            }
          }
        });
      }
    });
  menu.add(item);
  */

  /*
  item = new JMenuItem("Browser Test");
  item.addActionListener(new ActionListener() {
      public void actionPerformed(ActionEvent e) {
        //Base.openURL("http://processing.org/learning/gettingstarted/");
        //JFrame browserFrame = new JFrame("Browser");
        BrowserStartup bs = new BrowserStartup("jar:file:/Users/fry/coconut/processing/build/shared/reference.zip!/reference/setup_.html");
        bs.initUI();
        bs.launch();
      }
    });
  menu.add(item);
  */


  abstract public JMenu buildHelpMenu();


  public void showReference(String filename) {
    File file = new File(mode.getReferenceFolder(), filename);
    showReferenceFile(file);
  }


  /**
   * Given the .html file, displays it in the default browser.
   *
   * @param file
   */
  public void showReferenceFile(File file) {
    try {
      file = file.getCanonicalFile();
    } catch (IOException e) {
      e.printStackTrace();
    }

    // Prepend with file:// and also encode spaces & other characters
    Base.openURL(file.toURI().toString());
  }


  static public void showChanges() {
    // http://code.google.com/p/processing/issues/detail?id=1520
    if (!Base.isCommandLine()) {
      Base.openURL("http://wiki.processing.org/w/Changes");
    }
  }


  // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

  class CutAction extends AbstractAction {
    public CutAction() {
      super(Language.text("menu.edit.cut"));
      this.setEnabled(false);
    }

    public void actionPerformed(ActionEvent e) {
      handleCut();
    }

    public void updateCutState() {
      cutItems.setEnabled(canCut());
    }

    public boolean canCut() {
      return textarea.isSelectionActive();
    }
  }

  class CopyAction extends AbstractAction {
    public CopyAction() {
      super(Language.text("menu.edit.copy"));
      this.setEnabled(false);
    }

    public void actionPerformed(ActionEvent e) {
      textarea.copy();
    }

    public void updateCopyState() {
      copyItems.setEnabled(canCopy());
    }

    public boolean canCopy() {
      return textarea.isSelectionActive();
    }
  }

  class UndoAction extends AbstractAction {
    public UndoAction() {
      super(Language.text("menu.edit.undo"));
      this.setEnabled(false);
    }

    public void actionPerformed(ActionEvent e) {
      stopCompoundEdit();

      try {
        final Integer caret = caretUndoStack.pop();
        caretRedoStack.push(caret);
        textarea.setCaretPosition(caret);
        textarea.scrollToCaret();
      } catch (Exception ignore) {
      }
      try {
        undo.undo();
      } catch (CannotUndoException ex) {
        //System.out.println("Unable to undo: " + ex);
        //ex.printStackTrace();
      }
      updateUndoState();
      redoAction.updateRedoState();
      if (sketch != null) {
        sketch.setModified(!getText().equals(sketch.getCurrentCode().getSavedProgram()));
      }
    }

    protected void updateUndoState() {
      if (undo.canUndo() || compoundEdit != null && compoundEdit.isInProgress()) {
        this.setEnabled(true);
        undoItem.setEnabled(true);
        String newUndoPresentationName = Language.text("menu.edit.undo");
        if (undo.getUndoPresentationName().equals("Undo addition")) {
          newUndoPresentationName += " "+Language.text("menu.edit.action.addition");
        } else if (undo.getUndoPresentationName().equals("Undo deletion")) {
          newUndoPresentationName += " "+Language.text("menu.edit.action.deletion");
        }
        undoItem.setText(newUndoPresentationName);
        putValue(Action.NAME, newUndoPresentationName);
//        if (sketch != null) {
//          sketch.setModified(true);  // 0107, removed for 0196
//        }
      } else {
        this.setEnabled(false);
        undoItem.setEnabled(false);
        undoItem.setText(Language.text("menu.edit.undo"));
        putValue(Action.NAME, Language.text("menu.edit.undo"));
//        if (sketch != null) {
//          sketch.setModified(false);  // 0107
//        }
      }
    }
  }


  class RedoAction extends AbstractAction {
    public RedoAction() {
      super(Language.text("menu.edit.redo"));
      this.setEnabled(false);
    }

    public void actionPerformed(ActionEvent e) {
      stopCompoundEdit();

      try {
        undo.redo();
      } catch (CannotRedoException ex) {
        //System.out.println("Unable to redo: " + ex);
        //ex.printStackTrace();
      }
      try {
        final Integer caret = caretRedoStack.pop();
        caretUndoStack.push(caret);
        textarea.setCaretPosition(caret);
      } catch (Exception ignore) {
      }
      updateRedoState();
      undoAction.updateUndoState();
      if (sketch != null) {
        sketch.setModified(!getText().equals(sketch.getCurrentCode().getSavedProgram()));
      }
    }

    protected void updateRedoState() {
      if (undo.canRedo()) {
        redoItem.setEnabled(true);
        String newRedoPresentationName = Language.text("menu.edit.redo");
        if (undo.getRedoPresentationName().equals("Redo addition")) {
          newRedoPresentationName += " "+Language.text("menu.edit.action.addition");
        } else if (undo.getRedoPresentationName().equals("Redo deletion")) {
          newRedoPresentationName += " "+Language.text("menu.edit.action.deletion");
        }
        redoItem.setText(newRedoPresentationName);
        putValue(Action.NAME, newRedoPresentationName);
      } else {
        this.setEnabled(false);
        redoItem.setEnabled(false);
        redoItem.setText(Language.text("menu.edit.redo"));
        putValue(Action.NAME, Language.text("menu.edit.redo"));
      }
    }
  }


  // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .


  // these will be done in a more generic way soon, more like:
  // setHandler("action name", Runnable);
  // but for the time being, working out the kinks of how many things to
  // abstract from the editor in this fashion.


//  public void setHandlers(Runnable runHandler, Runnable presentHandler,
//                          Runnable stopHandler,
//                          Runnable exportHandler, Runnable exportAppHandler) {
//    this.runHandler = runHandler;
//    this.presentHandler = presentHandler;
//    this.stopHandler = stopHandler;
//    this.exportHandler = exportHandler;
//    this.exportAppHandler = exportAppHandler;
//  }


//  public void resetHandlers() {
//    runHandler = new DefaultRunHandler();
//    presentHandler = new DefaultPresentHandler();
//    stopHandler = new DefaultStopHandler();
//    exportHandler = new DefaultExportHandler();
//    exportAppHandler = new DefaultExportAppHandler();
//  }


  // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .


  /**
   * Gets the current sketch object.
   */
  public Sketch getSketch() {
    return sketch;
  }


  /**
   * Get the JEditTextArea object for use (not recommended). This should only
   * be used in obscure cases that really need to hack the internals of the
   * JEditTextArea. Most tools should only interface via the get/set functions
   * found in this class. This will maintain compatibility with future releases,
   * which will not use JEditTextArea.
   */
  public JEditTextArea getTextArea() {
    return textarea;
  }


  /**
   * Get the contents of the current buffer. Used by the Sketch class.
   */
  public String getText() {
    return textarea.getText();
  }


  /**
   * Get a range of text from the current buffer.
   */
  public String getText(int start, int stop) {
    return textarea.getText(start, stop - start);
  }


  /**
   * Replace the entire contents of the front-most tab.
   */
  public void setText(String what) {
    startCompoundEdit();
    textarea.setText(what);
    stopCompoundEdit();
  }


  public void insertText(String what) {
    startCompoundEdit();
    int caret = getCaretOffset();
    setSelection(caret, caret);
    textarea.setSelectedText(what);
    stopCompoundEdit();
  }


  /**
   * Called to update the text but not switch to a different set of code
   * (which would affect the undo manager).
   */
//  public void setText2(String what, int start, int stop) {
//    beginCompoundEdit();
//    textarea.setText(what);
//    endCompoundEdit();
//
//    // make sure that a tool isn't asking for a bad location
//    start = Math.max(0, Math.min(start, textarea.getDocumentLength()));
//    stop = Math.max(0, Math.min(start, textarea.getDocumentLength()));
//    textarea.select(start, stop);
//
//    textarea.requestFocus();  // get the caret blinking
//  }


  public String getSelectedText() {
    return textarea.getSelectedText();
  }


  public void setSelectedText(String what) {
    textarea.setSelectedText(what);
  }


  public void setSelection(int start, int stop) {
    // make sure that a tool isn't asking for a bad location
    start = PApplet.constrain(start, 0, textarea.getDocumentLength());
    stop = PApplet.constrain(stop, 0, textarea.getDocumentLength());

    textarea.select(start, stop);
  }


  /**
   * Get the position (character offset) of the caret. With text selected,
   * this will be the last character actually selected, no matter the direction
   * of the selection. That is, if the user clicks and drags to select lines
   * 7 up to 4, then the caret position will be somewhere on line four.
   */
  public int getCaretOffset() {
    return textarea.getCaretPosition();
  }


  /**
   * True if some text is currently selected.
   */
  public boolean isSelectionActive() {
    return textarea.isSelectionActive();
  }


  /**
   * Get the beginning point of the current selection.
   */
  public int getSelectionStart() {
    return textarea.getSelectionStart();
  }


  /**
   * Get the end point of the current selection.
   */
  public int getSelectionStop() {
    return textarea.getSelectionStop();
  }


  /**
   * Get text for a specified line.
   */
  public String getLineText(int line) {
    return textarea.getLineText(line);
  }


  /**
   * Replace the text on a specified line.
   */
  public void setLineText(int line, String what) {
    startCompoundEdit();
    textarea.select(getLineStartOffset(line), getLineStopOffset(line));
    textarea.setSelectedText(what);
    stopCompoundEdit();
  }


  /**
   * Get character offset for the start of a given line of text.
   */
  public int getLineStartOffset(int line) {
    return textarea.getLineStartOffset(line);
  }


  /**
   * Get character offset for end of a given line of text.
   */
  public int getLineStopOffset(int line) {
    return textarea.getLineStopOffset(line);
  }


  /**
   * Get the number of lines in the currently displayed buffer.
   */
  public int getLineCount() {
    return textarea.getLineCount();
  }


  /**
   * Use before a manipulating text to group editing operations together as a
   * single undo. Use stopCompoundEdit() once finished.
   */
  public void startCompoundEdit() {
    stopCompoundEdit();
    compoundEdit = new CompoundEdit();
  }


  /**
   * Use with startCompoundEdit() to group edit operations in a single undo.
   */
  public void stopCompoundEdit() {
    if (compoundEdit != null) {
      compoundEdit.end();
      undo.addEdit(compoundEdit);
      caretUndoStack.push(textarea.getCaretPosition());
      caretRedoStack.clear();
      undoAction.updateUndoState();
      redoAction.updateRedoState();
      compoundEdit = null;
    }
  }


  public int getScrollPosition() {
    return textarea.getVerticalScrollPosition();
  }


  // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .


  /**
   * Switch between tabs, this swaps out the Document object
   * that's currently being manipulated.
   */
  protected void setCode(SketchCode code) {
    SyntaxDocument document = (SyntaxDocument) code.getDocument();

    if (document == null) {  // this document not yet inited
      document = new SyntaxDocument();
      code.setDocument(document);

      // turn on syntax highlighting
      document.setTokenMarker(mode.getTokenMarker());

      // insert the program text into the document object
      try {
        document.insertString(0, code.getProgram(), null);
      } catch (BadLocationException bl) {
        bl.printStackTrace();
      }

      // set up this guy's own undo manager
//      code.undo = new UndoManager();

      document.addDocumentListener(new DocumentListener() {

        public void removeUpdate(DocumentEvent e) {
          if (isInserting && isDirectEdit()) {
            endTextEditHistory();
          }
          isInserting = false;
        }

        public void insertUpdate(DocumentEvent e) {
          if (!isInserting && isDirectEdit()) {
            endTextEditHistory();
          }
          isInserting = true;
        }

        public void changedUpdate(DocumentEvent e) {
          endTextEditHistory();
        }
      });

      // connect the undo listener to the editor
      document.addUndoableEditListener(new UndoableEditListener() {

          public void undoableEditHappened(UndoableEditEvent e) {
            // if an edit is in progress, reset the timer
            if (endUndoEvent != null) {
              endUndoEvent.cancel();
              endUndoEvent = null;
              startTimerEvent();
            }

            // if this edit is just getting started, create a compound edit
            if (compoundEdit == null) {
              startCompoundEdit();
              startTimerEvent();
            }

            compoundEdit.addEdit(e.getEdit());
            undoAction.updateUndoState();
            redoAction.updateRedoState();
          }
        });
    }

    // update the document object that's in use
    textarea.setDocument(document,
                         code.getSelectionStart(), code.getSelectionStop(),
                         code.getScrollPosition());

//    textarea.requestFocus();  // get the caret blinking
    textarea.requestFocusInWindow();  // required for caret blinking

    this.undo = code.getUndo();
    undoAction.updateUndoState();
    redoAction.updateRedoState();
  }

  /**
   * @return true if the text is being edited from direct input from typing and
   *         not shortcuts that manipulate text
   */
  boolean isDirectEdit() {
    return endUndoEvent != null;
  }

  void startTimerEvent() {
    endUndoEvent = new TimerTask() {
      public void run() {
        endTextEditHistory();
      }
    };
    timer.schedule(endUndoEvent, 3000);
    // let the gc eat the cancelled events
    timer.purge();
  }

  void endTextEditHistory() {
    if (endUndoEvent != null) {
      endUndoEvent.cancel();
      endUndoEvent = null;
    }
    stopCompoundEdit();
  }

  // . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .


  /**
   * Implements Edit → Cut.
   */
  public void handleCut() {
    textarea.cut();
    sketch.setModified(true);
  }


  /**
   * Implements Edit → Copy.
   */
  public void handleCopy() {
    textarea.copy();
  }


  public void handleCopyAsHTML() {
    textarea.copyAsHTML();
    statusNotice(Language.text("editor.status.copy_as_html"));
  }


  /**
   * Implements Edit → Paste.
   */
  public void handlePaste() {
    textarea.paste();
    sketch.setModified(true);
  }


  /**
   * Implements Edit → Select All.
   */
  public void handleSelectAll() {
    textarea.selectAll();
  }

//  /**
//   * @param moveUp
//   *          true to swap the selected lines with the line above, false to swap
//   *          with the line beneath
//   */
  /*
  public void handleMoveLines(boolean moveUp) {
    startCompoundEdit();

    int startLine = textarea.getSelectionStartLine();
    int stopLine = textarea.getSelectionStopLine();

    // if more than one line is selected and none of the characters of the end
    // line are selected, don't move that line
    if (startLine != stopLine
        && textarea.getSelectionStop() == textarea.getLineStartOffset(stopLine))
      stopLine--;

    int replacedLine = moveUp ? startLine - 1 : stopLine + 1;
    if (replacedLine < 0 || replacedLine >= textarea.getLineCount())
      return;

    final String source = getText();

    int replaceStart = textarea.getLineStartOffset(replacedLine);
    int replaceEnd = textarea.getLineStopOffset(replacedLine);
    if (replaceEnd == source.length() + 1)
      replaceEnd--;

    int selectionStart = textarea.getLineStartOffset(startLine);
    int selectionEnd = textarea.getLineStopOffset(stopLine);
    if (selectionEnd == source.length() + 1)
      selectionEnd--;

    String replacedText = source.substring(replaceStart, replaceEnd);
    String selectedText = source.substring(selectionStart, selectionEnd);
    if (replacedLine == textarea.getLineCount() - 1) {
      replacedText += "\n";
      selectedText = selectedText.substring(0, selectedText.length() - 1);
    } else if (stopLine == textarea.getLineCount() - 1) {
      selectedText += "\n";
      replacedText = replacedText.substring(0, replacedText.length() - 1);
    }

    int newSelectionStart, newSelectionEnd;
    if (moveUp) {
      // Change the selection, then change the line above
      textarea.select(selectionStart, selectionEnd);
      textarea.setSelectedText(replacedText);

      textarea.select(replaceStart, replaceEnd);
      textarea.setSelectedText(selectedText);

      newSelectionStart = textarea.getLineStartOffset(startLine - 1);
      newSelectionEnd = textarea.getLineStopOffset(stopLine - 1) -  1;
    } else {
      // Change the line beneath, then change the selection
      textarea.select(replaceStart, replaceEnd);
      textarea.setSelectedText(selectedText);

      textarea.select(selectionStart, selectionEnd);
      textarea.setSelectedText(replacedText);

      newSelectionStart = textarea.getLineStartOffset(startLine + 1);
      newSelectionEnd = textarea.getLineStopOffset(stopLine + 1) - 1;
    }

    textarea.select(newSelectionStart, newSelectionEnd);
    stopCompoundEdit();
  }
  */


  /*
  public void handleDeleteLines() {
    int startLine = textarea.getSelectionStartLine();
    int stopLine = textarea.getSelectionStopLine();

    int start = textarea.getLineStartOffset(startLine);
    int end = textarea.getLineStopOffset(stopLine);
    if (end == getText().length() + 1)
      end--;

    textarea.select(start, end);
    textarea.setSelectedText("");
  }
  */


  public void handleAutoFormat() {
    final String source = getText();

    try {
      final String formattedText = createFormatter().format(source);
      // save current (rough) selection point
      int selectionEnd = getSelectionStop();

//      boolean wasVisible =
//        textarea.getSelectionStopLine() >= textarea.getFirstLine() &&
//        textarea.getSelectionStopLine() < textarea.getLastLine();

      // make sure the caret would be past the end of the text
      if (formattedText.length() < selectionEnd - 1) {
        selectionEnd = formattedText.length() - 1;
      }

      if (formattedText.equals(source)) {
        statusNotice(Language.text("editor.status.autoformat.no_changes"));
      } else {
        // replace with new bootiful text
        // selectionEnd hopefully at least in the neighborhood
        int scrollPos = textarea.getVerticalScrollPosition();
        setText(formattedText);
        setSelection(selectionEnd, selectionEnd);

        // Put the scrollbar position back, otherwise it jumps on each format.
        // Since we're not doing a good job of maintaining position anyway,
        // a more complicated workaround here is fairly pointless.
        // http://code.google.com/p/processing/issues/detail?id=1533
        if (scrollPos != textarea.getVerticalScrollPosition()) {
//          boolean wouldBeVisible =
//            scrollPos >= textarea.getFirstLine() &&
//            scrollPos < textarea.getLastLine();
//
//          // if it was visible, and now it's not, then allow the scroll
//          if (!(wasVisible && !wouldBeVisible)) {
          textarea.setVerticalScrollPosition(scrollPos);
//          }
        }
        getSketch().setModified(true);
        // mark as finished
        statusNotice(Language.text("editor.status.autoformat.finished"));
      }

    } catch (final Exception e) {
      statusError(e);
    }
  }


  abstract public String getCommentPrefix();


  protected void handleCommentUncomment() {
    // log("Entering handleCommentUncomment()");
    startCompoundEdit();

    String prefix = getCommentPrefix();
    int prefixLen = prefix.length();

    int startLine = textarea.getSelectionStartLine();
    int stopLine = textarea.getSelectionStopLine();

    int lastLineStart = textarea.getLineStartOffset(stopLine);
    int selectionStop = textarea.getSelectionStop();
    // If the selection ends at the beginning of the last line,
    // then don't (un)comment that line.
    if (selectionStop == lastLineStart) {
      // Though if there's no selection, don't do that
      if (textarea.isSelectionActive()) {
        stopLine--;
      }
    }

    // If the text is empty, ignore the user.
    // Also ensure that all lines are commented (not just the first)
    // when determining whether to comment or uncomment.
    boolean commented = true;
    for (int i = startLine; commented && (i 

Web Proxy Viewer  |  New URL  |  Original Page