001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2024
005 * Space Science and Engineering Center (SSEC)
006 * University of Wisconsin - Madison
007 * 1225 W. Dayton Street, Madison, WI 53706, USA
008 * https://www.ssec.wisc.edu/mcidas/
009 * 
010 * All Rights Reserved
011 * 
012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
013 * some McIDAS-V source code is based on IDV and VisAD source code.  
014 * 
015 * McIDAS-V is free software; you can redistribute it and/or modify
016 * it under the terms of the GNU Lesser Public License as published by
017 * the Free Software Foundation; either version 3 of the License, or
018 * (at your option) any later version.
019 * 
020 * McIDAS-V is distributed in the hope that it will be useful,
021 * but WITHOUT ANY WARRANTY; without even the implied warranty of
022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
023 * GNU Lesser Public License for more details.
024 * 
025 * You should have received a copy of the GNU Lesser Public License
026 * along with this program.  If not, see https://www.gnu.org/licenses/.
027 */
028
029package edu.wisc.ssec.mcidasv.ui;
030
031import javax.swing.JPanel;
032import javax.swing.JPopupMenu;
033import javax.swing.JScrollPane;
034import javax.swing.event.UndoableEditEvent;
035import javax.swing.event.UndoableEditListener;
036import javax.swing.text.JTextComponent;
037import javax.swing.undo.UndoManager;
038
039import edu.wisc.ssec.mcidasv.McIDASV;
040import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
041import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
042import org.fife.ui.rtextarea.RTextScrollPane;
043import org.slf4j.Logger;
044import org.slf4j.LoggerFactory;
045
046/**
047 * A bare-bones text editor that can do relatively robust syntax highlighting
048 * of Jython code.
049 *
050 * <p>This class relies <i>very heavily</i> upon the wonderful
051 * <a href="https://github.com/bobbylight/RSyntaxTextArea">RSyntaxTextArea</a>
052 * project.</p>
053 */
054public class JythonEditor implements UndoableEditListener {
055
056    private static final Logger logger = LoggerFactory.getLogger(JythonEditor.class);
057
058    /** Text area that contains the syntax-highlighted text. */
059    private final McvJythonTextArea textArea;
060
061    /** Scroll pane for {@link #textArea}. */
062    private final RTextScrollPane scrollPane;
063
064    /** Undo manager. */
065    private final UndoManager undo;
066
067    /**
068     * Creates a new JythonEditor.
069     */
070    public JythonEditor() {
071        textArea = new McvJythonTextArea(20, 60);
072        if (McIDASV.isDarkMode()) {
073            UIManager.applyTextTheme(getClass(), textArea, "/edu/wisc/ssec/mcidasv/resources/monokai-flatlaf.xml");
074        }
075
076        textArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_PYTHON);
077
078        textArea.setCodeFoldingEnabled(true);
079        textArea.setWhitespaceVisible(true);
080
081        // hrm?
082        textArea.setBracketMatchingEnabled(true);
083        textArea.setPaintMatchedBracketPair(true);
084
085        textArea.setAnimateBracketMatching(true);
086        textArea.setPaintTabLines(true);
087        textArea.setTabsEmulated(true);
088        textArea.setTabSize(4);
089
090        scrollPane = new RTextScrollPane(textArea);
091
092        undo = new UndoManager();
093
094        addUndoableEditListener(this);
095    }
096
097    /**
098     * Returns the text area responsible for syntax highlighting.
099     *
100     * @return Reference to {@link #textArea}.
101     */
102    public JTextComponent getTextComponent() {
103        return textArea;
104    }
105
106    /**
107     * Returns the {@code JScrollPane} that contains {@link #textArea}.
108     *
109     * @return {@code JScrollPane} with the text area. Suitable for adding to
110     * a {@code JPanel}.
111     */
112    public JScrollPane getScrollPane() {
113        return scrollPane;
114    }
115
116    /**
117     * Sets the text of this document to the given {@code String}.
118     *
119     * @param text New text to use in {@link #textArea}.
120     */
121    public void setText(String text) {
122        textArea.setText(text);
123    }
124
125    /**
126     * Returns a string containing the text of {@link #textArea}.
127     *
128     * @return The current contents of the text area.
129     */
130    public String getText() {
131        return textArea.getText();
132    }
133
134    /**
135     * Controls whether or not changes can be made to the contents of
136     * {@link #textArea}.
137     *
138     * @param enabled {@code true} if the editor should be enabled,
139     *                {@code false} otherwise.
140     */
141    public void setEnabled(boolean enabled) {
142        textArea.setEnabled(enabled);
143        scrollPane.getGutter().setEnabled(enabled);
144        scrollPane.getGutter().setBackground(textArea.getBackground());
145    }
146
147    /**
148     * Returns the component (aka {@literal "the gutter"} that contains
149     * optional information like line numbers.
150     *
151     * @return {@code JPanel} that contains the line numbers.
152     */
153    public JPanel getLineNumberComponent() {
154        return scrollPane.getGutter();
155    }
156
157    /**
158     * Copies selected text into system clipboard.
159     */
160    public void copy() {
161        textArea.copy();
162    }
163
164    /**
165     * Whether or not changes can be made to {@link #textArea}.
166     *
167     * @return {@code true} if changes are allowed, {@code false} otherwise.
168     */
169    public boolean isEnabled() {
170        return textArea.isEnabled();
171    }
172
173    /**
174     * Insert the given text at the caret.
175     *
176     * @param textToInsert Text to insert.
177     */
178    public void insertText(String textToInsert) {
179        int pos = textArea.getCaretPosition();
180        String t = textArea.getText();
181        t = t.substring(0, pos) + textToInsert + t.substring(pos);
182        textArea.setText(t);
183        textArea.setCaretPosition(pos + textToInsert.length());
184    }
185
186    /**
187     * Handles undoable edits
188     *
189     * @param e Event that represents the undoable edit.
190     */
191    @Override public void undoableEditHappened(UndoableEditEvent e) {
192        if (e.getEdit().isSignificant()) {
193            undo.addEdit(e.getEdit());
194        }
195    }
196
197    /**
198     * Adds the given undoable edit listener to {@link #textArea}.
199     *
200     * @param l Listener to add.
201     */
202    public void addUndoableEditListener(UndoableEditListener l) {
203        textArea.getDocument().addUndoableEditListener(l);
204    }
205
206    /**
207     * Remove the given undoable edit listener from {@link #textArea}.
208     *
209     * @param l Listener to remove.
210     */
211    public void removeUndoableEditListener(UndoableEditListener l) {
212        textArea.getDocument().removeUndoableEditListener(l);
213    }
214
215    /**
216     * Returns the default {@link JPopupMenu} created by
217     * {@link RSyntaxTextArea#createPopupMenu()}.
218     *
219     * @return Popup menu.
220     */
221    public JPopupMenu createPopupMenu() {
222        return textArea.makePopupMenu();
223    }
224
225    public static class McvJythonTextArea extends RSyntaxTextArea {
226
227        McvJythonTextArea(int rows, int columns) {
228            super(rows, columns);
229        }
230
231        @Override protected JPopupMenu createPopupMenu() {
232            // this is needed so that the popup is disabled by default, which
233            // allows JythonManager's addMouseListener stuff to work a bit
234            // better.
235            return null;
236        }
237
238        public JPopupMenu makePopupMenu() {
239            // this method is mostly for getting around the fact that
240            // createPopupMenu is protected
241            return super.createPopupMenu();
242        }
243    }
244}