Alternate Processing Editor Using processing-java.exe on the Command line

Intro: The demo below will create an entry level code editor for running Processing source code from the command line using the processing-java.exe file. It was developed and tested mainly on MacOS(M2); I was unable to get it to run on other platforms due to difficulty finding either processing-java or processing-cli for the other operating systems. There are five Tabs with additional code which are also shown below. This project was undertaken for proof of concept and for those users who, for one reason or another, do not want to use the official Processing IDE. My presumption is that processing-java.exe from the command line will function even if the official Processing IDE is not on the user’s system; this may or may not be correct (I have no way to test since I don’t want to give up my official Processing IDE and do not have a new computer to experiment with). The exported app is smaller in size than the official Processing IDE (4.4.10) at 254.3 MB (including embedded Java) compared to 660.6 MB for the official editor. Without Java embedded this app’s size falls to 13.8 MB and still runs on my system.

Dependency: The editor uses RSyntaxTextArea for syntax highlighting which may be downloaded here: https://sourceforge.net/projects/rsyntaxtextarea/files%2Frsyntaxtextarea%2F1.5.1%2Frsyntaxtextarea_1.5.1.zip/download . To install on your system, first create a folder in your Processing/libraries folder entitled ‘RSyntaxtTextArea’ and inside of that create another folder entitled ‘library’. Place the downloaded 'rsyntaxtextarea.jar’ file in the ‘library’ folder and then you should be able to use syntax highlighting. If you want to use code folding you will additionally need ‘rsyntaxtextarea-3.6.0.jar’ downloaded to your Processing/libraries/RSyntaxTextArea/library folder, available at: https://repo1.maven.org/maven2/com/fifesoft/rsyntaxtextarea/3.6.0/ . You can get by with only the earlier version if you don’t care about code folding. Source code line number 412 is REMmed out to avoid an error message from not having the latter jar file available. If you choose to use code folding (after downloading and installing rsyntaxtextarea-3.6.0.jar) then just unREM that line of code (the editor was initially setup to include code folding).

Setup: 1) The first thing to do after installing RSyntaxTextArea is set the path to the processing-java.exe file. I found its location on my system by entering 'whereis processing-java’ on the Terminal command line. It was at /usr/local/bin; I presume that is where the Processing IDE installed it after selecting 'Tools/Install “processing-java” on the menubar. There may be other ways to obtain this .exe file; I didn’t have much luck running another copy from my home directory.

  1. The second thing to do is set the folder which you want JFileChooser to open each time you select File/Open. This is handy to keep from having to migrate to the desired folder each time you want to open another file; the chooser will automatically take you to that folder once set. Both settings are stored in a separate file in the sketch folder.

Design Features: 1) Each time you open a new file a new tab is created using the file’s name, and a new instance of RSyntaxTextArea is embedded into the JTabbedPane. There is a menubar item to remove unwanted tabs (except for the first one which may not be removed). This makes it possible to copy/paste code from one tab to another.

  1. Note that the folder path is used in the –sketch= string (not the full path of the .pde file).

  2. On MacOS the menubar is placed at the top of the screen after the project is exported.

  3. This demo editor uses only the –run option in ProcessBuilder but others, including –export, may also be used but were not fully tested.

  4. There is no sketch ‘Stop’ button on this app. To stop a running sketch simply hit the sketch’s window close button or hit cmd-Q with the sketch in focus.

Command line edition for Processing 4.4.10 (Java Mode):

--help               Show this help text. Congratulations.

--sketch=<name>      Specify the sketch folder (required)

--output=<name>      Specify the output folder (optional and

                     cannot be the same as the sketch folder.)

--force              The sketch will not build if the output

                     folder already exists, because the contents

                     will be replaced. This option erases the

                     folder first. Use with extreme caution!

--build              Preprocess and compile a sketch into .class files.

--run                Preprocess, compile, and run a sketch.

--present            Preprocess, compile, and run a sketch in presentation mode.

--export             Export an application.

--variant            Specify the platform and architecture (Export only).

--no-java            Do not embed Java.

Starting with 4.0, the --platform option has been removed because of the variety of platforms and architectures now available.

Use the --variant option instead, for instance:

variant        platform

-------------  ---------------------------

macos-x86_64   macOS (Intel 64-bit)

macos-aarch64  macOS (Apple Silicon)

windows-amd64  Windows (Intel 64-bit)

linux-amd64    Linux (Intel 64-bit)

linux-arm      Linux (Raspberry Pi 32-bit)

linux-aarch64  Linux (Raspberry Pi 64-bit)


The --build, --run, --present, or --export must be the final parameter

passed to Processing. Arguments passed following one of those four will

be passed through to the sketch itself, and therefore available to the

sketch via the 'args' field. To pass options understood by PApplet.main(),

write a custom main() method so that the preprocessor does not add one.


Source code:

import java.awt.*;
import java.awt.event.*;
import java.awt.print.*;
import javax.swing.*;
import javax.swing.event.*;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.io.*;
import java.io.File;
import java.nio.file.Path;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import javax.swing.text.BadLocationException;
import static java.awt.event.InputEvent.META_DOWN_MASK;

import org.fife.ui.rsyntaxtextarea.*;
import org.fife.ui.rtextarea.*;

