Package DisplayProject.controls

Source Code of DisplayProject.controls.MultiLineTextField$CaratPositionChangeHandler

/*
Copyright (c) 2003-2009 ITerative Consulting Pty Ltd. 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 This jcTOOL Helper Class software, whether in binary or source form may not be used within,
or to derive, any other product without the specific prior written permission of the copyright holder

 
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS "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 DisplayProject.controls;

import java.awt.Color;
import java.awt.Container;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.lang.ref.WeakReference;
import java.util.Hashtable;

import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JViewport;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.event.CaretEvent;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;

import DisplayProject.CharacterField;
import DisplayProject.Constants;
import DisplayProject.FixedLengthDocument;
import DisplayProject.FocusHelper;
import DisplayProject.UIutils;
import DisplayProject.actions.ActionMgr;
import DisplayProject.actions.PendingAction;
import DisplayProject.binding.value.ValueModel;
import DisplayProject.events.ChildEventHelper;
import DisplayProject.events.ClientEventManager;
import DisplayProject.factory.TextEditFactory;
import DisplayProject.table.ArrayFieldCellHelper;
import Framework.Array_Of_ImageData;
import Framework.CloneHelper;
import Framework.ErrorMgr;
import Framework.EventManager;
import Framework.ForteKeyboardFocusManager;
import Framework.ImageData;
import Framework.ParameterHolder_integer;
import Framework.UsageException;

/**
* Subclass of {@link javax.swing.JTextArea} that attempts to behave in the same way as
* the corresponding Forte component(s). In particular, the behaviour that is
* replicated is to keep the carat at the
* beginning of the text area when initially displaying text that is bound to a
* property in a model bean. This ensures that any associated {@link javax.swing.JViewPort}
* displays the beginning and not the end of the text (as in Forte). When a user changes the value of the bean property
* via this text area, the behaviour should be the same as in <code>JTextArea</code>.
* <code>BoundPropertyTextArea</code> is implemented to work with the property binding framework,
* particularly {@link DisplayProject.binding.adapter.DocumentAdapter}.
* <code>DocumentAdapter</code> is a wrapper around a {@link javax.swing.text.Document} which listens
* for {@link java.beans.PropertyChangeEvent}s fired by an associated {@link com.jgoodies.binding.value.ValueModel}
* and updates the underlying document to reflect the changes. <code>DocumentAdapter</code> detects changes by the user
* by intercepting
* {@link javax.swing.event.DocumentEvent}s and updates the <code>ValueModel</code> accordingly.
* <p>
* The Forte behaviour is realised by associating an instance of <code>ValueModel</code>
* with an instance of <code>BoundPropertyTextArea</code>.
* <code>BoundPropertyTextArea</code> makes use of two internal {@link java.beans.PropertyChangeListener}s and a 
* {@link javax.swing.event.DocumentListener}.
* These determine whether a change to the associated bean model property was caused by the user typing in the text area
* or by a change in the backend.
* One <code>PropertyChangeListener</code> intercepts <code>PropertyChangeEvent</code>s before the <code>DocumentAdapter</code>.
* The second <code>PropertyChangeListener</code> intercepts <code>PropertyChangeEvent</code>s after the <code>DocumentAdapter</code>.
* It is the responsibility of the client to ensure that these listeners are registered in the correct sequence.
* <p>
* Refer to {@link DisplayProject.binding.TextComponentBinder} for an example of
* usage.
* <p>
* The other advantage of this class is that it fires the same events as the Forte event model at the same time.
* @see DisplayProject.binding.TextComponentBinder
* @author Tim Faulkes
*/
@SuppressWarnings("serial")
public class MultiLineTextField extends JTextArea implements DocumentListener, FocusListener, CharacterField {
    private String lastUserEnteredText = "";
    private boolean hasDoneInsertUpdateText = false; // CraigM:23/12/2008 - Set to true when the user or code starts entering text
    private String lastModelSuppliedText = null;
    private ValueModel valueModel;
    private PropertyChangeHandler propertyChangeHandler = new PropertyChangeHandler(this);
    private CaratPositionChangeHandler caratPositionChangeHandler = new CaratPositionChangeHandler(this);
    protected boolean hasDataChanged = false;
    protected boolean purging = false;
    private Color overriddenBackgroundColour = null;
    private boolean validateOnKeystroke = false;

