/* * Copyright 2000-2009 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.community.intellij.plugins.communitycase.ui; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.ui.DocumentAdapter; import com.intellij.util.containers.HashMap; import org.community.intellij.plugins.communitycase.Branch; import org.community.intellij.plugins.communitycase.Remote; import org.community.intellij.plugins.communitycase.Tag; import org.community.intellij.plugins.communitycase.commands.StringScanner; import org.community.intellij.plugins.communitycase.i18n.Bundle; import org.community.intellij.plugins.communitycase.validators.BranchNameValidator; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.Nullable; import javax.swing.*; import javax.swing.event.DocumentEvent; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.table.AbstractTableModel; import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.ArrayList; import java.util.Arrays; import java.util.SortedSet; import java.util.TreeSet; /** * The component that allows specifying a list of references */ public class RefspecPanel extends JPanel { /** * The logger for the class */ private static final Logger log = Logger.getInstance("#"+RefspecPanel.class.getName()); /** * Named remotes associated with the current root */ private final HashMap<String, Remote> myRemotes = new HashMap<String, Remote>(); /** * The project */ private Project myProject; /** * The root for mapping */ private VirtualFile myRoot; /** * Remote heads (for Add... dialog) */ private final SortedSet<String> myRemoteHeads = new TreeSet<String>(); /** * Remote tags (for Add... dialog) */ private final SortedSet<String> myRemoteTags = new TreeSet<String>(); /** * The button that adds all branches button */ private JButton myAddAllBranchesButton; /** * The button that adds selected references */ private JButton myAddButton; /** * The button that removes currently selected entries from the table */ private JButton myRemoveButton; /** * The text that contains remote name */ private JTextField myRemoteNameTextField; /** * The root panel of the form */ private JPanel myPanel; /** * The references table */ private JTable myReferences; /** * The button that adds entry that maps all tags */ private JButton myAddAllTagsButton; /** * Restore default mapping button */ private JButton myDefaultButton; /** * The name of the remote */ private String myRemote; /** * The source of default references */ private ReferenceSource myReferenceSource; /** * Mapping table model */ private final MyMappingTableModel myReferencesModel = new MyMappingTableModel(); /** * A constructor */ public RefspecPanel() { super(new GridBagLayout()); GridBagConstraints c = new GridBagConstraints(); c.gridx = 0; c.gridy = 0; c.weightx = 1; c.weighty = 1; c.fill = GridBagConstraints.BOTH; add(myPanel, c); setupTable(); setupButtons(); } /** * Validates fields * * @return null if there is no error; empty string means that there is no error yet but OK should be disabled; otherwise error text should be used as the current error for dialog */ @Nullable public String validateFields() { final String remote = getRemoteName(); if (remote.length() == 0) { if (myReferencesModel.isRemoteNameUsed()) { return Bundle.getString("refspec.validation.remote.is.blank"); } } else { if (!BranchNameValidator.INSTANCE.checkInput(remote)) { return Bundle.getString("refspec.validation.remote.invalid"); } } return null; } /** * Set project for panel * * @param project the context project */ public void setProject(Project project) { myProject = project; } /** * Setup add/remove buttons */ private void setupButtons() { // disable ok button if nothing is selected myReferences.getSelectionModel().addListSelectionListener(new ListSelectionListener() { public void valueChanged(final ListSelectionEvent e) { myRemoveButton.setEnabled(myReferences.getSelectedRowCount() != 0); } }); // remove selected mappings myRemoveButton.addActionListener(new ActionListener() { public void actionPerformed(final ActionEvent e) { myReferencesModel.removeSelectedMapping(); } }); // add all tags (mapped to tags directory) myAddAllTagsButton.addActionListener(new ActionListener() { public void actionPerformed(final ActionEvent e) { myReferencesModel.addMapping(false, tagName("*"), tagName("*")); } }); // all heads (mapped to remotes directory) myAddAllBranchesButton.addActionListener(new ActionListener() { public void actionPerformed(final ActionEvent e) { addAllBranches(); } }); // map selected tags and heads myAddButton.addActionListener(new ActionListener() { public void actionPerformed(final ActionEvent e) { if (myRoot == null) { throw new IllegalStateException(" root must be already set at this point."); } RefspecAddRefsDialog d = new RefspecAddRefsDialog(myProject, myRoot, myRemote, myRemoteTags, myRemoteHeads); d.show(); if (!d.isOK()) { return; } for (String tag : d.getSelected(true)) { myReferencesModel.addMapping(false, tag, tag); } for (String head : d.getSelected(false)) { myReferencesModel.addMapping(true, head, remoteName(head.substring(Branch.REFS_HEADS_PREFIX.length()))); } } }); myDefaultButton.addActionListener(new ActionListener() { public void actionPerformed(final ActionEvent e) { String remote = myRemote; setRemote(null); setRemote(remote); } }); } /** * Add mapping for all branches */ private void addAllBranches() { myReferencesModel.addMapping(false, headName("*"), remoteName("*")); } /** * Generate tag with remote name * * @param remoteName the name of remote in the local system * @param tagName the name of the tag * @return the full path to the head */ private static String tagRemoteName(final String remoteName, final String tagName) { return Tag.REFS_TAGS_PREFIX + remoteName + "/" + tagName; } /** * Simple tag name * * @param tagName the short name of tag * @return the fully qualified tag reference name */ private static String tagName(final String tagName) { return Tag.REFS_TAGS_PREFIX + tagName; } /** * Generate remote head name in local file system, note that as name of remote {@link #getRemoteName()} is used. * * @param headName the name head of remote in the local system * @return the full path to the head */ private String remoteName(final String headName) { return remoteName(getRemoteName(), headName); } /** * Generate remote name in local file system * * @param remote a remote name, if blank a local branch is returned. * @param headName the name head of remote in the local system * @return the full path to the head */ private static String remoteName(final String remote, final String headName) { return remote.length() != 0 ? Branch.REFS_REMOTES_PREFIX + remote + "/" + headName : headName(headName); } /** * Generate head name * * @param head the head name * @return the full path to the head */ private static String headName(final String head) { return Branch.REFS_HEADS_PREFIX + head; } /** * @return the current name of the remote */ public String getRemoteName() { return myRemoteNameTextField.getText(); } /** * Setup table header and table model */ private void setupTable() { // setup model myReferences.setModel(myReferencesModel); myReferences.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); myReferences.getColumnModel().getColumn(MyMappingTableModel.FORCE_COLUMN).sizeWidthToFit(); myRemoteNameTextField.getDocument().addDocumentListener(new DocumentAdapter() { protected void textChanged(final DocumentEvent e) { myReferencesModel.remoteUpdated(); } }); } /** * Set root for reference mapping * * @param Root a root */ public void setRoot(final VirtualFile Root) { if (Root == myRoot) { return; } myRoot = Root; myRemotes.clear(); if (myRoot != null) { try { for (Remote r : Remote.list(myProject, myRoot)) { myRemotes.put(r.name(), r); } } catch (VcsException e) { UiUtil.showOperationError(myProject, e, "listing remotes"); } } } /** * Set remote or url * * @param name a name of remote or URL */ public void setRemote(String name) { if (name != null && name.length() == 0) { name = null; } if (myRemote == null && name == null || myRemote != null && myRemote.equals(name)) { return; } myRemote = name; myAddButton.setEnabled(myRemote != null && myRemote.length() != 0); final Remote remote = myRemotes.get(name); if (remote != null) { myRemoteNameTextField.setText(name); myRemoteNameTextField.setEditable(false); myDefaultButton.setEnabled(true); } else { myRemoteNameTextField.setText(""); myRemoteNameTextField.setEditable(true); myDefaultButton.setEnabled(false); } myRemoteHeads.clear(); myRemoteTags.clear(); setDefaultMapping(); } /** * Set default mapping */ private void setDefaultMapping() { final Remote remote = myRemotes.get(myRemote); myReferencesModel.clear(); if (remote != null && myReferenceSource == ReferenceSource.FETCH) { try { for (String ref : Remote.getFetchSpecs(myProject, myRoot, remote.name())) { StringScanner s = new StringScanner(ref); boolean force = s.tryConsume('+'); String remotePart = s.boundedToken(':'); String localPart = s.line(); myReferencesModel.addMapping(force, remotePart, localPart); } } catch (VcsException e) { log.error("Failed to get fetch references ", e); } } else { addAllBranches(); } } /** * Add listener that is fired when validation is required * * @param l a listener to add */ public void addValidationRequiredListener(final ActionListener l) { myRemoteNameTextField.getDocument().addDocumentListener(new DocumentAdapter() { protected void textChanged(final DocumentEvent e) { //noinspection HardCodedStringLiteral l.actionPerformed(new ActionEvent(myRemoteNameTextField, ActionEvent.ACTION_PERFORMED, "validationRequired")); } }); } /** * Set default reference source for panel. * * @param referenceSource a reference source */ public void setReferenceSource(final ReferenceSource referenceSource) { myReferenceSource = referenceSource; } /** * @return references added to the model */ public String[] getReferences() { return myReferencesModel.getReferences(); } /** * Mapping table model */ private class MyMappingTableModel extends AbstractTableModel { /** * Force column in the table */ private static final int FORCE_COLUMN = 0; /** * Remote reference column in the table */ private static final int REMOTE_COLUMN = 1; /** * Local reference column in the table */ private static final int LOCAL_COLUMN = 2; /** * Remote name used for the table update */ private String mySavedRemoteName = null; /** * The currently constructed mapping */ private final ArrayList<RefMapping> myMapping = new ArrayList<RefMapping>(); /** * {@inheritDoc} */ public int getRowCount() { return myMapping.size(); } /** * Remove currently selected mappings */ public void removeSelectedMapping() { final int[] rows = myReferences.getSelectedRows(); Arrays.sort(rows); for (int i = rows.length - 1; i >= 0; i--) { myMapping.remove(rows[i]); } fireTableDataChanged(); } /** * {@inheritDoc} */ public int getColumnCount() { return LOCAL_COLUMN + 1; } /** * {@inheritDoc} */ @Override public boolean isCellEditable(final int rowIndex, final int columnIndex) { return true; } /** * {@inheritDoc} */ @Override public void setValueAt(final Object aValue, final int rowIndex, final int columnIndex) { RefMapping m = myMapping.get(rowIndex); switch (columnIndex) { case FORCE_COLUMN: m.force = ((Boolean)aValue).booleanValue(); break; case LOCAL_COLUMN: m.local = (String)aValue; break; case REMOTE_COLUMN: m.remote = (String)aValue; break; default: throw new IllegalStateException("Invalid column: " + columnIndex); } } /** * {@inheritDoc} */ @Override public String getColumnName(final int column) { switch (column) { case FORCE_COLUMN: return Bundle.getString("refspec.column.force"); case LOCAL_COLUMN: return Bundle.getString("refspec.column.local"); case REMOTE_COLUMN: return Bundle.getString("refspec.column.remote"); default: throw new IllegalStateException("Invalid column: " + column); } } /** * {@inheritDoc} */ public Object getValueAt(final int rowIndex, final int columnIndex) { RefMapping m = myMapping.get(rowIndex); switch (columnIndex) { case FORCE_COLUMN: return m.force; case LOCAL_COLUMN: return m.local; case REMOTE_COLUMN: return m.remote; default: throw new IllegalStateException("Invalid column: " + columnIndex); } } /** * Add mapping * * @param force a force flag * @param remote a remote reference * @param local a local reference */ public void addMapping(final boolean force, @NonNls final String remote, @NonNls final String local) { final RefMapping m = new RefMapping(); m.force = force; m.remote = remote; m.local = local; int row = myMapping.size(); myMapping.add(m); fireTableRowsInserted(row, row); if (mySavedRemoteName == null) { remoteUpdated(); } } /** * This method updates all local heads in the table with remote name */ private void remoteUpdated() { String newText = myRemoteNameTextField.getText(); if (mySavedRemoteName != null && !newText.equals(mySavedRemoteName)) { @NonNls String oldTagsPrefix = tagRemoteName(mySavedRemoteName, ""); @NonNls String newTagsPrefix = tagRemoteName(newText, ""); @NonNls String oldHeadsPrefix = remoteName(mySavedRemoteName, ""); @NonNls String newHeadsPrefix = remoteName(newText, ""); for (RefMapping m : myMapping) { if (m.local.startsWith(oldTagsPrefix)) { m.local = newTagsPrefix + m.local.substring(oldTagsPrefix.length()); } else if (m.local.startsWith(oldHeadsPrefix)) { m.local = newHeadsPrefix + m.local.substring(oldHeadsPrefix.length()); } } fireTableDataChanged(); } mySavedRemoteName = newText; } @Override public Class<?> getColumnClass(final int columnIndex) { if (columnIndex == FORCE_COLUMN) { return Boolean.class; } return super.getColumnClass(columnIndex); } /** * @return true if remote name is actually used in the entries */ boolean isRemoteNameUsed() { String text = myRemoteNameTextField.getText(); @NonNls String tagsPrefix = tagRemoteName(text, ""); for (RefMapping m : myMapping) { if (m.local.startsWith(tagsPrefix)) { return true; } } return false; } /** * Clear the mapping */ public void clear() { myMapping.clear(); fireTableDataChanged(); } /** * @return a list of references */ public String[] getReferences() { final int n = myMapping.size(); String[] rc = new String[n]; for (int i = 0; i < n; i++) { rc[i] = myMapping.get(i).toString(); } return rc; } /** * Reference mapping object used in the table model */ class RefMapping { /** * if true update is forced */ boolean force; /** * remote reference name */ String remote; /** * local reference name */ String local; /** * {@inheritDoc} */ @Override public String toString() { return (force ? "+" : "") + remote + ":" + local; } } } /** * The source of default references */ public enum ReferenceSource { /** * The references are pulled from fetch specification */ FETCH, /** * The references are pulled from push specification */ PUSH, } }