import java.awt.event.MouseEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseListener;

import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;

import java.util.ArrayList;
import java.util.List;

ArrayList<String> files = new ArrayList<String>();
ArrayList<RSyntaxTextArea> txtAreas = new ArrayList<RSyntaxTextArea>();

JFrame frame;
JScrollPane txtScrlPane;
RSyntaxTextArea txtArea;
JTextArea logArea;
JTabbedPane tabbedPane;
JSplitPane splitPane;
JScrollPane logScrlPane;
JSpinner fontSize;
JTextField findField;
JTextField replaceField;
JLabel folderLabel;
JLabel cmdStrLabel;

final int _wndW = 750;
final int _wndH = 700;

String codeDir = "";
String cmdStrExe = "";

Process process = null;
File file;

void newFileAction() {
  String folderPath = "";
  String folderName = "";
  String fileName = "";
  String newFilePath = "";
  
  JFileChooser fileChooser = new JFileChooser(codeDir);
  fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
  fileChooser.setDialogTitle("Create sketch folder as:");
  int result = fileChooser.showSaveDialog(frame);
  if (result == JFileChooser.APPROVE_OPTION) {
    File newDir = fileChooser.getSelectedFile();
    folderName = newDir.getName();
    fileName = folderName + ".pde";
    boolean created = newDir.mkdir();
    logArea.append("Dir created:" + created + "\n");   
    Path folder = newDir.toPath();
    folderPath = folder.toString();
    newFilePath = folderPath + "/" + fileName;
    frame.setTitle(fileName);
  } else {
    logArea.append("New file cancelled.");
  }
  txtArea();
  tabbedPane.addTab(fileName, txtScrlPane);
  int tabIndex = tabbedPane.indexOfTab(fileName);
  tabbedPane.setSelectedIndex(tabIndex);
  files.add(tabIndex, newFilePath);
  txtAreas.add(tabIndex, txtArea);  
}

String fileExtension(String fileName) {
  int lastDotIndex = fileName.lastIndexOf('.');
  if (lastDotIndex == -1 || lastDotIndex == 0) {
    return ""; // No extension
  }
  return fileName.substring(lastDotIndex + 1);
}

void openAction() {
  JFileChooser fileChooser = new JFileChooser(codeDir);
  int i = fileChooser.showOpenDialog(fileChooser);
  if (i == JFileChooser.APPROVE_OPTION) {
    file = fileChooser.getSelectedFile();
    String filePath = file.getPath();
    if (!fileExtension(filePath).equals("pde")) {
      Object[] options = {"CANCEL"};
      JOptionPane.showOptionDialog(frame, "Please make another selection. \n Editor requires .py extension.", "Extension Not Supported.",
        JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]);
      return;
    }

    frame.setTitle(filePath);
    txtArea();
    tabbedPane.addTab(file.getName(), txtScrlPane);
    int tabIndex = tabbedPane.indexOfTab(file.getName());
    tabbedPane.setSelectedIndex(tabIndex);
    files.add(tabIndex, filePath);
    txtAreas.add(tabIndex, txtArea);
    try {
      BufferedReader buffer = new BufferedReader(new FileReader(filePath));
      String s = "", s1 = "";
      while ((s = buffer.readLine())!= null) {
        s1 += s + "\n";
      }
      txtArea.setText(s1);
      buffer.close();
    }
    catch (Exception ex) {
      logArea.append(ex + "\n");
    }
  } else {
    logArea.append("Open cancelled.");
  }
}

void saveAction() {
  String saveFilePath = "";
  int selectedIndex = tabbedPane.getSelectedIndex();
  for (int i = 0; i < files.size(); i++) {
    String path = files.get(i);
    if (i == selectedIndex) {
      saveFilePath = path;
    }
  }
  for (int i = 0; i < txtAreas.size(); i++) {
    RSyntaxTextArea textArea = txtAreas.get(i);
    if (i == selectedIndex) {
      txtArea = textArea;
    }
  }
  if (saveFilePath != null) {
    try {
      String content = txtArea.getText();
      FileWriter fw = new FileWriter(saveFilePath) ;
      BufferedWriter bw = new BufferedWriter(fw);
      bw.write(content);
      bw.close();
    }
    catch (IOException e) {
      logArea.append(e + "\n");
    }
  } else {
    saveAsAction();
  }
}

