// GtpShell.java package net.sf.gogui.gui; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.Frame; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.File; import java.io.FileNotFoundException; import java.io.PrintStream; import java.util.ArrayList; import java.util.prefs.Preferences; import javax.swing.Box; import javax.swing.ComboBoxEditor; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextField; import javax.swing.JViewport; import javax.swing.SwingUtilities; import javax.swing.event.CaretEvent; import javax.swing.event.CaretListener; import net.sf.gogui.gtp.GtpUtil; import static net.sf.gogui.gui.I18n.i18n; import net.sf.gogui.util.ObjectUtil; import net.sf.gogui.util.Platform; import net.sf.gogui.util.PrefUtil; /** Dialog for displaying the GTP stream and for entering commands. */ public class GtpShell extends JDialog implements ActionListener { /** Callback for events generated by GtpShell. */ public interface Listener { void actionSendCommand(String command, boolean isCritical, boolean showError); /** Callback if some text is selected. */ void textSelected(String text); } public GtpShell(Frame owner, Listener listener, MessageDialogs messageDialogs) { super(owner, i18n("TIT_SHELL")); m_messageDialogs = messageDialogs; m_listener = listener; Preferences prefs = Preferences.userNodeForPackage(getClass()); m_historyMin = prefs.getInt("history-min", 2000); m_historyMax = prefs.getInt("history-max", 3000); JPanel panel = new JPanel(new BorderLayout()); getContentPane().add(panel, BorderLayout.CENTER); m_gtpShellText = new GtpShellText(m_historyMin, m_historyMax, false); CaretListener caretListener = new CaretListener() { public void caretUpdate(CaretEvent event) { if (m_listener == null) return; // Call the callback only if the selected text has changed. // This avoids that the callback is called multiple times // if the caret position changes, but the text selection // was null before and after the change (see also bug // #2964755) String selectedText = m_gtpShellText.getSelectedText(); if (! ObjectUtil.equals(selectedText, m_selectedText)) { m_listener.textSelected(selectedText); m_selectedText = selectedText; } } }; m_gtpShellText.addCaretListener(caretListener); m_scrollPane = new JScrollPane(m_gtpShellText, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER); if (Platform.isMac()) // Default Apple L&F uses no border, but Quaqua 3.7.4 does m_scrollPane.setBorder(null); panel.add(m_scrollPane, BorderLayout.CENTER); panel.add(createCommandInput(), BorderLayout.SOUTH); setMinimumSize(new Dimension(160, 112)); pack(); } public void actionPerformed(ActionEvent event) { String command = event.getActionCommand(); if (command.equals("run")) commandEntered(); else if (command.equals("close")) setVisible(false); } /** @see net.sf.gogui.gui.GtpShellText#isLastTextNonGTP */ public boolean isLastTextNonGTP() { return m_gtpShellText.isLastTextNonGTP(); } public void receivedInvalidResponse(final String response, boolean invokeLater) { if (SwingUtilities.isEventDispatchThread()) appendInvalidResponse(response); else { Runnable r = new Runnable() { public void run() { appendInvalidResponse(response); } }; if (invokeLater) SwingUtilities.invokeLater(r); else GuiUtil.invokeAndWait(r); } } public void receivedResponse(final boolean error, final String response, boolean invokeLater) { if (SwingUtilities.isEventDispatchThread()) appendResponse(error, response); else { Runnable r = new Runnable() { public void run() { appendResponse(error, response); } }; if (invokeLater) SwingUtilities.invokeLater(r); else GuiUtil.invokeAndWait(r); } } public void receivedStdErr(final String s, boolean invokeLater, final boolean isLiveGfx, final boolean isWarning) { if (SwingUtilities.isEventDispatchThread()) appendLog(s, isLiveGfx, isWarning); else { Runnable r = new Runnable() { public void run() { appendLog(s, isLiveGfx, isWarning); } }; if (invokeLater) SwingUtilities.invokeLater(r); else GuiUtil.invokeAndWait(r); } } public void saveLog(JFrame parent) { save(parent, m_gtpShellText.getLog(), m_gtpShellText.getLinesTruncated()); } public void saveCommands(JFrame parent) { save(parent, m_commands.toString(), m_linesTruncated); } public void saveHistory() { int maxHistory = 100; int max = m_history.size(); if (max > maxHistory) max = maxHistory; ArrayList<String> list = new ArrayList<String>(max); for (int i = m_history.size() - max; i < m_history.size(); ++i) list.add(m_history.get(i)); PrefUtil.putList("net/sf/gogui/gui/gtpshell/recentcommands", list); } public void setCommandInProgess(boolean commandInProgess) { m_commandInProgress = commandInProgess; } public void setCommandCompletion(boolean commandCompletion) { m_disableCompletions = ! commandCompletion; } public void setTimeStamp(boolean enable) { m_gtpShellText.setTimeStamp(enable); } public void sentCommand(final String command) { if (SwingUtilities.isEventDispatchThread()) appendSentCommand(command); else GuiUtil.invokeAndWait(new Runnable() { public void run() { appendSentCommand(command); } }); } public void setInitialCompletions(ArrayList<String> completions) { for (int i = completions.size() - 1; i >= 0; --i) { String command = completions.get(i); if (! GtpUtil.isStateChangingCommand(command)) appendToHistory(command); } ArrayList<String> list = PrefUtil.getList("net/sf/gogui/gui/gtpshell/recentcommands"); for (int i = 0; i < list.size(); ++i) appendToHistory(list.get(i)); addAllCompletions(m_history); } public void setProgramCommand(String command) { m_programCommand = command; } public void setProgramName(String name) { m_programName = name; } public void setProgramVersion(String version) { m_programVersion = version; } private boolean m_disableCompletions; private boolean m_commandInProgress; private final int m_historyMax; private final int m_historyMin; private int m_linesTruncated; private int m_numberCommands; private final Listener m_listener; private ComboBoxEditor m_editor; private JButton m_runButton; private JTextField m_textField; private JComboBox m_comboBox; private final JScrollPane m_scrollPane; private final GtpShellText m_gtpShellText; private final StringBuilder m_commands = new StringBuilder(4096); private final ArrayList<String> m_history = new ArrayList<String>(128); private String m_selectedText; private String m_programCommand = "unknown"; private String m_programName = "unknown"; private String m_programVersion = "unknown"; private final MessageDialogs m_messageDialogs; private void addAllCompletions(ArrayList<String> completions) { // On Windows JDK 1.4 changing the popup automatically // selects all text in the text field, so we remember and // restore the state. String oldText = m_textField.getText(); int oldCaretPosition = m_textField.getCaretPosition(); if (completions.size() > m_comboBox.getItemCount()) m_comboBox.hidePopup(); m_comboBox.removeAllItems(); for (int i = completions.size() - 1; i >= 0; --i) m_comboBox.addItem(GuiUtil.createComboBoxItem(completions.get(i))); m_comboBox.setSelectedIndex(-1); m_textField.setText(oldText); m_textField.setCaretPosition(oldCaretPosition); } private void appendInvalidResponse(String response) { assert SwingUtilities.isEventDispatchThread(); m_gtpShellText.appendInvalidResponse(response); } private void appendLog(String line, boolean isLiveGfx, boolean isWarning) { assert SwingUtilities.isEventDispatchThread(); m_gtpShellText.appendLog(line, isLiveGfx, isWarning); } private void appendResponse(boolean error, String response) { assert SwingUtilities.isEventDispatchThread(); if (error) m_gtpShellText.appendError(response); else m_gtpShellText.appendInput(response); } private void appendSentCommand(String command) { assert SwingUtilities.isEventDispatchThread(); m_commands.append(command); m_commands.append('\n'); ++m_numberCommands; if (m_numberCommands > m_historyMax) { int truncateLines = m_numberCommands - m_historyMin; String s = m_commands.toString(); int index = GtpShellText.findTruncateIndex(s, truncateLines); assert index != -1; m_commands.delete(0, index); m_linesTruncated += truncateLines; m_numberCommands = 0; } m_gtpShellText.appendOutput(command + "\n"); } private void appendToHistory(String command) { command = command.trim(); int i = m_history.indexOf(command); if (i >= 0) m_history.remove(i); m_history.add(command); } private void commandEntered() { assert SwingUtilities.isEventDispatchThread(); String command = m_textField.getText().trim(); if (command.trim().equals("")) return; if (command.startsWith("#")) { m_gtpShellText.appendComment(command + "\n"); } else { if (GtpUtil.isStateChangingCommand(command)) { showError(i18n("MSG_SHELL_BOARDCHANGING"), i18n("MSG_SHELL_BOARDCHANGING_2"), false); return; } if (m_commandInProgress) { showError(i18n("MSG_SHELL_CMD_IN_PROGRESS"), i18n("MSG_SHELL_CMD_IN_PROGRESS_2"), false); return; } m_listener.actionSendCommand(command, false, false); } appendToHistory(command); m_gtpShellText.setPositionToEnd(); m_comboBox.hidePopup(); addAllCompletions(m_history); m_editor.setItem(null); } private JComponent createCommandInput() { Box box = Box.createVerticalBox(); //JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); JPanel panel = new JPanel(new BorderLayout()); box.add(GuiUtil.createSmallFiller()); box.add(Box.createVerticalGlue()); box.add(panel); box.add(Box.createVerticalGlue()); m_comboBox = new JComboBox(); if (Platform.isMac()) // Workaround for bug in Quaqua Look and Feel 3.6.11 m_comboBox.setMaximumRowCount(7); m_editor = m_comboBox.getEditor(); m_textField = (JTextField)m_editor.getEditorComponent(); m_textField.setFocusTraversalKeysEnabled(false); KeyAdapter keyAdapter = new KeyAdapter() { public void keyReleased(KeyEvent e) { int c = e.getKeyCode(); int mod = e.getModifiers(); if (c == KeyEvent.VK_ESCAPE) return; else if (c == KeyEvent.VK_TAB) { findBestCompletion(); popupCompletions(); } else if (c == KeyEvent.VK_PAGE_UP && mod == ActionEvent.SHIFT_MASK) scrollPage(true); else if (c == KeyEvent.VK_PAGE_DOWN && mod == ActionEvent.SHIFT_MASK) scrollPage(false); else if (c == KeyEvent.VK_ENTER && ! m_comboBox.isPopupVisible()) commandEntered(); else if (e.getKeyChar() != KeyEvent.CHAR_UNDEFINED) popupCompletions(); } }; m_textField.addKeyListener(keyAdapter); m_comboBox.setEditable(true); m_comboBox.setFont(m_gtpShellText.getFont()); m_comboBox.addActionListener(this); addWindowListener(new WindowAdapter() { public void windowActivated(WindowEvent e) { m_comboBox.requestFocusInWindow(); m_textField.requestFocusInWindow(); } }); panel.add(m_comboBox, BorderLayout.CENTER); m_runButton = new JButton(); m_runButton.setIcon(GuiUtil.getIcon("gogui-key_enter", i18n("LB_RUN"))); m_runButton.setActionCommand("run"); m_runButton.setFocusable(false); m_runButton.setToolTipText(i18n("TT_SHELL_RUN")); m_runButton.addActionListener(this); GuiUtil.setMacBevelButton(m_runButton); JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0, 0)); panel.add(buttonPanel, BorderLayout.EAST); buttonPanel.add(GuiUtil.createSmallFiller()); buttonPanel.add(m_runButton); // add some empty space so that status bar does not overlap the // window resize widget on Mac OS X if (Platform.isMac()) { Dimension dimension = new Dimension(20, 1); Box.Filler filler = new Box.Filler(dimension, dimension, dimension); buttonPanel.add(filler); } return box; } private void findBestCompletion() { String text = m_textField.getText().trim(); if (text.equals("")) return; String bestCompletion = null; for (int i = 0; i < m_history.size(); ++i) { String completion = m_history.get(i); if (completion.startsWith(text)) { if (bestCompletion == null) { bestCompletion = completion; continue; } int j = text.length(); while (true) { if (j >= bestCompletion.length()) { break; } if (j >= completion.length()) break; if (bestCompletion.charAt(j) != completion.charAt(j)) break; ++j; } bestCompletion = completion.substring(0, j); } } if (bestCompletion != null) m_textField.setText(bestCompletion); } private void popupCompletions() { String text = m_textField.getText(); text = text.replaceAll("^ *", ""); ArrayList<String> completions = new ArrayList<String>(128); for (int i = 0; i < m_history.size(); ++i) { String c = m_history.get(i); if (c.startsWith(text)) completions.add(c); } addAllCompletions(completions); if (m_disableCompletions) return; int size = completions.size(); if (text.length() > 0 && (size > 1 || (size == 1 && ! text.equals(completions.get(0))))) m_comboBox.showPopup(); else m_comboBox.hidePopup(); } private void save(JFrame parent, String s, int linesTruncated) { File file = FileDialogs.showSave(parent, null, m_messageDialogs); if (file == null) return; try { PrintStream out = new PrintStream(file); out.println("# Name: " + m_programName); out.println("# Version: " + m_programVersion); out.println("# Command: " + m_programCommand); out.println("# Lines truncated: " + linesTruncated); out.print(s); out.close(); } catch (FileNotFoundException e) { m_messageDialogs.showError(parent, i18n("MSG_SHELL_SAVE_FAILURE"), ""); } } private void showError(String mainMessage, String optionalMessage, boolean isCritical) { m_messageDialogs.showError(this, mainMessage, optionalMessage, isCritical); } private void scrollPage(boolean up) { JViewport viewport = m_scrollPane.getViewport(); Point position = viewport.getViewPosition(); int delta = m_scrollPane.getSize().height - m_gtpShellText.getFont().getSize(); if (up) { position.y -= delta; if (position.y < 0) position.y = 0; } else { position.y += delta; int max = viewport.getViewSize().height - m_scrollPane.getSize().height; if (position.y > max) position.y = max; } viewport.setViewPosition(position); } }