Skip to content

Implementing custom objects

Jurgen edited this page May 3, 2019 · 11 revisions

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 (Requirement removed by #795)

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 operations on your object and its style).

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

See the Hyperlink Demo for an example.

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 {
    // relevant information

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

            @Override
            public String getName() {
                return "CustomObject";
            }

            @Override
            public void encode(DataOutputStream os, CustomObject object) throws IOException {
                if (object.isRealObject()) {
                    os.writeBoolean(true);
                    // encode object code here...
                } else {
                    os.writeBoolean(false);
                }
            }

            @Override
            public CustomObject<S> decode(DataInputStream is) throws IOException {
                if (is.readBoolean()) {
                    // decode real object here
                } else {
                    return new EmptyObject();
                }
            }
        };
    }
}
public class RealObject implements CustomObject {
    // implementation
    // Note: setters should always return a new immutable RealObject
}
public class EmptyObject implements CustomObject {
    // Note: setters should always return "this"
}

Then, in your CustomObjectOps class, you would extend SegmentOpsBase / TextOpsBase for text-related objects or NodeSegmentOpsBase for non-text-related objects (e.g. shapes, images, etc.).

Custom Text-Based Segment Ops

public class CustomTextObjectOps<Style> extends SegmentOpsBase<CustomObject, Style> {
    
    public CustomTextObjectOps() {
        super(new EmptyObject());
    }

    public int realLength(CustomObject seg) {
        return seg.length();
    }

    public char realCharAt(CustomObject seg, int index) {
        return seg.charAt(index);
    }

    public String realGetText(CustomObject seg) {
        return seg.getText();
    }

    public CustomObject realSubSequence(CustomObject seg, int start, int end) {
        return seg.subSequence(start, end);
    }

    public CustomObject realSubSequence(CustomObject seg, int start) {
        return seg.subSequence(start);
    }

    @Override
    public Optional<CustomObject> joinSeg(CustomObject currentSeg, CustomObject nextSeg) {
        // returns either an empty optional when the two cannot be merged
        //  or an optional containing the merged segment
        return Optional.empty();
    }

    @Override
    public Optional<Style> joinStyle(S currentStyle, S nextStyle) {
        // this only needs to be overridden when one wants to merge
        // styles together. By default, it returns Optional.empty()
    }

}

Custom Node-Based Segment Ops

public class CustomNodeObjectOps<Style> extends NodeSegmentOpsBase<CustomObject, Style> {

    public CustomNodeObjectOps() {
        super(new EmptyObject());
    }

    @Override
    public int length(CustomObject object) {
        // override this method and the base class will take care of the rest
    }
}