void saveAsAction() {
  String folderPath = "";
  String folderName = "";
  String fileName = "";
  String saveAsFilePath = "";

  JFileChooser fileChooser = new JFileChooser(codeDir);
  fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
  fileChooser.setDialogTitle("Save sketch folder as:");
  int result = fileChooser.showSaveDialog(frame);
  if (result == JFileChooser.APPROVE_OPTION) {
    File newDir = fileChooser.getSelectedFile();
    folderName = newDir.getName();
    fileName = folderName + ".pde";
    boolean created = newDir.mkdir();
    logArea.append("Dir created:" + created + "\n");
    Path folder = newDir.toPath();
    folderPath = folder.toString();
    saveAsFilePath = folderPath + "/" + fileName;
    frame.setTitle(fileName);
    try {
      // Make certain we have the selected text area
      int selectedIndex = tabbedPane.getSelectedIndex();
      for (int i = 0; i < txtAreas.size(); i++) {
        RSyntaxTextArea textArea = txtAreas.get(i);
        if (i == selectedIndex) {
          txtArea = textArea;
        }
      }
      String content = txtArea.getText();
      txtArea(); // create a new textArea
      tabbedPane.addTab(fileName, txtScrlPane);
      int tabIndex = tabbedPane.indexOfTab(fileName);
      tabbedPane.setSelectedIndex(tabIndex);

      txtAreas.add(tabIndex, txtArea); // Add to ArrayList
      files.add(tabIndex, saveAsFilePath); //Add to ArrayList

      FileWriter fw = new FileWriter(saveAsFilePath) ;
      BufferedWriter bw = new BufferedWriter(fw);
      bw.write(content);
      bw.close();
      // Save it and then read it back into tab
      BufferedReader buffer = new BufferedReader(new FileReader(saveAsFilePath));
      String s = "", s1 = "";
      while ((s = buffer.readLine())!= null) {
        s1 += s + "\n";
      }
      txtArea.setText(s1);
      buffer.close();
    }
    catch (IOException e) {
      logArea.append("Error:" + e + "\n");
    }
  } else {
    logArea.append("No directory selected.");
  }
}

void printerAction() {
  PrinterJob job = PrinterJob.getPrinterJob();
  PageFormat orient = job.defaultPage();
  orient.setOrientation(PageFormat.PORTRAIT);
  job.setPrintable(new MyPrinter(), job.defaultPage());
  if (job.printDialog()) {
    try {
      job.print();
    }
    catch (Exception e) {
      println("Printer error: ",e);
    }
  }
}

void findAction() {
  findDialog = new FindDialog();
}

void runAction() {
  String runFilePath = "";
  String folderPath = "";
  int selectedIndex = tabbedPane.getSelectedIndex();
  for (int i = 0; i < files.size(); i++) {
    String path = files.get(i);
    if (i == selectedIndex) {
      runFilePath = path;
     // logArea.append("runAction : runFilePath = " + runFilePath + "\n");
    }
  }
  int lastSlashIndex = runFilePath.lastIndexOf('/');
  if (lastSlashIndex != -1) {
    folderPath = runFilePath.substring(0, lastSlashIndex);
   // logArea.append("runAction : folderPath = " + folderPath + "\n");
  } else {
    logArea.append("No path separator found.");
  }

  String sketchStr = "--sketch=" + folderPath;

  ProcessBuilder processBuilder = new ProcessBuilder(cmdStrExe, sketchStr, " --run");
  try {
    process = processBuilder.start();
    BufferedReader stdIn = new BufferedReader(new InputStreamReader(process.getInputStream()));
    BufferedReader stdErr = new BufferedReader(new InputStreamReader(process.getErrorStream()));
    String outStr = "";
    while ((outStr = stdIn.readLine()) != null) {
      logArea.append(outStr + "\n");
    }
    String errStr = "";
    while ((errStr = stdErr.readLine()) != null) {
      logArea.append(errStr + "\n");
    }
    // Wait for the process to complete and get its exit value
    int exitCode = process.waitFor();
    logArea.append("\nProcess exited with code: " + exitCode + "\n");
  }
  catch (IOException | InterruptedException e) {
    logArea.append(e + "\n");
  }
}

void clearLogAction() {
  logArea.setText("");
}

void runBtnAction() {
  thread("runAction");
}

void removeTabProcedure() {
  int selectedIndex = tabbedPane.getSelectedIndex();
  if (selectedIndex > 0) {
    tabbedPane.removeTabAt(selectedIndex);
    files.remove(selectedIndex); // Also remove selectedIndex from ArrayLists
    txtAreas.remove(selectedIndex);
  } else {
    JOptionPane.showMessageDialog(frame, "Unable to remove baseline tab.");
  }
}

void quitBtnAction() {
  exit();
}

void openExecutableChooser() {
  JFileChooser fileChooser = new JFileChooser();
  int result = fileChooser.showOpenDialog(frame); // Or showSaveDialog(frame)
  if (result == JFileChooser.APPROVE_OPTION) {
    File selectedDirectory = fileChooser.getSelectedFile();
    cmdStrExe = selectedDirectory.getAbsolutePath();
    cmdStrLabel.setText("cmdStr:"+cmdStrExe);
    String[] settingsStr = {cmdStrExe, codeDir};
    saveStrings(sketchPath()+"/settings.txt", settingsStr);
  } else {
    cmdStrLabel.setText("No file selected.");
  }
}

void openFolderChooser() {
  JFileChooser fileChooser = new JFileChooser();
  fileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); // Set to select directories only
  int result = fileChooser.showOpenDialog(frame); // Or showSaveDialog(frame)
  if (result == JFileChooser.APPROVE_OPTION) {
    File selectedDirectory = fileChooser.getSelectedFile();
    codeDir = selectedDirectory.getAbsolutePath();
    folderLabel.setText("fldr:"+codeDir);
    String[] settingsStr = {cmdStrExe, codeDir};
    saveStrings(sketchPath()+"/settings.txt", settingsStr);
  } else {
    folderLabel.setText("No directory selected.");
  }
}

