001: /*
002: * (C) Copyright IBM Corp. 1998-2004. All Rights Reserved.
003: *
004: * The program is provided "as is" without any warranty express or
005: * implied, including the warranty of non-infringement and the implied
006: * warranties of merchantibility and fitness for a particular purpose.
007: * IBM will not be liable for any damages suffered by you as a result
008: * of using the Program. In no event will IBM be liable for any
009: * special, indirect or consequential damages or lost profits even if
010: * IBM has been advised of the possibility of their occurrence. IBM
011: * will not be liable for any third party claims against you.
012: */
013: package com.ibm.richtext.textpanel;
014:
015: import java.awt.Adjustable;
016: import java.awt.Component;
017: import java.awt.datatransfer.Clipboard;
018:
019: import com.ibm.richtext.textlayout.attributes.AttributeMap;
020:
021: import com.ibm.richtext.styledtext.StyleModifier;
022: import com.ibm.richtext.styledtext.MConstText;
023: import com.ibm.richtext.styledtext.MText;
024: import com.ibm.richtext.styledtext.StyledText;
025: import com.ibm.richtext.textformat.TextOffset;
026:
027: /**
028: * Implementation class for TextPanel and JTextPanel.
029: */
030: final class ATextPanelImpl {
031:
032: static final String COPYRIGHT = "(C) Copyright IBM Corp. 1998-1999 - All Rights Reserved";
033:
034: private RunStrategy fRunStrategy = null;
035: private TextComponent fTextComponent = null;
036: private TextSelection fSelection = null;
037: private TextEditBehavior fEditBehavior = null;
038: private MText fText = null;
039:
040: private PanelEventBroadcaster fBroadcaster;
041: private KeyRemap fRemap = KeyRemap.getIdentityRemap();
042:
043: // This is a little ugly. TextPanel supports its modified
044: // flag whether or not it is editable, or even selectable.
045: // So if there's no command log to keep track of the flag
046: // state then its done right here in TextPanel. If the
047: // panel is editable this flag is ignored.
048: private boolean fModified = false;
049:
050: static final TextPanelSettings fgDefaultSettings = new TextPanelSettings();
051:
052: static TextPanelSettings getDefaultSettings() {
053:
054: return (TextPanelSettings) fgDefaultSettings.clone();
055: }
056:
057: ATextPanelImpl(RunStrategy runStrategy, TextPanelSettings settings,
058: MConstText initialText, Clipboard clipboard,
059: MTextPanel client, Adjustable horzSb, Adjustable vertSb) {
060:
061: fRunStrategy = runStrategy;
062: fBroadcaster = new PanelEventBroadcaster(client);
063:
064: Scroller scroller = null;
065: if (settings.getScrollable()) {
066: scroller = new Scroller(horzSb, vertSb);
067: }
068:
069: StyledTextClipboard textClipboard = StyledTextClipboard
070: .getClipboardFor(clipboard);
071:
072: fText = new StyledText();
073: if (initialText != null) {
074: fText.append(initialText);
075: }
076:
077: fTextComponent = new TextComponent(fText, settings
078: .getDefaultValues(), settings.getWraps(),
079: TextComponent.WINDOW_WIDTH,
080: TextComponent.DEFAULT_INSET, textClipboard, settings
081: .getScrollable(), scroller, fBroadcaster);
082:
083: if (scroller != null) {
084: scroller.setClient(fTextComponent);
085: }
086:
087: // May have to wait until component has host to do this:
088: if (settings.getSelectable()) {
089: fSelection = new TextSelection(fTextComponent,
090: fBroadcaster, fRunStrategy);
091: fSelection.addToOwner(fTextComponent);
092: if (settings.getEditable()) {
093: fEditBehavior = new TextEditBehavior(fTextComponent,
094: fSelection, fBroadcaster, fRemap);
095: fEditBehavior.addToOwner(fTextComponent);
096: }
097: }
098: }
099:
100: FakeComponent getTextComponent() {
101:
102: return fTextComponent;
103: }
104:
105: /**
106: * Add the given TextPanelListener to the listeners which will
107: * receive update notifications from this TextPanel.
108: * @param listener the listener to add
109: */
110: public void addListener(TextPanelListener listener) {
111:
112: fBroadcaster.addListener(listener);
113: }
114:
115: /**
116: * Remove the given TextPanelListener from the listeners which will
117: * receive update notifications from this TextPanel.
118: * @param listener the listener to remove
119: */
120: public void removeListener(TextPanelListener listener) {
121:
122: fBroadcaster.removeListener(listener);
123: }
124:
125: /**
126: * You know what this does...
127: */
128: private static int pin(int value, int min, int max) {
129:
130: if (min > max) {
131: throw new IllegalArgumentException("Invalid range");
132: }
133:
134: if (value < min) {
135: value = min;
136: } else if (value > max) {
137: value = max;
138: }
139: return value;
140: }
141:
142: //============
143: // Text Access
144: //============
145:
146: /**
147: * Set the document to <tt>newText</tt>. This operation
148: * modifies the text in the TextPanel. It does not modify or adopt
149: * <tt>newText</tt>. This method sets the selection an insertion point at
150: * the end of the text.
151: * @param newText the text which will replace the current text.
152: */
153: public void setText(MConstText newText) {
154:
155: replaceRange(newText, 0, getTextLength());
156: }
157:
158: /**
159: * Append the given text to the end of the document. Equivalent to
160: * <tt>insert(newText, getTextLength())</tt>.
161: * @param newText the text to append to the document
162: */
163: public void append(MConstText newText) {
164:
165: int length = getTextLength();
166: replaceRange(newText, length, length);
167: }
168:
169: /**
170: * Insert the given text into the document at the given position.
171: * Equivalent to
172: * <tt>replaceRange(newText, position, position)</tt>.
173: * @param newText the text to insert into the document.
174: * @param position the position in the document where the
175: * text will be inserted
176: */
177: public void insert(MConstText newText, int position) {
178:
179: replaceRange(newText, position, position);
180: }
181:
182: /**
183: * Replace the given range with <tt>newText</tt>. After this
184: * operation the selection range is an insertion point at the
185: * end of the new text.
186: * @param newText the text with which to replace the range
187: * @param start the beginning of the range to replace
188: * @param end the end of the range to replace
189: */
190: public void replaceRange(MConstText newText, int start, int end) {
191:
192: int length = getTextLength();
193:
194: start = pin(start, 0, length);
195: end = pin(end, start, length);
196:
197: if (fSelection != null) {
198:
199: // If we're selectable, but not editable, we'll temporarily
200: // make ourselves editable to change the text. A little funny
201: // but there's a lot of code for getting caret stuff right,
202: // and this is not a common operation anyway.
203:
204: TextEditBehavior behavior;
205:
206: if (fEditBehavior == null) {
207: behavior = new TextEditBehavior(fTextComponent,
208: fSelection, fBroadcaster, fRemap);
209: behavior.addToOwner(fTextComponent);
210: } else {
211: behavior = fEditBehavior;
212: }
213:
214: TextOffset newSelection = new TextOffset(start
215: + newText.length(), TextOffset.AFTER_OFFSET);
216:
217: TextReplacement replacement = new TextReplacement(start,
218: end, newText, newSelection, newSelection);
219:
220: fTextComponent.textControlEventOccurred(Behavior.REPLACE,
221: replacement);
222: if (fEditBehavior == null) {
223: behavior.removeFromOwner();
224: }
225: } else {
226:
227: MText oldText = fTextComponent.getModifiableText();
228: fTextComponent.stopBackgroundFormatting();
229: oldText.replaceAll(newText);
230: fTextComponent.reformatAndDrawText(0, newText.length(),
231: null, null, null, null);
232: }
233: }
234:
235: /**
236: * Return the length of the text document in the TextPanel.
237: * @return the length of the text document in the TextPanel
238: */
239: public int getTextLength() {
240:
241: return fTextComponent.getText().length();
242: }
243:
244: /**
245: * Return the text document in the TextPanel.
246: * @return the text document in the TextPanel.
247: */
248: public MConstText getText() {
249:
250: return fTextComponent.getText();
251: }
252:
253: //============
254: // Selection Access
255: //============
256:
257: /**
258: * Return the offset of the start of the selection.
259: */
260: public int getSelectionStart() {
261:
262: if (fSelection != null) {
263: return fSelection.getStart().fOffset;
264: } else {
265: return 0;
266: }
267: }
268:
269: /**
270: * Return the offset of the end of the selection.
271: */
272: public int getSelectionEnd() {
273:
274: if (fSelection != null) {
275: return fSelection.getEnd().fOffset;
276: } else {
277: return 0;
278: }
279: }
280:
281: /**
282: * Set the beginning of the selection range. This is
283: * equivalent to <tt>select(selectionStart, getSelectionEnd())</tt>.
284: * @param selectionStart the start of the new selection range
285: */
286: public void setSelectionStart(int selectionStart) {
287:
288: select(selectionStart, getSelectionEnd());
289: }
290:
291: /**
292: * Set the end of the selection range. This is
293: * equivalent to <tt>select(getSelectionStart(), selectionEnd)</tt>.
294: * @param selectionStart the start of the new selection range
295: */
296: public void setSelectionEnd(int selectionEnd) {
297:
298: select(getSelectionStart(), selectionEnd);
299: }
300:
301: /**
302: * Set the selection range to an insertion point at the given
303: * offset. This is equivalent to
304: * <tt>select(position, position)</tt>.
305: * @param position the offset of the new insertion point
306: */
307: public void setCaretPosition(int position) {
308:
309: select(position, position);
310: }
311:
312: /**
313: * Set the selection range to the given range. The range start
314: * is pinned between 0 and the text length; the range end is pinned
315: * between the range start and the end of the text. These semantics
316: * are identical to those of <tt>java.awt.TextComponent</tt>.
317: * This method has no effect if the text is not selectable.
318: * @param selectionStart the beginning of the selection range
319: * @param selectionEnd the end of the selection range
320: */
321: public void select(int selectionStart, int selectionEnd) {
322:
323: int length = getTextLength();
324:
325: selectionStart = pin(selectionStart, 0, length);
326: selectionEnd = pin(selectionEnd, selectionStart, length);
327:
328: TextRange range = new TextRange(selectionStart, selectionEnd);
329: fTextComponent.textControlEventOccurred(Behavior.SELECT, range);
330: }
331:
332: /**
333: * Select all of the text in the document. This method has no effect if
334: * the text is not selectable.
335: */
336: public void selectAll() {
337:
338: select(0, getTextLength());
339: }
340:
341: //============
342: // Format Width
343: //============
344:
345: /**
346: * Return the total format width, in pixels. The format width is the
347: * width to which text is wrapped.
348: * @return the format width
349: */
350: public int getFormatWidth() {
351:
352: return fTextComponent.getFormatWidth();
353: }
354:
355: /**
356: * Return true if the paragraph at the given offset is left-to-right.
357: * @param offset an offset in the text
358: * @return true if the paragraph at the given offset is left-to-right
359: */
360: public boolean paragraphIsLeftToRight(int offset) {
361:
362: return fTextComponent.paragraphIsLeftToRight(offset);
363: }
364:
365: /**
366: * Return true if there is a change which can be undone.
367: * @return true if there is a change which can be undone.
368: */
369: public boolean canUndo() {
370:
371: if (fEditBehavior != null) {
372: return fEditBehavior.canUndo();
373: } else {
374: return false;
375: }
376: }
377:
378: /**
379: * Return true if there is a change which can be redone.
380: * @return true if there is a change which can be redone.
381: */
382: public boolean canRedo() {
383:
384: if (fEditBehavior != null) {
385: return fEditBehavior.canRedo();
386: } else {
387: return false;
388: }
389: }
390:
391: /**
392: * Return true if the clipboard contains contents which could be
393: * transfered into the text.
394: * @return true if the clipboard has text content.
395: */
396: public boolean clipboardNotEmpty() {
397:
398: return fTextComponent.getClipboard().hasContents();
399: }
400:
401: /**
402: * Return an AttributeMap of keys with default values. The default
403: * values are used when displaying text for values which are not
404: * specified in the text.
405: * @return an AttributeMap of default key-value pairs
406: */
407: public AttributeMap getDefaultValues() {
408:
409: return fTextComponent.getDefaultValues();
410: }
411:
412: private static boolean objectsAreEqual(Object lhs, Object rhs) {
413:
414: if (lhs == null) {
415: return rhs == null;
416: } else {
417: return lhs.equals(rhs);
418: }
419: }
420:
421: private static Object consistentCharStyle(MConstText text,
422: int start, int limit, Object key, Object defaultValue) {
423:
424: if (start >= limit) {
425: throw new IllegalArgumentException("Invalid range.");
426: }
427:
428: int runStart = start;
429: Object initialValue = text.characterStyleAt(runStart).get(key);
430:
431: if (initialValue == null) {
432: initialValue = defaultValue;
433: }
434:
435: for (runStart = text.characterStyleLimit(runStart); runStart < limit; runStart = text
436: .characterStyleLimit(runStart)) {
437:
438: Object nextValue = text.characterStyleAt(runStart).get(key);
439:
440: if (nextValue == null) {
441: nextValue = defaultValue;
442: }
443:
444: if (!objectsAreEqual(initialValue, nextValue)) {
445: return MTextPanel.MULTIPLE_VALUES;
446: }
447: }
448:
449: return initialValue;
450: }
451:
452: /**
453: * This method inspects the character style runs in the selection
454: * range (or the typing style at the insertion point) and returns:
455: * <ul>
456: * <li>The value of <tt>key</tt>, if the value of <tt>key</tt>
457: * is the same in all of the style runs in the selection, or</li>
458: * <li>null, if two or more style runs have different values for <tt>key</tt>.</li>
459: * </ul>
460: * If a style run does not contain <tt>key</tt>,
461: * its value is considered to be <tt>defaultStyle</tt>.
462: * This method is useful for configuring style menus.
463: * @param key the key used to retrieve values for comparison
464: * @param defaultValue the implicit value of <tt>key</tt> in
465: * style runs where <tt>key</tt> is not defined
466: */
467: public Object getCharacterStyleOverSelection(Object key) {
468:
469: TextRange selRange;
470: if (fSelection != null)
471: selRange = fSelection.getSelectionRange();
472: else
473: selRange = new TextRange(0, 0);
474:
475: if (selRange.start == selRange.limit) {
476:
477: AttributeMap compStyle;
478:
479: if (fEditBehavior != null) {
480: compStyle = fEditBehavior.getInsertionPointStyle();
481: } else {
482: compStyle = TextEditBehavior.typingStyleAt(fText,
483: selRange.start, selRange.limit);
484: }
485:
486: Object value = compStyle.get(key);
487: return value == null ? getDefaultValues().get(key) : value;
488: } else {
489: return consistentCharStyle(fText, selRange.start,
490: selRange.limit, key, getDefaultValues().get(key));
491: }
492: }
493:
494: /**
495: * This method inspects the paragraph style runs in the selection
496: * range (or the typing style at the insertion point) and returns:
497: * <ul>
498: * <li>The value of <tt>key</tt>, if the value of <tt>key</tt>
499: * is the same in all of the style runs in the selection, or</li>
500: * <li>null, if two or more style runs have different values for <tt>key</tt>.</li>
501: * </ul>
502: * If a style run does not contain <tt>key</tt>,
503: * its value is considered to be <tt>defaultStyle</tt>.
504: * This method is useful for configuring style menus.
505: * @param key the key used to retrieve values for comparison
506: * @param defaultValue the implicit value of <tt>key</tt> in
507: * style runs where <tt>key</tt> is not defined
508: */
509: public Object getParagraphStyleOverSelection(Object key) {
510:
511: TextRange selRange;
512: if (fSelection != null) {
513: selRange = fSelection.getSelectionRange();
514: } else {
515: selRange = new TextRange(0, 0);
516: }
517:
518: if (selRange.start == selRange.limit) {
519: AttributeMap pStyle = fText
520: .paragraphStyleAt(selRange.start);
521: Object value = pStyle.get(key);
522: return value == null ? getDefaultValues().get(key) : value;
523: } else {
524: int paragraphStart = selRange.start;
525: Object defaultValue = getDefaultValues().get(key);
526: Object initialValue = fText
527: .paragraphStyleAt(paragraphStart).get(key);
528: if (initialValue == null) {
529: initialValue = defaultValue;
530: }
531:
532: for (paragraphStart = fText.paragraphLimit(paragraphStart); paragraphStart < selRange.limit; paragraphStart = fText
533: .paragraphLimit(paragraphStart)) {
534:
535: Object nextValue = fText.paragraphStyleAt(
536: paragraphStart).get(key);
537: if (nextValue == null) {
538: nextValue = defaultValue;
539: }
540:
541: if (!objectsAreEqual(initialValue, nextValue)) {
542: return MTextPanel.MULTIPLE_VALUES;
543: }
544: }
545:
546: return initialValue;
547: }
548: }
549:
550: /**
551: * Remove the selected text from the document and place it
552: * on the clipboard. This method has no effect if the text
553: * is not editable, or if no text is selected.
554: */
555: public void cut() {
556: fTextComponent.textControlEventOccurred(Behavior.CUT, null);
557: }
558:
559: /**
560: * Place the selected text on the clipboard. This method has
561: * no effect if no text is selected.
562: */
563: public void copy() {
564: fTextComponent.textControlEventOccurred(Behavior.COPY, null);
565: }
566:
567: /**
568: * Replace the currently selected text with the text on the clipboard.
569: * This method has no effect if the text is not editable, or if no
570: * text is on the clipboard.
571: */
572: public void paste() {
573: fTextComponent.textControlEventOccurred(Behavior.PASTE, null);
574: }
575:
576: /**
577: * Remove selected text from the document, without altering the clipboard.
578: * This method has no effect if the
579: * text is not editable.
580: */
581: public void clear() {
582: fTextComponent.textControlEventOccurred(Behavior.CLEAR, null);
583: }
584:
585: /**
586: * Undo the most recent text change. This method has no effect if
587: * there is no change to undo.
588: */
589: public void undo() {
590: fTextComponent.textControlEventOccurred(Behavior.UNDO, null);
591: }
592:
593: /**
594: * Redo the most recent text change. This method has no effect if
595: * there is no change to redo.
596: */
597: public void redo() {
598: fTextComponent.textControlEventOccurred(Behavior.REDO, null);
599: }
600:
601: /**
602: * Return the number of commands the command log can hold.
603: * @return the number of commands the command log can hold
604: */
605: public int getCommandLogSize() {
606:
607: if (fEditBehavior != null) {
608: return fEditBehavior.getCommandLogSize();
609: } else {
610: return 0;
611: }
612: }
613:
614: /**
615: * Set the number of commands the command log can hold. All
616: * redoable commands are removed when this method is called.
617: * @param size the number of commands kept in the command log
618: */
619: public void setCommandLogSize(int size) {
620: fTextComponent.textControlEventOccurred(
621: Behavior.SET_COMMAND_LOG_SIZE, new Integer(size));
622: }
623:
624: /**
625: * Remove all commands from the command log.
626: */
627: public void clearCommandLog() {
628: fTextComponent.textControlEventOccurred(
629: Behavior.CLEAR_COMMAND_LOG, null);
630: }
631:
632: /**
633: * Modify the character styles on the selected characters. If no characters
634: * are selected, modify the typing style.
635: * @param modifier the StyleModifier with which to modify the styles
636: */
637: public void modifyCharacterStyleOnSelection(StyleModifier modifier) {
638: fTextComponent.textControlEventOccurred(
639: Behavior.CHARACTER_STYLE_MOD, modifier);
640: }
641:
642: /**
643: * Modify the paragraph styles in paragraphs containing selected characters, or
644: * the paragraph containing the insertion point.
645: * @param modifier the StyleModifier with which to modify the styles
646: */
647: public void modifyParagraphStyleOnSelection(StyleModifier modifier) {
648: fTextComponent.textControlEventOccurred(
649: Behavior.PARAGRAPH_STYLE_MOD, modifier);
650: }
651:
652: /**
653: * Return the KeyRemap used to process key events.
654: * @return the key remap used to process key events
655: * @see #setKeyRemap
656: */
657: public KeyRemap getKeyRemap() {
658:
659: return fRemap;
660: }
661:
662: /**
663: * Use the given KeyRemap to map key events to characters.
664: * Only key
665: * events are affected by the remap; other text entering the
666: * control (via the clipboard, for example) is not affected
667: * by the KeyRemap.
668: * <p>
669: * Do not pass <tt>null</tt> to this method to leave key
670: * events unmapped. Instead, use <tt>KeyRemap.getIdentityRemap()</tt>
671: * @param remap the KeyRemap to use for mapping key events to characters
672: * @exception java.lang.NullPointerException if parameter is null
673: * @see KeyRemap
674: */
675: public void setKeyRemap(KeyRemap remap) {
676:
677: if (remap == null) {
678: throw new NullPointerException("remap can't be null");
679: }
680:
681: fRemap = remap;
682: if (fEditBehavior != null) {
683: fEditBehavior.setKeyRemap(remap);
684: }
685:
686: fBroadcaster.textStateChanged(TextPanelEvent.KEYREMAP_CHANGED);
687: }
688:
689: /**
690: * Return the modification flag of the current text change.
691: * @see #setModified
692: */
693: public boolean isModified() {
694:
695: if (fEditBehavior != null) {
696: return fEditBehavior.isModified();
697: } else {
698: return fModified;
699: }
700: }
701:
702: /**
703: * Set the modification flag of the current text change.
704: */
705: public void setModified(boolean modified) {
706:
707: boolean handled = fTextComponent.textControlEventOccurred(
708: Behavior.SET_MODIFIED, modified ? Boolean.TRUE
709: : Boolean.FALSE);
710: if (!handled) {
711: fModified = modified;
712: }
713: }
714:
715: /**
716: * This method is for perf-testing only!
717: */
718: void handleKeyEvent(java.awt.event.KeyEvent keyEvent) {
719:
720: Component host = fTextComponent.getHost();
721: if (host != null) {
722: host.dispatchEvent(keyEvent);
723: }
724: }
725: }
|