/* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2016 Maxence Bernard * * muCommander is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * muCommander is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.mucommander.ui.dialog.shell; import java.awt.BorderLayout; import java.awt.Container; import java.awt.Dimension; import java.awt.FlowLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.PrintStream; import javax.swing.Box; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTextArea; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.mucommander.commons.file.protocol.FileProtocols; import com.mucommander.process.AbstractProcess; import com.mucommander.process.ProcessListener; import com.mucommander.shell.Shell; import com.mucommander.shell.ShellHistoryManager; import com.mucommander.text.Translator; import com.mucommander.ui.action.ActionProperties; import com.mucommander.ui.action.impl.RunCommandAction; import com.mucommander.ui.dialog.DialogToolkit; import com.mucommander.ui.dialog.FocusDialog; import com.mucommander.ui.icon.SpinningDial; import com.mucommander.ui.layout.XBoxPanel; import com.mucommander.ui.layout.YBoxPanel; import com.mucommander.ui.main.MainFrame; import com.mucommander.ui.theme.Theme; import com.mucommander.ui.theme.ThemeManager; /** * Dialog used to execute a user-defined command. * <p> * Creates and displays a new dialog allowing the user to input a command which will be executed once the action is confirmed. * The command output of the user command is displayed in a text area * </p> * <p> * Note that even though this component is affected by themes, it's impossible to edit the current theme while it's being displayed. * For this reason, the RunDialog doesn't listen to theme modifications. * </p> * @author Maxence Bernard, Nicolas Rinaudo */ public class RunDialog extends FocusDialog implements ActionListener, ProcessListener, KeyListener { private static final Logger LOGGER = LoggerFactory.getLogger(RunDialog.class); // - UI components ------------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** Main frame this dialog depends on. */ private MainFrame mainFrame; /** Editable combo box used for shell input and history. */ private ShellComboBox inputCombo; /** Run/stop button. */ private JButton runStopButton; /** Cancel button. */ private JButton cancelButton; /** Clear shell history button. */ private JButton clearButton; /** Text area used to display the shell output. */ private JTextArea outputTextArea; /** Used to let the user known that the command is still running. */ private SpinningDial dial; // - Process management -------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** Stream used to send characters to the process' stdin process. */ private PrintStream processInput; /** Process currently running, <code>null</code> if none. */ private AbstractProcess currentProcess; // - Misc. class variables ----------------------------------------------------------- // ----------------------------------------------------------------------------------- /** Minimum dimensions for the dialog. */ private final static Dimension MINIMUM_DIALOG_DIMENSION = new Dimension(600, 400); // - Initialisation ------------------------------------------------------------------ // ----------------------------------------------------------------------------------- /** * Creates the dialog's shell output area. * @return a scroll pane containing the dialog's shell output area. */ private JScrollPane createOutputArea() { // Creates and initialises the output area. outputTextArea = new JTextArea(); outputTextArea.setLineWrap(true); outputTextArea.setCaretPosition(0); outputTextArea.setRows(10); outputTextArea.setEditable(false); outputTextArea.addKeyListener(this); // Applies the current theme to the shell output area. outputTextArea.setForeground(ThemeManager.getCurrentColor(Theme.SHELL_FOREGROUND_COLOR)); outputTextArea.setCaretColor(ThemeManager.getCurrentColor(Theme.SHELL_FOREGROUND_COLOR)); outputTextArea.setBackground(ThemeManager.getCurrentColor(Theme.SHELL_BACKGROUND_COLOR)); outputTextArea.setSelectedTextColor(ThemeManager.getCurrentColor(Theme.SHELL_SELECTED_FOREGROUND_COLOR)); outputTextArea.setSelectionColor(ThemeManager.getCurrentColor(Theme.SHELL_SELECTED_BACKGROUND_COLOR)); outputTextArea.setFont(ThemeManager.getCurrentFont(Theme.SHELL_FONT)); // Creates a scroll pane on the shell output area. return new JScrollPane(outputTextArea, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED); } /** * Creates the shell input part of the dialog. * @return the shell input part of the dialog. */ private YBoxPanel createInputArea() { YBoxPanel mainPanel; JPanel labelPanel; mainPanel = new YBoxPanel(); // Adds a textual description: // - if we're working in a local directory, 'run in current folder'. // - if we're working on a non-standard FS, 'run in home folder'. mainPanel.add(new JLabel(mainFrame.getActivePanel().getCurrentFolder().getURL().getScheme().equals(FileProtocols.FILE)? Translator.get("run_dialog.run_command_description")+":" : Translator.get("run_dialog.run_in_home_description")+":")); // Adds the shell input combo box. mainPanel.add(inputCombo = new ShellComboBox(this)); inputCombo.setEnabled(true); // Adds a textual description of the shell output area. mainPanel.addSpace(10); labelPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); labelPanel.add(new JLabel(Translator.get("run_dialog.command_output")+":")); labelPanel.add(new JLabel(dial = new SpinningDial())); mainPanel.add(labelPanel); return mainPanel; } /** * Creates a panel containing the dialog's buttons. * @return a panel containing the dialog's buttons. */ private XBoxPanel createButtonsArea() { // Buttons panel XBoxPanel buttonsPanel; buttonsPanel = new XBoxPanel(); // 'Clear history' button. buttonsPanel.add(clearButton = new JButton(Translator.get("run_dialog.clear_history"))); clearButton.addActionListener(this); // Separator. buttonsPanel.add(Box.createHorizontalGlue()); // 'Run / stop' and 'Cancel' buttons. buttonsPanel.add(DialogToolkit.createOKCancelPanel( runStopButton = new JButton(Translator.get("run_dialog.run")), cancelButton = new JButton(Translator.get("cancel")), getRootPane(), this)); return buttonsPanel; } /** * Creates and displays a new RunDialog. * @param mainFrame the main frame this dialog is attached to. */ public RunDialog(MainFrame mainFrame) { super(mainFrame, ActionProperties.getActionLabel(RunCommandAction.Descriptor.ACTION_ID), mainFrame); this.mainFrame = mainFrame; // Initializes the dialog's UI. Container contentPane = getContentPane(); contentPane.add(createInputArea(), BorderLayout.NORTH); contentPane.add(createOutputArea(), BorderLayout.CENTER); contentPane.add(createButtonsArea(), BorderLayout.SOUTH); // Sets default items. setInitialFocusComponent(inputCombo); getRootPane().setDefaultButton(runStopButton); // Makes sure that any running process will be killed when the dialog is closed. addWindowListener(new WindowAdapter() { @Override public void windowClosed(WindowEvent e) { if(currentProcess!=null) { processInput.close(); currentProcess.destroy(); } } }); // Sets the dialog's minimum size. setMinimumSize(MINIMUM_DIALOG_DIMENSION); } // - ProcessListener code ------------------------------------------------------------ // ----------------------------------------------------------------------------------- /** * Notifies the RunDialog that the current process has died. * @param retValue process' return code (not used). */ public void processDied(int retValue) { LOGGER.debug("process exit, return value= "+retValue); currentProcess = null; if(processInput!=null) { processInput.close(); processInput = null; } switchToRunState(); } /** * Ignored. */ public void processOutput(byte[] buffer, int offset, int length) {} /** * Notifies the RunDialog that the process has output some text. * @param output contains the process' output. */ public void processOutput(String output) {addToTextArea(output);} // - KeyListener code ---------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** * Notifies the RunDialog that a key has been pressed. * <p> * This method will ignore all events while a process is not running. If a process is running: * <ul> * <li><code>VK_ESCAPE</code> events are skipped and left to the <i>Cancel</i> button to handle.</li> * <li>Printable characters are passed to the process and consumed.</li> * <li>All other events are consumed.</li> * </ul> * </p> * <p> * At the time of writing, <code>tab</code> characters do not seem to be caught. * </p> * @param event describes the key event. */ public void keyPressed(KeyEvent event) { // Only handle keyPressed events when a process is running. if(currentProcess != null) { // Ignores VK_ESCAPE events, as their behavior is a bit strange: they register // as a printable character, and reacting to their being typed apparently consumes // the event - preventing the dialog from being closed. if(event.getKeyCode() != KeyEvent.VK_ESCAPE) { char character; // Only printable key typed are passed to the shell. if((character = event.getKeyChar()) != KeyEvent.CHAR_UNDEFINED) { processInput.print(character); addToTextArea(String.valueOf(character)); } event.consume(); } } } /** * Not used. */ public void keyTyped(KeyEvent event) {} /** * Not used. */ public void keyReleased(KeyEvent event) {} // - ActionListener code ------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** * Notifies the RunDialog that an action has been performed. * @param e describes the action that occurred. */ public void actionPerformed(ActionEvent e) { Object source = e.getSource(); // 'Clear shell history' has been pressed, clear shell history. if(source == clearButton) { ShellHistoryManager.clear(); // Sets the new focus depending on whether a process is currently running or not. if(currentProcess == null) { inputCombo.requestFocus(); outputTextArea.setText(""); } else { outputTextArea.requestFocus(); outputTextArea.getCaret().setVisible(true); } } // 'Run / stop' button has been pressed. else if(source == runStopButton) { // If we're not running a process, start a new one. if(currentProcess == null) runCommand(inputCombo.getCommand()); // If we're running a process, kill it. else { processInput.close(); currentProcess.destroy(); this.currentProcess = null; switchToRunState(); } } // Cancel button disposes the dialog and kills the process else if(source == cancelButton) { if(currentProcess != null) currentProcess.destroy(); dispose(); } } // - Misc. --------------------------------------------------------------------------- // ----------------------------------------------------------------------------------- /** * Switches the UI back to 'Run command' state. */ private void switchToRunState() { // Stops the spinning dial. dial.setAnimated(false); // Change 'Stop' button to 'Run' this.runStopButton.setText(Translator.get("run_dialog.run")); // Make command field active again this.inputCombo.setEnabled(true); inputCombo.requestFocus(); // Disables the caret in the process output area. outputTextArea.getCaret().setVisible(false); // Repaint this dialog repaint(); } /** * Runs the specified command. * @param command command to run. */ public void runCommand(String command) { try { // Starts the spinning dial. dial.setAnimated(true); // Change 'Run' button to 'Stop' this.runStopButton.setText(Translator.get("run_dialog.stop")); // Resets the process output area. outputTextArea.setText(""); outputTextArea.setCaretPosition(0); outputTextArea.getCaret().setVisible(true); outputTextArea.requestFocus(); // No new command can be entered while a process is running. inputCombo.setEnabled(false); currentProcess = Shell.execute(command, mainFrame.getActivePanel().getCurrentFolder(), this); processInput = new PrintStream(currentProcess.getOutputStream(), true); // Repaints the dialog. repaint(); } catch(Exception e) { // Notifies the user that an error occurred and resets to normal state. addToTextArea(Translator.get("generic_error")); switchToRunState(); } } /** * Appends the specified string to the shell output area. * @param s string to append to the shell output area. */ private void addToTextArea(String s) { outputTextArea.append(s); outputTextArea.setCaretPosition(outputTextArea.getText().length()); outputTextArea.getCaret().setVisible(true); outputTextArea.repaint(); } }