void runBtn(int x, int y, int w, int h) {
  runBtn = new RunButton();
  runBtn.setBounds(x, y, w, h);
  runBtn.setToolTipText("Run code.");
  runBtn.addMouseListener(new MouseAdapter() {
    public void mousePressed(MouseEvent me) {
      runBtnAction();
    }
  }
  );
  frame.add(runBtn);
  runBtn.repaint();
}

void quitBtn(int x, int y, int w, int h) {
  quitBtn = new QuitButton();
  quitBtn.setBounds(x, y, w, h);
  quitBtn.setToolTipText("Quit editor.");
  quitBtn.addMouseListener(new MouseAdapter() {
    public void mousePressed(MouseEvent me) {
      quitBtnAction();
    }
  }
  );
  frame.add(quitBtn);
  quitBtn.repaint();
}

void fontSizeLabel(int x, int y, int w, int h) {
  JLabel fontSizeLabel = new JLabel("Font size:");
  fontSizeLabel.setHorizontalAlignment(JLabel.LEFT);
  fontSizeLabel.setBounds(x, y, w, h);
  frame.add(fontSizeLabel);
}

void cmdStrLabel(int x, int y, int w, int h) {
  cmdStrLabel = new JLabel("cmdStr:"+cmdStrExe);
  cmdStrLabel.setHorizontalAlignment(JLabel.LEFT);
  cmdStrLabel.setForeground(Color.BLUE);
  Font myFont = new Font("Menlo", Font.ITALIC, 12);
  cmdStrLabel.setFont(myFont);
  cmdStrLabel.setBounds(x, y, w, h);
  frame.add(cmdStrLabel);
}

void folderLabel(int x, int y, int w, int h) {
  folderLabel = new JLabel("fldr:"+codeDir);
  folderLabel.setHorizontalAlignment(JLabel.LEFT);
  folderLabel.setForeground(Color.RED);
  Font myFont = new Font("Menlo", Font.ITALIC, 12);
  folderLabel.setFont(myFont);
  folderLabel.setBounds(x, y, w, h);
  frame.add(folderLabel);
}

void fontSizeSpinner(int x, int y, int w, int h) {
  //syntax: (init, min, max, step)
  SpinnerModel value = new SpinnerNumberModel(13, 8, 30, 1);
  fontSize = new JSpinner(value);
  fontSize.setBounds(x, y, w, h);
  fontSize.setToolTipText("<html>For current and <br>successive files.</html>");
  ((JSpinner.DefaultEditor) fontSize.getEditor()).getTextField().setEditable(false);
  fontSize.addChangeListener(e -> {
    int fntSizeValue = (Integer)fontSize.getValue();
    Font currentFont = txtArea.getFont();
    Font newFont = currentFont.deriveFont((float)fntSizeValue);
    txtArea.setFont(newFont);
  }
  );
  frame.add(fontSize);
}

void txtArea() {
  txtArea = new RSyntaxTextArea();
  txtArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA);
  //  txtArea.setCodeFoldingEnabled(true);
  txtArea.setBracketMatchingEnabled(true);
  txtArea.setFont(new Font("Menlo", Font.PLAIN, (Integer)fontSize.getValue()));
  txtScrlPane = new RTextScrollPane(txtArea);
  txtArea.setEditable(true);
  txtArea.setLineWrap(false);
  txtArea.setWrapStyleWord(true);
  txtArea.repaint();
}

void logArea() {
  logArea = new JTextArea();
  logScrlPane = new JScrollPane(logArea);
  logArea.setEditable(true);
  logArea.setFont(new Font("Menlo", Font.PLAIN, 12));
  logArea.setLineWrap(false);
  logArea.setWrapStyleWord(true);
  logArea.repaint();
}

void tabbedPane() {
  tabbedPane = new JTabbedPane();
  tabbedPane.addChangeListener(e -> {
    int index = tabbedPane.getSelectedIndex();
    String selectedFileStr = tabbedPane.getTitleAt(index);
    frame.setTitle(selectedFileStr);
  }
  );
}

//Create a split pane with two scroll panes in it.
void splitPane() {
  splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, tabbedPane, logScrlPane);
  splitPane.setBounds(0, 50, _wndW, _wndH - 80);
  splitPane.setOneTouchExpandable(true);
  frame.add(splitPane);
  splitPane.setDividerLocation(_wndH - 210);
  splitPane.repaint();
  //Provide minimum sizes for the two components in the split pane
  Dimension minimumSize = new Dimension(_wndW, 50);
  txtScrlPane.setMinimumSize(minimumSize);
  logScrlPane.setMinimumSize(minimumSize);
}

void checkForSettings() {
  String filePath = sketchPath()+"/settings.txt";
  File file = new File(filePath);
  if (file.exists()) {
    String[] s = loadStrings(sketchPath()+"/settings.txt");
    for (int i = 0; i < s.length; i++) {
      if (i == 0) {
        cmdStrExe = s[0];
      }
      if (i == 1) {
        codeDir = s[1];
      }
    }
  } else {
    println("The file does not exist but will be created.");
    String[] settingsStr = {"null", "null"};
    saveStrings(sketchPath()+"/settings.txt", settingsStr);
  }
}

