// BlogBridge -- RSS feed reader, manager, and web based service
// Copyright (C) 2002-2006 by R. Pito Salas
//
// This program 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 2 of the License, or (at your option) any later version.
//
// This program 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 program;
// if not, write to the Free Software Foundation, Inc., 59 Temple Place,
// Suite 330, Boston, MA 02111-1307 USA
//
// Contact: R. Pito Salas
// mailto:pitosalas@users.sourceforge.net
// More information: about BlogBridge
// http://www.blogbridge.com
// http://sourceforge.net/projects/blogbridge
//
// $Id: SearchDialog.java,v 1.26 2007/07/20 16:51:27 spyromus Exp $
//
package com.salas.bb.search;
import com.jgoodies.forms.layout.CellConstraints;
import com.jgoodies.uif.AbstractDialog;
import com.jgoodies.uif.util.Resizer;
import com.jgoodies.uif.util.ResourceUtils;
import com.jgoodies.uif.util.SystemUtils;
import com.salas.bb.utils.i18n.Strings;
import com.salas.bb.utils.uif.BBFormBuilder;
import com.salas.bb.utils.uif.ProgressSpinner;
import com.salas.bb.utils.uif.UifUtilities;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.text.MessageFormat;
/**
* Search dialog box.
*/
public class SearchDialog extends AbstractDialog
{
/** Minimum dialog size. */
private static final Dimension MIN_SIZE = new Dimension(500, 350);
private final SearchEngine searchEngine;
private ResultsList itemsList;
private JLabel lbResults;
private ProgressSpinner pgSpinner;
private final SearchDialog.ResultsListener resultsListener;
private SearchResultsListModel model;
/**
* Creates search dialog.
*
* @param owner the dialog's parent frame.
* @param engine search engine to use.
* @param listener selection listener.
*/
public SearchDialog(Frame owner, SearchEngine engine, ActionListener listener)
{
super(owner, Strings.message("search.dialog.title"));
searchEngine = engine;
model = new SearchResultsListModel();
itemsList = new ResultsList(model);
itemsList.addActionListener(listener);
lbResults = new JLabel();
UifUtilities.smallerFont(lbResults);
pgSpinner = new ProgressSpinner();
resultsListener = new ResultsListener();
searchEngine.getResult().addChangesListener(resultsListener);
setModal(false);
}
/** Release resources before closing. */
public void close()
{
searchEngine.getResult().removeChangesListener(resultsListener);
itemsList = null;
super.close();
getContentPane().removeAll();
}
/**
* Sets the dialog's resizable state. By default dialogs are non-resizable; subclasses may
* override.
*/
protected void setResizable()
{
setResizable(true);
}
/**
* Creates main content pane.
*
* @return the dialog's main content without header and border.
*/
protected JComponent buildContent()
{
JPanel panel = new JPanel(new BorderLayout());
panel.add(buildTopBar(), BorderLayout.NORTH);
panel.add(buildResultsPanel(), BorderLayout.CENTER);
return panel;
}
/**
* Creates top bar with progress indicator, results count and search field.
*
* @return top bar component.
*/
private Component buildTopBar()
{
final JLabel helpIcon = new JLabel(ResourceUtils.getIcon("search.ext.icon"));
helpIcon.setToolTipText(Strings.message("search.ext.text"));
helpIcon.setEnabled(false);
final JCheckBox chPinnedArticlesOnly = new JCheckBox(Strings.message("search.pinned.articles.only"));
UifUtilities.smallerFont(chPinnedArticlesOnly);
if (SystemUtils.IS_OS_MAC) chPinnedArticlesOnly.setMargin(new Insets(0, 0, 2, 0));
final SearchField tfSearch = new SearchField();
tfSearch.addKeyListener(new NavigationListener());
// Register changes monitor
ActionListener monitor = new SearchCriteriaChangeMonitor(chPinnedArticlesOnly, tfSearch, helpIcon);
tfSearch.addActionListener(monitor);
chPinnedArticlesOnly.addActionListener(monitor);
BBFormBuilder builder = new BBFormBuilder("p, 2dlu, 50dlu, 2dlu, p, 4dlu, p, 7dlu, p, 14dlu:grow, p");
JLabel lbSearch = builder.append(Strings.message("search.prompt"), 1);
lbSearch.setLabelFor(tfSearch);
UifUtilities.smallerFont(lbSearch);
UifUtilities.smallerFont(tfSearch);
builder.append(tfSearch);
builder.append(helpIcon);
builder.append(chPinnedArticlesOnly, 1, CellConstraints.DEFAULT, CellConstraints.CENTER);
builder.append(lbResults);
builder.append(pgSpinner);
builder.appendUnrelatedComponentsGapRow();
return builder.getPanel();
}
/**
* Creates results panel with results list and controls.
*
* @return results panel component.
*/
private Component buildResultsPanel()
{
JPanel panel = new JPanel(new BorderLayout());
JPanel panel2 = new JPanel(new BorderLayout());
panel2.add(itemsList, BorderLayout.NORTH);
panel2.setBackground(Color.WHITE);
panel.add(panel2, BorderLayout.CENTER);
panel.add(buildControlPanel(), BorderLayout.EAST);
JScrollPane sp = new JScrollPane(panel);
sp.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
return sp;
}
/**
* Returns currently selected item.
*
* @return item.
*/
public ResultItem getSelectedItem()
{
return itemsList == null ? null : itemsList.getSelectedItem();
}
/**
* Creates control panel with grouping, sorting and filtering options.
*
* @return control panel.
*/
private JComponent buildControlPanel()
{
BBFormBuilder builder = new BBFormBuilder("5dlu, p, 5dlu");
builder.appendRelatedComponentsGapRow();
builder.setLeadingColumnOffset(1);
builder.nextLine();
// Grouping
Action actGroup = new AbstractAction()
{
private ActionLabel selection;
public void actionPerformed(ActionEvent e)
{
if (selection == e.getSource()) return;
if (selection != null) selection.setSelected(false);
onGroupingChange(e.getID());
selection = (ActionLabel)e.getSource();
}
};
ActionLabel albFlat = new ActionLabel(actGroup,
Strings.message("search.groupping.flat"), SearchResultsListModel.GROUP_FLAT);
ActionLabel albKind = new ActionLabel(actGroup,
Strings.message("search.groupping.kind"), SearchResultsListModel.GROUP_KIND);
ActionLabel albDate = new ActionLabel(actGroup,
Strings.message("search.groupping.date"), SearchResultsListModel.GROUP_DATE);
albKind.setSelected(true);
// Filtering
Action actFiltering = new AbstractAction()
{
private ActionLabel selection;
public void actionPerformed(ActionEvent e)
{
if (selection == e.getSource()) return;
if (selection != null) selection.setSelected(false);
onFilteringChange(e.getID());
selection = (ActionLabel)e.getSource();
}
};
ActionLabel albAnyDate = new ActionLabel(actFiltering,
Strings.message("search.when.any.date"),
ResultsList.DATE_ANY);
ActionLabel albToday = new ActionLabel(actFiltering,
Strings.message("search.when.today"),
ResultsList.DATE_TODAY);
ActionLabel albYesterday =new ActionLabel(actFiltering,
Strings.message("search.when.since.yesterday"),
ResultsList.DATE_YESTERDAY);
ActionLabel albThisWeek = new ActionLabel(actFiltering,
Strings.message("search.when.this.week"),
ResultsList.DATE_WEEK);
ActionLabel albThisMonth = new ActionLabel(actFiltering,
Strings.message("search.when.this.month"),
ResultsList.DATE_MONTH);
ActionLabel albThisYear = new ActionLabel(actFiltering,
Strings.message("search.when.this.year"),
ResultsList.DATE_YEAR);
albAnyDate.setSelected(true);
builder.append(smallLabel(Strings.message("search.groupping")));
builder.nextLine();
builder.append(albFlat);
builder.nextLine();
builder.append(albKind);
builder.nextLine();
builder.append(albDate);
// builder.nextLine();
builder.appendUnrelatedComponentsGapRow(2);
builder.setLeadingColumnOffset(0);
builder.append(new JLabel(), 3);
// builder.append(new JPopupMenu.Separator(), 3);
builder.setLeadingColumnOffset(1);
builder.append(smallLabel(Strings.message("search.when")));
builder.nextLine();
builder.append(albAnyDate);
builder.nextLine();
builder.append(albToday);
builder.nextLine();
builder.append(albYesterday);
builder.nextLine();
builder.append(albThisWeek);
builder.nextLine();
builder.append(albThisMonth);
builder.nextLine();
builder.append(albThisYear);
builder.nextLine();
return builder.getPanel();
}
/**
* Creates small label component.
*
* @param txt text.
*
* @return label component.
*/
private static JComponent smallLabel(String txt)
{
JLabel label = new JLabel(txt);
UifUtilities.smallerFont(label);
return label;
}
/**
* Inoked when grouping changes.
*
* @param grouping new grouping option.
*/
private void onGroupingChange(int grouping)
{
model.setGroupBy(grouping);
}
/**
* Invoked when filtering changes.
*
* @param filtering new filtering option.
*/
private void onFilteringChange(int filtering)
{
itemsList.setDateRange(filtering);
}
/**
* Resizes the specified component. This method is called during the build process and enables
* subclasses to achieve a better aspect ratio, by applying a resizer, e.g. the
* <code>Resizer</code>.
*
* @param component the component to be resized
*/
protected void resizeHook(JComponent component)
{
component.setPreferredSize(Resizer.ONE2ONE.fromWidth(MIN_SIZE.width));
}
// ---------------------------------------------------------------------------------------------
/** Action label with selection indication and click handler. */
private static class ActionLabel extends JLabel
{
private final Action action;
private final int id;
/**
* Creates label.
*
* @param anAction action to call when clicked.
* @param title label title.
* @param anID ID.
*/
public ActionLabel(Action anAction, String title, int anID)
{
super(title);
UifUtilities.smallerFont(this);
this.action = anAction;
id = anID;
setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
enableEvents(AWTEvent.MOUSE_EVENT_MASK);
}
/**
* Selects / deselects item.
*
* @param aSelected <code>TRUE</code> to select.
*/
public void setSelected(boolean aSelected)
{
Color cl = aSelected ? Color.BLUE : Color.BLACK;
setForeground(cl);
if (aSelected) action.actionPerformed(new ActionEvent(this, id, null));
}
/**
* Processes clicks.
*
* @param e event.
*/
protected void processMouseEvent(MouseEvent e)
{
if (e.getID() == MouseEvent.MOUSE_PRESSED)
{
setSelected(true);
}
}
}
/**
* Listener for results updates.
*/
private class ResultsListener implements ISearchResultListener
{
private int results = 0;
/**
* Invoked when new result item is added to the list.
*
* @param result results list object.
* @param item item added.
* @param index item index.
*/
public void itemAdded(ISearchResult result, final ResultItem item, int index)
{
results++;
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
lbResults.setText(MessageFormat.format(Strings.message("search.0.results"),
Integer.toString(results)));
model.add(item);
}
});
}
/**
* Invoked when the result items are removed from the list.
*
* @param result results list object.
*/
public void itemsRemoved(ISearchResult result)
{
results = 0;
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
lbResults.setText(Strings.message("search.no.results"));
model.clear();
}
});
}
/**
* Invoked when underlying search is finished.
*
* @param result results list object.
*/
public void finished(ISearchResult result)
{
SwingUtilities.invokeLater(new Runnable()
{
public void run()
{
pgSpinner.stop();
}
});
}
}
/**
* Listens to key taps and passes them to the list component.
*/
private class NavigationListener extends KeyAdapter
{
/**
* Invoked when a key has been pressed.
*
* @param e event.
*/
public void keyPressed(KeyEvent e)
{
switch (e.getKeyCode())
{
case KeyEvent.VK_UP:
itemsList.onPrevItemSelected();
break;
case KeyEvent.VK_DOWN:
itemsList.onNextItemSelected();
break;
case KeyEvent.VK_ESCAPE:
doCancel();
break;
}
}
}
private class SearchCriteriaChangeMonitor implements ActionListener
{
private String lastText;
public boolean lastPinnedArticlesOnly;
private final JCheckBox chPinnedArticlesOnly;
private final JLabel helpIcon;
private final SearchField tfSearch;
public SearchCriteriaChangeMonitor(JCheckBox chPinnedArticlesOnly, SearchField tfSearch, JLabel helpIcon)
{
this.chPinnedArticlesOnly = chPinnedArticlesOnly;
this.helpIcon = helpIcon;
this.tfSearch = tfSearch;
lastPinnedArticlesOnly = chPinnedArticlesOnly.isSelected();
}
public void actionPerformed(ActionEvent e)
{
String text = tfSearch.getText();
boolean pinnedArticlesOnly = chPinnedArticlesOnly.isSelected();
showHelpIfNecessary(text);
if (!text.equalsIgnoreCase(lastText) ||
pinnedArticlesOnly != lastPinnedArticlesOnly)
{
pgSpinner.start();
searchEngine.setSearchText(text, pinnedArticlesOnly);
lastText = text;
lastPinnedArticlesOnly = pinnedArticlesOnly;
} else
{
itemsList.onItemFired();
}
}
private void showHelpIfNecessary(String text)
{
helpIcon.setEnabled(SearchEngine.isComplexSeachPattern(text));
}
}
}