    /**
     * A class used to roll back this last edit when PurgeEvents is called.
     */
    private class RollbackAction implements Runnable {
        Object oldValue;
        public RollbackAction() {
//        try {
//          Document doc = TextField.this.getDocument();
//          this.oldValue = doc.getText(0, doc.getLength());
//        }
//        catch (Exception e) {}
        }
        public void run() {
//            lastValue = DataField.this.getValue();
//            DataField.this.toData(oldValue);
            hasDataChanged = true;
            purging = true;
        }
    }

    /**
     * Constructs a new BoundPropertyTextArea.  A default model is set, the initial string
     * is null, and rows/columns are set to 0.
     */
    public MultiLineTextField() {
        this(null, 0, 0);
    }

    /**
     * Constructs a new BoundPropertyTextArea.  A default model is set, the initial string
     * is null, and rows/columns are set to 0.
     */
    public MultiLineTextField(String name) {
      this(name, 0, 0);
    }

    /**
     * Constructs a new empty BoundPropertyTextArea with the specified number of
     * rows and columns.  A default model is created, and the initial
     * string is null.
     *
     * @param rows the number of rows >= 0
     * @param columns the number of columns >= 0
     * @exception UsageException if the rows or columns
     *  arguments are negative.
     */
    public MultiLineTextField(String name, int rows, int columns) {
        super(rows, columns);
        this.setName(name);
        getDocument().addDocumentListener(this);
        this.setMargin(new Insets(0,3,0,0));
        this.installShiftTabListener();
        this.addFocusListener(this);
        this.setFocusTraversalKeysEnabled(false); // We handle our own focus traversal. CraigM 05/10/2007.
        this.setBackground(null)// TF:24/07/2008:Set the background colour to inherit
        this.setWrapStyleWord(false)// TF:12/12/2008:Set these values to the same default as forte
        this.setLineWrap(false);
    }
   
    /**
     * @return the left column that contains the line numbers and/or images.
     * CraigM:12/02/2009.
     */
    private MultiLineTextFieldLeftColumn getLeftColumn() {
      // TF:06/03/2009:Fixed this up so it didn't throw a null pointer exception when the control is in an array field
      if (this.getParent() != null && this.getParent().getParent() instanceof JScrollPane) {
        JScrollPane sp = ((JScrollPane)this.getParent().getParent());
        if (sp.getRowHeader() == null) {
          sp.setRowHeaderView(new MultiLineTextFieldLeftColumn(this));
        }
       
        return (MultiLineTextFieldLeftColumn)sp.getRowHeader().getComponents()[0];
      }
      return null;
    }

    /**
     * Turn line numbers on/off.  CraigM:12/02/2009.
     */
    public void lineNumbers(boolean enable) {
      if (enable) {
        this.getLeftColumn().lineNumbersEnable();
      }
      else {
        this.getLeftColumn().lineNumbersDisable();
      }
    }
   
    /**
     * The SetImageTag method sets the icon image for a given row and tag column to the specified image.
     * If the TagColumns attribute for this field is set to a value greater than 0, a set of columns are
     * reserved on the left side of the field for displaying icons for each line. For efficiency, only
     * images that have been entered into the ImageTags array can be used as tag icons, so you must set
     * up the ImageTags array before setting any of the individual row icons.
     * CraigM:13/02/2009.
     *
     * @param row (1 based)
     * @param column (1 based)
     * @param image (1 based)
     *
     * To set an individual tag icon for a line in a text edit field, use this method, specifying the
     * line number in the text as the row parameter (numbered from 1), the tag column number as the
     * column (numbered from 1), and the row number within the ImageTags array as the image parameter
     * (numbered from 1). The tag icon will then be displayed in that position.
     */
    public void setImageTag(int row, int column, int image) {
      this.getLeftColumn().setImageTag(row, column, image);
    }
   