void buildWnd() {
  checkForSettings();
  menu = new Menu();
  fontSizeSpinner(90, 20, 50, 24);
  tabbedPane();
  txtArea(); // RSyntaxTextArea
  logArea();
  splitPane();
  runBtn(30, 5, 34, 34);
  fontSizeLabel(85, 0, 75, 24);
  cmdStrLabel(170, 0, 510, 24);
  folderLabel(170, 20, 510, 24);
  quitBtn(_wndW - 50, 5, 34, 34);

  frame.setVisible(true);
}

void setup() {
  surface.setVisible(false);
  frame = new JFrame();
  frame.setBounds(100, 100, _wndW, _wndH);
  frame.setTitle("Processing Editor");
  frame.setLayout(null);
  frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
  frame.addComponentListener(new java.awt.event.ComponentAdapter() {
    void componentResized(ComponentEvent e) {
      JFrame tmp = (JFrame)e.getSource(); // For window resizing
      splitPane.setBounds(0, 50, tmp.getWidth(), tmp.getHeight() - 80);
    }
  }
  );
  SwingUtilities.invokeLater(() -> {
    buildWnd(); // Build components on EventDispatchThread(EDT).
  }
  );
}

“MenuBar” Tab:

class Menu implements ActionListener {
  
  JMenu fileMenu, editMenu, cmdMenu, settingsMenu, clearMenu, tabMenu;
  JMenuItem newMenuItem,openMenuItem, saveMenuItem, saveAsMenuItem, printMenuItem;
  JMenuItem cut, copy, paste, selectAll, findMenuItem, runPython;
  JMenuItem folderMenuItem, cmdStrMenuItem, clearLog, removeTabMenuItem;

  JMenuBar menuBar = new JMenuBar();

  Menu() {
    fileMenu = new JMenu("File");
    newMenuItem = new JMenuItem("New");
    openMenuItem = new JMenuItem("Open...");
    openMenuItem.setAccelerator(KeyStroke.getKeyStroke('O', META_DOWN_MASK));
    saveMenuItem = new JMenuItem("Save");
    saveMenuItem.setAccelerator(KeyStroke.getKeyStroke('S', META_DOWN_MASK));
    saveAsMenuItem = new JMenuItem("SaveAs...");
    printMenuItem = new JMenuItem("Print...");
    printMenuItem.setAccelerator(KeyStroke.getKeyStroke('P', META_DOWN_MASK));    
    
    editMenu = new JMenu("Edit");
    cut = new JMenuItem("Cut");
    cut.setAccelerator(KeyStroke.getKeyStroke('X', META_DOWN_MASK));
    copy = new JMenuItem("Copy");
    copy.setAccelerator(KeyStroke.getKeyStroke('C', META_DOWN_MASK));
    paste = new JMenuItem("Paste");
    paste.setAccelerator(KeyStroke.getKeyStroke('V', META_DOWN_MASK));
    selectAll = new JMenuItem("SelectAll");
    selectAll.setAccelerator(KeyStroke.getKeyStroke('A', META_DOWN_MASK));
    findMenuItem = new JMenuItem("Find");
    findMenuItem.setAccelerator(KeyStroke.getKeyStroke('F', META_DOWN_MASK));
     
    cmdMenu = new JMenu("Cmd");
    runPython = new JMenuItem("Run");
    runPython.setAccelerator(KeyStroke.getKeyStroke('R', META_DOWN_MASK));
    
    clearMenu = new JMenu("Clear");
    clearLog = new JMenuItem("Log");
    
    settingsMenu = new JMenu("Settings");
    cmdStrMenuItem = new JMenuItem("CmdStr exe...");
    folderMenuItem = new JMenuItem("Code Folder...");
    
    tabMenu = new JMenu("Tab");
    removeTabMenuItem = new JMenuItem("Remove Selected Tab");
        
    fileMenu.add(newMenuItem);
    fileMenu.add(openMenuItem);
    fileMenu.add(saveMenuItem);
    fileMenu.add(saveAsMenuItem);
    fileMenu.addSeparator();
    fileMenu.add(printMenuItem);
    
    editMenu.add(cut);
    editMenu.add(copy);
    editMenu.add(paste);
    editMenu.add(selectAll);
    editMenu.addSeparator();
    editMenu.add(findMenuItem);
    
    cmdMenu.add(runPython);

    clearMenu.add(clearLog);
    
    settingsMenu.add(cmdStrMenuItem);
    settingsMenu.add(folderMenuItem);
    
    tabMenu.add(removeTabMenuItem);
        
    menuBar.add(fileMenu);
    menuBar.add(editMenu);
    menuBar.add(cmdMenu);
    menuBar.add(clearMenu);
    menuBar.add(settingsMenu);
    menuBar.add(tabMenu);
     
    newMenuItem.addActionListener(this);
    openMenuItem.addActionListener(this);
    saveMenuItem.addActionListener(this);
    saveAsMenuItem.addActionListener(this);
    printMenuItem.addActionListener(this);
        
    cut.addActionListener(this);
    copy.addActionListener(this);
    paste.addActionListener(this);
    selectAll.addActionListener(this);
    findMenuItem.addActionListener(this);
    
    runPython.addActionListener(this);
    
    clearLog.addActionListener(this);
    
    cmdStrMenuItem.addActionListener(this);
    folderMenuItem.addActionListener(this);
    removeTabMenuItem.addActionListener(this);
    
    frame.setJMenuBar(menuBar);   
  }
  
