/* * Copyright (c) 2009 The Jackson Laboratory * * This software was developed by Gary Churchill's Lab at The Jackson * Laboratory (see http://research.jax.org/faculty/churchill). * * This 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. * * This software 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 software. If not, see <http://www.gnu.org/licenses/>. */ package org.jax.r.rintegration.gui; import java.awt.Graphics; import java.awt.event.ItemEvent; import java.io.File; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.JFileChooser; import javax.swing.JOptionPane; import javax.swing.ListSelectionModel; import javax.swing.SwingUtilities; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.table.DefaultTableModel; import javax.swing.table.TableColumn; import javax.swing.table.TableColumnModel; import org.jax.r.rintegration.PlatformSpecificRFunctions; import org.jax.r.rintegration.RInstallation; import org.jax.r.rintegration.RInstallationScanner; import org.jax.r.rintegration.RLaunchConfiguration; import org.jax.r.rintegration.RLaunchConfiguration.LaunchUsingEnum; import org.jax.r.rintegration.VersionStringComparator; import org.jax.util.TextWrapper; import org.jax.util.concurrent.SettableFuture; /** * This panel was mostly generated by the netbeans visual editor 6.0, so be * careful to respect the comments when editing this class. It may be best * to make modifications using Netbeans. * The main purpose for this panel is to make it easy for the user to select * an R_HOME value. * @author <A HREF="mailto:keith.sheppard@jax.org">Keith Sheppard</A> */ public class RHomeSelectorPanel extends javax.swing.JPanel { /** * our logger */ private static final Logger LOG = Logger.getLogger(RHomeSelectorPanel.class.getName()); /** * all {@link java.io.Serializable}s are supposed to have one of these */ private static final long serialVersionUID = 6441170357710988036L; /** * the scanner that we use to detect R Homes */ private final RInstallationScanner rHomeScanner = new RInstallationScanner(); /** * the install dir structure to assume */ private final PlatformSpecificRFunctions rPlatformSpecific; /** * the future R Home that lets us block callers */ private volatile SettableFuture<RLaunchConfiguration> futureSelectedLaunchConfiguration = new SettableFuture<RLaunchConfiguration>(); /** * the selected row number */ private volatile int selectedRHomeRowNumber = -1; /** * Flag that tells us to warn the user (once) if we don't find any * R installations. */ private volatile boolean warnAboutNoRInstallations; /** * Constructor * @param rPlatformSpecific * see {@link PlatformSpecificRFunctions} * @param warnAboutNoRInstallations * set this to true if the panel should warn the user * when no R installations are automatically detected */ // TODO remove inherit from environment stuff unless we're going to use it public RHomeSelectorPanel( PlatformSpecificRFunctions rPlatformSpecific, boolean warnAboutNoRInstallations) { this(null, rPlatformSpecific, warnAboutNoRInstallations); } /** * Constructor * @param initialInstallation * the initial selection for R installation * @param rPlatformSpecific * see {@link PlatformSpecificRFunctions} * @param warnAboutNoRInstallations * set this to true if the panel should warn the user * when no R installations are automatically detected */ public RHomeSelectorPanel( RInstallation initialInstallation, PlatformSpecificRFunctions rPlatformSpecific, boolean warnAboutNoRInstallations) { this.rPlatformSpecific = rPlatformSpecific; this.warnAboutNoRInstallations = warnAboutNoRInstallations; // call the netbeans built init method this.initComponents(); // call our init method this.postGuiInitialize(initialInstallation); } /** * This initialization method should be called after netbeans' * initialization method. * @param initialInstallation * the installation to start off with */ private void postGuiInitialize(RInstallation initialInstallation) { if(LOG.isLoggable(Level.FINE)) { LOG.fine( "Initializing RHomeSelectorPanel with initial installation: " + initialInstallation); } // detect as many R_HOME's as you can RInstallation[] detectedRHomes = this.rHomeScanner.scanForRInstallations( this.rPlatformSpecific); // populate the table with the R Homes that we found DefaultTableModel detectedRHomesTableModel = new DefaultTableModel() { /** * so we don't get serial id warnings */ private static final long serialVersionUID = 5258785851158027018L; /** * {@inheritDoc} */ @Override public boolean isCellEditable(int row, int column) { // no cellse are editable return false; } }; detectedRHomesTableModel.setColumnIdentifiers( TableColumnEnum.values()); for(int i = 0; i < detectedRHomes.length; i++) { // build a new table row RHomesTableCell[] currTableRow = new RHomesTableCell[TableColumnEnum.values().length]; for(int j = 0; j < TableColumnEnum.values().length; j++) { // build a new table cell currTableRow[j] = new RHomesTableCell( detectedRHomes[i], TableColumnEnum.values()[j]); } // add the new row to the table model detectedRHomesTableModel.addRow( currTableRow); } // toss our table model into the JTable this.detectedRHomesTable.setModel(detectedRHomesTableModel); this.detectedRHomesTable.setCellSelectionEnabled(false); this.detectedRHomesTable.setRowSelectionAllowed(true); ListSelectionModel selectionModel = this.detectedRHomesTable.getSelectionModel(); selectionModel.setSelectionMode( ListSelectionModel.SINGLE_SELECTION); if(initialInstallation != null) { for(int i = 0; i < detectedRHomes.length; i++) { if(detectedRHomes[i].equals(initialInstallation)) { selectionModel.setSelectionInterval(i, i); break; } } this.setSelectedRHomeFile(initialInstallation.getRHomeDirectory()); } // now take care of the row selection eventing selectionModel.addListSelectionListener( new ListSelectionListener() { public void valueChanged(ListSelectionEvent e) { RHomeSelectorPanel.this.detectedRHomeSelectionChanged(); } }); // resize the columns TableColumnModel columnModel = this.detectedRHomesTable.getColumnModel(); for(int i = 0; i < columnModel.getColumnCount(); i++) { TableColumn currColumn = columnModel.getColumn(i); if(TableColumnEnum.values()[i] == TableColumnEnum.VERSION_COLUMN) { currColumn.setPreferredWidth(50); } else { currColumn.setPreferredWidth(400); } } } /** * Respond to a selection change in the {@link #detectedRHomesTable}. */ private void detectedRHomeSelectionChanged() { // get the r home for the selected row int newlySelectedRowNumber = this.detectedRHomesTable.getSelectedRow(); if(newlySelectedRowNumber != this.selectedRHomeRowNumber) { this.selectedRHomeRowNumber = newlySelectedRowNumber; if(newlySelectedRowNumber >= 0) { if(LOG.isLoggable(Level.FINEST)) { LOG.finest( "newly selected R_HOME row is: " + newlySelectedRowNumber); } RHomesTableCell selectedCell = (RHomesTableCell)this.detectedRHomesTable.getValueAt( newlySelectedRowNumber, 0); this.setSelectedRHomeFile( selectedCell.getRHome().getRHomeDirectory()); } else { this.setSelectedRHomeFile(null); } } } /** * Apply the current settings. * @return * true if the apply completes, false if the apply doesn't complete * normally */ public boolean apply() { // see if the user wants us to get the R_HOME from the environment // or not if(!this.getInheritRHomeFromEnvironment()) { RInstallation selectedRHome = this.getValidatedSelectedRHomeNonBlocking(); // don't do anything unless we have a non-null validated reference if(selectedRHome != null) { try { if(!this.futureSelectedLaunchConfiguration.isDone()) { this.futureSelectedLaunchConfiguration.set(new RLaunchConfiguration( LaunchUsingEnum.LAUNCH_USING_SELECTED_INSTALLATION, selectedRHome)); } } catch(Exception ex) { LOG.log(Level.SEVERE, "failed to set future R_HOME", ex); } return true; } else { // validation failed (we got a null) so return false return false; } } else { try { if(!this.futureSelectedLaunchConfiguration.isDone()) { this.futureSelectedLaunchConfiguration.set(new RLaunchConfiguration( LaunchUsingEnum.LAUNCH_USING_ENVIRONMENT, null)); } } catch(Exception ex) { LOG.log(Level.SEVERE, "failed to set future R_HOME", ex); } return true; } } /** * Cancel this selection. * @return * true if cancel completes, false if the panel wants to stick * around */ public boolean cancel() { try { if(!this.futureSelectedLaunchConfiguration.isDone()) { this.futureSelectedLaunchConfiguration.set(null); } } catch(Exception ex) { LOG.log(Level.SEVERE, "failed to set future R_HOME to null", ex); } return true; } /** * Reset the state of this R_HOME selector panel. */ public void reset() { this.futureSelectedLaunchConfiguration = new SettableFuture<RLaunchConfiguration>(); this.detectedRHomesTable.getSelectionModel().clearSelection(); this.selectedRHomeTextField.setText(""); } /** * Blocks until the user cancels or accepts an R launch configuration * @return * the launch configuration or null if the user cancels */ public RLaunchConfiguration getSelectedLaunchConfiguration() { try { return this.futureSelectedLaunchConfiguration.get(); } catch(Exception ex) { LOG.log(Level.SEVERE, "caught exception while blocking for selected " + "launch configuration", ex); return null; } } /** This method is called from within the constructor to * initialize the form. * WARNING: Do NOT modify this code. The content of this method is * always regenerated by the Form Editor. */ @SuppressWarnings("all") // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents private void initComponents() { inheritRHomeFromEnvironmentCheckBox = new javax.swing.JCheckBox(); detectedRHomesLabel = new javax.swing.JLabel(); detectedRHomesScrollPane = new javax.swing.JScrollPane(); detectedRHomesTable = new javax.swing.JTable(); selectedRHomeLabel = new javax.swing.JLabel(); selectedRHomeTextField = new javax.swing.JTextField(); browseRHomesButton = new javax.swing.JButton(); inheritRHomeFromEnvironmentCheckBox.setText("Use Environment Variables (Not Recomended)"); inheritRHomeFromEnvironmentCheckBox.addItemListener(new java.awt.event.ItemListener() { public void itemStateChanged(java.awt.event.ItemEvent evt) { inheritRHomeFromEnvironmentCheckBoxItemStateChanged(evt); } }); detectedRHomesLabel.setText("Detected R Installations:"); detectedRHomesTable.setModel(new javax.swing.table.DefaultTableModel( new Object [][] { }, new String [] { } )); detectedRHomesScrollPane.setViewportView(detectedRHomesTable); selectedRHomeLabel.setText("R Home:"); selectedRHomeTextField.addInputMethodListener(new java.awt.event.InputMethodListener() { public void inputMethodTextChanged(java.awt.event.InputMethodEvent evt) { selectedRHomeTextFieldInputMethodTextChanged(evt); } public void caretPositionChanged(java.awt.event.InputMethodEvent evt) { } }); browseRHomesButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/images/action/browse-16x16.png"))); // NOI18N browseRHomesButton.setText("Browse..."); browseRHomesButton.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { browseRHomesButtonActionPerformed(evt); } }); org.jdesktop.layout.GroupLayout layout = new org.jdesktop.layout.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING) .add(layout.createSequentialGroup() .addContainerGap() .add(layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING) .add(detectedRHomesScrollPane, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, 513, Short.MAX_VALUE) .add(layout.createSequentialGroup() .add(selectedRHomeLabel) .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED) .add(selectedRHomeTextField, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, 364, Short.MAX_VALUE) .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED) .add(browseRHomesButton)) .add(detectedRHomesLabel)) .addContainerGap()) ); layout.setVerticalGroup( layout.createParallelGroup(org.jdesktop.layout.GroupLayout.LEADING) .add(layout.createSequentialGroup() .addContainerGap() .add(detectedRHomesLabel) .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED) .add(detectedRHomesScrollPane, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, 239, Short.MAX_VALUE) .addPreferredGap(org.jdesktop.layout.LayoutStyle.RELATED) .add(layout.createParallelGroup(org.jdesktop.layout.GroupLayout.BASELINE) .add(selectedRHomeLabel) .add(browseRHomesButton) .add(selectedRHomeTextField, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE, org.jdesktop.layout.GroupLayout.DEFAULT_SIZE, org.jdesktop.layout.GroupLayout.PREFERRED_SIZE)) .addContainerGap()) ); }// </editor-fold>//GEN-END:initComponents /** * Called when the user wants to browse the file system for the R_HOME * directory. * @param evt * the button event */ private void browseRHomesButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_browseRHomesButtonActionPerformed JFileChooser rHomeFileChooser = new JFileChooser(); rHomeFileChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); rHomeFileChooser.setDialogTitle( "Select an R Home Directory"); rHomeFileChooser.setAcceptAllFileFilterUsed(false); rHomeFileChooser.setApproveButtonMnemonic('S'); int optionUserSelected = rHomeFileChooser.showDialog( this, "Select R Home"); if(optionUserSelected == JFileChooser.APPROVE_OPTION) { this.setSelectedRHomeFile( rHomeFileChooser.getSelectedFile()); } }//GEN-LAST:event_browseRHomesButtonActionPerformed private void inheritRHomeFromEnvironmentCheckBoxItemStateChanged(java.awt.event.ItemEvent evt) {//GEN-FIRST:event_inheritRHomeFromEnvironmentCheckBoxItemStateChanged this.editsOccured(); this.inheritRHomeFromEnvironmentChanged( evt.getStateChange() == ItemEvent.SELECTED); }//GEN-LAST:event_inheritRHomeFromEnvironmentCheckBoxItemStateChanged private void selectedRHomeTextFieldInputMethodTextChanged(java.awt.event.InputMethodEvent evt) {//GEN-FIRST:event_selectedRHomeTextFieldInputMethodTextChanged this.editsOccured(); }//GEN-LAST:event_selectedRHomeTextFieldInputMethodTextChanged /** * this function is called when the "inherit from environment" state * changes * @param inheritRHomeFromEnvironment * the new value */ private void inheritRHomeFromEnvironmentChanged( boolean inheritRHomeFromEnvironment) { // enable or disable user selector controls this.detectedRHomesLabel.setEnabled(!inheritRHomeFromEnvironment); this.detectedRHomesTable.setEnabled(!inheritRHomeFromEnvironment); this.selectedRHomeLabel.setEnabled(!inheritRHomeFromEnvironment); this.selectedRHomeTextField.setEnabled(!inheritRHomeFromEnvironment); this.browseRHomesButton.setEnabled(!inheritRHomeFromEnvironment); } /** * Determines if the R_HOME and PATH come from the environment or if * they come from the selected {@link RInstallation} * <BR/> * <em> * WARNING: it is unsafe to call this function from any thread other than * the AWT thread * </em> * @return * true iff we're inheriting from our environment */ private boolean getInheritRHomeFromEnvironment() { return this.inheritRHomeFromEnvironmentCheckBox.isSelected(); } // Variables declaration - do not modify//GEN-BEGIN:variables private javax.swing.JButton browseRHomesButton; private javax.swing.JLabel detectedRHomesLabel; private javax.swing.JScrollPane detectedRHomesScrollPane; private javax.swing.JTable detectedRHomesTable; private javax.swing.JCheckBox inheritRHomeFromEnvironmentCheckBox; private javax.swing.JLabel selectedRHomeLabel; private javax.swing.JTextField selectedRHomeTextField; // End of variables declaration//GEN-END:variables /** * Get the selected r home as a file (no validation is performed) * @return * the selected R_HOME or null if there is no selected * or entered R_HOME */ private File getSelectedRHomeFile() { String selectedRHomeText = this.selectedRHomeTextField.getText().trim(); if(selectedRHomeText.length() > 0) { return new File( this.selectedRHomeTextField.getText()); } else { return null; } } /** * Set the selected r home * @param selectedRHomeFile * the new {@link RInstallation} selection */ private void setSelectedRHomeFile(File selectedRHomeFile) { if(selectedRHomeFile == null) { this.selectedRHomeTextField.setText(""); } else { this.selectedRHomeTextField.setText( selectedRHomeFile.getAbsolutePath()); } } /** * Get the user selected R Home. * <em> * WARNING: it is unsafe to call this function from any thread other than * the AWT thread * </em> * @return * the validated selected {@link RInstallation} or null if there is no * selected {@link RInstallation} or validation fails */ private RInstallation getValidatedSelectedRHomeNonBlocking() { RInstallation returnRHome = null; // 1st see if the user wants this to come from environment variables // before we even try to validate if(!this.getInheritRHomeFromEnvironment()) { File selectedRHomeFile = this.getSelectedRHomeFile(); if(selectedRHomeFile == null) { String message = "Please either enter or select an R Home directory."; JOptionPane.showMessageDialog( this, TextWrapper.wrapText( message, TextWrapper.DEFAULT_DIALOG_COLUMN_COUNT), "No R Home selected", JOptionPane.ERROR_MESSAGE); } else { returnRHome = this.rHomeScanner.rHomeDirectoryToRInstallation( this.rPlatformSpecific, selectedRHomeFile); if(returnRHome == null) { String errorMessage = "Failed to detect an R installation at the R Home selected \"" + selectedRHomeFile.getAbsolutePath() + "\"."; JOptionPane.showMessageDialog( this, TextWrapper.wrapText( errorMessage, TextWrapper.DEFAULT_DIALOG_COLUMN_COUNT), "Invalid R Home", JOptionPane.ERROR_MESSAGE); if(LOG.isLoggable(Level.FINE)) { LOG.fine("User error: " + errorMessage); } } else { String[] supportedSuperVersions = this.rPlatformSpecific.getSupportedRSuperVersions(); VersionStringComparator versionComparator = VersionStringComparator.getInstance(); if(supportedSuperVersions == null || supportedSuperVersions.length == 0) { String errorMessage = "Internal error. No known supported R versions."; JOptionPane.showMessageDialog( this, TextWrapper.wrapText( errorMessage, TextWrapper.DEFAULT_DIALOG_COLUMN_COUNT), "Internal Error", JOptionPane.ERROR_MESSAGE); LOG.severe( errorMessage + "(" + supportedSuperVersions + ")"); returnRHome = null; } else { if(returnRHome.getRVersion() == null) { String errorMessage = "Could not determine an R version for the selected " + "R Home: " + returnRHome.getRHomeDirectory(); JOptionPane.showMessageDialog( this, TextWrapper.wrapText( errorMessage, TextWrapper.DEFAULT_DIALOG_COLUMN_COUNT), "Unknown R Version", JOptionPane.ERROR_MESSAGE); LOG.warning(errorMessage); returnRHome = null; } else { if(!versionComparator.areAnySuperVersionOf( supportedSuperVersions, returnRHome.getRVersion())) { StringBuffer errorMessage = new StringBuffer( "The selected R Home (version " + returnRHome.getRVersion() + ") does not appear to " + "to match any of the supported R versions: "); for(int i = 0; i < supportedSuperVersions.length; i++) { String currSupportedVersion = supportedSuperVersions[i]; errorMessage.append(currSupportedVersion); errorMessage.append(".*"); if(i < supportedSuperVersions.length - 1) { // if it isn't the last one, add a comma errorMessage.append(", "); } } JOptionPane.showMessageDialog( this, TextWrapper.wrapText( errorMessage.toString(), TextWrapper.DEFAULT_DIALOG_COLUMN_COUNT), "Unsupported R Version", JOptionPane.ERROR_MESSAGE); LOG.warning( "User error: " + errorMessage); returnRHome = null; } } } } } } return returnRHome; } /** * The table column enum. Columns are declared in the order that they * should appear in the table. */ private static enum TableColumnEnum { /** * the version column */ VERSION_COLUMN { /** * {@inheritDoc} */ @Override public String toString() { return "Version"; } }, /** * the r home column */ R_HOME_COLUMN { /** * {@inheritDoc} */ @Override public String toString() { return "R Home Directory"; } } } /** * The type to throw into our R_HOME table's cells */ private static class RHomesTableCell { /** * @see #getTableColumn() */ private final TableColumnEnum tableColumn; /** * @see #getRHome() */ private final RInstallation rHome; /** * Constructor * @param rHome * see {@link #getRHome()} * @param tableColumn * see {@link #getTableColumn()} */ public RHomesTableCell(RInstallation rHome, TableColumnEnum tableColumn) { this.rHome = rHome; this.tableColumn = tableColumn; } /** * Get the table column for this cell. * @return the table column */ public TableColumnEnum getTableColumn() { return this.tableColumn; } /** * Get the R Home for this cell * @return the rHome */ public RInstallation getRHome() { return this.rHome; } /** * gets a string representation of this cell. the string will be * different depending on which column we're in */ @Override public String toString() { // return a different string depending on which column we're at switch(this.tableColumn) { case R_HOME_COLUMN: { return this.rHome.getRHomeDirectory().getAbsolutePath(); } case VERSION_COLUMN: { String rVersion = this.rHome.getRVersion(); if(rVersion == null) { // use a question mark if we don't know the version return "?"; } else { return rVersion; } } default: { // this is bad... we missed an enum LOG.severe( "Failed to have a case for R Home table column: " + this.tableColumn.toString()); return "?"; } } } } /** * deal with edit events */ private void editsOccured() { // if the future user selections are already set, then we should // start over with a clean slate if(this.futureSelectedLaunchConfiguration.isDone()) { this.futureSelectedLaunchConfiguration = new SettableFuture<RLaunchConfiguration>(); } } /** * Paint method overridden with code to warn the user if we need to. * @param g * the graphics context */ @Override public void paint(Graphics g) { super.paint(g); // see if we need to warn the user about not being able to detect // directories if(this.detectedRHomesTable.getRowCount() == 0 && this.warnAboutNoRInstallations) { // warn the user that we cant find any R installations final StringBuilder message = new StringBuilder(); message.append("Could not find any R installations in the default "); message.append("installation directory(s): "); File[] expectedInstallRoots = this.rPlatformSpecific.getExpectedInstallRoots(); for(int i = 0; i < expectedInstallRoots.length; i++) { if(i >= 1) { message.append(", "); } message.append(expectedInstallRoots[i]); } message.append(". If you know the location of an existing installation "); message.append("you can use the browse button to locate it. Otherwise, "); message.append("you should cancel and restart the application after "); message.append("installing R."); SwingUtilities.invokeLater(new Runnable() { public void run() { JOptionPane.showMessageDialog( RHomeSelectorPanel.this, TextWrapper.wrapText( message.toString(), TextWrapper.DEFAULT_DIALOG_COLUMN_COUNT), "No R Installations Detected", JOptionPane.WARNING_MESSAGE); } }); this.warnAboutNoRInstallations = false; } } }