    /**
     * The ImageTags attribute (Array of ImageData) is an array of images that can be used as icons in the
     * tag columns to the left of the text edit field. Only ImageData objects in the ImageTags array can
     * be used as tag column images. The array can contain a maximum of 255 images.
     *
     * To put an icon in the tag column, use the row number of the ImageData object in the ImageTags array
     * as the image parameter in the SetImageTag method.
     *
     * If you make changes to the rows or ImageData objects in the ImageTags array, be sure to set the
     * ImageTags attribute again to reinitialize the images and refresh the icons. If you do not set the
     * attribute again, the changes will not appear in the tag column.
     *
     * The default setting is NIL (for no images).
     * 
     * @param images
     */
    public void setImageTags(Array_Of_ImageData<ImageData> images) {
      // TF:06/03/2009:Fixed this up so it didn't throw a null pointer exception when the control is in an array field
      MultiLineTextFieldLeftColumn column = this.getLeftColumn();
      if (column != null) {
        column.setImageTags(images);
      }
    }
   
    public Array_Of_ImageData<ImageData> getImageTags() {
      // TF:06/03/2009:Fixed this up so it didn't throw a null pointer exception when the control is in an array field
      MultiLineTextFieldLeftColumn column = this.getLeftColumn();
      if (column != null) {
        return column.getImageTags();
      }
      return null;
    }
   
    /**
     * Sets the number of columns to reserve on the left edge of the text edit field for use as tag
     * columns, which can be used to display images that are stored in the ImageTags array.
     * CraigM:13/02/2009.
     *
     * @param cols
     */
    public void setTagColumns(int cols) {
      // TF:06/03/2009:Fixed this up so it didn't throw a null pointer exception when the control is in an array field
      MultiLineTextFieldLeftColumn column = this.getLeftColumn();
      if (column != null) {
        column.setTagColumns(cols);
      }
    }

    public int getTagColumns() {
      // TF:06/03/2009:Fixed this up so it didn't throw a null pointer exception when the control is in an array field
      MultiLineTextFieldLeftColumn column = this.getLeftColumn();
      if (column != null) {
        return column.getTagColumns();
      }
      return 0;
    }
   
    /**
     * In Forte, the foreground colour is always the same, irrespective of the state
     * the widget is in. This is important in some applications, so we override this
     * to enforce this rule.
     */
    @Override
    public Color getDisabledTextColor() {
      return this.getForeground();
    }
   
    /**
     * In Forte, Shift-Tab would always tab the focus to the previous component, irrespective
     * of whether the NeedsTab mark was set or not. We install an appropriate listener when
     * we create a text edit control.
     *
     */
    private void installShiftTabListener() {
        this.getInputMap().put(KeyStroke.getKeyStroke("shift TAB"), "cycle-backwards");
        this.addKeyListener(new KeyAdapter() {
            public void keyPressed(KeyEvent event) {
                if (event.getKeyCode() == KeyEvent.VK_TAB && event.getModifiersEx() == KeyEvent.SHIFT_DOWN_MASK) {
                    ForteKeyboardFocusManager.setTabTraversal(false);
                    MultiLineTextField.this.transferFocusBackward();
                }
            }
        });
    }

    private void resetPurgingFlag() {
        if (this.purging) {
            this.purging = false;
            this.hasDataChanged = true;
        }
    }

    public void focusGained(FocusEvent e) {
        // TF:12/10/07: Don't do this for array fields, because they handle the focus gain/loss themselves
        if (ArrayFieldCellHelper.getArrayField(this) == null) {
            this.focusGained();
        }
        if (!purging) {
            this.hasDataChanged = false;
        }
        else {
            this.resetPurgingFlag();
        }
    }

    public void focusGained() {
        EventManager.startEventChain();
        int reason = ForteKeyboardFocusManager.getTraversalReason();
        if (reason != Constants.FC_SUPRESS) {
            Hashtable<String, Object> params = new Hashtable<String, Object>();
            params.put("reason", new ParameterHolder_integer(reason));
            ClientEventManager.postEvent( this, "AfterFocusGain", params );
        }
        EventManager.endEventChain();

        if (!purging) {
            this.hasDataChanged = false;
        }
        else {
            this.resetPurgingFlag();
        }

        // Show the cursor, even in view only mode. CraigM 05/10/2007
        if (!this.getCaret().isVisible() && this.isEnabled()) {
            this.getCaret().setVisible(true);
        }
    }

    public void focusLost(FocusEvent e) {
        // TF:12/10/07: Don't do this for array fields, because they handle the focus gain/loss themselves
        if (ArrayFieldCellHelper.getArrayField(this) == null) {
            this.focusLost();
        }
    }
    public void focusLost() {
        EventManager.startEventChain();
        postAfterValueChange();
        postBeforeFocusLoss();
        EventManager.endEventChain();
    }

