001: package snow.sortabletable;
002:
003: import snow.utils.gui.*;
004: import java.util.*;
005: import java.awt.Component;
006: import java.awt.event.*;
007: import java.awt.Dimension;
008: import java.awt.Insets;
009: import javax.swing.*;
010:
011: /**
012: * A search panel which allows the user to specify multiple search queries.
013: * Many thanks to Peter B for this class.
014: */
015: public class MultiSearchPanel extends AnimatedColorPanel {
016:
017: private GridLayout3 layout;
018: protected final JTextField firstSearchTF = new JTextField(8);
019: protected final JComboBox firstSearchColumnCB = new JComboBox();
020:
021: protected final JComboBox firstComparisonCB = new JComboBox(
022: Query.comparisonTypeNames);
023: protected final JButton firstDelSearchBtn;
024:
025: protected KeyAdapter keyAdapter;
026: protected ActionListener updateListener;
027:
028: protected SortableTableModel sortableTableModel;
029: protected final Vector<String> colNames = new Vector<String>();
030:
031: private final ArrayList<JComboBox> searchComparisonCBList = new ArrayList<JComboBox>();
032: private final ArrayList<JTextField> searchTFList = new ArrayList<JTextField>();
033: private final ArrayList<JComboBox> searchColumnCBList = new ArrayList<JComboBox>();
034: private final ArrayList<JComboBox> searchOperationCBList = new ArrayList<JComboBox>();
035: private final ArrayList<JButton> searchDelBtnList = new ArrayList<JButton>();
036:
037: private final TextFieldPopupListener textFieldPopupListener = new TextFieldPopupListener();
038:
039: public MultiSearchPanel(final String searchLabelText,
040: final Icon searchIcon, final SortableTableModel stm) {
041: super (null, 100);
042: layout = new GridLayout3(6, this );
043: layout.setColumnWeights(new double[] { 0, 0, 0, 0, 0, 100 });
044:
045: this .sortableTableModel = stm;
046:
047: colNames.add("all columns");
048: for (int i = 0; i < stm.getBasicTableModel().getColumnCount(); i++) {
049: colNames.add(stm.getBasicTableModel().getColumnName(i));
050: }
051: firstSearchColumnCB
052: .setModel(new DefaultComboBoxModel(colNames));
053: firstSearchColumnCB.setMaximumRowCount(20);
054: formatComboBoxUI(firstSearchColumnCB);
055: formatComboBoxUI(firstComparisonCB);
056:
057: // key listener
058: //
059: keyAdapter = new KeyAdapter() {
060: @Override
061: public void keyReleased(KeyEvent ee) {
062: doSearch();
063: }
064: };
065:
066: updateListener = new ActionListener() {
067: public void actionPerformed(ActionEvent e) {
068: doSearch();
069: }
070: };
071:
072: // first row is fixed
073: this .firstDelSearchBtn = new JButton(Icons.sharedCross); //"X"
074: firstDelSearchBtn.setToolTipText("Remove this query");
075: firstDelSearchBtn.setMargin(new Insets(0, 1, 0, 1));
076: this .firstDelSearchBtn.setEnabled(false);
077: this .firstDelSearchBtn.addActionListener(new ActionListener() {
078: public void actionPerformed(ActionEvent e) {
079: // move one up
080: firstSearchTF.setText(searchTFList.get(0).getText());
081: firstSearchColumnCB.setSelectedIndex(searchColumnCBList
082: .get(0).getSelectedIndex());
083: firstComparisonCB
084: .setSelectedIndex(searchComparisonCBList.get(0)
085: .getSelectedIndex());
086: removeCriteriaAt(0);
087: }
088: });
089: JLabel sLabel = new JLabel(searchLabelText, searchIcon,
090: JLabel.LEFT);
091: layout.add(sLabel);
092: firstSearchColumnCB.setFont(UIManager.getFont("smallFont"));
093: layout.add(firstSearchColumnCB);
094: firstComparisonCB.setFont(UIManager.getFont("smallFont"));
095: layout.add(firstComparisonCB);
096: layout.add(firstSearchTF);
097:
098: JButton addCriteriaBtn = new JButton(Icons.sharedPlus); //"+"
099: addCriteriaBtn.setToolTipText("Add another query line");
100: addCriteriaBtn.setFocusPainted(false);
101: addCriteriaBtn.setPreferredSize(firstDelSearchBtn
102: .getPreferredSize());
103: addCriteriaBtn.setFont(UIManager.getFont("smallFont"));
104: addCriteriaBtn.addActionListener(new ActionListener() {
105: public void actionPerformed(ActionEvent e) {
106: addCriteria();
107: }
108: });
109: addCriteriaBtn.setMargin(new Insets(0, 1, 0, 1));
110: layout.add(this .firstDelSearchBtn);
111: layout.add(addCriteriaBtn);
112:
113: firstSearchColumnCB.addActionListener(updateListener);
114: firstComparisonCB.addActionListener(updateListener);
115: firstSearchTF.addKeyListener(keyAdapter);
116: registerTextFieldForPopup(firstSearchTF);
117:
118: // focus behaviour
119: firstSearchTF.addFocusListener(new FocusAdapter() {
120: @Override
121: public void focusGained(FocusEvent e) {
122: firstSearchTF.selectAll();
123:
124: }
125: });
126:
127: } // constr
128:
129: @Override
130: public void requestFocus() {
131: //System.out.println("RFOC");
132: firstSearchTF.requestFocus();
133: }
134:
135: /** Used in special cases where the model reference may change but the view not
136: */
137: public void setSortableTableModel(SortableTableModel stm) {
138: this .sortableTableModel = stm;
139: // cause a reload...
140: this .setQueries(this .getQueries());
141: }
142:
143: public boolean isSearchActive() {
144: return firstSearchTF.getText().length() > 0
145: || searchOperationCBList.size() > 0; // Added [Feb2008]
146: }
147:
148: private void registerTextFieldForPopup(final JTextField tf) {
149: tf.addMouseListener(textFieldPopupListener);
150: }
151:
152: class TextFieldPopupListener extends MouseAdapter {
153: @Override
154: public void mousePressed(MouseEvent me) {
155: if (me.isPopupTrigger()) {
156: showPopup(me);
157: }
158: }
159:
160: @Override
161: public void mouseReleased(MouseEvent me) {
162: if (me.isPopupTrigger()) {
163: showPopup(me);
164: }
165: }
166:
167: public void showPopup(MouseEvent me) {
168: JPopupMenu pop = new JPopupMenu();
169:
170: if (!(me.getSource() instanceof JTextField))
171: return;
172: final JTextField tf = (JTextField) me.getSource();
173:
174: // TODO: History of searches
175: int columnForClickedField = -1; // -1=all
176: String colName = "column";
177: if (tf == firstSearchTF) {
178: columnForClickedField = firstSearchColumnCB
179: .getSelectedIndex() - 1; // first is "all"
180: colName = "" + firstSearchColumnCB.getSelectedItem();
181: } else {
182: int pos = searchTFList.indexOf(tf);
183: if (pos >= 0) {
184: JComboBox cb = searchColumnCBList.get(pos);
185: columnForClickedField = cb.getSelectedIndex() - 1;
186: colName = "" + cb.getSelectedItem();
187: }
188: }
189:
190: //System.out.println("columnForClickedField="+columnForClickedField);
191: if (columnForClickedField >= 0) {
192: //
193: Set<String> vals = sortableTableModel
194: .getDifferentColumnValues(columnForClickedField);
195: Map<String, Integer> stats = sortableTableModel
196: .getDifferentColumnValuesStat(columnForClickedField);
197:
198: int maxToDisplay = 30;
199:
200: if (vals.size() <= maxToDisplay) {
201: pop.add("" + vals.size() + " distinct value"
202: + (vals.size() == 1 ? "" : "s") + " in "
203: + colName + ": ");
204: TreeSet<String> hs = new TreeSet<String>(vals);
205: for (final String vi : hs) {
206: JMenuItem mi = new JMenuItem(vi + ": "
207: + stats.get(vi));
208: pop.add(mi);
209: mi.addActionListener(new ActionListener() {
210: public void actionPerformed(ActionEvent ae) {
211: tf.setText(vi);
212: doSearch();
213: }
214: });
215: }
216: hs.clear();
217: } else {
218: pop.add("" + vals.size()
219: + " distinct values in column. The "
220: + maxToDisplay + " first are:");
221: TreeSet<String> hs = new TreeSet<String>(vals);
222: int n = 0;
223: for (final String vi : hs) {
224: n++;
225: if (n >= maxToDisplay) {
226: JMenuItem mi = new JMenuItem("... "
227: + (hs.size() - maxToDisplay)
228: + " more ...");
229: pop.add(mi);
230: mi.addActionListener(new ActionListener() {
231: public void actionPerformed(
232: ActionEvent ae) {
233: // explore them in a sortable clickable table ? that writes back in this field ?
234: }
235: });
236: break;
237: }
238:
239: JMenuItem mi = new JMenuItem(vi + ": "
240: + stats.get(vi));
241: pop.add(mi);
242: mi.addActionListener(new ActionListener() {
243: public void actionPerformed(ActionEvent ae) {
244: tf.setText(vi);
245: doSearch();
246: }
247: });
248: }
249: hs.clear();
250: }
251: }
252:
253: if (pop.getComponentCount() > 0) {
254: pop.show((Component) me.getSource(), me.getX(), me
255: .getY() + 12);
256: }
257: }
258: }
259:
260: public JTextField getFirstTextField() {
261: return firstSearchTF;
262: }
263:
264: public void doSearch() {
265: if (!SwingUtilities.isEventDispatchThread()) {
266: new Throwable("Should be called from EDT !")
267: .printStackTrace();
268: }
269:
270: this .activateAnimation(isSearchActive());
271:
272: this .sortableTableModel.multiSearch(getQueries());
273: }
274:
275: public Query[] getQueries() {
276: if (!isSearchActive()) {
277: return null;
278: }
279:
280: int subCnt = searchOperationCBList.size();
281: Query[] result = new Query[subCnt + 1]; // first is fixed
282:
283: int col = firstSearchColumnCB.getSelectedIndex() - 1; // -1 => all
284: String searchTxt = firstSearchTF.getText();
285: int boolOp = 0;
286: int compMode = firstComparisonCB.getSelectedIndex();
287: Query.Comparison type = Query.Comparison.forIndex(compMode);
288:
289: result[0] = new Query(col, searchTxt, Query.Combine.And, type);
290:
291: for (int i = 0; i < subCnt; i++) {
292: col = searchColumnCBList.get(i).getSelectedIndex() - 1; // -1 => all
293: searchTxt = searchTFList.get(i).getText();
294: boolOp = searchOperationCBList.get(i).getSelectedIndex();
295: compMode = searchComparisonCBList.get(i).getSelectedIndex();
296: type = Query.Comparison.forIndex(compMode);
297:
298: result[i + 1] = new Query(col, searchTxt, Query.Combine
299: .forIndex(boolOp), type);
300: }
301:
302: return result;
303: }
304:
305: public void setQueries(final Query[] _queries) {
306: // remove all
307: for (JComboBox item : searchOperationCBList) {
308: MultiSearchPanel.this .remove(item);
309: }
310: for (JTextField item : searchTFList) {
311: MultiSearchPanel.this .remove(item);
312: }
313: for (JComboBox item : searchComparisonCBList) {
314: MultiSearchPanel.this .remove(item);
315: }
316: for (JComboBox item : searchColumnCBList) {
317: MultiSearchPanel.this .remove(item);
318: }
319: for (JButton item : searchDelBtnList) {
320: MultiSearchPanel.this .remove(item);
321: }
322:
323: searchOperationCBList.clear();
324: searchTFList.clear();
325: searchComparisonCBList.clear();
326: searchColumnCBList.clear();
327: searchDelBtnList.clear();
328:
329: layout.invalidateLayout(MultiSearchPanel.this );
330: layout.layoutContainer(MultiSearchPanel.this );
331:
332: if ((_queries == null) || (_queries.length == 0)) {
333: this .firstSearchColumnCB.setSelectedIndex(0);
334: this .firstSearchTF.setText("");
335: } else {
336: // set first
337: this .firstSearchColumnCB
338: .setSelectedIndex(_queries[0].column + 1);
339: this .firstSearchTF.setText(_queries[0].searchTxt);
340: this .firstComparisonCB
341: .setSelectedIndex(_queries[0].comparison.ordinal());
342:
343: // set others
344: for (int i = 1; i < _queries.length; i++) {
345: addCriteria(_queries[i].boolOp.ordinal(),
346: _queries[i].searchTxt, _queries[i].column,
347: _queries[i].comparison.ordinal());
348: }
349: }
350:
351: doSearch();
352: }
353:
354: /** sets the size */
355: private void formatComboBoxUI(JComboBox cb) {
356: cb.setMaximumRowCount(30);
357: cb.setPreferredSize(new Dimension((int) cb.getPreferredSize()
358: .getWidth(), (int) firstSearchTF.getPreferredSize()
359: .getHeight()));
360: }
361:
362: private void addCriteria() {
363: addCriteria(-1, null, -1, 0);
364: }
365:
366: private void addCriteria(int opIndex, String qStr, int colIndex,
367: int compMode) {
368: final JComboBox searchOperationCB = new JComboBox(new String[] {
369: "and", "or", "and not", "or not" }); // xor?
370: if (opIndex >= 0) {
371: searchOperationCB.setSelectedIndex(opIndex);
372: }
373: searchOperationCB.setFont(UIManager.getFont("smallFont"));
374: searchOperationCBList.add(searchOperationCB);
375:
376: final JTextField searchTF = new JTextField(8);
377: registerTextFieldForPopup(searchTF);
378: if (qStr != null) {
379: searchTF.setText(qStr);
380: }
381: searchTFList.add(searchTF);
382:
383: final JComboBox comparisonCB = new JComboBox(
384: Query.comparisonTypeNames);
385: comparisonCB.setFont(UIManager.getFont("smallFont"));
386: comparisonCB.setSelectedIndex(compMode);
387: searchComparisonCBList.add(comparisonCB);
388:
389: final JComboBox searchColumnCB = new JComboBox();
390: searchColumnCB.setFont(UIManager.getFont("smallFont"));
391: searchColumnCB.setModel(new DefaultComboBoxModel(colNames));
392: searchColumnCB.setSelectedIndex(colIndex + 1);
393: searchColumnCBList.add(searchColumnCB);
394:
395: final JButton delCriteriaBtn = new JButton(Icons.sharedCross);
396: delCriteriaBtn.setMargin(new Insets(0, 1, 0, 1));
397: delCriteriaBtn.addActionListener(new ActionListener() {
398: public void actionPerformed(ActionEvent e) {
399: int arrayPos = searchOperationCBList
400: .indexOf(searchOperationCB);
401:
402: removeCriteriaAt(arrayPos);
403: }
404: });
405: searchDelBtnList.add(delCriteriaBtn);
406: layout.add(searchOperationCB);
407: layout.add(searchColumnCB);
408: layout.add(comparisonCB);
409: layout.add(searchTF);
410: layout.insertLineBreakAfterNextComponent();
411: layout.add(delCriteriaBtn);
412:
413: firstDelSearchBtn.setEnabled(true);
414:
415: searchOperationCB.addActionListener(updateListener);
416: searchColumnCB.addActionListener(updateListener);
417: comparisonCB.addActionListener(updateListener);
418: searchTF.addKeyListener(keyAdapter);
419:
420: // focus behaviour
421: searchTF.addFocusListener(new FocusAdapter() {
422: @Override
423: public void focusGained(FocusEvent e) {
424: searchTF.selectAll();
425: }
426: });
427:
428: this .updateUI();
429:
430: formatComboBoxUI(searchColumnCB);
431: formatComboBoxUI(searchOperationCB);
432: formatComboBoxUI(comparisonCB);
433:
434: }
435:
436: /**
437: * note:
438: * 0 is the index of the first SUB-criteria,
439: * the first (main) criteria cannot be removed.
440: */
441: private void removeCriteriaAt(int pos) {
442: MultiSearchPanel.this .remove(searchOperationCBList.get(pos));
443: MultiSearchPanel.this .remove(searchTFList.get(pos));
444: MultiSearchPanel.this .remove(searchComparisonCBList.get(pos));
445: MultiSearchPanel.this .remove(searchColumnCBList.get(pos));
446: MultiSearchPanel.this .remove(searchDelBtnList.get(pos));
447:
448: searchOperationCBList.remove(searchOperationCBList.get(pos));
449: searchTFList.remove(searchTFList.get(pos));
450: searchComparisonCBList.remove(searchComparisonCBList.get(pos));
451: searchColumnCBList.remove(searchColumnCBList.get(pos));
452: searchDelBtnList.remove(searchDelBtnList.get(pos));
453:
454: firstDelSearchBtn.setEnabled(searchOperationCBList.size() > 0);
455:
456: layout.invalidateLayout(MultiSearchPanel.this );
457: layout.layoutContainer(MultiSearchPanel.this );
458: updateUI();
459:
460: doSearch();
461: }
462:
463: /** standalone test
464: */
465: public static void main(String[] arguments) {
466: Test.main(arguments);
467: }
468: }
|