| /* |
| * @(#)$RCSfile: ATextPanelImpl.java,v $ $Revision: 1.2 $ $Date: 2000/04/22 17:10:10 $ |
| * |
| * (C) Copyright IBM Corp. 1998-1999. All Rights Reserved. |
| * |
| * The program is provided "as is" without any warranty express or |
| * implied, including the warranty of non-infringement and the implied |
| * warranties of merchantibility and fitness for a particular purpose. |
| * IBM will not be liable for any damages suffered by you as a result |
| * of using the Program. In no event will IBM be liable for any |
| * special, indirect or consequential damages or lost profits even if |
| * IBM has been advised of the possibility of their occurrence. IBM |
| * will not be liable for any third party claims against you. |
| */ |
| package com.ibm.richtext.textpanel; |
| |
| import java.awt.Adjustable; |
| import java.awt.Component; |
| import java.awt.Graphics; |
| import java.awt.datatransfer.Clipboard; |
| |
| import com.ibm.textlayout.attributes.AttributeMap; |
| import com.ibm.textlayout.attributes.AttributeSet; |
| |
| import com.ibm.richtext.styledtext.StyleModifier; |
| import com.ibm.richtext.styledtext.MConstText; |
| import com.ibm.richtext.styledtext.MText; |
| import com.ibm.richtext.styledtext.StyledText; |
| import com.ibm.richtext.textformat.TextOffset; |
| |
| /** |
| * Implementation class for TextPanel and JTextPanel. |
| */ |
| final class ATextPanelImpl { |
| |
| static final String COPYRIGHT = |
| "(C) Copyright IBM Corp. 1998-1999 - All Rights Reserved"; |
| |
| private RunStrategy fRunStrategy = null; |
| private TextComponent fTextComponent = null; |
| private TextSelection fSelection = null; |
| private TextEditBehavior fEditBehavior = null; |
| private MText fText = null; |
| |
| private PanelEventBroadcaster fBroadcaster; |
| private KeyRemap fRemap = KeyRemap.getIdentityRemap(); |
| |
| // This is a little ugly. TextPanel supports its modified |
| // flag whether or not it is editable, or even selectable. |
| // So if there's no command log to keep track of the flag |
| // state then its done right here in TextPanel. If the |
| // panel is editable this flag is ignored. |
| private boolean fModified = false; |
| |
| static final TextPanelSettings fgDefaultSettings = new TextPanelSettings(); |
| |
| static TextPanelSettings getDefaultSettings() { |
| |
| return (TextPanelSettings) fgDefaultSettings.clone(); |
| } |
| |
| ATextPanelImpl(RunStrategy runStrategy, |
| TextPanelSettings settings, |
| MConstText initialText, |
| Clipboard clipboard, |
| MTextPanel client, |
| Adjustable horzSb, |
| Adjustable vertSb) { |
| |
| fRunStrategy = runStrategy; |
| fBroadcaster = new PanelEventBroadcaster(client); |
| |
| Scroller scroller = null; |
| if (settings.getScrollable()) { |
| scroller = new Scroller(horzSb, vertSb); |
| } |
| |
| StyledTextClipboard textClipboard = |
| StyledTextClipboard.getClipboardFor(clipboard); |
| |
| fText = new StyledText(); |
| if (initialText != null) { |
| fText.append(initialText); |
| } |
| |
| fTextComponent = new TextComponent(fText, |
| settings.getDefaultValues(), |
| settings.getWraps(), |
| TextComponent.WINDOW_WIDTH, |
| TextComponent.DEFAULT_INSET, |
| textClipboard, |
| settings.getScrollable(), |
| scroller, |
| fBroadcaster); |
| |
| if (scroller != null) { |
| scroller.setClient(fTextComponent); |
| } |
| |
| // May have to wait until component has host to do this: |
| if (settings.getSelectable()) { |
| fSelection = new TextSelection(fTextComponent, |
| fBroadcaster, |
| fRunStrategy); |
| fSelection.addToOwner(fTextComponent); |
| if (settings.getEditable()) { |
| fEditBehavior = new TextEditBehavior( |
| fTextComponent, fSelection, fBroadcaster, fRemap); |
| fEditBehavior.addToOwner(fTextComponent); |
| } |
| } |
| } |
| |
| FakeComponent getTextComponent() { |
| |
| return fTextComponent; |
| } |
| |
| /** |
| * Add the given TextPanelListener to the listeners which will |
| * receive update notifications from this TextPanel. |
| * @param listener the listener to add |
| */ |
| public void addListener(TextPanelListener listener) { |
| |
| fBroadcaster.addListener(listener); |
| } |
| |
| /** |
| * Remove the given TextPanelListener from the listeners which will |
| * receive update notifications from this TextPanel. |
| * @param listener the listener to remove |
| */ |
| public void removeListener(TextPanelListener listener) { |
| |
| fBroadcaster.removeListener(listener); |
| } |
| |
| /** |
| * You know what this does... |
| */ |
| private static int pin(int value, int min, int max) { |
| |
| if (min > max) { |
| throw new IllegalArgumentException("Invalid range"); |
| } |
| |
| if (value < min) { |
| value = min; |
| } |
| else if (value > max) { |
| value = max; |
| } |
| return value; |
| } |
| |
| //============ |
| // Text Access |
| //============ |
| |
| /** |
| * Set the document to <tt>newText</tt>. This operation |
| * modifies the text in the TextPanel. It does not modify or adopt |
| * <tt>newText</tt>. This method sets the selection an insertion point at |
| * the end of the text. |
| * @param newText the text which will replace the current text. |
| */ |
| public void setText(MConstText newText) { |
| |
| replaceRange(newText, 0, getTextLength()); |
| } |
| |
| /** |
| * Append the given text to the end of the document. Equivalent to |
| * <tt>insert(newText, getTextLength())</tt>. |
| * @param newText the text to append to the document |
| */ |
| public void append(MConstText newText) { |
| |
| int length = getTextLength(); |
| replaceRange(newText, length, length); |
| } |
| |
| /** |
| * Insert the given text into the document at the given position. |
| * Equivalent to |
| * <tt>replaceRange(newText, position, position)</tt>. |
| * @param newText the text to insert into the document. |
| * @param position the position in the document where the |
| * text will be inserted |
| */ |
| public void insert(MConstText newText, int position) { |
| |
| replaceRange(newText, position, position); |
| } |
| |
| /** |
| * Replace the given range with <tt>newText</tt>. After this |
| * operation the selection range is an insertion point at the |
| * end of the new text. |
| * @param newText the text with which to replace the range |
| * @param start the beginning of the range to replace |
| * @param end the end of the range to replace |
| */ |
| public void replaceRange(MConstText newText, int start, int end) { |
| |
| int length = getTextLength(); |
| |
| start = pin(start, 0, length); |
| end = pin(end, start, length); |
| |
| if (fSelection != null) { |
| |
| // If we're selectable, but not editable, we'll temporarily |
| // make ourselves editable to change the text. A little funny |
| // but there's a lot of code for getting caret stuff right, |
| // and this is not a common operation anyway. |
| |
| TextEditBehavior behavior; |
| |
| if (fEditBehavior == null) { |
| behavior = new TextEditBehavior(fTextComponent, fSelection, fBroadcaster, fRemap); |
| behavior.addToOwner(fTextComponent); |
| } |
| else { |
| behavior = fEditBehavior; |
| } |
| |
| TextOffset newSelection = new TextOffset(start + newText.length(), |
| TextOffset.AFTER_OFFSET); |
| |
| TextReplacement replacement = new TextReplacement(start, end, |
| newText, |
| newSelection, |
| newSelection); |
| |
| fTextComponent.textControlEventOccurred(Behavior.REPLACE, |
| replacement); |
| if (fEditBehavior == null) { |
| behavior.removeFromOwner(); |
| } |
| } |
| else { |
| |
| MText oldText = fTextComponent.getModifiableText(); |
| fTextComponent.stopBackgroundFormatting(); |
| oldText.replaceAll(newText); |
| fTextComponent.reformatAndDrawText(0, newText.length(), null, null, null, null); |
| } |
| } |
| |
| /** |
| * Return the length of the text document in the TextPanel. |
| * @return the length of the text document in the TextPanel |
| */ |
| public int getTextLength() { |
| |
| return fTextComponent.getText().length(); |
| } |
| |
| /** |
| * Return the text document in the TextPanel. |
| * @return the text document in the TextPanel. |
| */ |
| public MConstText getText() { |
| |
| return fTextComponent.getText(); |
| } |
| |
| //============ |
| // Selection Access |
| //============ |
| |
| /** |
| * Return the offset of the start of the selection. |
| */ |
| public int getSelectionStart() { |
| |
| if (fSelection != null) { |
| return fSelection.getStart().fOffset; |
| } |
| else { |
| return 0; |
| } |
| } |
| |
| /** |
| * Return the offset of the end of the selection. |
| */ |
| public int getSelectionEnd() { |
| |
| if (fSelection != null) { |
| return fSelection.getEnd().fOffset; |
| } |
| else { |
| return 0; |
| } |
| } |
| |
| /** |
| * Set the beginning of the selection range. This is |
| * equivalent to <tt>select(selectionStart, getSelectionEnd())</tt>. |
| * @param selectionStart the start of the new selection range |
| */ |
| public void setSelectionStart(int selectionStart) { |
| |
| select(selectionStart, getSelectionEnd()); |
| } |
| |
| /** |
| * Set the end of the selection range. This is |
| * equivalent to <tt>select(getSelectionStart(), selectionEnd)</tt>. |
| * @param selectionStart the start of the new selection range |
| */ |
| public void setSelectionEnd(int selectionEnd) { |
| |
| select(getSelectionStart(), selectionEnd); |
| } |
| |
| /** |
| * Set the selection range to an insertion point at the given |
| * offset. This is equivalent to |
| * <tt>select(position, position)</tt>. |
| * @param position the offset of the new insertion point |
| */ |
| public void setCaretPosition(int position) { |
| |
| select(position, position); |
| } |
| |
| /** |
| * Set the selection range to the given range. The range start |
| * is pinned between 0 and the text length; the range end is pinned |
| * between the range start and the end of the text. These semantics |
| * are identical to those of <tt>java.awt.TextComponent</tt>. |
| * This method has no effect if the text is not selectable. |
| * @param selectionStart the beginning of the selection range |
| * @param selectionEnd the end of the selection range |
| */ |
| public void select(int selectionStart, int selectionEnd) { |
| |
| int length = getTextLength(); |
| |
| selectionStart = pin(selectionStart, 0, length); |
| selectionEnd = pin(selectionEnd, selectionStart, length); |
| |
| TextRange range = new TextRange(selectionStart, selectionEnd); |
| fTextComponent.textControlEventOccurred(Behavior.SELECT, range); |
| } |
| |
| /** |
| * Select all of the text in the document. This method has no effect if |
| * the text is not selectable. |
| */ |
| public void selectAll() { |
| |
| select(0, getTextLength()); |
| } |
| |
| |
| //============ |
| // Format Width |
| //============ |
| |
| /** |
| * Return the total format width, in pixels. The format width is the |
| * width to which text is wrapped. |
| * @return the format width |
| */ |
| public int getFormatWidth() { |
| |
| return fTextComponent.getFormatWidth(); |
| } |
| |
| /** |
| * Return true if the paragraph at the given offset is left-to-right. |
| * @param offset an offset in the text |
| * @return true if the paragraph at the given offset is left-to-right |
| */ |
| public boolean paragraphIsLeftToRight(int offset) { |
| |
| return fTextComponent.paragraphIsLeftToRight(offset); |
| } |
| |
| /** |
| * Return true if there is a change which can be undone. |
| * @return true if there is a change which can be undone. |
| */ |
| public boolean canUndo() { |
| |
| if (fEditBehavior != null) { |
| return fEditBehavior.canUndo(); |
| } |
| else { |
| return false; |
| } |
| } |
| |
| /** |
| * Return true if there is a change which can be redone. |
| * @return true if there is a change which can be redone. |
| */ |
| public boolean canRedo() { |
| |
| if (fEditBehavior != null) { |
| return fEditBehavior.canRedo(); |
| } |
| else { |
| return false; |
| } |
| } |
| |
| /** |
| * Return true if the clipboard contains contents which could be |
| * transfered into the text. |
| * @return true if the clipboard has text content. |
| */ |
| public boolean clipboardNotEmpty() { |
| |
| return fTextComponent.getClipboard().hasContents(); |
| } |
| |
| /** |
| * Return an AttributeMap of keys with default values. The default |
| * values are used when displaying text for values which are not |
| * specified in the text. |
| * @return an AttributeMap of default key-value pairs |
| */ |
| public AttributeMap getDefaultValues() { |
| |
| return fTextComponent.getDefaultValues(); |
| } |
| |
| private static boolean objectsAreEqual(Object lhs, Object rhs) { |
| |
| if (lhs == null) { |
| return rhs == null; |
| } |
| else { |
| return lhs.equals(rhs); |
| } |
| } |
| |
| private static Object consistentCharStyle(MConstText text, |
| int start, |
| int limit, |
| Object key, |
| Object defaultValue) { |
| |
| if (start >= limit) { |
| throw new IllegalArgumentException("Invalid range."); |
| } |
| |
| int runStart = start; |
| Object initialValue = text.characterStyleAt(runStart).get(key); |
| |
| if (initialValue == null) { |
| initialValue = defaultValue; |
| } |
| |
| for (runStart = text.characterStyleLimit(runStart); |
| runStart < limit; |
| runStart = text.characterStyleLimit(runStart)) { |
| |
| Object nextValue = text.characterStyleAt(runStart).get(key); |
| |
| if (nextValue == null) { |
| nextValue = defaultValue; |
| } |
| |
| if (!objectsAreEqual(initialValue, nextValue)) { |
| return MTextPanel.MULTIPLE_VALUES; |
| } |
| } |
| |
| return initialValue; |
| } |
| |
| /** |
| * This method inspects the character style runs in the selection |
| * range (or the typing style at the insertion point) and returns: |
| * <ul> |
| * <li>The value of <tt>key</tt>, if the value of <tt>key</tt> |
| * is the same in all of the style runs in the selection, or</li> |
| * <li>null, if two or more style runs have different values for <tt>key</tt>.</li> |
| * </ul> |
| * If a style run does not contain <tt>key</tt>, |
| * its value is considered to be <tt>defaultStyle</tt>. |
| * This method is useful for configuring style menus. |
| * @param key the key used to retrieve values for comparison |
| * @param defaultValue the implicit value of <tt>key</tt> in |
| * style runs where <tt>key</tt> is not defined |
| */ |
| public Object getCharacterStyleOverSelection(Object key) { |
| |
| TextRange selRange; |
| if (fSelection != null) |
| selRange = fSelection.getSelectionRange(); |
| else |
| selRange = new TextRange(0, 0); |
| |
| if (selRange.start == selRange.limit) { |
| |
| AttributeMap compStyle; |
| |
| if (fEditBehavior != null) { |
| compStyle = fEditBehavior.getInsertionPointStyle(); |
| } |
| else { |
| compStyle = TextEditBehavior.typingStyleAt(fText, selRange.start, selRange.limit); |
| } |
| |
| Object value = compStyle.get(key); |
| return value==null? getDefaultValues().get(key) : value; |
| } |
| else { |
| return consistentCharStyle(fText, |
| selRange.start, |
| selRange.limit, |
| key, |
| getDefaultValues().get(key)); |
| } |
| } |
| |
| /** |
| * This method inspects the paragraph style runs in the selection |
| * range (or the typing style at the insertion point) and returns: |
| * <ul> |
| * <li>The value of <tt>key</tt>, if the value of <tt>key</tt> |
| * is the same in all of the style runs in the selection, or</li> |
| * <li>null, if two or more style runs have different values for <tt>key</tt>.</li> |
| * </ul> |
| * If a style run does not contain <tt>key</tt>, |
| * its value is considered to be <tt>defaultStyle</tt>. |
| * This method is useful for configuring style menus. |
| * @param key the key used to retrieve values for comparison |
| * @param defaultValue the implicit value of <tt>key</tt> in |
| * style runs where <tt>key</tt> is not defined |
| */ |
| public Object getParagraphStyleOverSelection(Object key) { |
| |
| TextRange selRange; |
| if (fSelection != null) { |
| selRange = fSelection.getSelectionRange(); |
| } |
| else { |
| selRange = new TextRange(0, 0); |
| } |
| |
| if (selRange.start == selRange.limit) { |
| AttributeMap pStyle = fText.paragraphStyleAt(selRange.start); |
| Object value = pStyle.get(key); |
| return value==null? getDefaultValues().get(key) : value; |
| } |
| else { |
| int paragraphStart = selRange.start; |
| Object defaultValue = getDefaultValues().get(key); |
| Object initialValue = fText.paragraphStyleAt(paragraphStart).get(key); |
| if (initialValue == null) { |
| initialValue = defaultValue; |
| } |
| |
| for (paragraphStart = fText.paragraphLimit(paragraphStart); |
| paragraphStart < selRange.limit; |
| paragraphStart = fText.paragraphLimit(paragraphStart)) { |
| |
| Object nextValue = fText.paragraphStyleAt(paragraphStart).get(key); |
| if (nextValue == null) { |
| nextValue = defaultValue; |
| } |
| |
| if (!objectsAreEqual(initialValue, nextValue)) { |
| return MTextPanel.MULTIPLE_VALUES; |
| } |
| } |
| |
| return initialValue; |
| } |
| } |
| |
| /** |
| * Remove the selected text from the document and place it |
| * on the clipboard. This method has no effect if the text |
| * is not editable, or if no text is selected. |
| */ |
| public void cut() { |
| fTextComponent.textControlEventOccurred(Behavior.CUT, null); |
| } |
| |
| /** |
| * Place the selected text on the clipboard. This method has |
| * no effect if no text is selected. |
| */ |
| public void copy() { |
| fTextComponent.textControlEventOccurred(Behavior.COPY, null); |
| } |
| |
| /** |
| * Replace the currently selected text with the text on the clipboard. |
| * This method has no effect if the text is not editable, or if no |
| * text is on the clipboard. |
| */ |
| public void paste() { |
| fTextComponent.textControlEventOccurred(Behavior.PASTE, null); |
| } |
| |
| /** |
| * Remove selected text from the document, without altering the clipboard. |
| * This method has no effect if the |
| * text is not editable. |
| */ |
| public void clear() { |
| fTextComponent.textControlEventOccurred(Behavior.CLEAR, null); |
| } |
| |
| /** |
| * Undo the most recent text change. This method has no effect if |
| * there is no change to undo. |
| */ |
| public void undo() { |
| fTextComponent.textControlEventOccurred(Behavior.UNDO, null); |
| } |
| |
| /** |
| * Redo the most recent text change. This method has no effect if |
| * there is no change to redo. |
| */ |
| public void redo() { |
| fTextComponent.textControlEventOccurred(Behavior.REDO, null); |
| } |
| |
| /** |
| * Return the number of commands the command log can hold. |
| * @return the number of commands the command log can hold |
| */ |
| public int getCommandLogSize() { |
| |
| if (fEditBehavior != null) { |
| return fEditBehavior.getCommandLogSize(); |
| } |
| else { |
| return 0; |
| } |
| } |
| |
| /** |
| * Set the number of commands the command log can hold. All |
| * redoable commands are removed when this method is called. |
| * @param size the number of commands kept in the command log |
| */ |
| public void setCommandLogSize(int size) { |
| fTextComponent.textControlEventOccurred(Behavior.SET_COMMAND_LOG_SIZE, |
| new Integer(size)); |
| } |
| |
| /** |
| * Remove all commands from the command log. |
| */ |
| public void clearCommandLog() { |
| fTextComponent.textControlEventOccurred(Behavior.CLEAR_COMMAND_LOG, null); |
| } |
| |
| /** |
| * Modify the character styles on the selected characters. If no characters |
| * are selected, modify the typing style. |
| * @param modifier the StyleModifier with which to modify the styles |
| */ |
| public void modifyCharacterStyleOnSelection(StyleModifier modifier) { |
| fTextComponent.textControlEventOccurred(Behavior.CHARACTER_STYLE_MOD, modifier); |
| } |
| |
| /** |
| * Modify the paragraph styles in paragraphs containing selected characters, or |
| * the paragraph containing the insertion point. |
| * @param modifier the StyleModifier with which to modify the styles |
| */ |
| public void modifyParagraphStyleOnSelection(StyleModifier modifier) { |
| fTextComponent.textControlEventOccurred(Behavior.PARAGRAPH_STYLE_MOD, modifier); |
| } |
| |
| /** |
| * Return the KeyRemap used to process key events. |
| * @return the key remap used to process key events |
| * @see #setKeyRemap |
| */ |
| public KeyRemap getKeyRemap() { |
| |
| return fRemap; |
| } |
| |
| /** |
| * Use the given KeyRemap to map key events to characters. |
| * Only key |
| * events are affected by the remap; other text entering the |
| * control (via the clipboard, for example) is not affected |
| * by the KeyRemap. |
| * <p> |
| * Do not pass <tt>null</tt> to this method to leave key |
| * events unmapped. Instead, use <tt>KeyRemap.getIdentityRemap()</tt> |
| * @param remap the KeyRemap to use for mapping key events to characters |
| * @exception java.lang.NullPointerException if parameter is null |
| * @see KeyRemap |
| */ |
| public void setKeyRemap(KeyRemap remap) { |
| |
| if (remap == null) { |
| throw new NullPointerException("remap can't be null"); |
| } |
| |
| fRemap = remap; |
| if (fEditBehavior != null) { |
| fEditBehavior.setKeyRemap(remap); |
| } |
| |
| fBroadcaster.textStateChanged(TextPanelEvent.KEYREMAP_CHANGED); |
| } |
| |
| /** |
| * Return the modification flag of the current text change. |
| * @see #setModified |
| */ |
| public boolean isModified() { |
| |
| if (fEditBehavior != null) { |
| return fEditBehavior.isModified(); |
| } |
| else { |
| return fModified; |
| } |
| } |
| |
| /** |
| * Set the modification flag of the current text change. |
| */ |
| public void setModified(boolean modified) { |
| |
| boolean handled = fTextComponent.textControlEventOccurred( |
| Behavior.SET_MODIFIED, |
| modified? Boolean.TRUE : Boolean.FALSE); |
| if (!handled) { |
| fModified = modified; |
| } |
| } |
| |
| /** |
| * This method is for perf-testing only! |
| */ |
| void handleKeyEvent(java.awt.event.KeyEvent keyEvent) { |
| |
| Component host = fTextComponent.getHost(); |
| if (host != null) { |
| host.dispatchEvent(keyEvent); |
| } |
| } |
| } |