    public void postBeforeFocusLoss() {
        EventManager.startEventChain();
        int reason = ForteKeyboardFocusManager.getTraversalReason();
        if (reason != Constants.FC_SUPRESS) {
            Hashtable<String, Object> params = new Hashtable<String, Object>();
            params.put("reason", new ParameterHolder_integer(reason));
            ClientEventManager.postEvent( this, "BeforeFocusLoss", params );
        }
        EventManager.endEventChain();
    }

    public void postAfterValueChange() {
        if (hasDataChanged) {
            try {
                EventManager.startEventChain();
                FocusHelper.addSetFocusPurgeAction(this);
                FocusHelper.addPurgeAction(new RollbackAction(), this, "AfterValueChange");
                hasDataChanged = false;
                UIutils.setDataChangedFlag(this);
                ClientEventManager.postEvent( this, "AfterValueChange" );
                if (EventManager.isPostingEnabled()) {
                    UIutils.setDataChangedFlag(this);
                }
                // TF:27/9/07:Revamped to use new event poster
                ChildEventHelper.postEventToAllParents(this, "ChildAfterValueChange");
            }
            finally {
                EventManager.endEventChain();
            }
        }
    }

    /**
     * This method fires any necessary events associated with changes in the documents. As a
     * side effect is marks the document as dirty.
     */
    private void fireDocumentChangedEvents() {
        this.resetPurgingFlag();

        if (this.validateOnKeystroke) {
            this.hasDataChanged = true;
            this.postAfterValueChange();
        }
        else {
            if (!this.hasDataChanged){
                this.hasDataChanged = true;
                EventManager.startEventChain();
                ClientEventManager.postEvent(MultiLineTextField.this, "AfterFirstKeystroke", null);
                // TF:27/9/07:Revamped to use new event poster
                ChildEventHelper.postEventToAllParents(this, "ChildAfterFirstKeystroke");
                EventManager.endEventChain();
            }
        }
    }

    /**
     * Associates the editor with a text document.
     * The currently registered factory is used to build a view for
     * the document, which gets displayed by the editor after revalidation.
     * A PropertyChange event ("document") is propagated to each listener.
     *
     * @param doc  the document to display/edit
     * @see #getDocument
     * @beaninfo
     *  description: the text document model
     *        bound: true
     *       expert: true
     */
    public void setDocument(Document doc) {
        if (getDocument() != null) {
            getDocument().removeDocumentListener(this);
        }
        super.setDocument(doc);
        if (doc != null) {
            doc.addDocumentListener(this);
        }
    }

    /**
     * Gives notification that an attribute or set of attributes changed.
     *
     * @param e the document event
     */
    public void changedUpdate(DocumentEvent e) {
        if (!isModelText()) {
            recordLastUserEnteredText();
        }
        fireDocumentChangedEvents();
    }

    /**
     * Gives notification that there was an insert into the document.  The
     * range given by the DocumentEvent bounds the freshly inserted region.
     *
     * @param e the document event
     */
    public void insertUpdate(DocumentEvent e) {
        // CraigM:23/12/2008 - This is the first time that text has been entered
      if (hasDoneInsertUpdateText == false) {
        // If the text is being set via code, then set it to the code value
        if (isModelText()) {
        this.recordLastUserEnteredText();
        }
        // Otherwise, just initialise it
        else {
          this.lastUserEnteredText = "";
        }
        hasDoneInsertUpdateText = true;
        }
      // CraigM:08/01/2009 - If the code is setting the text, reset the hasDoneInsertUpdateText flag
      else if (isModelText()) {
          hasDoneInsertUpdateText = false;
          // TF:23/01/2009:DET-58:If we're setting the code via text, we need to also reset the last user entered text
          this.recordLastUserEnteredText();
      }

        // Java calls this method twice for every insert, so we do our own
        // check to see if anything actually changed.  CraigM: 17/01/2008.
      if (!this.lastUserEnteredText.equals(getText())) {
            if (!isModelText()) {
//System.out.println("insertUpdate changing lastUserEnteredText from \"" + this.lastUserEnteredText + "\" to \"" + getText() + "\"");
                recordLastUserEnteredText();
            }
            fireDocumentChangedEvents();
        }
    }

    /**
     * Gives notification that a portion of the document has been
     * removed.  The range is given in terms of what the view last
     * saw (that is, before updating sticky positions).
     *
     * @param e the document event
     */
    public void removeUpdate(DocumentEvent e) {
        // Java calls this method twice for every delete, so we do our own
        // check to see if anything actually changed.  CraigM: 17/01/2008.
        if (!this.lastUserEnteredText.equals(getText())) {
            recordLastUserEnteredText();
            fireDocumentChangedEvents();
        }
    }

    /**
     * Associates <code>this</code> with a model bean property represented by <code>valueModel</code>.
     * @param valueModel
     */
    public void setValueModel(ValueModel valueModel) {
        if (this.valueModel != null) {
            valueModel.removeValueChangeListener(propertyChangeHandler);
            valueModel.removeValueChangeListener(caratPositionChangeHandler);
        }
        if (valueModel != null) {
            this.valueModel = valueModel;
        }
        else {
            UsageException errorVar = new UsageException("parameter valueModel is null");
            ErrorMgr.addError(errorVar);
            throw errorVar;
        }
    }

    @Override
    public void setBackground(Color bg) {
        super.setBackground(bg);
        // TF:19/3/08:We want our colour to apply to any numbers in the left hand margin for a numbered text area.
        if (getParent() != null && getParent().getParent() instanceof JScrollPane) {
          JScrollPane scrollPane = (JScrollPane)getParent().getParent();
          JViewport viewport = scrollPane.getRowHeader();
          if (viewport != null && viewport.getView() != null) {
            viewport.getView().setBackground(bg);
          }
        }
        this.overriddenBackgroundColour = bg;
    }

    // Temporarily set the background, but don't override it.  (Used by Table Cell Renderer for highlighting the row)
    public void setBackgroundTemporarily(Color bg) {
        super.setBackground(bg);
    }

    public Color getOverriddenBackgroundColor() {
        return this.overriddenBackgroundColour;
    }
   
    /**
     * Sets the top line that is shown.
     *
     * CraigM:16/05/2008
     * @param pLine
     */
    public void setTopLine(final int pLine) {
      ActionMgr.addAction(new PendingAction(null) {
        @Override
        public String toString() {
          return "MultiLineTextField.setTopLine(" + pLine + ")";
        }
       
        @Override
        public void performAction() {
          int index = 0;
          String txt = MultiLineTextField.this.getText();
         
          // Find where the caret should be for the line number
          for (int i=1; i<pLine; i++) {
            index = txt.indexOf('\n', index)+1;
           
            if (index == -1) {
              index = txt.length()-1;
              break;
            }
          }
            MultiLineTextField.this.setCaretPosition(index);
           
            // Allow the caret to be updated
            SwingUtilities.invokeLater(new Runnable() {
              public void run() {
                // Request the position of the caret
                  Point cursorPos = MultiLineTextField.this.getCaret().getMagicCaretPosition();
                 
                  // Scroll so the caret will be at the top
                  MultiLineTextField.this.scrollRectToVisible(new Rectangle(cursorPos.x, cursorPos.y, MultiLineTextField.this.getWidth(), MultiLineTextField.this.getHeight()));
              }
            });
        }
      });
    }

    /**
     * From Forte Help:<br>
     * <br>
     * The ValidateOnKeystroke attribute (boolean) specifies that the display system
     * is to complete the processing of the data in the field after every keystroke
     * by the end user. The default is FALSE, which means that the system does not
     * send the AfterValueChange event or update the mapped TOOL variable until the
     * end user leaves the field in some way, by tabbing out or by picking up the
     * mouse and clicking on another field, or if the AfterFinalize event is sent to
     * the window.<br>
     * If ValidateOnKeystroke is set to TRUE, the display system sends an
     * AfterValueChange event, and transfers the value of the field to the mapped
     * TOOL variable whenever the end user types a keystroke in the field.<br>
     * <br>
     * If you have the ValidateOnKeystroke attribute set to TRUE, the field will not
     * post the AfterFirstkeystroke event when the user types the first character in
     * the field. Instead, you can use the AfterValueChange event to detect a change
     * in the data.<br>
     * This attribute should be turned on sparingly, only when you need to track the
     * value of a field on every keystroke (the Menu Workshop uses this attribute to
     * keep the name of the menu command and the name in the menu hierarchy display
     * synchronized). It does require use of considerable additional resources.
     *
     * @param pValidateOnKeystroke
     *
     * Written by Craig Mitchell
     * @since 17/01/2008
     */
    public void setValidateOnKeystroke(boolean pValidateOnKeystroke) {
        this.validateOnKeystroke = pValidateOnKeystroke;
    }

    public boolean isValidateOnKeystroke() {
        return this.validateOnKeystroke;
    }

    /**
     * Registers a <code>PropertyChangeListener</code> on the <code>valueModel</code> property.
     * In conjunction with
     * a <code>DocumentListener</code> it is used to determine
     * whether the property change originated from this text field or otherwise.
     * For this to happen, the registration must occur before any <code>DocumentAdapter</code>
     * subscribes for <code>PropertyChangeEvent</code>s on the same <code>valueModel</code>.
     *
     */
    public void registerPropertyChangeHandler() {
        if (valueModel != null) {
            valueModel.addValueChangeListener(propertyChangeHandler);
        }
        else {
            IllegalStateException errorVar = new IllegalStateException("valueModel not set");
            ErrorMgr.addError(errorVar);
            throw errorVar;
        }
    }

    /**
     * Registers a <code>PropertyChangeListener</code> on the <code>valueModel</code> property.
     * The listener is used to handle the positioning of the carat when a property change
     * on <code>valueModel</code> does not originate from this text field.
     * The registration must be invoked after any other listeners that manipulate the text
     * on this text area have subscribed to <code>valueModel</code>. This includes the subscription
     * carried out by any <code>DocumentAdapter</code>.
     *
     */
    public void registerCaratPositionHandler() {
        if (valueModel != null) {
            valueModel.addValueChangeListener(caratPositionChangeHandler);
        }
        else {
            IllegalStateException errorVar = new IllegalStateException("valueModel not set");
            ErrorMgr.addError(errorVar);
            throw errorVar;
        }
    }

    private void recordLastUserEnteredText() {
        lastUserEnteredText = getText();
    }

    /**
     * Returns true if the text contained in this is the same as the text of the associated model property.
     * Returns false otherwise.
     */
    private boolean isModelText() {
        boolean result = false;
        result = lastModelSuppliedText != null && getText().equals(lastModelSuppliedText);
        return result;
    }

    private void handleFirstPropertyChangeEvent(PropertyChangeEvent evt) {
        if (evt.getPropertyName() == "value") {
            getDocument().removeDocumentListener(this);
            if (evt.getNewValue() == null) {
                lastModelSuppliedText = null;
            }
            else {
                if (!getText().equals(evt.getNewValue().toString())) {
                    lastModelSuppliedText = evt.getNewValue().toString();
                }
                else {
                    if (lastUserEnteredText.equals(evt.getNewValue().toString())) {
                        lastModelSuppliedText = null;
                    }
                }
            }
            getDocument().addDocumentListener(this);
        }
    }

    private void handleLastPropertyChangeEvent(PropertyChangeEvent evt) {
        if (evt.getPropertyName() == "value") {
            getDocument().removeDocumentListener(this);
            if (lastModelSuppliedText != null && evt.getNewValue() != null) {
                if (lastModelSuppliedText.equals(evt.getNewValue().toString())) {
                    setCaretPosition(0);
                }
            }
            getDocument().addDocumentListener(this);
        }
    }

    private static class PropertyChangeHandler implements PropertyChangeListener {
      // TF:25/06/2010:Changed this class to be static and to provide weak references to the underlying objects. Failure to do
      // this will result in us keeping a window around if the multiline text fiels which was mapped is kept around (since the field has property
      // change listeners, which will keep this class in memory, which will keep the field in memory)
      private WeakReference<MultiLineTextField> field;
      public PropertyChangeHandler(MultiLineTextField mtf) {
        field = new WeakReference<MultiLineTextField>(mtf);
    }
     
        public void propertyChange(PropertyChangeEvent evt) {
          MultiLineTextField mtf = field.get();
          if (mtf != null) {
            mtf.handleFirstPropertyChangeEvent(evt);
          }
        }
    }

