-
Notifications
You must be signed in to change notification settings - Fork 236
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Custom object support #356
Conversation
public abstract class CustomObject<S> implements Segment<S> { | ||
|
||
private S style; | ||
private SegmentType typeId; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A minor detail, but this should probably be renamed to type
now, not typeId
as it was when you were using an int
rather than your current enum/interface approach
Thanks for the work you have done on this so far! My main question (for everyone) is: Can we find a way to turn this |
Couldn't we store the public interface Segment<S> {
public <T extends Segment<S>> NodeFactory<S, T> getFactory();
}
@FunctionalInterface
public interface NodeFactory<S, T extends Segment<S>> {
public Node apply(T seg);
}
// implementations
class StyledText<S> extends Segment<S> {
public NodeFactory<S, StyledText<S>> getFactory() {
return DefaultNodeFactories.getStyledTextFactory();
}
}
public class DefaultNodeFactories<S> {
private static final NodeFactory<S, StyledText<S>> STYLED_TEXT_FACTORY =
seg -> { /* view construction code here... */ };
public static final StyledTextFactory<S, StyledText<S>> getStyledTextFactory() {
return STYLED_TEXT_FACTORY;
}
// other default factories here (e.g. table, shapes, etc.)
} |
Something like that would probably work, but isn't a very good separation On Sep 11, 2016 7:00 PM, "JordanMartinez" notifications@github.com wrote:
|
Edit: Updated to read more clearly public class StyledText<S> implements Segment<S> {}
public class Table<S> implements Segment<S> {}
public interface NodeFactory<S> {
public Node apply(Segment<S> seg);
}
public class DefaultNodeFactory<S> implements NodeFactory<S> {
public Node apply(StyledText<S> seg) {
// code
}
public Node apply(Table<S> seg) {
// code
}
public Node apply(Segement<S> seg) {
throw RunTimeException("No known way to display Segment of class: " +
Segment.getClass().getName());
}
}
public class StyledTextArea<PS, S, NF extends NodeFactory<S>> {
private final NF nodeFactory = new DefaultNodeFactory();
// code to construct node for a segment
return nodeFactory.apply(seg);
} ...except it still isn't a compile-time error but a runtime one... |
What if we abstract over the type of segments in a type parameter: /**
* Segment operations on type Seg.
* Notice that each method takes the segment as an argument.
*/
public interface SegmentOps<Seg> {
int length(Seg segment);
char charAt(Seg segment, int index);
/* ... */
}
public class StyledTextArea0<PS, Seg, S> {
public StyledTextArea0(
PS initialParagraphStyle, BiConsumer<TextFlow, PS> applyParagraphStyle, // as before
S initialTextStyle, // as before
SegmentOps<Seg> segmentOps,
Function<Seg, Node> nodeFactory,
BiFunction<Seg, S, Seg> applyStyle // Given a segment, update its style. The result is a new segment.
) {
/* ... */
}
} Note that any type Our original public class StyledTextArea<PS, S> extends StyledTextArea0<PS, StyledText<S>, S> {} If one wanted to embed images in addition to public class MyArea<PS, S> extends StyledTextArea0<PS, Either<StyledText<S>, LinkedImage<S>>, S> {} Extending is not even really necessary, it is just to provide a shorthand for the rather long type |
That seems like it would work. Additionally, it looks like it would address the |
Hi Tomas and Jordan, thanks for your feedback!
Right, that is what I also observed while I tried to implement the proposal from @JordanMartinez ... I will check if the additional type parameter is feasible (even though I dont feel completely comfortable with adding yet another type parameter ...) |
I feel the pain from the user point of view. As an implementor, I'm completely comfortable with having lots of type parameters and a very generic implementation. I even think abstracting something away as a type parameter reduces the potential for bugs in the implementation, since you don't have access to the specifics of the concrete type. From the user point of view, it is a major pain if you have to keep specifying 3 type arguments, especially when they are the same 3 concrete types throughout your whole application. I believe this issue could be resolved satisfactorily by using a type alias. Unfortunately, Java doesn't have type aliases. Type aliases exist e.g. in C++: using Style = Collection<String>
using MyArea = StyledTextArea0<Style, StyledText<Style>, Style> Scala: type Style = Collection[String]
type MyArea = StyledTextArea0[Style, StyledText[Style], Style] and other languages. What we can do in Java is class MyArea extends StyledTextArea0<Collection<String>, StyledText<Collection<String>>, Collection<String>> {
/* define constructors */
} but then I'm somewhat reluctant to compromise what otherwise seems like a reasonable design due to a deficiency in the language.
|
I'd stick with the good design since it will lead to less bugs and only Java-language users are affected. Java will hopefully add that feature in a later release, but until then one can work around this annoyance through ways that work for them. For example, typing in a placeholder text (e.g. |
Those IDE tricks help with writing those programs, but not with reading. Code is much more often read than written. Otherwise, I agree with the rest. |
Good point. 😞 |
How long would it take for a feature like this to make it into Java if a JSR was started for it (I'm not sure if there already is one / has been one)? |
Probably no earlier than Java 11. |
Lame.... Let's put it this way. There have been four approaches so far:
It seems like we've exhausted all the approaches we could take to this problem, correct? So, which issue is the least problematic holistically?
In other words, is there a way for us to minimize Tomas' approach's flaw as I do think it's the best approach to resolving this problem? |
Definitely, using appropriate language features to make the implementation more robust is the right approach. However, its also important to consider the API design - complex APIs prevent people from using the component. When evaluating a component, the first things people look at are probably the code examples and the JavaDoc - for the time being, the component itself is a black box for them. Anyway, @JordanMartinez thanks for summarizing the various approaches - I will see if I can come up with a proposal for Tomas' generics approach especially to see how the API "feels" with that approach. |
Would
|
I believe you're right. As it stands, we can't store that info in public class Segment {}
public interface SegOp<Segment> {
public int length(Segment segment);
public S getStyle(Segment segment); // compilation error
} So we'd need another interface like public interface Styleable<S> {
public S getStyle();
}
public class Foo implements Styleable<String> {}
Segment<Foo> seg = //
assert seg.getStyle().getClass() == String.getClass(); |
I believe we could preserve current
Good catch! In my sketch, I intended the BiFunction<Seg, S, Seg> applyStyle But I forgot about extracting style back ( public interface SegmentOps<Seg, Style> {
Style getStyle(Seg segment);
// and then `setStyle` can go here as well
Seg setStyle(Seg segment, Style style);
} Or add the setter to the parameter list of the Btw, a pair of getter and setter functions is also called a public interface Lens<T, U> {
U get(T t); // getStyle
T set(T t, U u); // setStyle
// factory method to create a Lens given two lambdas
static <A, B> Lens<A, B> create(Function<A, B> getter, BiFunction<A, B, A> setter) {
return new Lens<A, B> {
// implementation trivial
}
}
} so that it is a single constructor parameter. Also, I think in the most generic version of |
I didn't list your suggestion in my previous comment because I thought it would be easier for users if they did not need to keep another generic type in mind when doing segment operations. Thinking about it now, that's probably not true. Would there be any advantage to adding it to the
:-) I feel like that could get confusing after a while... Could we just call |
And then add a Just kidding, naming is hard.
Yeah, makes more sense to me as well. If we then decide to generalize to |
😆 Lol!
Or we could have a base interface with no style, |
@afester Can you give us an update? Or have you been working on other things recently? |
I started with the Generics approach (you can have a look at it at https://github.com/afester/RichTextFX/tree/genericSegment), but I found that the separate implementation of the operations unnecessarily bloats up the code, since the operations have to be looped through to many classes and in some cases also to static methods. If we still want to persue a Generics based approach, I would suggest to at least provide a base class for the generic segment, like Anyway, I would like to make another proposal which I just committed: Instead of adding an To create the JavaFX nodes, the segments now provide a The segments now also have a method Finally, I had to rework the algorithm which restyles part of the document. The earlier approach split the corresponding text and re-created the StyledText objects for each style span, but this is not possible anymore since there might be other Segment types involved. I created some Unit tests for that and the new approach works fine (including the RichText demo when selecting and restyling a range which includes an image), but there might still be corner cases and/or better approaches. Please let me know any feedback ... |
Thanks for looking into the generic approach!
I went through your code in my IDE and I think I see your point about how the In my PR to your branch, I also added a Part of the issue here is also readability, correct? For example, the repetitive calls of if(startSegIdx == endSegIdx) {
SEG seg = segments.get(startSegIdx);
builder.add(segmentOps.getStyle(seg), to - from);
} else {
SEG startSeg = segments.get(startSegIdx);
builder.add(segmentOps.getStyle(startSeg), segmentOps.length(startSeg) - start.getMinor());
for(int i = startSegIdx + 1; i < endSegIdx; ++i) {
SEG seg = segments.get(i);
builder.add(segmentOps.getStyle(seg),
segmentOps.length(seg));
}
SEG endSeg = segments.get(endSegIdx);
builder.add(segmentOps.getStyle(endSeg), end.getMinor());
} We could add method wrappers to make it easier to read, but I'm not sure how that would affect performance: class Paragraph {
private final SegmentOps<Seg, S> segOps
// method wrappers for "segOps.length(seg)"
private int segLength(Seg segment) { return segOps.length(segment); }
} Then the above method might look like this: if(startSegIdx == endSegIdx) {
SEG seg = segments.get(startSegIdx);
builder.add(segStyle(seg), to - from);
} else {
SEG startSeg = segments.get(startSegIdx);
builder.add(segStyle(startSeg), segLength(startSeg) - start.getMinor());
for(int i = startSegIdx + 1; i < endSegIdx; ++i) {
SEG seg = segments.get(i);
builder.add(segStyle(seg), segLength(seg));
}
SEG endSeg = segments.get(endSegIdx);
builder.add(segStyle(endSeg), end.getMinor());
} |
I've updated my PR to your generic approach (once you merge it) to now compile completely. The only demo that actually works (I used You can see my branch here: https://github.com/JordanMartinez/RichTextFX/tree/genericSegment |
Can you point me to this problematic code?
That limits the flexibility somewhat. Without requiring inheritance from a base class (or implementing an interface), even classes that were not originally intended to represent a segment can be used as such.
I'm afraid we are asking for a lot of trouble down the road if we use reflection. How would your class MySegment<T, S> implements Segment<S> {
private T t;
public void decode(DataInputStream is, Codec<S> styleCodec) throws IOException {
// how to implement this???
// We have no way to decode T (because we have no Codec<T>)
}
} Not to mention that all Btw, you are addressing two concerns in this PR: custom objects and serialization. I think we can address them separately, which would make your work on this PR easier.
My objection is the same as before, that this doesn't allow for separation of model from view. |
But can we implement custom objects without it also affecting the serialization? Or are you suggesting we export the 'serialization' part to be outside the area? For example, putting it into Wouldn't the "custom object" feature also require some API for how/when to display the caret (e.g. tables)? |
@JordanMartinez if you can still remember what you implemented, it would be great if you could redo this work ... 😃 |
See my work here. |
- To insure paragraph creation always has at least 1 segment, added a method to SegmentOps interface to create an empty segment. - EitherOps defaults to left ops when creating an empty segment
@JordanMartinez I am getting a NullPointerException in the RichTextEditor demo when I insert an image into an empty document, set the cursor before the image and enter more than two letters:
I'll continue to investigate this issue, but if you have any additional feedback please let me know 😃 [UPDATE]
leads to the output
when adding an image and then typing "j" followed by "s" in front of the image. The null style finally leads to the NPE. Default method |
@afester if it's a null style, the fix is very easy. If you look at I've changed it on my side and the NPE goes away now. See the branch to which I referred above with the fix. However, if I now follow the steps you said above
An odd thing occurs on step 3. Once "j" is typed, the caret moved left one position, so that the next letter I insert ("s") is then inserted in front of the "j". Thus I have Do you know why this might occur? It only occurs when the image is the first segment in a paragraph and the caret is at position 0 in the line (i.e. to the left of the image). Update: Figured it out. LinkedImageOps always returns Update: On second thought, the issue seems to arise because |
We'll definitely need to write a guide to help developers implement this code correctly as there are a number of things one needs to be aware of when developing their own custom objects. |
@TomasMikula I believe this custom object PR is now ready to be merged. |
@afester One thought that came to mind today that I haven't thought about / investigated more: will this PR break undo/redo? |
Provided the following three (3) conditions are met undo/redo generally works:
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
|
@Jugen Thanks for that analysis - I believe that this is the kind of information we need to add to the developer's guide which was proposed by @JordanMartinez some time ago. |
Let's move any discussion about the developer guide to the issue I just opened. |
Thanks guys for moving things forward! @afester @JordanMartinez are you both happy with the current state of this PR? |
@TomasMikula Glad to see you're back again! Yes, I think this PR is good. |
@TomasMikula Me too ! Happy holidays |
🎉 |
This is an enhanced proposal to support custom objects which can be rendered by user specific nodes.
In the model layer, all occurrences of
StyledText
have been replaced by the new interfaceSegment
.StyledText
is now just one flavor of segment types.CustomObject
is an abstract class which implements theSegment
interface and which provides a base class for user defined objects. Out-of-the-box, RichTextFX would support images to be inserted into the document, but the user can define any other custom objects by registering a factory method for the JavaFX node and a decode method for deserializing the object through a Codec. SeeCustomObjectDemo
for an example how to register and use custom objects.The code already supports copy&paste (if the selection contains a custom object, it is serialized when copying). Selecting one single custom object by clicking on it is not supported yet.
Any feedback/comments are welcome - especially registering the decode method is very unclean yet, see the SegmentFactory class. Essentially, decoding must be delegated to the custom object implementation since only the custom object knows how to recreate itself from a DataInputStream, but the method in ReadOnlyStyledDocument which does the decoding is static which makes it difficult to access a delegate. I tried to make those methods non-static, but this looked like a huge bunch of refactoring is necessary then.