  public void actionPerformed(ActionEvent evnt) {
    if (evnt.getSource() == newMenuItem)
      newFileAction();
    if (evnt.getSource() == openMenuItem)
      openAction();
    if (evnt.getSource() == saveMenuItem)
      saveAction();
    if (evnt.getSource() == saveAsMenuItem)
      saveAsAction();
    if (evnt.getSource() == printMenuItem)
      printerAction();    
      
    if (evnt.getSource() == cut)
      txtArea.cut();
    if (evnt.getSource() == paste)
      txtArea.paste();
    if (evnt.getSource() == copy)
      txtArea.copy();
    if (evnt.getSource() == selectAll)
      txtArea.selectAll();
     if (evnt.getSource() == findMenuItem)
      findAction(); 
      
    if (evnt.getSource() == runPython)
      runBtnAction();

    if (evnt.getSource() == clearLog)
      clearLogAction();  
      
    if (evnt.getSource() == cmdStrMenuItem)
      openExecutableChooser();
    if (evnt.getSource() == folderMenuItem)
      openFolderChooser();
      
    if (evnt.getSource() == removeTabMenuItem)
      removeTabProcedure();  
  }

}
Menu menu;

“Find” Tab:

class FindDialog {
  JDialog dlg;
  JTextField findFld;
  JTextField replaceFld;

  void cancelBtnAction() {
    dlg.setVisible(false);
  }

  void replaceBtnAction() {
    // Make certain we have the selected text area
    int selectedIndex = tabbedPane.getSelectedIndex();
    for (int i = 0; i < txtAreas.size(); i++) {
      RSyntaxTextArea textArea = txtAreas.get(i);
      if (i == selectedIndex) {
        txtArea = textArea;
      }
    }
    String textToFind = findFld.getText();
    String textToReplace = replaceFld.getText();
    String currentText = txtArea.getText();
    String newText = currentText.replaceAll(textToFind, textToReplace);
    txtArea.setText(newText);
  }

  FindDialog() {
    dlg = new JDialog(frame, "Find:");
    dlg.setLayout(null);
    dlg.setBounds(50, 50, 400, 200);
    JLabel findLabel = new JLabel("Find:");
    findLabel.setBounds(20, 20, 40, 24);
    dlg.add(findLabel);
    findFld = new JTextField();
    findFld.setBounds(60, 20, 230, 24);
    dlg.add(findFld);
    JLabel replaceLabel = new JLabel("Replace:");
    replaceLabel.setBounds(20, 60, 65, 24);
    dlg.add(replaceLabel);
    replaceFld = new JTextField();
    replaceFld.setBounds(85, 60, 240, 24);
    dlg.add(replaceFld);

    JButton replaceBtn = new JButton("Replace All");
    replaceBtn.setBounds(30, 120, 130, 24);
    dlg.add(replaceBtn);
    replaceBtn.addActionListener(e -> {
      replaceBtnAction();
    }
    );

    JButton cancelBtn = new JButton("Cancel");
    cancelBtn.setBounds(180, 120, 130, 24);
    dlg.add(cancelBtn);
    cancelBtn.addActionListener(e -> {
      cancelBtnAction();
    }
    );
    dlg.setVisible(true);
  }
}

FindDialog findDialog;

“Printer” Tab:

class MyPrinter implements Printable {
  
  String filePath;
  int[] pageBreaks;  // array of page break line positions.
  ArrayList<String> txtLines = new ArrayList<String>();

  void initTextLines() {
    int selectedIndex = tabbedPane.getSelectedIndex();
    for (int i = 0; i < files.size(); i++) {
      String path = files.get(i);
      if (i == selectedIndex) {
        filePath = path;
      }
    }

    try {
      BufferedReader buffer = new BufferedReader(new FileReader(filePath));
      String s1 = "", s2 = "";
      while ((s1 = buffer.readLine())!= null) {
        s2 = s1 + "\n";
        txtLines.add(s2); // add to ArrayList
      }
      buffer.close();
    }
    catch (Exception ex) {
      ex.printStackTrace();
    }
  }

  public int print(Graphics g, PageFormat pf, int pageIndex) throws PrinterException {

    Font font = new Font("Menlo", Font.PLAIN, 12);
    FontMetrics metrics = g.getFontMetrics(font);
    int lineHeight = metrics.getHeight();

    if (pageBreaks == null) {
      initTextLines();
      int linesPerPage = 44; // Hard coded to make it fit on page better.
      //  int linesPerPage = (int)(pf.getImageableHeight()/lineHeight);
      int numBreaks = (txtLines.size()-1)/linesPerPage;
      pageBreaks = new int[numBreaks];
      for (int b=0; b<numBreaks; b++) {
        pageBreaks[b] = (b+1)*linesPerPage;
      }
    }

    if (pageIndex > pageBreaks.length) {
      return NO_SUCH_PAGE;
    }

    // User (0,0) is typically outside the imageable area.
    // Translate by the X and Y values in the PageFormat to avoid clipping
     
    Graphics2D g2d = (Graphics2D)g;
    g2d.translate(pf.getImageableX(), pf.getImageableY());
    
     // Increment 'y' position by lineHeight for each line.     
    int x = 40; // left margin
    int y = 40; // top Margin
    int start = (pageIndex == 0) ? 0 : pageBreaks[pageIndex-1];
    int end   = (pageIndex == pageBreaks.length)
      ? txtLines.size() : pageBreaks[pageIndex];
    for (int line=start; line<end; line++) {
      y += lineHeight;
      g.drawString(txtLines.get(line), x, y);
    }

    // Tell caller that this page is part of the printed document
    return PAGE_EXISTS;
  }
}

