org.netbeans.modules.quicksearch.QuickSearchPopup Maven / Gradle / Ivy
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.netbeans.modules.quicksearch;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FontMetrics;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.Preferences;
import javax.swing.JComponent;
import javax.swing.JLayeredPane;
import javax.swing.JList;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import org.netbeans.modules.quicksearch.ProviderModel.Category;
import org.netbeans.modules.quicksearch.recent.RecentSearches;
import org.netbeans.modules.quicksearch.ResultsModel.ItemResult;
import org.openide.util.NbBundle;
import org.openide.util.NbPreferences;
import org.openide.util.RequestProcessor;
import org.openide.util.Task;
import org.openide.util.TaskListener;
/**
* Component representing drop down for quick search
* @author Jan Becicka
*/
public class QuickSearchPopup extends javax.swing.JPanel
implements ListDataListener, ActionListener, TaskListener, Runnable {
private static final String CUSTOM_WIDTH = "customWidth"; //NOI18N
private static final int RESIZE_AREA_WIDTH = 5;
private AbstractQuickSearchComboBar comboBar;
private ResultsModel rModel;
/* Rect to store repetitive bounds computation */
private Rectangle popupBounds = new Rectangle();
private Timer updateTimer;
private static final int COALESCE_TIME = 300;
/** text to search for */
private String searchedText;
private int catWidth;
private int resultWidth;
private int defaultResultWidth = -1;
private int customWidth = -1;
private int longestText = -1;
private boolean canResize = false;
private Task evalTask;
private Task saveTask;
private static final RequestProcessor RP = new RequestProcessor(QuickSearchPopup.class);
private static final RequestProcessor evaluatorRP = new RequestProcessor(QuickSearchPopup.class + ".evaluator"); //NOI18N
private static final Logger LOG = Logger.getLogger(QuickSearchPopup.class.getName());
public QuickSearchPopup (AbstractQuickSearchComboBar comboBar) {
this.comboBar = comboBar;
initComponents();
loadSettings();
makeResizable();
rModel = ResultsModel.getInstance();
jList1.setModel(rModel);
jList1.setCellRenderer(new SearchResultRender(this));
rModel.addListDataListener(this);
if( "Aqua".equals(UIManager.getLookAndFeel().getID()) ) //NOI18N
jList1.setBackground(QuickSearchComboBar.getResultBackground());
updateStatusPanel(evalTask != null);
setVisible(false);
}
void invoke() {
ItemResult result = ((ItemResult) jList1.getModel().getElementAt(jList1.getSelectedIndex()));
if (result != null) {
RecentSearches.getDefault().add(result);
result.getAction().run();
if (comboBar.getCommand().isFocusOwner()) {
// Needed in case the focus couldn't be returned to the caller,
// see #228668.
comboBar.getCommand().setText(""); //NOI18N
}
clearModel();
}
}
void selectNext() {
int oldSel = jList1.getSelectedIndex();
if (oldSel >= 0 && oldSel < jList1.getModel().getSize() - 1) {
jList1.setSelectedIndex(oldSel + 1);
}
if (jList1.getModel().getSize() > 0) {
setVisible(true);
}
}
void selectPrev() {
int oldSel = jList1.getSelectedIndex();
if (oldSel > 0) {
jList1.setSelectedIndex(oldSel - 1);
}
if (jList1.getModel().getSize() > 0) {
setVisible(true);
}
}
public JList getList() {
return jList1;
}
public void clearModel () {
rModel.setContent(null);
longestText = -1;
}
public void maybeEvaluate (String text) {
this.searchedText = text;
if (text.length()>0) {
updateStatusPanel(true);
updatePopup(true);
}
if (updateTimer == null) {
updateTimer = new Timer(COALESCE_TIME, this);
}
if (!updateTimer.isRunning()) {
// first change in possible flurry, start timer
updateTimer.start();
} else {
// text change came too fast, let's wait until user calms down :)
updateTimer.restart();
}
}
/** implementation of ActionListener, called by timer,
* actually runs search */
@Override
public void actionPerformed(ActionEvent e) {
updateTimer.stop();
// search only if we are not cancelled already
if (comboBar.getCommand().isFocusOwner()) {
evaluatorRP.post(new Runnable() {
@Override
public void run() {
if (evalTask != null) {
evalTask.removeTaskListener(QuickSearchPopup.this);
}
evalTask = CommandEvaluator.evaluate(searchedText, rModel);
evalTask.addTaskListener(QuickSearchPopup.this);
// start waiting on all providers execution
RP.post(evalTask);
}
});
}
}
/** 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("unchecked")
// //GEN-BEGIN:initComponents
private void initComponents() {
java.awt.GridBagConstraints gridBagConstraints;
jScrollPane1 = new javax.swing.JScrollPane();
jList1 = new javax.swing.JList();
statusPanel = new javax.swing.JPanel();
searchingSep = new javax.swing.JSeparator();
searchingLabel = new javax.swing.JLabel();
noResultsLabel = new javax.swing.JLabel();
hintSep = new javax.swing.JSeparator();
hintLabel = new javax.swing.JLabel();
setBorder(javax.swing.BorderFactory.createLineBorder(QuickSearchComboBar.getPopupBorderColor()));
setLayout(new java.awt.BorderLayout());
jScrollPane1.setBorder(null);
jScrollPane1.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
jScrollPane1.setVerticalScrollBarPolicy(javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER);
jList1.setFocusable(false);
jList1.addMouseListener(new java.awt.event.MouseAdapter() {
public void mouseClicked(java.awt.event.MouseEvent evt) {
jList1MouseClicked(evt);
}
public void mousePressed(java.awt.event.MouseEvent evt) {
jList1MousePressed(evt);
}
});
jList1.addMouseMotionListener(new java.awt.event.MouseMotionAdapter() {
public void mouseDragged(java.awt.event.MouseEvent evt) {
jList1MouseDragged(evt);
}
public void mouseMoved(java.awt.event.MouseEvent evt) {
jList1MouseMoved(evt);
}
});
jScrollPane1.setViewportView(jList1);
add(jScrollPane1, java.awt.BorderLayout.CENTER);
statusPanel.setBackground(QuickSearchComboBar.getResultBackground());
statusPanel.setLayout(new java.awt.GridBagLayout());
gridBagConstraints = new java.awt.GridBagConstraints();
gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
gridBagConstraints.weightx = 1.0;
statusPanel.add(searchingSep, gridBagConstraints);
searchingLabel.setText(org.openide.util.NbBundle.getMessage(QuickSearchPopup.class, "QuickSearchPopup.searchingLabel.text")); // NOI18N
gridBagConstraints = new java.awt.GridBagConstraints();
gridBagConstraints.gridx = 0;
gridBagConstraints.gridy = 1;
gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST;
statusPanel.add(searchingLabel, gridBagConstraints);
noResultsLabel.setForeground(java.awt.Color.red);
noResultsLabel.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
noResultsLabel.setText(org.openide.util.NbBundle.getMessage(QuickSearchPopup.class, "QuickSearchPopup.noResultsLabel.text")); // NOI18N
noResultsLabel.setFocusable(false);
gridBagConstraints = new java.awt.GridBagConstraints();
gridBagConstraints.gridx = 0;
gridBagConstraints.gridy = 3;
gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
gridBagConstraints.weightx = 1.0;
statusPanel.add(noResultsLabel, gridBagConstraints);
gridBagConstraints = new java.awt.GridBagConstraints();
gridBagConstraints.gridx = 0;
gridBagConstraints.gridy = 4;
gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
gridBagConstraints.weightx = 1.0;
statusPanel.add(hintSep, gridBagConstraints);
hintLabel.setBackground(QuickSearchComboBar.getResultBackground());
hintLabel.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
gridBagConstraints = new java.awt.GridBagConstraints();
gridBagConstraints.gridx = 0;
gridBagConstraints.gridy = 5;
gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH;
statusPanel.add(hintLabel, gridBagConstraints);
add(statusPanel, java.awt.BorderLayout.PAGE_END);
}// //GEN-END:initComponents
private void jList1MouseMoved(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_jList1MouseMoved
// toggle resize/default cursor
if (evt.getX() < RESIZE_AREA_WIDTH) {
QuickSearchPopup.this.setCursor(Cursor.getPredefinedCursor(
Cursor.W_RESIZE_CURSOR));
} else {
QuickSearchPopup.this.setCursor(Cursor.getDefaultCursor());
}
// selection follows mouse move
Point loc = evt.getPoint();
int index = jList1.locationToIndex(loc);
if (index == -1) {
return;
}
Rectangle rect = jList1.getCellBounds(index, index);
if (rect != null && rect.contains(loc)) {
jList1.setSelectedIndex(index);
}
}//GEN-LAST:event_jList1MouseMoved
private void jList1MouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_jList1MouseClicked
if (!SwingUtilities.isLeftMouseButton(evt)) {
return;
}
// mouse left button click works the same as pressing Enter key
comboBar.invokeSelectedItem();
}//GEN-LAST:event_jList1MouseClicked
private void jList1MouseDragged(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_jList1MouseDragged
QuickSearchPopup.this.processMouseMotionEvent(evt);
}//GEN-LAST:event_jList1MouseDragged
private void jList1MousePressed(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_jList1MousePressed
QuickSearchPopup.this.processMouseEvent(evt);
}//GEN-LAST:event_jList1MousePressed
// Variables declaration - do not modify//GEN-BEGIN:variables
private javax.swing.JLabel hintLabel;
private javax.swing.JSeparator hintSep;
private javax.swing.JList jList1;
private javax.swing.JScrollPane jScrollPane1;
private javax.swing.JLabel noResultsLabel;
private javax.swing.JLabel searchingLabel;
private javax.swing.JSeparator searchingSep;
private javax.swing.JPanel statusPanel;
// End of variables declaration//GEN-END:variables
/*** impl of reactions to results data change */
public void intervalAdded(ListDataEvent e) {
updatePopup(evalTask != null);
}
public void intervalRemoved(ListDataEvent e) {
updatePopup(evalTask != null);
}
public void contentsChanged(ListDataEvent e) {
if (customWidth < 0) {
if (rModel.getContent() == null) {
longestText = -1;
resultWidth = -1;
} else {
for (CategoryResult r : rModel.getContent()) {
for (ItemResult i : r.getItems()) {
int l = i.getDisplayName().length();
if (l > longestText) {
longestText = l;
resultWidth = -1;
}
}
}
}
}
updatePopup(evalTask != null);
}
/**
* Updates size and visibility of this panel according to model content
*/
public void updatePopup (boolean isInProgress) {
updatePopup(isInProgress, true);
}
private void updatePopup (boolean isInProgress, boolean canRetry) {
int modelSize = rModel.getSize();
if (modelSize > 0 && jList1.getSelectedIndex()<0) {
jList1.setSelectedIndex(0);
}
// plug this popup into layered pane if needed
JLayeredPane lPane = JLayeredPane.getLayeredPaneAbove(comboBar);
if (lPane == null) {
// #162075 - return when comboBar not yet seeded in AWT hierarchy
return;
}
if (!isDisplayable()) {
lPane.add(this, new Integer(JLayeredPane.POPUP_LAYER + 1) );
}
boolean statusVisible = updateStatusPanel(isInProgress);
try {
computePopupBounds(popupBounds, lPane, modelSize);
} catch (Exception e) { //sometimes the hack in computePopupBounds fails
LOG.log(canRetry ? Level.INFO : Level.SEVERE, null, e);
retryUpdatePopup(canRetry, isInProgress);
return;
}
setBounds(popupBounds);
// popup visibility constraints
if ((modelSize > 0 || statusVisible) && comboBar.getCommand().isFocusOwner()) {
if (modelSize > 0 && !isVisible()) {
jList1.setSelectedIndex(0);
}
if (jList1.getSelectedIndex() >= modelSize) {
jList1.setSelectedIndex(modelSize - 1);
}
if (explicitlyInvoked || !searchedText.isEmpty()) {
setVisible(true);
}
} else {
setVisible(false);
}
explicitlyInvoked = false;
// needed on JDK 1.5.x to repaint correctly
revalidate();
}
/**
* Retry to update popup if something went wrong, but only if it is allowed.
* See bug 205356.
*/
private void retryUpdatePopup(boolean canRetry,
final boolean isInProgress) {
if (canRetry) {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
updatePopup(isInProgress, false); // do not retry again
}
});
}
}
private boolean explicitlyInvoked = false;
/** User actually pressed Ctrl-I; display popup even just for Recent Searches. */
void explicitlyInvoked() {
explicitlyInvoked = true;
}
public int getCategoryWidth () {
if (catWidth <= 0) {
catWidth = computeWidth(jList1, 20, 30);
}
return catWidth;
}
public int getResultWidth () {
if (customWidth > 0) {
return Math.max(customWidth, getDefaultResultWidth());
} else {
if (resultWidth <= 0) {
resultWidth = computeWidth(
jList1, limit(longestText, 42, 128), 50);
}
return resultWidth;
}
}
private int getDefaultResultWidth() {
if (defaultResultWidth <= 0) {
defaultResultWidth = computeWidth(jList1, 42, 50);
}
return defaultResultWidth;
}
private int limit(int value, int min, int max) {
assert min <= max;
return Math.min(max, Math.max(min, value));
}
public int getPopupWidth() {
int maxWidth = this.getParent() == null
? Integer.MAX_VALUE : this.getParent().getWidth() - 10;
return Math.min(getCategoryWidth() + getResultWidth() + 3, maxWidth);
}
/** Implementation of TaskListener, listen to when providers are finished
* with their searching work
*/
public void taskFinished(Task task) {
evalTask = null;
// update UI in ED thread
if (SwingUtilities.isEventDispatchThread()) {
run();
} else {
SwingUtilities.invokeLater(this);
}
}
/** Runnable implementation, updates popup */
public void run() {
updatePopup(evalTask != null);
}
private void computePopupBounds (Rectangle result, JLayeredPane lPane, int modelSize) {
Dimension cSize = comboBar.getSize();
int width = getPopupWidth();
Point location = new Point(cSize.width - width - 1, comboBar.getBottomLineY() - 1);
if (SwingUtilities.getWindowAncestor(comboBar) != null) {
location = SwingUtilities.convertPoint(comboBar, location, lPane);
}
result.setLocation(location);
// hack to make jList.getpreferredSize work correctly
// JList is listening on ResultsModel same as us and order of listeners
// is undefined, so we have to force update of JList's layout data
jList1.setFixedCellHeight(15);
jList1.setFixedCellHeight(-1);
// end of hack
jList1.setVisibleRowCount(modelSize);
Dimension preferredSize = jList1.getPreferredSize();
preferredSize.width = width;
preferredSize.height += statusPanel.getPreferredSize().height + 3;
result.setSize(preferredSize);
}
/** Computes width of string up to maxCharCount, with font of given JComponent
* and with maximum percentage of owning Window that can be taken */
private static int computeWidth (JComponent comp, int maxCharCount, int percent) {
FontMetrics fm = comp.getFontMetrics(comp.getFont());
int charW = fm.charWidth('X');
int result = charW * maxCharCount;
// limit width to 50% of containing window
Window w = SwingUtilities.windowForComponent(comp);
if (w != null) {
result = Math.min(result, w.getWidth() * percent / 100);
}
return result;
}
/** Updates visibility and content of status labels.
*
* @return true when update panel should be visible (some its part is visible),
* false otherwise
*/
private boolean updateStatusPanel (boolean isInProgress) {
boolean shouldBeVisible = false;
searchingSep.setVisible(isInProgress);
searchingLabel.setVisible(isInProgress);
if (comboBar instanceof QuickSearchComboBar) {
if (isInProgress) {
((QuickSearchComboBar) comboBar).startProgressAnimation();
} else {
((QuickSearchComboBar) comboBar).stopProgressAnimation();
}
}
shouldBeVisible = shouldBeVisible || isInProgress;
boolean searchedNotEmpty = searchedText != null && searchedText.trim().length() > 0;
boolean areNoResults = rModel.getSize() <= 0 && searchedNotEmpty && !isInProgress;
noResultsLabel.setVisible(areNoResults);
comboBar.setNoResults(areNoResults);
shouldBeVisible = shouldBeVisible || areNoResults;
hintLabel.setText(getHintText());
boolean isNarrowed = CommandEvaluator.isTemporaryCatSpecified() && searchedNotEmpty;
hintSep.setVisible(isNarrowed);
hintLabel.setVisible(isNarrowed);
shouldBeVisible = shouldBeVisible || isNarrowed;
return shouldBeVisible;
}
private String getHintText () {
Category temp = CommandEvaluator.getTemporaryCat();
if (temp != null) {
return NbBundle.getMessage(QuickSearchPopup.class,
"QuickSearchPopup.hintLabel.text", //NOI18N
temp.getDisplayName(), SearchResultRender.getKeyStrokeAsText(
comboBar.getKeyStroke()));
} else {
return null;
}
}
/**
* Register listeners that make this pop-up resizable.
*/
private void makeResizable() {
this.addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseDragged(MouseEvent e) {
if (canResize) {
customWidth = Math.max(1, getResultWidth() - e.getX());
run();
saveSettings();
}
}
});
this.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
canResize = e.getX() < RESIZE_AREA_WIDTH;
}
});
}
/**
* Load settings from preferences file.
*/
private void loadSettings() {
RP.post(new Runnable() {
@Override
public void run() {
Preferences p = NbPreferences.forModule(QuickSearchPopup.class);
customWidth = p.getInt(CUSTOM_WIDTH, -1);
}
});
}
/**
* Save settings to preferences file. Do nothing if this operation is
* already scheduled.
*/
private synchronized void saveSettings() {
if (saveTask == null) {
saveTask = RP.create(new Runnable() {
@Override
public void run() {
Preferences p = NbPreferences.forModule(
QuickSearchPopup.class);
p.putInt(CUSTOM_WIDTH, customWidth);
synchronized (QuickSearchPopup.this) {
saveTask = null;
}
}
});
RP.post(saveTask, 1000);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy