/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 1997-2009 Sun Microsystems, Inc. All rights reserved.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common
* Development and Distribution License("CDDL") (collectively, the
* "License"). You may not use this file except in compliance with the
* License. You can obtain a copy of the License at
* http://www.netbeans.org/cddl-gplv2.html
* or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
* specific language governing permissions and limitations under the
* License. When distributing the software, include this License Header
* Notice in each file and include the License file at
* nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
* particular file as subject to the "Classpath" exception as provided
* by Sun in the GPL Version 2 section of the License file that
* accompanied this code. If applicable, add the following below the
* License Header, with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*
* Contributor(s):
*
* The Original Software is NetBeans. The Initial Developer of the Original
* Software is Sun Microsystems, Inc. Portions Copyright 1997-2009 Sun
* Microsystems, Inc. All Rights Reserved.
* Portions Copyright 2009 Alexander Coles (Ikonoklastik Productions).
*
* If you wish your version of this file to be governed by only the CDDL
* or only the GPL Version 2, indicate your decision by adding
* "[Contributor] elects to include this software in this distribution
* under the [CDDL or GPL Version 2] license." If you do not indicate a
* single choice of license, a recipient has the option to distribute
* your version of this file under either the CDDL, the GPL Version 2 or
* to extend the choice of license to its licensees as provided above.
* However, if you add GPL Version 2 code and therefore, elected the GPL
* Version 2 license, then the option applies only if the new code is
* made subject to such option by the copyright holder.
*/
package org.nbgit.ui.repository;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dialog;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Vector;
import javax.swing.ComboBoxEditor;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JPanel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import org.nbgit.GitModuleConfig;
import org.netbeans.api.options.OptionsDisplayer;
import org.netbeans.modules.versioning.util.DialogBoundsPreserver;
import org.openide.DialogDescriptor;
import org.openide.DialogDisplayer;
import org.openide.util.HelpCtx;
import org.openide.util.NbBundle;
import org.openide.util.Utilities;
import org.eclipse.jgit.transport.Transport;
import org.eclipse.jgit.transport.URIish;
/**
* @author Tomas Stupka
* @author Marian Petras
*/
public class GitRepositoryUI implements ActionListener, FocusListener, ItemListener {
public final static int FLAG_URL_ENABLED = 4;
public final static int FLAG_ACCEPT_REVISION = 8;
public final static int FLAG_SHOW_HINTS = 32;
public final static int FLAG_SHOW_PROXY = 64;
private final static String LOCAL_URL_HELP = "file:///repository_path"; // NOI18N
private final static String HTTP_URL_HELP = Utilities.isWindows()?
"http://[DOMAIN%5C]hostname/repository_path": // NOI18N
"http://hostname/repository_path"; // NOI18N
private final static String HTTPS_URL_HELP = Utilities.isWindows()?
"https://[DOMAIN%5C]hostname/repository_path": // NOI18N
"https://hostname/repository_path"; // NOI18N
private final static String GIT_URL_HELP = "git://hostname/repository_path"; // NOI18N
private final static String GIT_SSH_URL_HELP = "git@hostname:repository_path"; // NOI18N
private final static String SSH_URL_HELP = "ssh://hostname/repository_path"; // NOI18N
private RepositoryPanel repositoryPanel;
private boolean valid = true;
private List<ChangeListener> listeners;
private final ChangeEvent changeEvent = new ChangeEvent(this);
private Transport repositoryConnection;
private URIish url;
public static final String PROP_VALID = "valid"; // NOI18N
private String message;
private int modeMask;
private Dimension maxNeededSize;
private boolean bPushPull;
private static int GIT_PUSH_PULL_VERT_PADDING = 30;
private JTextComponent urlComboEditor;
private Document urlDoc, usernameDoc, passwordDoc, tunnelCmdDoc;
private boolean urlBeingSelectedFromPopup = false;
public GitRepositoryUI(int modeMask, String titleLabel, boolean bPushPull) {
this.modeMask = modeMask;
initPanel();
repositoryPanel.titleLabel.setText(titleLabel);
repositoryPanel.urlComboBox.setEnabled(isSet(FLAG_URL_ENABLED));
repositoryPanel.tunnelHelpLabel.setVisible(isSet(FLAG_SHOW_HINTS));
repositoryPanel.tipLabel.setVisible(isSet(FLAG_SHOW_HINTS));
//repositoryPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 0, 0));
// retrieve the dialog size for the largest configuration
if(bPushPull)
updateVisibility("foo:"); // NOI18N
else
updateVisibility("https:"); // NOI18N
maxNeededSize = repositoryPanel.getPreferredSize();
//TODO: implement this
//repositoryPanel.savePasswordCheckBox.setSelected(GitModuleConfig.getDefault().getSavePassword());
repositoryPanel.schedulePostInitRoutine(new Runnable() {
public void run() {
refreshUrlHistory();
}
});
}
public void actionPerformed(ActionEvent e) {
assert e.getSource() == repositoryPanel.proxySettingsButton;
onProxyConfiguration();
}
private void onProxyConfiguration() {
OptionsDisplayer.getDefault().open("General"); // NOI18N
}
private void initPanel() {
repositoryPanel = new RepositoryPanel();
urlComboEditor = (JTextComponent) repositoryPanel.urlComboBox
.getEditor().getEditorComponent();
urlDoc = urlComboEditor.getDocument();
usernameDoc = repositoryPanel.userTextField.getDocument();
passwordDoc = repositoryPanel.userPasswordField.getDocument();
tunnelCmdDoc = repositoryPanel.tunnelCommandTextField.getDocument();
DocumentListener documentListener = new DocumentChangeHandler();
urlDoc.addDocumentListener(documentListener);
passwordDoc.addDocumentListener(documentListener);
usernameDoc.addDocumentListener(documentListener);
tunnelCmdDoc.addDocumentListener(documentListener);
repositoryPanel.savePasswordCheckBox.addItemListener(this);
repositoryPanel.urlComboBox.addItemListener(this);
repositoryPanel.proxySettingsButton.addActionListener(this);
repositoryPanel.userPasswordField.addFocusListener(this);
tweakComboBoxEditor();
}
private void tweakComboBoxEditor() {
final ComboBoxEditor origEditor = repositoryPanel.urlComboBox.getEditor();
if (origEditor.getClass() == UrlComboBoxEditor.class) {
/* attempt to tweak the combo-box multiple times */
assert false;
return;
}
repositoryPanel.urlComboBox.setEditor(new UrlComboBoxEditor(origEditor));
}
/**
* Customized combo-box editor for displaying/modification of URL
* of a Git repository.
* It is customized in the following aspects:
* <ul>
* <li>When a RepositoryConnection is selected, displays its URL
* without user data (name and password).</li>
* <li>If a {@code RepositoryConnection} is set via method
* {@code setItem}, it holds a reference to it until another item
* is set via method {@code setItem()} or until the user modifies
* the text. This allows method {@code getItem()} to return
* the same item ({@code RepositoryConnection}).
* The allows the combo-box to correctly detect whether the item
* has been changed (since the last call of {@code setItem()}
* or not.</li>
* </ul>
*/
private final class UrlComboBoxEditor implements ComboBoxEditor,
DocumentListener {
private final ComboBoxEditor origEditor;
private Reference<Transport> repoConnRef;
private UrlComboBoxEditor(ComboBoxEditor originalEditor) {
this.origEditor = originalEditor;
((JTextComponent) originalEditor.getEditorComponent())
.getDocument().addDocumentListener(this);
}
public void setItem(Object anObject) {
urlBeingSelectedFromPopup = true;
try {
setItemImpl(anObject);
} finally {
urlBeingSelectedFromPopup = false;
}
}
private void setItemImpl(Object anObject) {
assert urlBeingSelectedFromPopup;
if (anObject instanceof Transport) {
Transport repoConn = (Transport) anObject;
repoConnRef = new WeakReference<Transport>(repoConn);
origEditor.setItem(repoConn.getURI().toString());
} else {
clearRepoConnRef();
origEditor.setItem(anObject);
}
}
public Component getEditorComponent() {
return origEditor.getEditorComponent();
}
public Object getItem() {
Transport repoConn = getRepoConn();
if (repoConn != null) {
return repoConn;
}
return origEditor.getItem();
}
public void selectAll() {
origEditor.selectAll();
}
public void addActionListener(ActionListener l) {
origEditor.addActionListener(l);
}
public void removeActionListener(ActionListener l) {
origEditor.removeActionListener(l);
}
public void insertUpdate(DocumentEvent e) {
textChanged();
}
public void removeUpdate(DocumentEvent e) {
textChanged();
}
public void changedUpdate(DocumentEvent e) {
textChanged();
}
private void textChanged() {
if (urlBeingSelectedFromPopup) {
return;
}
clearRepoConnRef();
}
private Transport getRepoConn() {
if (repoConnRef != null) {
Transport repoConn = repoConnRef.get();
if (repoConn != null) {
return repoConn;
}
}
return null;
}
private void clearRepoConnRef() {
if (repoConnRef != null) {
repoConnRef.clear();
}
}
}
public void refreshUrlHistory() {
repositoryPanel.urlComboBox.setModel(
new DefaultComboBoxModel(createPresetComboEntries()));
urlComboEditor.selectAll();
}
private Vector<?> createPresetComboEntries() {
assert repositoryPanel.urlComboBox.isEditable();
Vector<Object> result;
List<Transport> recentUrls = new ArrayList<Transport>(); // TODO: implement GitModuleConfig.getDefault().getRecentUrls();
GitURIScheme[] schemes = GitURIScheme.values();
result = new Vector<Object>(recentUrls.size() + schemes.length);
result.addAll(recentUrls);
for (GitURIScheme scheme : schemes) {
result.add(createURIPrefixForScheme(scheme));
}
return result;
}
private static String createURIPrefixForScheme(GitURIScheme scheme) {
if (scheme == GitURIScheme.FILE) {
return scheme + ":/"; //NOI18N
} else {
return scheme + "://"; //NOI18N
}
}
private final class DocumentChangeHandler implements DocumentListener {
DocumentChangeHandler() { }
public void insertUpdate(DocumentEvent e) {
textChanged(e);
}
public void removeUpdate(DocumentEvent e) {
textChanged(e);
}
public void changedUpdate(DocumentEvent e) {
textChanged(e);
}
private void textChanged(final DocumentEvent e) {
assert EventQueue.isDispatchThread();
Document modifiedDocument = e.getDocument();
assert modifiedDocument != null;
assert (modifiedDocument == urlDoc) || !urlBeingSelectedFromPopup;
if (modifiedDocument == urlDoc) {
onUrlChange();
} else if (modifiedDocument == usernameDoc) {
onUsernameChange();
} else if (modifiedDocument == passwordDoc) {
onPasswordChange();
} else if (modifiedDocument == tunnelCmdDoc) {
onTunnelCommandChange();
}
}
}
/**
* Always updates UI fields visibility.
*/
private void onUrlChange() {
if (!urlBeingSelectedFromPopup) {
repositoryConnection = null;
url = null;
repositoryPanel.userTextField.setText(null);
repositoryPanel.userPasswordField.setText(null);
repositoryPanel.tunnelCommandTextField.setText(null);
repositoryPanel.savePasswordCheckBox.setSelected(false);
}
// TODO: implementation of validation quickValidateUrl();
updateVisibility();
}
private void updateVisibility() {
updateVisibility(getUrlString());
}
/** Shows proper fields depending on Git connection method. */
private void updateVisibility(String selectedUrlString) {
boolean authFields = false;
boolean proxyFields = false;
boolean sshFields = false;
if(selectedUrlString.startsWith("http:")) { // NOI18N
repositoryPanel.tipLabel.setText(HTTP_URL_HELP);
authFields = true;
proxyFields = true;
} else if(selectedUrlString.startsWith("https:")) { // NOI18N
repositoryPanel.tipLabel.setText(HTTPS_URL_HELP);
//authFields = true;
proxyFields = true;
} else if (selectedUrlString.startsWith("git:")) { // NOI18N
repositoryPanel.tipLabel.setText(GIT_URL_HELP);
} else if(selectedUrlString.startsWith("git@")) { // NOI18N
repositoryPanel.tipLabel.setText(GIT_SSH_URL_HELP);
authFields = true;
proxyFields = true;
} else if(selectedUrlString.startsWith("ssh")) { // NOI18N
repositoryPanel.tipLabel.setText(SSH_URL_HELP);
sshFields = true;
} else if(selectedUrlString.startsWith("file:")) { // NOI18N
repositoryPanel.tipLabel.setText(LOCAL_URL_HELP);
} else {
repositoryPanel.tipLabel.setText(NbBundle.getMessage(GitRepositoryUI.class, "MSG_Repository_Url_Help", new Object [] { // NOI18N
LOCAL_URL_HELP, HTTP_URL_HELP, HTTPS_URL_HELP, GIT_URL_HELP, GIT_SSH_URL_HELP, SSH_URL_HELP
}));
}
repositoryPanel.userPasswordField.setVisible(authFields);
repositoryPanel.passwordLabel.setVisible(authFields);
repositoryPanel.userTextField.setVisible(authFields);
repositoryPanel.leaveBlankLabel.setVisible(authFields);
repositoryPanel.userLabel.setVisible(authFields);
//repositoryPanel.savePasswordCheckBox.setVisible(authFields);
repositoryPanel.savePasswordCheckBox.setVisible(false);
repositoryPanel.proxySettingsButton.setVisible(proxyFields && ((modeMask & FLAG_SHOW_PROXY) != 0));
repositoryPanel.savePasswordCheckBox.setVisible(false);
repositoryPanel.tunnelCommandTextField.setVisible(false);
repositoryPanel.tunnelCommandLabel.setVisible(false);
repositoryPanel.tunnelLabel.setVisible(false);
repositoryPanel.tunnelHelpLabel.setVisible(false);
}
public void setEditable(boolean editable) {
assert EventQueue.isDispatchThread();
repositoryPanel.urlComboBox.setEnabled(editable && isSet(FLAG_URL_ENABLED));
repositoryPanel.userTextField.setEnabled(editable && valid);
repositoryPanel.userPasswordField.setEnabled(editable && valid);
repositoryPanel.savePasswordCheckBox.setEnabled(editable && valid);
repositoryPanel.tunnelCommandTextField.setEnabled(editable && valid);
repositoryPanel.proxySettingsButton.setEnabled(editable && valid);
}
/**
* Load selected root from Swing structures (from arbitrary thread).
* @return null on failure
*/
public String getUrlString() {
return urlComboEditor.getText().trim();
}
private String getUsername() {
return repositoryPanel.userTextField.getText().trim();
}
private String getPassword() {
char[] password = repositoryPanel.userPasswordField.getPassword();
String result = new String(password);
Arrays.fill(password, (char) 0);
return result;
}
private String getExternalCommand() {
return repositoryPanel.tunnelCommandTextField.getText();
}
private boolean isSavePassword() {
return repositoryPanel.savePasswordCheckBox.isSelected();
}
public URIish getUrl() throws URISyntaxException, MalformedURLException {
prepareUrl();
assert (url != null);
return url;
}
public Transport getRepositoryConnection() throws URISyntaxException {
prepareRepositoryConnection();
assert (repositoryConnection != null);
return repositoryConnection;
}
private void prepareUrl() throws URISyntaxException, MalformedURLException {
if (url != null) {
return;
}
String urlString = getUrlString();
String username = getUsername();
if (username.length() == 0) {
url = new URIish(urlString);
} else {
// Parse the URL, create a URI and convert back to URL just to
// add the user + password.
URL jurl = new URL(urlString);
String protocol = jurl.getProtocol();
String host = jurl.getHost();
int port = jurl.getPort();
String path = jurl.getPath();
String userInfo = username + ":" + getPassword();
URI juri = new URI(protocol, userInfo, host, port, path, null, null);
url = new URIish(juri.toURL());
}
}
private void prepareRepositoryConnection() {
if (repositoryConnection != null) {
return;
}
String extCommand = getExternalCommand();
boolean savePassword = isSavePassword();
//repositoryConnection = new Transport(url, extCommand, savePassword);
// FIXME: we need the local repository
}
private void onUsernameChange() {
repositoryConnection = null;
url = null;
}
private void onPasswordChange() {
repositoryConnection = null;
url = null;
}
private void onTunnelCommandChange() {
repositoryConnection = null;
}
private void onSavePasswordChange() {
repositoryConnection = null;
}
public RepositoryPanel getPanel() {
return repositoryPanel;
}
public boolean isValid() {
return valid;
}
private void setValid() {
setValid(true, ""); //NOI18N
}
public void setInvalid() {
setValid(false, "");
}
private void setValid(boolean valid, String message) {
if ((valid == this.valid) && message.equals(this.message)) {
return;
}
if (valid != this.valid) {
repositoryPanel.proxySettingsButton.setEnabled(valid);
repositoryPanel.userPasswordField.setEnabled(valid);
repositoryPanel.userTextField.setEnabled(valid);
//repositoryPanel.savePasswordCheckBox.setEnabled(valid);
}
this.valid = valid;
this.message = message;
fireStateChanged();
}
private void fireStateChanged() {
if ((listeners != null) && !listeners.isEmpty()) {
for (ChangeListener l : listeners) {
l.stateChanged(changeEvent);
}
}
}
public void addChangeListener(ChangeListener l) {
if(listeners==null) {
listeners = new ArrayList<ChangeListener>(4);
}
listeners.add(l);
}
public void removeChangeListener(ChangeListener l) {
if(listeners==null) {
return;
}
listeners.remove(l);
}
public String getMessage() {
return message;
}
public void focusGained(FocusEvent focusEvent) {
if(focusEvent.getSource()==repositoryPanel.userPasswordField) {
repositoryPanel.userPasswordField.selectAll();
}
}
public void focusLost(FocusEvent focusEvent) {
// do nothing
}
public void itemStateChanged(ItemEvent evt) {
Object source = evt.getSource();
if (source == repositoryPanel.urlComboBox) {
if(evt.getStateChange() == ItemEvent.SELECTED) {
comboBoxItemSelected(evt.getItem());
}
} else if (source == repositoryPanel.savePasswordCheckBox) {
onSavePasswordChange();
} else {
assert false;
}
}
private void comboBoxItemSelected(Object selectedItem) {
if (selectedItem.getClass() == String.class) {
urlPrefixSelected();
} else if (selectedItem instanceof Transport) {
repositoryConnectionSelected((Transport) selectedItem);
} else {
assert false;
}
}
private void urlPrefixSelected() {
repositoryPanel.userTextField.setText(null);
repositoryPanel.userPasswordField.setText(null);
repositoryPanel.tunnelCommandTextField.setText(null);
repositoryPanel.savePasswordCheckBox.setSelected(false);
url = null;
repositoryConnection = null;
}
private void repositoryConnectionSelected(Transport rc) {
url = rc.getURI();
repositoryPanel.userTextField.setText(url.getUser());
repositoryPanel.userPasswordField.setText(url.getPass());
repositoryConnection = rc;
}
public void setTipVisible(Boolean flag) {
repositoryPanel.tipLabel.setVisible(flag);
}
public boolean show(String title, HelpCtx helpCtx, boolean setMaxNeddedSize) {
RepositoryDialogPanel corectPanel = new RepositoryDialogPanel();
corectPanel.panel.setLayout(new BorderLayout());
JPanel p = getPanel();
if(setMaxNeddedSize) {
if(bPushPull){
maxNeededSize.setSize(maxNeededSize.width, maxNeededSize.height + GIT_PUSH_PULL_VERT_PADDING);
}
p.setPreferredSize(maxNeededSize);
}
corectPanel.panel.add(p, BorderLayout.NORTH);
DialogDescriptor dialogDescriptor = new DialogDescriptor(corectPanel, title); // NOI18N
showDialog(dialogDescriptor, helpCtx, null);
return dialogDescriptor.getValue() == DialogDescriptor.OK_OPTION;
}
public Object show(String title, HelpCtx helpCtx, Object[] options, boolean setMaxNeededSize, String name) {
RepositoryDialogPanel corectPanel = new RepositoryDialogPanel();
corectPanel.panel.setLayout(new BorderLayout());
corectPanel.panel.add(getPanel(), BorderLayout.NORTH);
DialogDescriptor dialogDescriptor = new DialogDescriptor(corectPanel, title); // NOI18N
JPanel p = getPanel();
if(setMaxNeededSize) {
if(bPushPull){
maxNeededSize.setSize(maxNeededSize.width, maxNeededSize.height + GIT_PUSH_PULL_VERT_PADDING);
}
p.setPreferredSize(maxNeededSize);
}
if(options!= null) {
dialogDescriptor.setOptions(options); // NOI18N
}
showDialog(dialogDescriptor, helpCtx, name);
return dialogDescriptor.getValue();
}
private void showDialog(DialogDescriptor dialogDescriptor, HelpCtx helpCtx, String name) {
dialogDescriptor.setModal(true);
dialogDescriptor.setHelpCtx(helpCtx);
Dialog dialog = DialogDisplayer.getDefault().createDialog(dialogDescriptor);
if (name != null) {
dialog.addWindowListener(new DialogBoundsPreserver(GitModuleConfig.getDefault().getPreferences(), name)); // NOI18N
}
dialog.getAccessibleContext().setAccessibleDescription(NbBundle.getMessage(GitRepositoryUI.class, "ACSD_RepositoryPanel"));
dialog.setVisible(true);
}
private boolean isSet(int flag) {
return (modeMask & flag) != 0;
}
}