    private static class CaratPositionChangeHandler implements PropertyChangeListener {
      // TF:25/06/2010:Changed this class to be static and to provide weak references to the underlying objects. Failure to do
      // this will result in us keeping a window around if the multiline text field which was mapped is kept around (since the field has property
      // change listeners, which will keep this class in memory, which will keep the field in memory)
      private WeakReference<MultiLineTextField> field;
      public CaratPositionChangeHandler(MultiLineTextField mtf) {
        field = new WeakReference<MultiLineTextField>(mtf);
    }
     
         public void propertyChange(PropertyChangeEvent evt) {
          MultiLineTextField mtf = field.get();
          if (mtf != null) {
            mtf.handleLastPropertyChangeEvent(evt);
          }
        }
    }

    /**
     * Clone method to only clone the properties that should be cloned.
     * Working out which properties should be cloned is a bit of a black art.
     *
     * Written by Craig Mitchell
     * @since 18/02/2008
     */
    public JTextArea cloneComponent(){
        JTextArea clone = TextEditFactory.newTextEditField(getName());

//    PropertyDescriptor[] targetPds = BeanUtils.getPropertyDescriptors(this.getClass());
//   
//    for (int i=0; i<targetPds.length; i++) {
//      System.out.println(targetPds[i].getName());
//    }

        CloneHelper.cloneComponent(this, clone, new String[] {
                "UI",
                "UIClassID",
                "accessibleContext",
                "actionMap",
                "actions",
                // "alignmentX",
                // "alignmentY",
                "ancestorListeners",
                // "autoscrolls",
                // "background",
                "backgroundTemporarily",
                // "border",
                "caret",
                "caretColor",
                "caretListeners",
                "caretPosition",
                // "columns",
                "component",
                "componentCount",
                "componentOrientation",
                // "componentPopupMenu",
                "components",
                "containerListeners",
                "debugGraphicsOptions",
                // "disabledTextColor",
                "document",
                // "doubleBuffered",
                // "dragEnabled",
                // "editable",
                // "enabled",
                "focusAccelerator",
                "focusCycleRoot",
                "focusTraversalKeys",
                "focusTraversalPolicy",
                "focusTraversalPolicyProvider",
                "focusTraversalPolicySet",
                // "focusable",
                // "font",
                // "foreground",
                "graphics",
                // "height",
                "highlighter",
                // "inheritsPopupMenu",
                "inputMap",
                "inputMethodRequests",
                "inputVerifier",
                // "insets",
                "keymap",
                "layout",
                "lineCount",
                "lineEndOffset",
                "lineOfOffset",
                "lineStartOffset",
                // "lineWrap",  CraigM: 13/03/2008
                "managingFocus",
                // "margin",
                // "maximumSize",
                // "minimumSize",
                "name",
                // "navigationFilter",
                "nextFocusableComponent",
                // "opaque",
                // "optimizedDrawingEnabled",
                "overriddenBackgroundColor",
                "paintingTile",
                "preferredScrollableViewportSize",
                "preferredSize"//  Do NOT copy the preferred size
                "registeredKeyStrokes",
                // "requestFocusEnabled",
                "rootPane",
                "rows",
                "scrollableTracksViewportHeight",
                "scrollableTracksViewportWidth",
                "selectedText",
                "selectedTextColor",
                "selectionColor",
                "selectionEnd",
                "selectionStart",
                "tabSize",
                "text",
                "toolTipText",
                "topLevelAncestor",
                "transferHandler",
                // "validateOnKeystroke",
                "validateRoot",
                "valueModel",
                // "verifyInputWhenFocusTarget",
                "vetoableChangeListeners",
                // "visible",
                "visibleRect",
                // "width",
                // "wrapStyleWord",  CraigM: 13/03/2008
                // "x",
                // "y"
        });

        return clone;
    }

