001: /*
002: * Copyright (c) 2002-2007 JGoodies Karsten Lentzsch. All Rights Reserved.
003: *
004: * Redistribution and use in source and binary forms, with or without
005: * modification, are permitted provided that the following conditions are met:
006: *
007: * o Redistributions of source code must retain the above copyright notice,
008: * this list of conditions and the following disclaimer.
009: *
010: * o Redistributions in binary form must reproduce the above copyright notice,
011: * this list of conditions and the following disclaimer in the documentation
012: * and/or other materials provided with the distribution.
013: *
014: * o Neither the name of JGoodies Karsten Lentzsch nor the names of
015: * its contributors may be used to endorse or promote products derived
016: * from this software without specific prior written permission.
017: *
018: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
019: * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
020: * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
021: * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
022: * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
023: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
024: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
025: * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
026: * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
027: * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
028: * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029: */
030:
031: package com.jgoodies.binding.adapter;
032:
033: import java.beans.PropertyChangeEvent;
034: import java.beans.PropertyChangeListener;
035:
036: import javax.swing.JTextArea;
037: import javax.swing.JTextField;
038: import javax.swing.SwingUtilities;
039: import javax.swing.event.DocumentEvent;
040: import javax.swing.event.DocumentListener;
041: import javax.swing.text.Document;
042: import javax.swing.text.JTextComponent;
043: import javax.swing.text.PlainDocument;
044:
045: import com.jgoodies.binding.BindingUtils;
046: import com.jgoodies.binding.PresentationModel;
047: import com.jgoodies.binding.beans.BeanAdapter;
048: import com.jgoodies.binding.value.ValueModel;
049:
050: /**
051: * Connects a String typed ValueModel and a JTextField or JTextArea.
052: * At construction time the text component content is updated
053: * with the subject's contents.<p>
054: *
055: * This connector has been designed for text components that display a plain
056: * String. In case of a JEditorPane, the binding may require more information
057: * then the plain String, for example styles. Since this is outside the scope
058: * of this connector, the public constructors prevent a construction for
059: * general JTextComponents. If you want to establish a one-way binding for
060: * a display JEditorPane, use a custom listener instead.<p>
061: *
062: * This class provides limited support for handling
063: * subject value modifications while updating the subject.
064: * If a Document change initiates a subject value update, the subject
065: * will be observed and a property change fired by the subject will be
066: * handled - if any. In most cases, the subject will notify about a
067: * change to the text that was just set by this connector.
068: * However, in some cases the subject may decide to modify this text,
069: * for example to ensure upper case characters.
070: * Since at this moment, this adapter's Document is still write-locked,
071: * the Document update is performed later using
072: * <code>SwingUtilities#invokeLater</code>.<p>
073: *
074: * <strong>Note:</strong>
075: * Such an update will typically change the Caret position in JTextField's
076: * and other JTextComponent's that are synchronized using this class.
077: * Hence, the subject value modifications can be used with
078: * commit-on-focus-lost text components, but typically not with a
079: * commit-on-key-typed component. For the latter case, you may consider
080: * using a custom <code>DocumentFilter</code>.<p>
081: *
082: * <strong>Constraints:</strong>
083: * The ValueModel must be of type <code>String</code>.<p>
084: *
085: * <strong>Examples:</strong><pre>
086: * ValueModel lastNameModel = new PropertyAdapter(customer, "lastName", true);
087: * JTextField lastNameField = new JTextField();
088: * TextComponentConnector.connect(lastNameModel, lastNameField);
089: *
090: * ValueModel codeModel = new PropertyAdapter(shipment, "code", true);
091: * JTextField codeField = new JTextField();
092: * TextComponentConnector connector =
093: * new TextComponentConnector(codeModel, codeField);
094: * connector.updateTextComponent();
095: * </pre>
096: *
097: * @author Karsten Lentzsch
098: * @version $Revision: 1.9 $
099: *
100: * @see ValueModel
101: * @see Document
102: * @see PlainDocument
103: *
104: * @since 1.2
105: */
106: public final class TextComponentConnector {
107:
108: /**
109: * Holds the underlying ValueModel that is used to read values,
110: * to update the document and to write values if the document changes.
111: */
112: private final ValueModel subject;
113:
114: /**
115: * Refers to the text component that shall be synchronized
116: * with the subject.
117: */
118: private final JTextComponent textComponent;
119:
120: /**
121: * Holds the text component's current document.
122: * Used for the rare case where the text component fires
123: * a PropertyChangeEvent for the "document" property
124: * with oldValue or newValue == <code>null</code>.
125: */
126: private Document document;
127:
128: private final SubjectValueChangeHandler subjectValueChangeHandler;
129:
130: private final DocumentListener textChangeHandler;
131:
132: private final PropertyChangeListener documentChangeHandler;
133:
134: // Instance Creation ******************************************************
135:
136: /**
137: * Constructs a TextComponentConnector that connects the specified
138: * String-typed subject ValueModel with the given text area.<p>
139: *
140: * In case you don't need the TextComponentConnector instance, you better
141: * use one of the static <code>#connect</code> methods.
142: * This constructor may confuse developers, if you just use
143: * the side effects performed in the constructor; this is because it is
144: * quite unconventional to instantiate an object that you never use.
145: *
146: * @param subject the underlying String typed ValueModel
147: * @param textArea the JTextArea to be synchronized with the ValueModel
148: *
149: * @throws NullPointerException if the subject or text area is <code>null</code>
150: */
151: public TextComponentConnector(ValueModel subject, JTextArea textArea) {
152: this (subject, (JTextComponent) textArea);
153: }
154:
155: /**
156: * Constructs a TextComponentConnector that connects the specified
157: * String-typed subject ValueModel with the given text field.<p>
158: *
159: * In case you don't need the TextComponentConnector instance, you better
160: * use one of the static <code>#connect</code> methods.
161: * This constructor may confuse developers, if you just use
162: * the side effects performed in the constructor; this is because it is
163: * quite unconventional to instantiate an object that you never use.
164: *
165: * @param subject the underlying String typed ValueModel
166: * @param textField the JTextField to be synchronized with the ValueModel
167: *
168: * @throws NullPointerException if the subject or text field is <code>null</code>
169: */
170: public TextComponentConnector(ValueModel subject,
171: JTextField textField) {
172: this (subject, (JTextComponent) textField);
173: }
174:
175: /**
176: * Constructs a TextComponentConnector that connects the specified
177: * String-typed subject ValueModel with the given JTextComponent.<p>
178: *
179: * In case you don't need the TextComponentConnector instance, you better
180: * use one of the static <code>#connect</code> methods.
181: * This constructor may confuse developers, if you just use
182: * the side effects performed in the constructor; this is because it is
183: * quite unconventional to instantiate an object that you never use.
184: *
185: * @param subject the underlying String typed ValueModel
186: * @param textComponent the JTextComponent to be synchronized with the ValueModel
187: *
188: * @throws NullPointerException if the subject or text component is <code>null</code>
189: */
190: private TextComponentConnector(ValueModel subject,
191: JTextComponent textComponent) {
192: if (subject == null)
193: throw new NullPointerException(
194: "The subject must not be null.");
195: if (textComponent == null)
196: throw new NullPointerException(
197: "The text component must not be null.");
198: this .subject = subject;
199: this .textComponent = textComponent;
200: this .subjectValueChangeHandler = new SubjectValueChangeHandler();
201: this .textChangeHandler = new TextChangeHandler();
202: document = textComponent.getDocument();
203: reregisterTextChangeHandler(null, document);
204: subject.addValueChangeListener(subjectValueChangeHandler);
205: documentChangeHandler = new DocumentChangeHandler();
206: textComponent.addPropertyChangeListener("document",
207: documentChangeHandler);
208: }
209:
210: /**
211: * Establishes a synchronization between the specified String-typed
212: * subject ValueModel and the given text area. Does not synchronize now.
213: *
214: * @param subject the underlying String typed ValueModel
215: * @param textArea the JTextArea to be synchronized with the ValueModel
216: *
217: * @throws NullPointerException if the subject or text area is <code>null</code>
218: */
219: public static void connect(ValueModel subject, JTextArea textArea) {
220: new TextComponentConnector(subject, textArea);
221: }
222:
223: /**
224: * Establishes a synchronization between the specified String-typed
225: * subject ValueModel and the given text field. Does not synchronize now.
226: *
227: * @param subject the underlying String typed ValueModel
228: * @param textField the JTextField to be synchronized with the ValueModel
229: *
230: * @throws NullPointerException if the subject or text area is <code>null</code>
231: */
232: public static void connect(ValueModel subject, JTextField textField) {
233: new TextComponentConnector(subject, textField);
234: }
235:
236: // Synchronization ********************************************************
237:
238: /**
239: * Reads the current text from the document
240: * and sets it as new value of the subject.
241: */
242: public void updateSubject() {
243: setSubjectText(getDocumentText());
244: }
245:
246: public void updateTextComponent() {
247: setDocumentTextSilently(getSubjectText());
248: }
249:
250: /**
251: * Returns the text contained in the document.
252: *
253: * @return the text contained in the document
254: */
255: private String getDocumentText() {
256: return textComponent.getText();
257: }
258:
259: /**
260: * Sets the document contents without notifying the subject of changes.
261: * Invoked by the subject change listener. Removes the existing text first,
262: * then inserts the new text; therefore a BadLocationException should not
263: * happen. In case the delegate is an <code>AbstractDocument</code>
264: * the text is replaced instead of a combined remove plus insert.<p>
265: *
266: * @param newText the text to be set in the document
267: */
268: private void setDocumentTextSilently(String newText) {
269: textComponent.getDocument().removeDocumentListener(
270: textChangeHandler);
271: textComponent.setText(newText);
272: textComponent.setCaretPosition(0);
273: textComponent.getDocument().addDocumentListener(
274: textChangeHandler);
275: }
276:
277: /**
278: * Returns the subject's text value.
279: *
280: * @return the subject's text value
281: * @throws ClassCastException if the subject value is not a String
282: */
283: private String getSubjectText() {
284: String str = (String) subject.getValue();
285: return str == null ? "" : str;
286: }
287:
288: /**
289: * Sets the given text as new subject value. Since the subject may modify
290: * this text, we cannot update silently, i.e. we cannot remove and add
291: * the subjectValueChangeHandler before/after the update. Since this
292: * change is invoked during a Document write operation, the document
293: * is write-locked and so, we cannot modify the document before all
294: * document listeners have been notified about the change.<p>
295: *
296: * Therefore we listen to subject changes and defer any document changes
297: * using <code>SwingUtilities.invokeLater</code>. This mode is activated
298: * by setting the subject change handler's <code>updateLater</code> to true.
299: *
300: * @param newText the text to be set in the subject
301: */
302: private void setSubjectText(String newText) {
303: subjectValueChangeHandler.setUpdateLater(true);
304: try {
305: subject.setValue(newText);
306: } finally {
307: subjectValueChangeHandler.setUpdateLater(false);
308: }
309: }
310:
311: private void reregisterTextChangeHandler(Document oldDocument,
312: Document newDocument) {
313: if (oldDocument != null) {
314: oldDocument.removeDocumentListener(textChangeHandler);
315: }
316: if (newDocument != null) {
317: newDocument.addDocumentListener(textChangeHandler);
318: }
319: }
320:
321: // Misc *******************************************************************
322:
323: /**
324: * Removes the internal listeners from the subject, text component,
325: * and text component's document.
326: * This connector must not be used after calling <code>#release</code>.<p>
327: *
328: * To avoid memory leaks it is recommended to invoke this method,
329: * if the ValueModel lives much longer than the text component.
330: * Instead of releasing a text connector, you typically make the ValueModel
331: * obsolete by releasing the PresentationModel or BeanAdapter that has
332: * created the ValueModel.<p>
333: *
334: * As an alternative you may use ValueModels that in turn use
335: * event listener lists implemented using <code>WeakReference</code>.
336: *
337: * @see PresentationModel#release()
338: * @see BeanAdapter#release()
339: * @see java.lang.ref.WeakReference
340: */
341: public void release() {
342: reregisterTextChangeHandler(document, null);
343: subject.removeValueChangeListener(subjectValueChangeHandler);
344: textComponent.removePropertyChangeListener("document",
345: documentChangeHandler);
346: }
347:
348: // DocumentListener *******************************************************
349:
350: /**
351: * Updates the subject if the text has changed.
352: */
353: private final class TextChangeHandler implements DocumentListener {
354:
355: /**
356: * There was an insert into the document; update the subject.
357: *
358: * @param e the document event
359: */
360: public void insertUpdate(DocumentEvent e) {
361: updateSubject();
362: }
363:
364: /**
365: * A portion of the document has been removed; update the subject.
366: *
367: * @param e the document event
368: */
369: public void removeUpdate(DocumentEvent e) {
370: updateSubject();
371: }
372:
373: /**
374: * An attribute or set of attributes has changed; do nothing.
375: *
376: * @param e the document event
377: */
378: public void changedUpdate(DocumentEvent e) {
379: // Do nothing on attribute changes.
380: }
381: }
382:
383: /**
384: * Handles changes in the subject value and updates this document
385: * - if necessary.<p>
386: *
387: * Document changes update the subject text and result in a subject
388: * property change. Most of these changes will just reflect the
389: * former subject change. However, in some cases the subject may
390: * modify the text set, for example to ensure upper case characters.
391: * This method reduces the number of document updates by checking
392: * the old and new text. If the old and new text are equal or
393: * both null, this method does nothing.<p>
394: *
395: * Since subject changes as a result of a document change may not
396: * modify the write-locked document immediately, we defer the update
397: * if necessary using <code>SwingUtilities.invokeLater</code>.<p>
398: *
399: * See the TextComponentConnector's JavaDoc class comment
400: * for the limitations of the deferred document change.
401: */
402: private final class SubjectValueChangeHandler implements
403: PropertyChangeListener {
404:
405: private boolean updateLater;
406:
407: void setUpdateLater(boolean updateLater) {
408: this .updateLater = updateLater;
409: }
410:
411: /**
412: * The subject value has changed; updates the document immediately
413: * or later - depending on the <code>updateLater</code> state.
414: *
415: * @param evt the event to handle
416: */
417: public void propertyChange(PropertyChangeEvent evt) {
418: final String oldText = getDocumentText();
419: final Object newValue = evt.getNewValue();
420: final String newText = newValue == null ? getSubjectText()
421: : (String) newValue;
422: if (BindingUtils.equals(oldText, newText))
423: return;
424:
425: if (updateLater) {
426: SwingUtilities.invokeLater(new Runnable() {
427: public void run() {
428: setDocumentTextSilently(newText);
429: }
430: });
431: } else {
432: setDocumentTextSilently(newText);
433: }
434: }
435:
436: }
437:
438: /**
439: * Re-registers the text change handler after document changes.
440: */
441: private final class DocumentChangeHandler implements
442: PropertyChangeListener {
443: public void propertyChange(PropertyChangeEvent evt) {
444: Document oldDocument = document;
445: Document newDocument = textComponent.getDocument();
446: reregisterTextChangeHandler(oldDocument, newDocument);
447: document = newDocument;
448: }
449: }
450:
451: }
|