“Quit” Tab:

class QuitButton extends JPanel {
  
  int radius = 13; // Size of Btn

  QuitButton() {
    setOpaque(false); 
  }

  void paintComponent(Graphics g) {
    Graphics2D g2 = (Graphics2D)g;
    super.paintComponent(g);
    BasicStroke bs = new BasicStroke(6.0);
    Font myFont = new Font("Menlo",Font.BOLD,22);
    g2.setStroke(bs);
    g2.setColor(Color.lightGray);
    g2.drawOval(getWidth()/2 - radius, getHeight()/2 - radius, radius * 2, radius * 2);
//    g2.setColor(new Color(209, 209, 209));
    g2.setColor(new Color(66,66,66));
    g2.fillOval(getWidth()/2 - radius, getHeight()/2 - radius, radius * 2, radius * 2);
    g2.setColor(Color.white);
    g2.setFont(myFont);
    g2.drawString("Q",11,25);
  }
}

QuitButton quitBtn;

“Run” Tab:

class RunButton extends JPanel {
  
  int radius = 13; // Size of Btn
  
  // Coordinates for triangle
  int x[] = {16, 16, 18};
  int y[] = {16, 18, 17};

  RunButton() {
    setOpaque(false);   
  }

  void paintComponent(Graphics g) {
    Graphics2D g2 = (Graphics2D)g;
    super.paintComponent(g);
    BasicStroke bs = new BasicStroke(6.0);
    g2.setStroke(bs);
    g2.setColor (Color.lightGray);
    g2.drawOval(getWidth()/2 - radius, getHeight()/2 - radius, radius * 2, radius * 2);
    g2.setColor (new Color(24, 70, 183));
    g2.fillOval(getWidth()/2 - radius, getHeight()/2 - radius, radius * 2, radius * 2);
    g2.setColor(Color.white);
    g2.drawPolygon(x, y, 3);
  }
}

RunButton runBtn;

processing-java.exe (MacOS)

Output:

1 Like

Hello @svan,

Cool beans!

I managed to get this to run with Windows with some necessary modifications:

Changes made here:

void runAction()
void runAction() {
  String runFilePath = "";
  String folderPath = "";
  int selectedIndex = tabbedPane.getSelectedIndex();
  for (int i = 0; i < files.size(); i++) {
    String path = files.get(i);
    if (i == selectedIndex) {
      runFilePath = path;
      println(path);
     // logArea.append("runAction : runFilePath = " + runFilePath + "\n");
    }
  }
  int lastSlashIndex = runFilePath.lastIndexOf(File.separator);
  if (lastSlashIndex != -1) {
    folderPath = runFilePath.substring(0, lastSlashIndex);
   // logArea.append("runAction : folderPath = " + folderPath + "\n");
  } else {
    logArea.append("No path separator found.");
  }

if (lastSlashIndex != -1) {
    folderPath = runFilePath.substring(0, lastSlashIndex);
} else {
    folderPath = System.getProperty("user.dir"); // Default to current directory
    println(folderPath);
    logArea.append("No path separator found. Using current directory: " + folderPath + "\n");
}
  println(codeDir);
  String sketchStr = "--sketch=" + codeDir;
  
  logArea.append("Generated command: " + cmdStrExe + " cli " + sketchStr + " --run\n");

  ProcessBuilder processBuilder = new ProcessBuilder(cmdStrExe, "cli", sketchStr, "--run");
  
  try {
    process = processBuilder.start();
    BufferedReader stdIn = new BufferedReader(new InputStreamReader(process.getInputStream()));
    BufferedReader stdErr = new BufferedReader(new InputStreamReader(process.getErrorStream()));
    String outStr = "";
    while ((outStr = stdIn.readLine()) != null) {
      logArea.append(outStr + "\n");
    println(outStr);  
  }
    String errStr = "";
    while ((errStr = stdErr.readLine()) != null) {
      logArea.append(errStr + "\n");
    }
    // Wait for the process to complete and get its exit value
    int exitCode = process.waitFor();
    logArea.append("\nProcess exited with code: " + exitCode + "\n");
  }
  catch (IOException | InterruptedException e) {
    logArea.append(e + "\n");
  }
}

This was a quick challenge. I did not comment changes. You can do a file compare. :)

The Processing PDE files are required.

Processing 4.3.4 is the last one to use processing-java.exe for Windows and works with that version. It did not work with Processing 4.4.10 or standalone.

This does not exist as a file:

processing-cli

The correct usage of the cli command is in example I provided.

Windows Terminal (command prompt) output
C:\Users\GLV>processing --help | more
Usage: processing [<options>] [<sketches>]... <command> [<args>]...

  Start the Processing IDE

Options:
  -v, --version  Print version information
  -h, --help     Show this message and exit