    @Override
    public void paste() {
        /* PM:12/10/07
         * Forte would allow the past of text into a field even
         * if the text in the clipboard was longer than the allowed.
         * It would simply truncate it
         */
      // CONV:TF:21 Jul 2009:Fixed this to work properly
      if (isEditable() && isEnabled()) {
        int finalCursorPosition = 0;
        boolean didManualPaste = false;
        if (getDocument() instanceof FixedLengthDocument){
          FixedLengthDocument doc = (FixedLengthDocument)getDocument();
          Transferable t = Toolkit.getDefaultToolkit().getSystemClipboard().getContents(null);
          try {
            if (doc.getMaxLength() > 0 && t != null && t.isDataFlavorSupported(DataFlavor.stringFlavor)) {
              String text = (String)t.getTransferData(DataFlavor.stringFlavor);
              // CONV:TF:21 Jul 2009:Insert the text at the correct spot in the current String. The String is truncated
              // to the maximum number of characters, and the cursor is moved to the end of the selection or the
              // end of string, whichever is smaller.
              if (text.length()+doc.getLength() > doc.getMaxLength()) {
                String currentText = doc.getText(0, doc.getLength());
                int caretMark = this.getCaret().getMark();
                int caretDot = this.getCaret().getDot();
                String newText = currentText.substring(0, Math.min(caretMark, caretDot)) + text + currentText.substring(Math.max(caretMark, caretDot));
                this.setText(newText);
                finalCursorPosition = Math.min(caretDot+text.length(), doc.getMaxLength());
                didManualPaste = true;
              }
            }
          } catch (UnsupportedFlavorException e) {
          } catch (IOException e) {
          } catch (BadLocationException e) {
          }
        }
        if (!didManualPaste) {
          super.paste();
          finalCursorPosition = this.getCaretPosition();
        }
        // CONV:TF:21 Jul 2009:We need to reset the cursor to the appropriate place, in case
        // a menu pick will give focus back to this control, thus resetting the location to the start of the field
        final int position = finalCursorPosition;
      SwingUtilities.invokeLater(new Runnable() {
        public void run() {
          setCaretPosition(position);
        }
      });

      }
    }
    //PM:11/04/08 JCT-534
    @Override
    protected void fireCaretUpdate(CaretEvent e) {
      String txt = getSelectedText();
      if (txt != null && txt.length() > 0){
        ClientEventManager.getTextMenuStatusChangedListener().setSelectedTextState(Constants.MS_ENABLED);
      } else {
        ClientEventManager.getTextMenuStatusChangedListener().setSelectedTextState(Constants.MS_USAGESTATE);
      }
      super.fireCaretUpdate(e);
    }

  /**
   * Get the background colour. This obeys the forte-style rules for background colour:<p>
   * <ul>
   *   <li>If an explicit background colour has been set, use that background colour</li>
   <li>Otherwise, if any of our parents have background colour set, use that colour</li>
   <li>Otherwise, use the default colour (white)
   * </ul>
   */
  @Override
  public Color getBackground() {
    if (!isBackgroundSet()) {
      // We've been set to have a colour of inherit
        Container parent = this.getParent();
        // Keep going up until we find a parent with the background set. We don't care about
        // JScrollPanes or JViewports, because these should derive their colour from the control,
        // not the other way around
        while (parent != null && (!parent.isBackgroundSet() || parent instanceof JScrollPane || parent instanceof JViewport)) {
          parent = parent.getParent();
        }
        if (parent != null && !(parent instanceof Window)) {
          return parent.getBackground();
        }
        else {
          // There's no parent that's valid, put the default colour of white
          return Color.white;
        }
    }
    return super.getBackground();
  }

  /**
   * Get the foreground colour. This obeys the forte-style rules for foreground colour:<p>
   * <ul>
   *   <li>If an explicit foreground colour has been set, use that foreground colour</li>
   <li>Otherwise, if any of our parents have foreground colour set, use that colour</li>
   <li>Otherwise, use the default colour (black)
   * </ul>
   */
  @Override
  public Color getForeground() {
    if (!isForegroundSet()) {
      // We've been set to have a colour of inherit
        Container parent = this.getParent();
        // Keep going up until we find a parent with the background set. We don't care about
        // JScrollPanes or JViewports, because these should derive their colour from the control,
        // not the other way around
        while (parent != null && (!parent.isForegroundSet() || parent instanceof JScrollPane || parent instanceof JViewport)) {
          parent = parent.getParent();
        }
        if (parent != null && !(parent instanceof Window)) {
          return parent.getForeground();
        }
        else {
          // There's no parent that's valid, return the default colour of black
          return Color.black;
        }
    }
    return super.getForeground();
  }
}
TOP

Related Classes of DisplayProject.controls.MultiLineTextField$CaratPositionChangeHandler

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.
/html>