1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240
|
/* Glazed Lists (c) 2003-2006 */
/* http://publicobject.com/glazedlists/ publicobject.com,*/
/* O'Dell Engineering Ltd.*/
package ca.odell.glazedlists.swing;
import ca.odell.glazedlists.TextFilterator;
import ca.odell.glazedlists.matchers.TextMatcherEditor;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeEvent;
/**
* A MatcherEditor that matches Objects that contain the filter text located
* within a {@link Document}. This {@link TextMatcherEditor} is directly
* coupled with a Document and fires MatcherEditor changes in response to
* Document changes. This matcher is fully concrete and is expected to be used
* by Swing applications.
*
* <p>The {@link TextComponentMatcherEditor} constructors require that either a
* {@link Document} or a {@link JTextComponent} (from which a {@link Document}
* is extracted) be specified.
*
* <p>The MatcherEditor registers itself as a {@link DocumentListener} on the
* given Document, or {@link ActionListener} on the {@link JTextComponent} for
* non-live filtering. If a {@link JTextComponent} is given on construction, it
* is also watched for changes of its Document and the Document used by this
* MatcherEditor is updated to reflect the latest Document behind the text
* component.
*
* If this MatcherEditor must be garbage collected before the underlying
* Document, or JTextComponent, the listeners can be unregistered by calling
* {@link #dispose()}.
*
* @author James Lemieux
*/
public class TextComponentMatcherEditor<E> extends TextMatcherEditor<E> {
/** the Document that provides the filter values */
private Document document;
/** the JTextComponent being observed for actions */
private final JTextComponent textComponent;
/** whether we're listening to each keystroke */
private boolean live;
/** The listener attached to the given {@link #document}. */
private final FilterHandler filterHandler = new FilterHandler();
/**
* Creates a TextMatcherEditor bound to the {@link Document} backing the
* given <code>textComponent</code> with the given
* <code>textFilterator</code>.
*
* @param textComponent the text component backed by the {@link Document}
* that is the source of text filter values
* @param textFilterator an object capable of producing Strings from the
* objects being filtered. If <code>textFilterator</code> is
* <code>null</code> then all filtered objects are expected to
* implement {@link ca.odell.glazedlists.TextFilterable}.
*/
public TextComponentMatcherEditor(JTextComponent textComponent, TextFilterator<? super E> textFilterator) {
this(textComponent, textFilterator, true);
}
/**
* Creates a TextMatcherEditor bound to the {@link Document} backing the
* given <code>textComponent</code> with the given
* <code>textFilterator</code>.
*
* @param textComponent the text component backed by the {@link Document}
* that is the source of text filter values
* @param textFilterator an object capable of producing Strings from the
* objects being filtered. If <code>textFilterator</code> is
* <code>null</code> then all filtered objects are expected to
* implement {@link ca.odell.glazedlists.TextFilterable}.
* @param live <code>true</code> to filter by the keystroke or <code>false</code>
* to filter only when {@link java.awt.event.KeyEvent#VK_ENTER Enter} is pressed
* within the {@link JTextComponent}. Note that non-live filtering is only
* supported if <code>textComponent</code> is a {@link JTextField}.
* @throws IllegalArgumentException if the <code>textComponent</code>
* is not a {@link JTextField} and non-live filtering is specified.
*/
public TextComponentMatcherEditor(JTextComponent textComponent, TextFilterator<? super E> textFilterator, boolean live) {
this(textComponent, textComponent.getDocument(), textFilterator, live);
}
/**
* Creates a TextMatcherEditor bound to the given <code>document</code>
* with the given <code>textFilterator</code>.
*
* @param document the {@link Document} that is the source of text filter
* values
* @param textFilterator an object capable of producing Strings from the
* objects being filtered. If <code>textFilterator</code> is
* <code>null</code> then all filtered objects are expected to
* implement {@link ca.odell.glazedlists.TextFilterable}.
*/
public TextComponentMatcherEditor(Document document, TextFilterator<? super E> textFilterator) {
this(null, document, textFilterator, true);
}
/**
* This private constructor implements the actual construction work and thus
* ensures that all public constructors agree on the construction logic.
*/
private TextComponentMatcherEditor(JTextComponent textComponent, Document document, TextFilterator<? super E> textFilterator, boolean live) {
super(textFilterator);
this.textComponent = textComponent;
this.document = document;
this.live = live;
registerListeners(live);
// if the document is non-empty to begin with!
refilter();
}
/**
* Whether filtering occurs by the keystroke or not.
*/
public boolean isLive() {
return live;
}
/**
* Toggle between filtering by the keystroke and not.
*
* @param live <code>true</code> to filter by the keystroke or <code>false</code>
* to filter only when {@link java.awt.event.KeyEvent#VK_ENTER Enter} is pressed
* within the {@link JTextComponent}. Note that non-live filtering is only
* supported if <code>textComponent</code> is a {@link JTextField}.
*/
public void setLive(boolean live) {
if (live == this.live) return;
deregisterListeners(this.live);
this.live = live;
registerListeners(this.live);
}
/**
* Listen live or on action performed.
*/
private void registerListeners(boolean live) {
if(live) {
document.addDocumentListener(filterHandler);
} else {
if(textComponent == null) throw new IllegalArgumentException("Non-live filtering supported only for JTextField (document provided)");
if(!(textComponent instanceof JTextField)) throw new IllegalArgumentException("Non-live filtering supported only for JTextField (argument class " + textComponent.getClass().getName() + ")");
JTextField textField = (JTextField) textComponent;
textField.addActionListener(filterHandler);
}
if (textComponent != null)
textComponent.addPropertyChangeListener(filterHandler);
}
/**
* Stop listening.
*/
private void deregisterListeners(boolean live) {
if (live) {
document.removeDocumentListener(filterHandler);
} else {
JTextField textField = (JTextField) textComponent;
textField.removeActionListener(filterHandler);
}
if (textComponent != null)
textComponent.removePropertyChangeListener(filterHandler);
}
/**
* A cleanup method which stops this MatcherEditor from listening to
* changes on the underlying {@link Document}, thus freeing the
* MatcherEditor or Document to be garbage collected.
*/
public void dispose() {
deregisterListeners(live);
}
/**
* Update the filter text from the contents of the Document.
*/
private void refilter() {
try {
final int mode = getMode();
final String text = document.getText(0, document.getLength());
final String[] filters;
// in CONTAINS mode we treat the string as whitespace delimited
if (mode == CONTAINS)
filters = text.split("[ \t]");
// in STARTS_WITH, REGULAR_EXPRESSION, or EXACT modes we use the string in its entirety
else if (mode == STARTS_WITH || mode == REGULAR_EXPRESSION || mode == EXACT)
filters = new String[] {text};
else throw new IllegalStateException("Unknown mode: " + mode);
setFilterText(filters);
} catch (BadLocationException ble) {
// this shouldn't ever, ever happen
throw new RuntimeException(ble);
}
}
/**
* This class responds to any change in the Document by setting the filter
* text of this TextMatcherEditor to the contents of the Document.
*/
private class FilterHandler implements DocumentListener, ActionListener, PropertyChangeListener {
public void insertUpdate(DocumentEvent e) { refilter(); }
public void removeUpdate(DocumentEvent e) { refilter(); }
public void changedUpdate(DocumentEvent e) { refilter(); }
public void actionPerformed(ActionEvent e) { refilter(); }
public void propertyChange(PropertyChangeEvent evt) {
if ("document" == evt.getPropertyName()) {
// stop listening to the old Document
deregisterListeners(live);
// start listening to the new Document
document = textComponent.getDocument();
registerListeners(live);
// refilter based on the new Document
refilter();
}
}
}
}
|