Arguments:
  <sketches>  Sketches to open

Commands:
  lsp            Start the Processing Language Server
  cli
  contributions  Manage Processing contributions
  sketchbook     Manage the sketchbook
  sketch         Manage a Processing sketch


C:\Users\GLV>processing cli --help | more

Command line edition for Processing 4.4.10 (Java Mode)

--help               Show this help text. Congratulations.

--sketch=<name>      Specify the sketch folder (required)
--output=<name>      Specify the output folder (optional and
                     cannot be the same as the sketch folder.)

--force              The sketch will not build if the output
                     folder already exists, because the contents
                     will be replaced. This option erases the
                     folder first. Use with extreme caution!

--build              Preprocess and compile a sketch into .class files.
--run                Preprocess, compile, and run a sketch.
--present            Preprocess, compile, and run a sketch in presentation mode.

--export             Export an application.
--variant            Specify the platform and architecture (Export only).
--no-java            Do not embed Java.

Starting with 4.0, the --platform option has been removed
because of the variety of platforms and architectures now available.
Use the --variant option instead, for instance:

variant        platform
-------------  ---------------------------
macos-x86_64   macOS (Intel 64-bit)
macos-aarch64  macOS (Apple Silicon)
windows-amd64  Windows (Intel 64-bit)
linux-amd64    Linux (Intel 64-bit)
linux-arm      Linux (Raspberry Pi 32-bit)
linux-aarch64  Linux (Raspberry Pi 64-bit)

The --build, --run, --present, or --export must be the final parameter
passed to Processing. Arguments passed following one of those four will
be passed through to the sketch itself, and therefore available to the
sketch via the 'args' field. To pass options understood by PApplet.main(),
write a custom main() method so that the preprocessor does not add one.
https://github.com/processing/processing/wiki/Command-Line

:)

I’ve never understood why some users are reluctant to download the Processing IDE; I suspect that it is because they want to use Processing code with a prototype board such as Raspberry and don’t have a lot of storage space and memory, but I have no direct proof of this and a lot of these users are reluctant to share the details of their setup. If that were the case, the command line technique that we are using would not work for them. command-java.exe is an extremely small file that it compressible and can easily be passed from system to system, but it won’t work for Windows; its platform specific. Apparently going forward the command line code is contained in the Processing IDE code and will work cross platform, but I can’t see how this is beneficial for those reluctant to use the IDE to start with; by definition they won’t be able to use it for whatever project they are working on.

Thanks for porting this demo to Windows. You can center the ‘Q‘ on the quit button by tweaking that code under its tab.

Does this work on your macOS?

See the CLI section:
Environment / Processing.org

And then try:

Processing cli --help

I am just curious!

References:
Processing 4 processing-java binary? - #3 by stefterv
Processing 4 processing-java binary? - #10 by stefterv

:)

The first one works, the second one doesn’t. ‘processing-java‘ still works in Terminal on Mac. ‘Processing cli‘ does not.

Based on your post, I now have it working on Windows 11. The command line code is now in processing.exe and is different from MacOS; hence the use of processing.exe in lieu of processing-java.exe. Another difference is Windows uses back slashes in file paths instead of forward slashes like the Unix systems; therefore I had to change both newFileAction() and runAction() to use back slashes and use folderPath with sketch= in runAction(). Actually when used in a string in Windows you have to use double back slashes as shown below.

The runAction() that works on my system follows:

void runAction() {
  String runFilePath = "";
  String folderPath = "";
  int selectedIndex = tabbedPane.getSelectedIndex();
  for (int i = 0; i < files.size(); i++) {
    String path = files.get(i);
    if (i == selectedIndex) {
      runFilePath = path;
      println(path);
      logArea.append("runAction : runFilePath = " + runFilePath + "\n");
    }
  }
  int lastSlashIndex = runFilePath.lastIndexOf('\\');
  if (lastSlashIndex != -1) {
    folderPath = runFilePath.substring(0, lastSlashIndex);
    logArea.append("runAction : folderPath = " + folderPath + "\n");
  } else {
    logArea.append("No path separator found.");
  }
  String sketchStr = "--sketch=" + folderPath;  
  logArea.append("Generated command: " + cmdStrExe + " cli " + sketchStr + " --run\n");
  ProcessBuilder processBuilder = new ProcessBuilder(cmdStrExe, "cli", sketchStr, "--run"); 
  try {
    process = processBuilder.start();
    BufferedReader stdIn = new BufferedReader(new InputStreamReader(process.getInputStream()));
    BufferedReader stdErr = new BufferedReader(new InputStreamReader(process.getErrorStream()));
    String outStr = "";
    while ((outStr = stdIn.readLine()) != null) {
      logArea.append(outStr + "\n");
    println(outStr);  
  }
    String errStr = "";
    while ((errStr = stdErr.readLine()) != null) {
      logArea.append(errStr + "\n");
    }
    // Wait for the process to complete and get its exit value
    int exitCode = process.waitFor();
    logArea.append("\nProcess exited with code: " + exitCode + "\n");
  }
  catch (IOException | InterruptedException e) {
    logArea.append(e + "\n");
  }
}

Output Windows11:

Any idea how to get that vertical scrollbar to not be clipped?