-
Notifications
You must be signed in to change notification settings - Fork 246
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
[SuperEditor] Split incompatible attributions (Resolves #1983) #2035
Conversation
@KevinBrendel Could you please try this PR? |
@angelosilvestre I'm wondering if an Requiring explicit intervention on this point might proliferate these same behaviors through different commands. Also every app that uses Being able to find conflicting attribution spans is probably fine either way, but I'm thinking it might make more sense to do something like splitting blue, and adding green, within AttributedText, itself. |
@matthew-carroll I guess it should be fine to handle that automatically. Can you think of an use case where an |
@angelosilvestre Thanks for the PR! For my purposes, the editor-level commands are what is needed, but I can see the argument for adding this feature directly into I also found one issue, which is likely related to this: |
Not off the top of my head. But you could add a boolean to whichever methods are relevant. Maybe by default we automatically split, but if you set the flag we'll throw an exception when you try to do that. |
@KevinBrendel - Please be sure to provide reproduction steps and a code sample if you run into issues so that @angelosilvestre can reproduce it and fix it. |
Will try to create a minimal sample when I have some more time tomorrow. Doesn't seem to be a very special case that triggers it. |
Here is the reproduction. Just try to select the red text ("Hello"): import 'package:flutter/material.dart';
import 'package:super_editor/super_editor.dart';
void main() {
runApp(const MainApp());
}
class MainApp extends StatefulWidget {
const MainApp({super.key});
@override
State<MainApp> createState() => _MainAppState();
}
class _MainAppState extends State<MainApp> {
static const _colorAttribution = ColorAttribution(Colors.red);
final _document = MutableDocument(nodes: [
ParagraphNode(
id: Editor.createNodeId(),
text: AttributedText(
'Hello, world!',
AttributedSpans(attributions: const [
SpanMarker(attribution: _colorAttribution, offset: 0, markerType: SpanMarkerType.start),
SpanMarker(attribution: _colorAttribution, offset: 4, markerType: SpanMarkerType.end),
])),
),
]);
final _composer = MutableDocumentComposer();
late final Editor _editor;
@override
void initState() {
super.initState();
_editor = createDefaultDocumentEditor(document: _document, composer: _composer);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: SuperEditor(
editor: _editor,
document: _document,
composer: _composer,
stylesheet: defaultStylesheet.copyWith(
selectedTextColorStrategy: ({required originalTextColor, required selectionHighlightColor}) =>
Colors.grey,
),
),
),
),
);
}
} |
@matthew-carroll Modified the implementation to handle attribution splitting directly in |
@matthew-carroll This flag might be a bit unintuitive, since we already have a With |
/// | ||
/// If [allowMerging] is `false`, attributions with the same id will always be reported | ||
/// as conflicting, even if [Attribution.canMergeWith] returns `true`. | ||
List<AttributionConflict> findConflictingAttributions({ |
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.
To verify, do you want this to be a public API? Do you think it should be?
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.
It might be a good idea not to hide too much functionality from the user. Some use-cases require some more low level knowledge/control and I have had to copy SuperEditor code into my app code on multiple occasions because it was private.
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.
If you find yourself doing that, you should file issue tickets to let us know where/why that's happening. Otherwise, the only predictable result is that it will continue.
I haven't heard about anyone needing to find conflicting attributions in their own apps. Have you needed to do that? If so, what was the use-case?
There's obviously a middle ground that needs to be maintained. Making everything public creates a large maintenance space and makes every change a breaking change. Making everything private forces developers to request every little thing they want, and also to copy lots of code. We need to operate in a middle ground where we use some level of assessment as to what we publish and what we don't.
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.
I am not 100% sure yet wether we will need this specific function, as implementation of the related features will only commence after this PR is merged.
It is just the kind of helper function that could be useful for some probably unforeseen use-cases and is currently often private. E.g. _textInSelection
from CommonEditorOperations
was very useful to build a search feature that already pre-populates the search field with the currently selected text upon pressing CMD+F.
I can create issues/pull requests for such things, but I also understand that it increases the maintenance burden. And I expect that dealing with all kinds of minor issues/pull requests would also serve as a distraction from the more important issues you guys are working on.
Currently I am trying to only create issues/pull requests when it seems something is more generally applicable, as our use-case is probably a bit more exotic (a hybrid flowchart/mind mapping app where there can be hundreds of nodes on a canvas, each containing a SuperEditor which has to interact with our node and graph-level systems for styling, undo/redo, etc. and which has to be integrated into often complex interactions requiring fine-grained control).
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.
Filing issues is pretty much free. It only takes the time needed to explain the use-case/motivation. No need to submit PRs for those things. We can consider the motivation and decide whether we think it warrants a public API.
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.
I guess we don't need to expose this method.
/// A conflict between the [newAttribution] and [existingAttribution] between [conflictStart] and [conflictEnd] (inclusive). | ||
/// | ||
/// This means [newAttribution] and [existingAttribution] have the same id, but they can't be merged. | ||
class AttributionConflict { |
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.
Verifying that you think this should be public.
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.
You resolved this but there's no comment here on why this should be public, nor do I think I saw any comment on the PR about this...
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.
It seems I forgot to make it private.
@@ -1276,108 +1276,201 @@ void main() { | |||
}); | |||
}); | |||
|
|||
testWidgetsOnArbitraryDesktop('does not merge different colors', (tester) async { | |||
final context = await tester // | |||
testWidgetsOnArbitraryDesktop('splits spans with different colors', (tester) async { |
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.
Let's also revisit the use of the term "split" in this test suite to make sure it's always the best name for the given situation.
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.
I think "overwrite" is a better name for all of those tests.
); | ||
} | ||
|
||
if (!ignoreMergeableOverlaps) { |
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.
At this point in the code, why does ignoreMergeableOverlaps
matter? Either there aren't any overlaps, in which case this value doesn't matter, or there are overlaps and you've already thrown an exception, right?
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.
This code is needed for the cases where we try to apply a mergeable attribution right after another one. For example:
|one two|
|bbb----|
When ignoreMergeableOverlaps
is true
, applying a bold attribution to the span " two" produces a single span covering the entire text:
|one two|
|bbbbbbb|
When ignoreMergeableOverlaps
is false
, applying a bold attribution to the span " two " produces two spans:
|one two|
|bbb----|
|---bbbb|
/// A conflict between the [newAttribution] and [existingAttribution] between [conflictStart] and [conflictEnd] (inclusive). | ||
/// | ||
/// This means [newAttribution] and [existingAttribution] have the same id, but they can't be merged. | ||
class AttributionConflict { |
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.
You resolved this but there's no comment here on why this should be public, nor do I think I saw any comment on the PR about this...
required int startOffset, | ||
required int endOffset, | ||
}) { | ||
return AttributedSpans( |
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.
Should we add a constructor that's more concise? For example: AttributedSpans.single(attribution, start, end)
?
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.
It seems that after this isn't being used anymore... I removed it
); | ||
node.text.spans.copy() | ||
..addAttribution( | ||
newAttribution: attribution, |
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.
Let's also leave this parameter called attribution
to avoid unnecessary breaking changes.
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.
The addAttribution
used before this PR is from AttributedText
. Now we are using the method from AttributedSpans
. We are not changing the parameter names.
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.
LGTM
@matthew-carroll @angelosilvestre This was not fixed and there is not way around it as the corresponding implementation hardcodes Is this intentional, and if so, is it possible to work around this? Otherwise, |
@KevinBrendel can you please file that reproduction and the problem as a new issue? I'll have @angelosilvestre investigate it when you do. |
[SuperEditor] Split incompatible attributions. Resolves #1983
Valued attributions, like
ColorAttribution
can't be applied in a range where anotherColorAttribution
exists.Although it doesn't make sense to have two
ColorAttribution
s in the same range, users should be able to change the color of an already attributed range.For example, consider the following range, fully attributed with blue:
Users should be able to apply green color to the word "green", causing the blue attributed range to be split:
This PR changes
AddTextAttributionsCommand
andToggleTextAttributionsCommand
to find and remove any conflicting attributions in the range before trying to apply the desired attribution.The following changes were made:
AttributedSpans.findConflictingAttributions
to return a list of conflicting attributions.AddTextAttributionsCommand
andToggleTextAttributionsCommand
to remove conflicting attributionsAlso, I found a bug in
AttributedSpans.getMatchingAttributionsWithin
, where we always tried to get the attributions from the same index. This PR fixes this bug.We'll need to release a new version of
attributed_text
because of this new method.