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.jython;
030
031import static java.util.Objects.requireNonNull;
032
033import java.awt.Toolkit;
034import java.awt.datatransfer.Clipboard;
035import java.awt.datatransfer.DataFlavor;
036import java.awt.datatransfer.Transferable;
037import java.awt.event.ActionEvent;
038import java.awt.event.ActionListener;
039import java.util.Map;
040
041import javax.swing.JMenu;
042import javax.swing.JMenuItem;
043import javax.swing.JPopupMenu;
044import javax.swing.border.BevelBorder;
045
046import org.python.core.PyObject;
047
048import org.slf4j.Logger;
049import org.slf4j.LoggerFactory;
050
051// TODO(jon): this will need to be reconsidered, but it's fine for the current
052// console.
053public class DefaultMenuWrangler implements MenuWrangler {
054
055    private static final Logger logger = LoggerFactory.getLogger(DefaultMenuWrangler.class);
056
057    /** The {@link Console} whose menus we're {@literal "wrangling"}. */
058    private final Console console;
059
060    /** Handles {@literal "cut"} requests that originate from the menu. */
061    private final CutTextAction cutAction;
062
063    /** Handles {@literal "copy"} requests that originate from the menu. */
064    private final CopyTextAction copyAction;
065
066    /** Handles {@literal "paste"} requests that originate from the menu. */
067    private final PasteTextAction pasteAction;
068
069    /** Allows the user to clear out the buffer via the menu. */
070    private final ClearBufferAction clearAction;
071
072    /** Allows the user to select the buffer's contents. */
073    private final SelectBufferAction selectAction;
074
075    public DefaultMenuWrangler(final Console console) {
076        this.console = requireNonNull(console, "Cannot provide a null console");
077
078        cutAction = new CutTextAction(console);
079        copyAction = new CopyTextAction(console);
080        pasteAction = new PasteTextAction(console);
081
082        clearAction = new ClearBufferAction(console);
083        selectAction = new SelectBufferAction(console);
084    }
085
086    public JPopupMenu buildMenu() {
087        JPopupMenu menu = new JPopupMenu();
088        menu.add(makeLocalsMenu());
089        menu.addSeparator();
090        menu.add(cutAction.getMenuItem());
091        menu.add(copyAction.getMenuItem());
092        menu.add(pasteAction.getMenuItem());
093        menu.addSeparator();
094        menu.add(clearAction.getMenuItem());
095        menu.add(selectAction.getMenuItem());
096        menu.setBorder(new BevelBorder(BevelBorder.RAISED));
097        return menu;
098    }
099
100    /**
101     * Don't need to handle this just yet.
102     */
103    public void stateChanged() {
104        logger.trace("noop!");
105    }
106
107    /**
108     * Returns the contents of Jython's local namespace as a {@link JMenu} that
109     * allows for (limited) introspection.
110     * 
111     * @return {@code JMenu} containing the local namespace.
112     */
113    private JMenu makeLocalsMenu() {
114        JMenu menu = new JMenu("Local Namespace");
115
116        ActionListener menuClickHandler = new ActionListener() {
117            public void actionPerformed(final ActionEvent e) {
118                String varName = e.getActionCommand();
119                // TODO(jon): IDLE doesn't appear to allow inserts on anything
120                // except for the last line. is this what we want?
121                console.insertAtCaret(Console.TXT_NORMAL, varName);
122            }
123        };
124
125        // TODO(jon): it would be really cool to allow customizable submenu
126        // stuff. [ working on it! ]
127        Map<String, PyObject> locals = console.getLocalNamespace();
128        for (Map.Entry<String, PyObject> entry : locals.entrySet()) {
129
130            String key = entry.getKey();
131            PyObject value = entry.getValue();
132
133            Class<?> c = value.getClass();
134//            if (value instanceof PyJavaInstance)
135//                c = value.__tojava__(Object.class).getClass();
136
137            String itemName = key + ": " + c.getSimpleName();
138
139            JMenuItem item = new JMenuItem(itemName);
140            item.setActionCommand(key);
141            item.addActionListener(menuClickHandler);
142            menu.add(item);
143        }
144        return menu;
145    }
146
147    /**
148     * Generalized representation of a {@literal "context popup menu"}. Handles
149     * the more trivial things that the menu items need to handle.
150     */
151    private static abstract class MenuAction {
152
153        protected final Console console;
154
155        protected final String label;
156
157        protected final JMenuItem item;
158
159        protected MenuAction(final Console console, final String label) {
160            this.console = console;
161            this.label = label;
162            item = buildMenuItem();
163        }
164
165        public ActionListener getActionListener() {
166            return new ActionListener() {
167                public void actionPerformed(final ActionEvent e) {
168                    doAction();
169                }
170            };
171        }
172
173        public JMenuItem getMenuItem() {
174            item.setEnabled(validConsoleState());
175            return item;
176        }
177
178        public JMenuItem buildMenuItem() {
179            JMenuItem menuItem = new JMenuItem(label);
180            menuItem.setEnabled(validConsoleState());
181            menuItem.addActionListener(getActionListener());
182            return menuItem;
183        }
184
185        abstract public boolean validConsoleState();
186
187        abstract public void doAction();
188    }
189
190    /**
191     * Allows the user to trigger a {@literal "cut"} operation. There must be 
192     * some text that is currently selected in order for this to be enabled.
193     * 
194     * @see javax.swing.text.JTextComponent#cut()
195     */
196    private static class CutTextAction extends MenuAction {
197
198        public CutTextAction(final Console console) {
199            super(console, "Cut");
200        }
201
202        @Override public boolean validConsoleState() {
203            if ((console == null) || (console.getTextPane() == null)) {
204                return false;
205            }
206
207            String selection = console.getTextPane().getSelectedText();
208            if ((selection != null) && !selection.isEmpty()) {
209                return true;
210            }
211            return false;
212        }
213
214        @Override public void doAction() {
215            console.getTextPane().cut();
216        }
217    }
218
219    /**
220     * Basic {@literal "copy"} operation. Requires that there is some selected
221     * text in the console's {@code JTextPane}.
222     * 
223     * @see javax.swing.text.JTextComponent#copy()
224     */
225    private static class CopyTextAction extends MenuAction {
226
227        public CopyTextAction(final Console console) {
228            super(console, "Copy");
229        }
230
231        @Override public boolean validConsoleState() {
232            if ((console == null) || (console.getTextPane() == null)) {
233                return false;
234            }
235
236            String selection = console.getTextPane().getSelectedText();
237            if ((selection != null) && !selection.isEmpty()) {
238                return true;
239            }
240
241            return false;
242        }
243
244        @Override public void doAction() {
245            console.getTextPane().copy();
246        }
247    }
248
249    /**
250     * Allows the user to (attempt) to paste the contents of the <i>system</i>
251     * clipboard. Clipboard must contain some kind of {@literal "text"} for
252     * this to work.
253     * 
254     * @see javax.swing.text.JTextComponent#paste()
255     */
256    private static class PasteTextAction extends MenuAction {
257
258        public PasteTextAction(final Console console) {
259            super(console, "Paste");
260        }
261
262        @Override public boolean validConsoleState() {
263            if ((console == null) || (console.getTextPane() == null)) {
264                return false;
265            }
266
267            Clipboard clippy =
268                Toolkit.getDefaultToolkit().getSystemClipboard();
269            Transferable contents = clippy.getContents(null);
270            if (contents != null) {
271                if (contents.isDataFlavorSupported(DataFlavor.stringFlavor)) {
272                    return true;
273                }
274            }
275            return false;
276        }
277
278        @Override public void doAction() {
279            console.getTextPane().paste();
280        }
281    }
282
283    /**
284     * Clears out the console's {@code JTextPane}, though a fresh jython 
285     * prompt is shown afterwards.
286     */
287    private static class ClearBufferAction extends MenuAction {
288
289        public ClearBufferAction(final Console console) {
290            super(console, "Clear Buffer");
291        }
292
293        @Override public boolean validConsoleState() {
294            if ((console == null) || (console.getTextPane() == null)) {
295                return false;
296            }
297            return true;
298        }
299
300        @Override public void doAction() {
301            console.getTextPane().selectAll();
302            console.getTextPane().replaceSelection("");
303            console.prompt();
304        }
305    }
306
307    /**
308     * Selects everything contained in the console's {@code JTextPane}.
309     * 
310     * @see javax.swing.text.JTextComponent#selectAll()
311     */
312    private static class SelectBufferAction extends MenuAction {
313
314        public SelectBufferAction(final Console console) {
315            super(console, "Select All");
316        }
317
318        @Override public boolean validConsoleState() {
319            if ((console == null) || (console.getTextPane() == null)) {
320                return false;
321            }
322            return true;
323        }
324
325        @Override public void doAction() {
326            console.getTextPane().selectAll();
327        }
328    }
329}