package processing.mode.cpp;
import java.awt.*;
import java.util.HashSet;
import java.util.Set;
import javax.swing.*;
import javax.swing.text.BadLocationException;
import processing.app.*;
import processing.app.syntax.*;
import processing.app.ui.*;
public class CppEditor extends Editor {
private CppRunner currentRunner;
private final Set errorLines = new HashSet<>();
private ErrorOverlay overlay;
public CppEditor(Base base, String path,
EditorState state, CppMode mode) throws EditorException {
super(base, path, state, mode);
}
// ââ Bug report link (placed in the footer's tab bar, just left of the
// version number / "Updates" indicator) ââââââââââââââââââââââââââââââ
private static final String BUG_REPORT_URL =
"https://github.com/processing-cpp/processing.cpp/issues";
@Override
public EditorFooter createFooter() {
EditorFooter ef = super.createFooter();
SwingUtilities.invokeLater(() -> addBugReportLink(ef));
return ef;
}
private void addBugReportLink(EditorFooter ef) {
JLabel prefix = new JLabel("Found a bug? Report it: ");
JLabel link = new JLabel("here");
link.setForeground(new Color(0x88, 0x66, 0xDD));
link.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
link.addMouseListener(new java.awt.event.MouseAdapter() {
@Override
public void mouseClicked(java.awt.event.MouseEvent e) {
openBugReportUrl();
}
});
JPanel row = new JPanel(new FlowLayout(FlowLayout.LEFT, 4, 0));
row.setOpaque(false);
row.add(prefix);
row.add(link);
// "Console" and "Updates" are both painted directly onto a single
// canvas component (drawString calls), not separate Swing children â
// there's no way to insert a real component "between" them through
// the normal layout, and adding a child directly into the tab bar
// would fight its active BoxLayout. Instead we float our link on the
// window's layered pane, positioned over the tab bar at the x-offset
// where the "Console" tab text ends (computed the same way the
// footer itself lays out tabs: LEFT_GUTTER + margin + text width +
// margin), and keep it synced to the tab bar's on-screen position.
JLabel versionLabel = findVersionLabel(ef);
Container tabBar = (versionLabel != null) ? versionLabel.getParent() : null;
if (tabBar == null) {
// Fallback: append to the footer itself so it's at least visible.
ef.add(row);
ef.revalidate();
ef.repaint();
return;
}
Font tabFont = versionLabel.getFont();
FontMetrics fm = tabBar.getFontMetrics(tabFont);
final int LEFT_GUTTER = Editor.LEFT_GUTTER;
final int MARGIN = 8; // matches EditorFooter's own tab margin
int consoleTextWidth = fm.stringWidth("Console");
int anchorX = LEFT_GUTTER + MARGIN + consoleTextWidth + MARGIN + 35;
JRootPane rootPane = getRootPane();
JLayeredPane layered = rootPane.getLayeredPane();
Dimension pref = row.getPreferredSize();
layered.add(row, JLayeredPane.PALETTE_LAYER);
row.setVisible(false); // hidden until we confirm there's room
final int SAFETY_GAP = 16; // minimum breathing room before Updates/version
Runnable reposition = () -> {
if (!tabBar.isShowing() || !versionLabel.isShowing()) {
row.setVisible(false);
return;
}
Point tabBarPos = SwingUtilities.convertPoint(tabBar, 0, 0, layered);
Point versionPos = SwingUtilities.convertPoint(versionLabel, 0, 0, tabBar);
// Only show the link if it fits entirely before the version label
// (which also means before "Updates", since that's painted right up
// against the version label's left edge) with some breathing room.
boolean fits = (anchorX + pref.width + SAFETY_GAP) <= versionPos.x;
row.setVisible(fits);
if (!fits) return;
int y = tabBarPos.y + (tabBar.getHeight() - pref.height) / 2;
row.setBounds(tabBarPos.x + anchorX, Math.max(0, y), pref.width, pref.height);
};
reposition.run();
java.awt.event.ComponentAdapter syncListener = new java.awt.event.ComponentAdapter() {
@Override public void componentResized(java.awt.event.ComponentEvent e) { reposition.run(); }
@Override public void componentMoved(java.awt.event.ComponentEvent e) { reposition.run(); }
};
tabBar.addComponentListener(syncListener);
rootPane.addComponentListener(syncListener);
// Safety net: if the window wasn't fully shown yet on the first pass,
// retry briefly until it is (covers the case where no resize/move ever
// happens after this runs, so the link would otherwise stay at 0,0).
Timer retry = new Timer(150, null);
int[] attempts = { 0 };
retry.addActionListener(e -> {
reposition.run();
attempts[0]++;
if (tabBar.isShowing() || attempts[0] > 10) {
retry.stop();
}
});
retry.start();
layered.revalidate();
layered.repaint();
}
private boolean looksLikeVersionLabel(Component c) {
if (!(c instanceof JLabel)) return false;
String text = ((JLabel) c).getText();
return text != null && text.matches("\\d+(\\.\\d+)+\\w*");
}
private JLabel findVersionLabel(Container parent) {
for (Component c : parent.getComponents()) {
if (looksLikeVersionLabel(c)) return (JLabel) c;
if (c instanceof Container) {
JLabel found = findVersionLabel((Container) c);
if (found != null) return found;
}
}
return null;
}
private void openBugReportUrl() {
// Try AWT Desktop first.
try {
if (java.awt.Desktop.isDesktopSupported()
&& java.awt.Desktop.getDesktop().isSupported(java.awt.Desktop.Action.BROWSE)) {
java.awt.Desktop.getDesktop().browse(new java.net.URI(BUG_REPORT_URL));
return;
}
} catch (Exception ignored) {
// fall through to OS-specific commands below
}
// Linux fallback: try common openers directly.
String[] candidates = { "xdg-open", "gio", "gnome-open", "kde-open" };
for (String cmd : candidates) {
try {
ProcessBuilder pb = (cmd.equals("gio"))
? new ProcessBuilder(cmd, "open", BUG_REPORT_URL)
: new ProcessBuilder(cmd, BUG_REPORT_URL);
pb.start();
return;
} catch (Exception ignored) {
// try next candidate
}
}
// Nothing worked â let the user copy the link themselves.
JOptionPane.showMessageDialog(this,
"Couldn't open the browser automatically.\n\nPlease visit:\n" + BUG_REPORT_URL,
"Open Link", JOptionPane.INFORMATION_MESSAGE);
}
@Override
protected JEditTextArea createTextArea() {
PdeTextArea ta = new PdeTextArea(new PdeTextAreaDefaults(),
new CppInputHandler(this), this);
InputHandler ih = ta.getInputHandler();
// NOTE: ENTER (auto-indent), TAB/S+TAB (indent/outdent/expand), and
// C+SLASH (comment toggle) are now handled by CppInputHandler's
// handlePressed(), mirroring JavaInputHandler, so they're no longer
// bound here individually.
// ââ Home / End âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ih.addKeyBinding("HOME", InputHandler.HOME);
ih.addKeyBinding("END", InputHandler.END);
ih.addKeyBinding("S+HOME", InputHandler.SELECT_HOME);
ih.addKeyBinding("S+END", InputHandler.SELECT_END);
ih.addKeyBinding("C+HOME", InputHandler.DOCUMENT_HOME);
ih.addKeyBinding("C+END", InputHandler.DOCUMENT_END);
ih.addKeyBinding("CS+HOME", InputHandler.SELECT_DOC_HOME);
ih.addKeyBinding("CS+END", InputHandler.SELECT_DOC_END);
// ââ Arrow keys with selection ââââââââââââââââââââââââââââââââââââââââââ
ih.addKeyBinding("S+LEFT", InputHandler.SELECT_PREV_CHAR);
ih.addKeyBinding("S+RIGHT", InputHandler.SELECT_NEXT_CHAR);
ih.addKeyBinding("S+UP", InputHandler.SELECT_PREV_LINE);
ih.addKeyBinding("S+DOWN", InputHandler.SELECT_NEXT_LINE);
// ââ Word navigation ââââââââââââââââââââââââââââââââââââââââââââââââââââ
ih.addKeyBinding("C+LEFT", InputHandler.PREV_WORD);
ih.addKeyBinding("C+RIGHT", InputHandler.NEXT_WORD);
ih.addKeyBinding("CS+LEFT", InputHandler.SELECT_PREV_WORD);
ih.addKeyBinding("CS+RIGHT",InputHandler.SELECT_NEXT_WORD);
// ââ Delete ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ih.addKeyBinding("DELETE", InputHandler.DELETE);
ih.addKeyBinding("BACK_SPACE",InputHandler.BACKSPACE);
ih.addKeyBinding("C+BACK_SPACE", InputHandler.BACKSPACE_WORD);
ih.addKeyBinding("C+DELETE", InputHandler.DELETE_WORD);
// ââ Page up/down ââââââââââââââââââââââââââââââââââââââââââââââââââââââ
ih.addKeyBinding("PAGE_UP", InputHandler.PREV_PAGE);
ih.addKeyBinding("PAGE_DOWN", InputHandler.NEXT_PAGE);
ih.addKeyBinding("S+PAGE_UP", InputHandler.SELECT_PREV_PAGE);
ih.addKeyBinding("S+PAGE_DOWN",InputHandler.SELECT_NEXT_PAGE);
// ââ Error overlay âââââââââââââââââââââââââââââââââââââââââââââââââââââ
overlay = new ErrorOverlay(ta);
ta.getPainter().setLayout(null);
ta.getPainter().add(overlay);
ta.getPainter().addComponentListener(new java.awt.event.ComponentAdapter() {
public void componentResized(java.awt.event.ComponentEvent e) {
overlay.setBounds(0, 0,
ta.getPainter().getWidth(), ta.getPainter().getHeight());
}
});
overlay.setBounds(0, 0, 600, 400);
return ta;
}
// ââ Error overlay âââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
class ErrorOverlay extends JComponent {
private final JEditTextArea ta;
ErrorOverlay(JEditTextArea ta) { this.ta = ta; setOpaque(false); }
@Override
protected void paintComponent(Graphics g) {
if (errorLines.isEmpty()) return;
Graphics2D g2 = (Graphics2D) g.create();
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(new Color(0xFF, 0x40, 0x40, 200));
g2.setStroke(new BasicStroke(1.5f));
int lineH = ta.getPainter().getFontMetrics().getHeight();
int firstVisible = ta.getFirstLine();
for (int docLine : errorLines) {
int visLine = docLine - firstVisible;
if (visLine < 0) continue;
int y = visLine * lineH + lineH - 3;
int w = ta.getPainter().getWidth();
for (int x = 0; x < w - 6; x += 6) {
g2.drawLine(x, y, x+3, y - 2);
g2.drawLine(x+3, y - 2, x+6, y);
}
}
g2.dispose();
}
}
// ââ Error highlighting ââââââââââââââââââââââââââââââââââââââââââââââââââââ
public void highlightErrorLine(int line) {
if (line <= 0) return;
int docLine = line - 1;
if (!(textarea instanceof PdeTextArea)) return;
PdeTextArea pta = (PdeTextArea) textarea;
try {
if (docLine >= pta.getLineCount()) return;
errorLines.clear();
errorLines.add(docLine);
pta.clearGutterText();
pta.setGutterText(docLine, "â¶");
pta.scrollTo(docLine, 0);
pta.select(pta.getLineStartOffset(docLine),
pta.getLineStopOffset(docLine) - 1);
if (overlay != null) overlay.repaint();
} catch (Exception ignored) {}
}
public void clearErrorHighlight() {
errorLines.clear();
if (textarea instanceof PdeTextArea)
((PdeTextArea) textarea).clearGutterText();
if (overlay != null) overlay.repaint();
}
// ââ Comment toggle ââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
void toggleComment(JEditTextArea ta) {
int selStart = ta.getSelectionStart();
int selEnd = ta.getSelectionStop();
int startLine = ta.getLineOfOffset(selStart);
int endLine = ta.getLineOfOffset(selEnd);
if (endLine > startLine && ta.getLineStartOffset(endLine) == selEnd)
endLine--;
boolean allCommented = true;
for (int i = startLine; i <= endLine; i++) {
if (!getLineText(ta, i).stripLeading().startsWith("//")) {
allCommented = false; break;
}
}
for (int i = endLine; i >= startLine; i--) {
String raw = getLineText(ta, i);
if (allCommented)
replaceLineText(ta, i, raw.replaceFirst("(\\s*)//\\s?", "$1"));
else {
int indent = 0;
while (indent < raw.length() &&
(raw.charAt(indent)==' ' || raw.charAt(indent)=='\t')) indent++;
replaceLineText(ta, i,
raw.substring(0, indent) + "// " + raw.substring(indent));
}
}
}
private String getLineText(JEditTextArea ta, int line) {
try {
int start = ta.getLineStartOffset(line);
int end = ta.getLineStopOffset(line) - 1;
return ta.getDocument().getText(start, Math.max(0, end - start));
} catch (BadLocationException e) { return ""; }
}
private void replaceLineText(JEditTextArea ta, int line, String text) {
try {
int start = ta.getLineStartOffset(line);
int end = ta.getLineStopOffset(line) - 1;
ta.getDocument().remove(start, Math.max(0, end - start));
ta.getDocument().insertString(start, text, null);
} catch (BadLocationException e) {}
}
@Override public EditorToolbar createToolbar() { return new CppToolbar(this); }
@Override public Formatter createFormatter() { return null; }
@Override public JMenu buildFileMenu() {
javax.swing.JMenuItem exportApp = new javax.swing.JMenuItem("Export Application...");
exportApp.setAccelerator(javax.swing.KeyStroke.getKeyStroke(
java.awt.event.KeyEvent.VK_E,
java.awt.Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx()
| java.awt.event.InputEvent.SHIFT_DOWN_MASK));
exportApp.addActionListener(e -> handleExportApplication());
return buildFileMenu(new javax.swing.JMenuItem[]{ exportApp });
}
@Override public JMenu buildHelpMenu() { return new JMenu("Help"); }
@Override public void handleImportLibrary(String name) {}
@Override public String getCommentPrefix() { return "//"; }
@Override public void startIndeterminate() { }
@Override public void stopIndeterminate() { }
@Override public void statusHalt() { }
@Override public boolean isHalted() { return currentRunner == null; }
@Override public void deactivateRun() { toolbar.deactivateRun(); }
@Override
public void internalCloseRunner() {
if (currentRunner != null) { currentRunner.stop(); currentRunner = null; }
}
@Override
public JMenu buildSketchMenu() {
JMenuItem run = processing.app.ui.Toolkit.newJMenuItem("Run", 'R');
JMenuItem stop = new JMenuItem("Stop");
run.addActionListener(e -> handleRun(false, null, null));
stop.addActionListener(e -> handleStop());
return buildSketchMenu(new JMenuItem[] { run, stop });
}
public void handleRun(boolean present, Runnable stopCb, Runnable startCb) {
clearErrorHighlight();
prepareRun();
statusNotice("Compiling...");
new Thread(() -> {
try {
CppMode mode = (CppMode) getMode();
currentRunner = mode.handleLaunch(sketch, this);
if (currentRunner != null) {
statusNotice("Running.");
if (startCb != null) startCb.run();
} else {
statusError("Build failed â see console for errors.");
deactivateRun();
}
} catch (InstallWizard.CancelledByUser e) {
statusNotice("Cancelled.");
deactivateRun();
} catch (Exception e) {
statusError(e.getMessage());
deactivateRun();
}
}).start();
}
public void handleStop() {
internalCloseRunner();
deactivateRun();
statusNotice("Stopped.");
}
public void handleExportApplication() {
// Block export only for truly unsaved sketches (untitled, never saved)
// Check if the sketch folder is inside a temp directory
java.io.File sketchFolder = sketch.getFolder();
boolean isTemp = sketchFolder == null
|| !sketchFolder.exists()
|| sketch.isUntitled();
if (isTemp) {
javax.swing.JOptionPane.showMessageDialog(null,
"Please save the sketch first.
" +
"Use File â Save As... to give it a name,
" +
"then try Export Application again.",
"Save Required", javax.swing.JOptionPane.WARNING_MESSAGE);
return;
}
// Save if modified
if (sketch.isModified()) {
int choice = javax.swing.JOptionPane.showOptionDialog(null,
"Sketch has unsaved changes.
" +
"Save before exporting?",
"Unsaved Changes",
javax.swing.JOptionPane.YES_NO_CANCEL_OPTION,
javax.swing.JOptionPane.WARNING_MESSAGE,
null,
new String[]{"Save & Export", "Export Anyway", "Cancel"},
"Save & Export");
if (choice == 2 || choice < 0) return;
if (choice == 0) {
try { sketch.save(); } catch (Exception e) {
javax.swing.JOptionPane.showMessageDialog(null,
"Could not save: " + e.getMessage(),
"Save Failed", javax.swing.JOptionPane.ERROR_MESSAGE);
return;
}
}
}
CppBuild build = new CppBuild(sketch, (CppMode) getMode());
build.showExportDialog(sketch, this);
}
}