--- a/bugtracking.bridge/src/org/netbeans/modules/bugtracking/vcs/Bundle.properties Thu Jul 09 11:33:55 2009 +0200 +++ a/bugtracking.bridge/src/org/netbeans/modules/bugtracking/vcs/Bundle.properties Thu Jul 09 15:04:00 2009 +0200 @@ -73,6 +73,7 @@ HookPanel.repositoryLabel.text=Issue Trac&ker: HookPanel.changeRevisionFormatButton.text=(Change format...) HookPanel.changeIssueFormatButton.text=(Change format...) +HookPanel.loadingRepositories=Loading repositories... FormatPanel.aboveRadio.text=&Above the Message FormatPanel.beloveRadio.text=&Below the Message HookPanel.jButton2.AccessibleContext.accessibleDescription=N/A --- a/bugtracking.bridge/src/org/netbeans/modules/bugtracking/vcs/HgHookImpl.java Thu Jul 09 11:33:55 2009 +0200 +++ a/bugtracking.bridge/src/org/netbeans/modules/bugtracking/vcs/HgHookImpl.java Thu Jul 09 15:04:00 2009 +0200 @@ -233,21 +233,15 @@ @Override public JPanel createComponent(HgHookContext context) { - Repository[] repos = BugtrackingUtil.getKnownRepositories(); + File referenceFile; if(context.getFiles().length == 0) { + referenceFile = null; LOG.warning("creating hg hook component for zero files"); // NOI18N - Repository repoToSelect - = BugtrackingOwnerSupport.getInstance() - .getRepository(BugtrackingOwnerSupport.ContextType.ALL_PROJECTS); - panel = new HookPanel(repos, repoToSelect); } else { - File file = context.getFiles()[0]; - Repository repoToSelect = BugtrackingOwnerSupport.getInstance().getRepository(file, false); - if(repoToSelect == null) { - LOG.log(Level.FINE, " could not find issue tracker for " + file); // NOI18N - } - panel = new HookPanel(repos, repoToSelect); + referenceFile = context.getFiles()[0]; } + panel = new HookPanel(); + RepositorySelector.setup(panel, referenceFile); panel.changeRevisionFormatButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { onShowRevisionFormat(); --- a/bugtracking.bridge/src/org/netbeans/modules/bugtracking/vcs/HookPanel.java Thu Jul 09 11:33:55 2009 +0200 +++ a/bugtracking.bridge/src/org/netbeans/modules/bugtracking/vcs/HookPanel.java Thu Jul 09 15:04:00 2009 +0200 @@ -48,25 +48,39 @@ import java.awt.BorderLayout; import java.awt.Component; import java.awt.Dimension; +import java.awt.EventQueue; +import java.awt.Font; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; +import java.util.logging.Logger; import javax.swing.DefaultComboBoxModel; import javax.swing.DefaultListCellRenderer; import javax.swing.JButton; +import javax.swing.JLabel; import javax.swing.JList; +import javax.swing.event.PopupMenuEvent; +import javax.swing.event.PopupMenuListener; import org.netbeans.modules.bugtracking.ui.search.QuickSearchComboBar; import org.netbeans.modules.bugtracking.spi.Issue; import org.netbeans.modules.bugtracking.spi.Repository; import org.netbeans.modules.bugtracking.util.BugtrackingUtil; import org.netbeans.modules.versioning.util.VerticallyNonResizingPanel; +import org.openide.util.NbBundle; +import static java.util.logging.Level.FINER; /** * * @author Tomas Stupka + * @author Marian Petras */ public class HookPanel extends VerticallyNonResizingPanel implements ItemListener, PropertyChangeListener { + + private static Logger LOG = Logger.getLogger("org.netbeans.modules.bugtracking.vcshooks.HookPanel"); // NOI18N + + private static final String LOADING_REPOSITORIES = "loading"; //NOI18N + private QuickSearchComboBar qs; private Repository selectedRepository; @@ -98,38 +112,134 @@ } private UpdateFiledsState updateFiledsState = null; - public HookPanel(Repository[] repos, Repository toSelect) { + public HookPanel() { initComponents(); qs = new QuickSearchComboBar(this); issuePanel.add(qs, BorderLayout.NORTH); issueLabel.setLabelFor(qs.getCommand()); - - repositoryComboBox.setModel(new DefaultComboBoxModel(repos != null ? repos : new Repository[0])); + + repositoryComboBox.setModel(new DefaultComboBoxModel(new Object[] {LOADING_REPOSITORIES})); repositoryComboBox.setRenderer(new DefaultListCellRenderer() { + private final String loadingReposText = NbBundle.getMessage( + HookPanel.class, + "HookPanel.loadingRepositories"); //NOI18N @Override public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { - if(value != null) { - Repository r = (Repository) value; - value = r.getDisplayName(); + String text; + if (value == null) { + text = null; + } else if (value == LOADING_REPOSITORIES) { + text = loadingReposText; + } else { + text = ((Repository) value).getDisplayName(); } - return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + Component result = super.getListCellRendererComponent(list, + text, + index, + isSelected, + cellHasFocus); + if ((value == LOADING_REPOSITORIES) && (result instanceof JLabel)) { + JLabel label = (JLabel) result; + Font font = label.getFont(); + label.setFont(new Font(font.getName(), + font.getStyle() | Font.ITALIC, + font.getSize())); + } + return result; } }); repositoryComboBox.addItemListener(this); - if(toSelect != null) { - repositoryComboBox.setSelectedItem(toSelect); - qs.setRepository(toSelect); + enableFields(); + } + + void setRepositories(Repository[] repos) { + Repository[] comboData; + if (repos == null) { + comboData = new Repository[1]; + comboData[0] = null; } else { - if(repositoryComboBox.getItemCount() > 0) { - Repository repo = (Repository) repositoryComboBox.getItemAt(0); - repositoryComboBox.setSelectedItem(repo); - qs.setRepository(repo); + comboData = new Repository[repos.length + 1]; + comboData[0] = null; + if (repos.length != 0) { + System.arraycopy(repos, 0, comboData, 1, repos.length); } } - enableFields(); - + repositoryComboBox.setModel(new DefaultComboBoxModel(comboData)); + } + + /** + * Selects the given repository in the combo-box if no repository has been + * selected yet by the user. + * If the user had already selected some repository before this method + * was called, this method does nothing. If this method is called at + * the moment the popup of the combo-box is opened, the operation of + * pre-selecting the repository is deferred until the popup is closed. If + * the popup had been displayed at the moment this method was called + * and the user selects some repository during the period since the + * call of this method until the deferred selection takes place, the + * deferred selection operation is cancelled. + * + * @param repoToPreselect repository to preselect + */ + void preselectRepository(final Repository repoToPreselect) { + assert EventQueue.isDispatchThread(); + + if (repoToPreselect == null) { + LOG.finer("preselectRepository(null)"); //NOI18N + return; + } + + if (LOG.isLoggable(FINER)) { + LOG.finer("preselectRepository(" + repoToPreselect.getDisplayName() + ')'); //NOI18N + } + + if (isRepositorySelected()) { + LOG.finest(" - cancelled - already selected by the user"); //NOI18N + return; + } + + if (repositoryComboBox.isPopupVisible()) { + LOG.finest(" - the popup is visible - deferred"); //NOI18N + repositoryComboBox.addPopupMenuListener(new PopupMenuListener() { + public void popupMenuWillBecomeVisible(PopupMenuEvent e) { } + public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { + LOG.finer("popupMenuWillBecomeInvisible()"); //NOI18N + repositoryComboBox.removePopupMenuListener(this); + } + public void popupMenuCanceled(PopupMenuEvent e) { + LOG.finer("popupMenuCanceled()"); //NOI18N + repositoryComboBox.removePopupMenuListener(this); + LOG.finest(" - processing deferred selection"); //NOI18N + preselectRepositoryUnconditionally(repoToPreselect); + } + }); + } else { + preselectRepositoryUnconditionally(repoToPreselect); + } + } + + private void preselectRepositoryUnconditionally(Repository repoToPreselect) { + assert !isRepositorySelected(); + + if (LOG.isLoggable(FINER)) { + LOG.finer("preselectRepositoryUnconditionally(" + repoToPreselect.getDisplayName() + ')'); //NOI18N + } + + repositoryComboBox.setSelectedItem(repoToPreselect); + } + + /** + * Determines whether some bug-tracking repository is selected in the + * Issue Tracker combo-box. + * + * @return {@code true} if some repository is selected, + * {@code false} otherwise + */ + private boolean isRepositorySelected() { + Object selectedItem = repositoryComboBox.getSelectedItem(); + return (selectedItem != null) && (selectedItem != LOADING_REPOSITORIES); } Issue getIssue() { @@ -141,8 +251,8 @@ } private void enableFields() { - boolean repoSelected = repositoryComboBox.getSelectedItem() != null; - boolean enableUpdateFields = getIssue() != null && repoSelected; + boolean repoSelected = isRepositorySelected(); + boolean enableUpdateFields = repoSelected && (getIssue() != null); if(updateFiledsState == null) { updateFiledsState = new UpdateFiledsState(); @@ -398,9 +508,14 @@ // End of variables declaration//GEN-END:variables public void itemStateChanged(ItemEvent e) { + if (LOG.isLoggable(FINER)) { + LOG.finer("itemStateChanged() - selected item: " + e.getItem()); //NOI18N + } enableFields(); if(e.getStateChange() == ItemEvent.SELECTED) { - Repository repo = (Repository) e.getItem(); + Object item = e.getItem(); + Repository repo = (item != LOADING_REPOSITORIES) ? (Repository) item + : null; selectedRepository = repo; if(repo != null) { qs.setRepository(repo); --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ a5b3457a1d7a Thu Jul 09 15:04:00 2009 +0200 @@ -0,0 +1,230 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 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]" + * + * 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. + * + * Contributor(s): + * + * Portions Copyrighted 2009 Sun Microsystems, Inc. + */ + +package org.netbeans.modules.bugtracking.vcs; + +import java.awt.EventQueue; +import java.awt.event.HierarchyEvent; +import java.awt.event.HierarchyListener; +import java.io.File; +import java.util.logging.Logger; +import org.netbeans.modules.bugtracking.spi.Repository; +import org.netbeans.modules.bugtracking.util.BugtrackingOwnerSupport; +import org.netbeans.modules.bugtracking.util.BugtrackingUtil; +import org.openide.util.Exceptions; +import org.openide.util.RequestProcessor; +import static java.util.logging.Level.FINEST; + +/** + * Loads the list of repositories and determines the default one off the AWT + * thread. Once the results are ready, updates the UI (the combo-box). + * It is activated by method {@code hierarchyChanged} when the hook panel + * is displayed. At this moment, a routine for finding the known repositories + * and for determination the default repository is started in a separated + * thread. As soon as the list of known repositories is ready, the repositories + * combo-box is updated. When the default repository is determined, it is + * pre-selected in the combo-box, unless the user had already selected some + * repository. + * + * @author Marian Petras + */ +final class RepositorySelector implements HierarchyListener, Runnable { + + private static final Logger LOG + = Logger.getLogger(RepositorySelector.class.getName()); + + private final HookPanel hookPanel; + private final File refFile; + private boolean tooLate; + private boolean repositoriesDisplayed = false; + private boolean defaultRepoSelected = false; + private volatile Repository[] repositories; + private volatile boolean defaultRepoComputed; + private volatile Repository defaultRepo; + + static void setup(HookPanel hookPanel, File referenceFile) { + hookPanel.addHierarchyListener( + new RepositorySelector(hookPanel, referenceFile)); + } + + private RepositorySelector(HookPanel hookPanel, File refFile) { + super(); + this.hookPanel = hookPanel; + this.refFile = refFile; + } + + public void hierarchyChanged(HierarchyEvent e) { + if ((e.getChangeFlags() & HierarchyEvent.DISPLAYABILITY_CHANGED) != 0) { + assert e.getChanged() == hookPanel; + if (hookPanel.isDisplayable()) { + hookPanelDisplayed(); + } else { + hookPanelClosed(); + } + } + } + + private void hookPanelDisplayed() { + LOG.finer("hookPanelDisplayed()"); //NOI18N + RequestProcessor.getDefault().post(this); + } + + private void hookPanelClosed() { + /* + * The panel had been closed sooner than the default repository has been + * determined. + */ + tooLate = true; + hookPanel.removeHierarchyListener(this); + } + + public void run() { + if (RequestProcessor.getDefault().isRequestProcessorThread()) { + + loadRepositories(); + EventQueue.invokeLater(this); + + try { + findDefaultRepository(); + } finally { + defaultRepoComputed = true; + } + EventQueue.invokeLater(this); + + } else { + assert EventQueue.isDispatchThread(); + if (defaultRepoSelected) { + /* + * The default repository selection was performed during the + * previous invocation of this method from AWT thread + * (in one shot with displaying the list of available + * repositories). + */ + LOG.finest("run() called from AWT - nothing to do - all work already done"); //NOI18N + return; + } + + if (LOG.isLoggable(FINEST)) { + LOG.finest(!repositoriesDisplayed + ? "run() called from AWT - going to display the list of repositories" //NOI18N + : "run() called from AWT - going to select the repository"); //NOI18N + } + + if (tooLate) { + LOG.finest(" - too late - the HookPanel has been already closed"); //NOI18N + return; + } + + hookPanel.removeHierarchyListener(this); + + boolean repositoriesJustDisplayed = false; + if (!repositoriesDisplayed) { + hookPanel.setRepositories(repositories); + repositoriesJustDisplayed = true; + repositoriesDisplayed = true; + } + if (defaultRepoComputed) { + if (repositoriesJustDisplayed) { + LOG.finest(" - going also to select the default repository (if any)"); //NOI18N + } + try { + if (defaultRepo != null) { + hookPanel.preselectRepository(defaultRepo); + } else { + LOG.finest(" - default repository not determined - abort"); //NOI18N + } + } finally { + defaultRepoSelected = true; + } + } + } + } + + private void loadRepositories() { + assert RequestProcessor.getDefault().isRequestProcessorThread(); + LOG.finer("loadRepositories()"); //NOI18N + + long startTimeMillis = System.currentTimeMillis(); + + repositories = BugtrackingUtil.getKnownRepositories(); + + long endTimeMillis = System.currentTimeMillis(); + if (LOG.isLoggable(FINEST)) { + LOG.finest("BugtrackingUtil.getKnownRepositories() took " //NOI18N + + (endTimeMillis - startTimeMillis) + " ms."); //NOI18N + } + } + + private void findDefaultRepository() { + assert RequestProcessor.getDefault().isRequestProcessorThread(); + LOG.finer("findDefaultRepository()"); //NOI18N + + long startTimeMillis, endTimeMillis; + Repository result; + + startTimeMillis = System.currentTimeMillis(); + + if (refFile != null) { + result = BugtrackingOwnerSupport.getInstance() + .getRepository(refFile, false); + if ((result == null) && LOG.isLoggable(FINEST)) { + LOG.finest(" could not find issue tracker for " + refFile); //NOI18N + } + } else { + result = BugtrackingOwnerSupport.getInstance() + .getRepository(BugtrackingOwnerSupport.ContextType.ALL_PROJECTS); + } + + endTimeMillis = System.currentTimeMillis(); + + if (LOG.isLoggable(FINEST)) { + LOG.finest("BugtrackingOwnerSupport.getRepository(...) took " //NOI18N + + (endTimeMillis - startTimeMillis) + " ms."); //NOI18N + } + + if (result != null) { + if (LOG.isLoggable(FINEST)) { + LOG.finest(" - default repository: " + result.getDisplayName()); //NOI18N + } + defaultRepo = result; + } else { + LOG.finest(" - default repository: "); //NOI18N + } + } +} --- a/bugtracking.bridge/src/org/netbeans/modules/bugtracking/vcs/SvnHookImpl.java Thu Jul 09 11:33:55 2009 +0200 +++ a/bugtracking.bridge/src/org/netbeans/modules/bugtracking/vcs/SvnHookImpl.java Thu Jul 09 15:04:00 2009 +0200 @@ -199,21 +199,15 @@ @Override public JPanel createComponent(SvnHookContext context) { - Repository[] repos = BugtrackingUtil.getKnownRepositories(); + File referenceFile; if(context.getFiles().length == 0) { + referenceFile = null; LOG.warning("creating svn hook component for zero files"); // NOI18N - Repository repoToSelect - = BugtrackingOwnerSupport.getInstance() - .getRepository(BugtrackingOwnerSupport.ContextType.ALL_PROJECTS); - panel = new HookPanel(repos, repoToSelect); } else { - File file = context.getFiles()[0]; - Repository repoToSelect = BugtrackingOwnerSupport.getInstance().getRepository(file, false); - if(repoToSelect == null) { - LOG.log(Level.FINE, " could not find issue tracker for " + file); // NOI18N - } - panel = new HookPanel(repos, repoToSelect); + referenceFile = context.getFiles()[0]; } + panel = new HookPanel(); + RepositorySelector.setup(panel, referenceFile); panel.commitRadioButton.setVisible(false); panel.pushRadioButton.setVisible(false); panel.changeRevisionFormatButton.addActionListener(new ActionListener() {