Skip to content

Implementing custom objects

JordanMartinez edited this page Feb 28, 2017 · 11 revisions

Things to write here:

  • Gotchas and other quirks
  • Bare minimum pattern to follow

Gotchas and Other Quirks

If you want undo/redo to work properly...

The following three (3) conditions need to be met for undo/redo to work on custom segment objects (as noted by Jugen in #403):

  1. The preserveStyle parameter must be TRUE when invoking the GenericStyledArea constructor.
  2. A properly implemented Codec for your SEG must be set via GenericStyledArea.setStyleCodecs()
  3. The Object.equals( Object obj ) method must be overridden and properly implemented by your SEG

If you want RichTextFX-specific CSS styling to work properly...

Your area's nodeFactory must return a TextExt object to display that text, not the regular Text object, in order for RichTextFX-specific CSS styling to work.

Bare Minimum Pattern to Follow

To implement a custom object, you will need to have three things:

  1. A non-empty immutable version of your object
  2. An empty immutable version of your object
  3. A SegOps (an object that handles segment operations on your object).

Non-Text Custom Object Pattern (e.g. image, emoticon, shape, etc.)

Ideally, you would use an interface for your custom object, have both object versions implement this interface, and use only one object for the empty version of your object. For example...

public interface CustomObject<S> {
    // relevant information

    // if you want to provide a Codec for your object, you should include it here
    static <S> Codec<CustomObject<S>> codec(Codec<S> styleCodec) {
        return new Codec<CustomObject<S>>() {

            @Override
            public String getName() {
                return "CustomObject<" + styleCodec.getName() + ">";
            }

            @Override
            public void encode(DataOutputStream os, LinkedImage<S> i) throws IOException {
                // don't encode EmptyObject
                if (i.getStyle() != null) {
                    // encode object code here...
                    // encode style
                    styleCodec.encode(os, i.getStyle());
                }
            }

            @Override
            public RealLinkedImage<S> decode(DataInputStream is) throws IOException {
                // decode object code here
                
                // decode style
                S style = styleCodec.decode(is);
                return new RealObject<>(/* object args */, style);
            }
        };
    }
}
public class RealObject<S> implements CustomObject<S> {
    // implementation
    // Note: setters should always return a new immutable RealObject
}
public class EmptyObject<S> implements CustomObject<S> {
    // style is null. No need to worry about NPEs being thrown
    // if you implement length-related methods correctly (see next point)
    private final S style = null;
    public final S getStyle() { return style; }

    // Note: setters should always return "this" or the single EmptyObject
}

Then, in your CustomObjectOps class, you would write the following code. Be sure to be careful how you implement methods that deal with length! RealObject will always have a length of 1 because charAt() and getText() will always return /ufffc.

public class CustomObjectOps implements SegmentOps<CustomObject<S>> {
    
    private final EmptyObject<S> emptySeg = new EmptyObject<>();

    @Override
    public int length(CustomObject<S> seg) {
        // non-empty seg's length is always 1
        return seg == emptySeg ? 0 : 1;
    }

    @Override
    public char charAt(CustomObject<S> seg, int index) {
        return seg == emptySeg ? '\0' : '\ufffc';
    }

    @Override
    public String getText(CustomObject<S> seg) {
        return seg == emptySeg ? "" : "\ufffc";
    }

    @Override
    public CustomObject<S> subSequence(CustomObject<S> seg, int start, int end) {
        if (start < 0) {
            throw new IllegalArgumentException("Start cannot be negative. Start = " + start);
        }
        if (end > length(seg)) {
            throw new IllegalArgumentException("End cannot be greater than segment's length");
        }
        return start == 0 && end == 1
                ? seg
                : emptySeg;
    }

    @Override
    public CustomObject<S> subSequence(CustomObject<S> seg, int start) {
        if (start < 0) {
            throw new IllegalArgumentException("Start cannot be negative. Start = " + start);
        }
        return start == 0
                ? seg
                : emptySeg;
    }

    @Override
    public S getStyle(CustomObject<S> seg) {
        return seg.getStyle();
    }

    @Override
    public CustomObject<S> setStyle(CustomObject<S> seg, S style) {
        return seg.setStyle(style);
    }

    @Override
    public Optional<CustomObject<S>> join(CustomObject<S> currentSeg, CustomObject<S> nextSeg) {
        // a custom non-text object can never be merged with another such object
        return Optional.empty();
    }

    @Override
    public CustomObject<S> createEmpty() {
        // save memory!
        return emptySeg;
    }
}