com.jgoodies.binding.adapter.TextComponentConnector Maven / Gradle / Ivy
Show all versions of jgoodies-binding Show documentation
/*
* Copyright (c) 2002-2015 JGoodies Software GmbH. All Rights Reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* o Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* o Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* o Neither the name of JGoodies Software GmbH nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jgoodies.binding.adapter;
import static com.jgoodies.common.base.Preconditions.checkNotNull;
import static com.jgoodies.common.internal.Messages.MUST_NOT_BE_NULL;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.Document;
import javax.swing.text.JTextComponent;
import javax.swing.text.PlainDocument;
import com.jgoodies.binding.PresentationModel;
import com.jgoodies.binding.beans.BeanAdapter;
import com.jgoodies.binding.value.ValueModel;
import com.jgoodies.common.base.Objects;
/**
* Connects a String typed ValueModel and a JTextField or JTextArea.
* At construction time the text component content is updated
* with the subject's contents.
*
* This connector has been designed for text components that display a plain
* String. In case of a JEditorPane, the binding may require more information
* then the plain String, for example styles. Since this is outside the scope
* of this connector, the public constructors prevent a construction for
* general JTextComponents. If you want to establish a one-way binding for
* a display JEditorPane, use a custom listener instead.
*
* This class provides limited support for handling
* subject value modifications while updating the subject.
* If a Document change initiates a subject value update, the subject
* will be observed and a property change fired by the subject will be
* handled - if any. In most cases, the subject will notify about a
* change to the text that was just set by this connector.
* However, in some cases the subject may decide to modify this text,
* for example to ensure upper case characters.
* Since at this moment, this adapter's Document is still write-locked,
* the Document update is performed later using
* {@code SwingUtilities#invokeLater}.
*
* Note:
* Such an update will typically change the Caret position in JTextField's
* and other JTextComponent's that are synchronized using this class.
* Hence, the subject value modifications can be used with
* commit-on-focus-lost text components, but typically not with a
* commit-on-key-typed component. For the latter case, you may consider
* using a custom {@code DocumentFilter}.
*
* Constraints:
* The ValueModel must be of type {@code String}.
*
* Examples:
* ValueModel lastNameModel = new PropertyAdapter(customer, "lastName", true);
* JTextField lastNameField = new JTextField();
* TextComponentConnector.connect(lastNameModel, lastNameField);
*
* ValueModel codeModel = new PropertyAdapter(shipment, "code", true);
* JTextField codeField = new JTextField();
* TextComponentConnector connector =
* new TextComponentConnector(codeModel, codeField);
* connector.updateTextComponent();
*
*
* @author Karsten Lentzsch
* @version $Revision: 1.17 $
*
* @see ValueModel
* @see Document
* @see PlainDocument
*
* @since 1.2
*/
public final class TextComponentConnector {
/**
* Holds the underlying ValueModel that is used to read values,
* to update the document and to write values if the document changes.
*/
private final ValueModel subject;
/**
* Refers to the text component that shall be synchronized
* with the subject.
*/
private final JTextComponent textComponent;
/**
* Holds the text component's current document.
* Used for the rare case where the text component fires
* a PropertyChangeEvent for the "document" property
* with oldValue or newValue == {@code null}.
*/
private Document document;
private final SubjectValueChangeHandler subjectValueChangeHandler;
private final DocumentListener textChangeHandler;
private final PropertyChangeListener documentChangeHandler;
// Instance Creation ******************************************************
/**
* Constructs a TextComponentConnector that connects the specified
* String-typed subject ValueModel with the given text area.
*
* In case you don't need the TextComponentConnector instance, you better
* use one of the static {@code #connect} methods.
* This constructor may confuse developers, if you just use
* the side effects performed in the constructor; this is because it is
* quite unconventional to instantiate an object that you never use.
*
* @param subject the underlying String typed ValueModel
* @param textArea the JTextArea to be synchronized with the ValueModel
*
* @throws NullPointerException if the subject or text area is {@code null}
*/
public TextComponentConnector(ValueModel subject, JTextArea textArea) {
this(subject, (JTextComponent) textArea);
}
/**
* Constructs a TextComponentConnector that connects the specified
* String-typed subject ValueModel with the given text field.
*
* In case you don't need the TextComponentConnector instance, you better
* use one of the static {@code #connect} methods.
* This constructor may confuse developers, if you just use
* the side effects performed in the constructor; this is because it is
* quite unconventional to instantiate an object that you never use.
*
* @param subject the underlying String typed ValueModel
* @param textField the JTextField to be synchronized with the ValueModel
*
* @throws NullPointerException if the subject or text field is {@code null}
*/
public TextComponentConnector(ValueModel subject, JTextField textField) {
this(subject, (JTextComponent) textField);
}
/**
* Constructs a TextComponentConnector that connects the specified
* String-typed subject ValueModel with the given JTextComponent.
*
* In case you don't need the TextComponentConnector instance, you better
* use one of the static {@code #connect} methods.
* This constructor may confuse developers, if you just use
* the side effects performed in the constructor; this is because it is
* quite unconventional to instantiate an object that you never use.
*
* @param subject the underlying String typed ValueModel
* @param textComponent the JTextComponent to be synchronized with the ValueModel
*
* @throws NullPointerException if the subject or text component is {@code null}
*/
private TextComponentConnector(ValueModel subject, JTextComponent textComponent) {
this.subject = checkNotNull(subject, MUST_NOT_BE_NULL, "subject");
this.textComponent = checkNotNull(textComponent, MUST_NOT_BE_NULL, "text component");
this.subjectValueChangeHandler = new SubjectValueChangeHandler();
this.textChangeHandler = new TextChangeHandler();
document = textComponent.getDocument();
reregisterTextChangeHandler(null, document);
subject.addValueChangeListener(subjectValueChangeHandler);
documentChangeHandler = new DocumentChangeHandler();
textComponent.addPropertyChangeListener("document", documentChangeHandler);
}
/**
* Establishes a synchronization between the specified String-typed
* subject ValueModel and the given text area. Does not synchronize now.
*
* @param subject the underlying String typed ValueModel
* @param textArea the JTextArea to be synchronized with the ValueModel
*
* @throws NullPointerException if the subject or text area is {@code null}
*/
@SuppressWarnings("unused")
public static void connect(ValueModel subject, JTextArea textArea) {
new TextComponentConnector(subject, textArea);
}
/**
* Establishes a synchronization between the specified String-typed
* subject ValueModel and the given text field. Does not synchronize now.
*
* @param subject the underlying String typed ValueModel
* @param textField the JTextField to be synchronized with the ValueModel
*
* @throws NullPointerException if the subject or text area is {@code null}
*/
@SuppressWarnings("unused")
public static void connect(ValueModel subject, JTextField textField) {
new TextComponentConnector(subject, textField);
}
// Synchronization ********************************************************
/**
* Reads the current text from the document
* and sets it as new value of the subject.
*/
public void updateSubject() {
setSubjectText(getDocumentText());
}
/**
* Reads the current value from the model and updates the text component.
*/
public void updateTextComponent() {
setDocumentTextSilently(getSubjectText());
}
/**
* Returns the text contained in the document.
*
* @return the text contained in the document
*/
private String getDocumentText() {
return textComponent.getText();
}
/**
* Sets the text component's contents without notifying the subject
* about the change. Invoked by the subject change listener.
* Sets the text, then sets the caret position to 0.
*
* @param newText the text to be set in the document
*/
private void setDocumentTextSilently(String newText) {
textComponent.getDocument().removeDocumentListener(textChangeHandler);
textComponent.setText(newText);
textComponent.setCaretPosition(0);
textComponent.getDocument().addDocumentListener(textChangeHandler);
}
/**
* Returns the subject's text value.
*
* @return the subject's text value
* @throws ClassCastException if the subject value is not a String
*/
private String getSubjectText() {
String str = (String) subject.getValue();
return str == null ? "" : str;
}
/**
* Sets the given text as new subject value. Since the subject may modify
* this text, we cannot update silently, i.e. we cannot remove and add
* the subjectValueChangeHandler before/after the update. Since this
* change is invoked during a Document write operation, the document
* is write-locked and so, we cannot modify the document before all
* document listeners have been notified about the change.
*
* Therefore we listen to subject changes and defer any document changes
* using {@code SwingUtilities.invokeLater}. This mode is activated
* by setting the subject change handler's {@code updateLater} to true.
*
* @param newText the text to be set in the subject
*/
private void setSubjectText(String newText) {
subjectValueChangeHandler.setUpdateLater(true);
try {
subject.setValue(newText);
} finally {
subjectValueChangeHandler.setUpdateLater(false);
}
}
private void reregisterTextChangeHandler(Document oldDocument, Document newDocument) {
if (oldDocument != null) {
oldDocument.removeDocumentListener(textChangeHandler);
}
if (newDocument != null) {
newDocument.addDocumentListener(textChangeHandler);
}
}
// Misc *******************************************************************
/**
* Removes the internal listeners from the subject, text component,
* and text component's document.
* This connector must not be used after calling {@code #release}.
*
* To avoid memory leaks it is recommended to invoke this method,
* if the ValueModel lives much longer than the text component.
* Instead of releasing a text connector, you typically make the ValueModel
* obsolete by releasing the PresentationModel or BeanAdapter that has
* created the ValueModel.
*
* As an alternative you may use ValueModels that in turn use
* event listener lists implemented using {@code WeakReference}.
*
* @see PresentationModel#release()
* @see BeanAdapter#release()
* @see java.lang.ref.WeakReference
*/
public void release() {
reregisterTextChangeHandler(document, null);
subject.removeValueChangeListener(subjectValueChangeHandler);
textComponent.removePropertyChangeListener("document", documentChangeHandler);
}
// DocumentListener *******************************************************
/**
* Updates the subject if the text has changed.
*/
private final class TextChangeHandler implements DocumentListener {
/**
* There was an insert into the document; update the subject.
*
* @param e the document event
*/
@Override
public void insertUpdate(DocumentEvent e) {
updateSubject();
}
/**
* A portion of the document has been removed; update the subject.
*
* @param e the document event
*/
@Override
public void removeUpdate(DocumentEvent e) {
updateSubject();
}
/**
* An attribute or set of attributes has changed; do nothing.
*
* @param e the document event
*/
@Override
public void changedUpdate(DocumentEvent e) {
// Do nothing on attribute changes.
}
}
/**
* Handles changes in the subject value and updates this document
* - if necessary.
*
* Document changes update the subject text and result in a subject
* property change. Most of these changes will just reflect the
* former subject change. However, in some cases the subject may
* modify the text set, for example to ensure upper case characters.
* This method reduces the number of document updates by checking
* the old and new text. If the old and new text are equal or
* both null, this method does nothing.
*
* Since subject changes as a result of a document change may not
* modify the write-locked document immediately, we defer the update
* if necessary using {@code SwingUtilities.invokeLater}.
*
* See the TextComponentConnector's JavaDoc class comment
* for the limitations of the deferred document change.
*/
private final class SubjectValueChangeHandler implements PropertyChangeListener {
private boolean updateLater;
void setUpdateLater(boolean updateLater) {
this.updateLater = updateLater;
}
/**
* The subject value has changed; updates the document immediately
* or later - depending on the {@code updateLater} state.
*
* @param evt the event to handle
*/
@Override
public void propertyChange(PropertyChangeEvent evt) {
final String oldText = getDocumentText();
final Object newValue = evt.getNewValue();
final String newText = newValue == null
? getSubjectText()
: (String) newValue;
if (Objects.equals(oldText, newText)) {
return;
}
if (updateLater) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
setDocumentTextSilently(newText);
}
});
} else {
setDocumentTextSilently(newText);
}
}
}
/**
* Re-registers the text change handler after document changes.
*/
private final class DocumentChangeHandler implements PropertyChangeListener {
@Override
public void propertyChange(PropertyChangeEvent evt) {
Document oldDocument = document;
Document newDocument = textComponent.getDocument();
reregisterTextChangeHandler(oldDocument, newDocument);
document = newDocument;
}
}
}