From d935ba41f6855b9e673819195f20046ed694afc5 Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Tue, 21 Jun 2022 00:59:05 -0700 Subject: [PATCH] Added listener for remote user selection change to repaint selections --- .../lib/src/core/document_composer.dart | 29 +++- .../_styler_user_selection.dart | 124 ++++++++++++------ 2 files changed, 111 insertions(+), 42 deletions(-) diff --git a/super_editor/lib/src/core/document_composer.dart b/super_editor/lib/src/core/document_composer.dart index f6e57fd77..26ab1d501 100644 --- a/super_editor/lib/src/core/document_composer.dart +++ b/super_editor/lib/src/core/document_composer.dart @@ -17,7 +17,8 @@ class DocumentComposer with ChangeNotifier { DocumentSelection? initialSelection, ImeConfiguration? imeConfiguration, }) : _selection = initialSelection, - imeConfiguration = ValueNotifier(imeConfiguration ?? const ImeConfiguration()), + imeConfiguration = + ValueNotifier(imeConfiguration ?? const ImeConfiguration()), _preferences = ComposerPreferences() { _preferences.addListener(() { editorLog.fine("Composer preferences changed"); @@ -141,15 +142,33 @@ class NonPrimarySelection { } /// Listener for changes to non-primary user selections. -abstract class NonPrimarySelectionListener { +class NonPrimarySelectionListener { + const NonPrimarySelectionListener({ + void Function(NonPrimarySelection)? onSelectionAdded, + void Function(NonPrimarySelection)? onSelectionChanged, + void Function(String id)? onSelectionRemoved, + }) : _onSelectionAdded = onSelectionAdded, + _onSelectionChanged = onSelectionChanged, + _onSelectionRemoved = onSelectionRemoved; + + final void Function(NonPrimarySelection)? _onSelectionAdded; + final void Function(NonPrimarySelection)? _onSelectionChanged; + final void Function(String id)? _onSelectionRemoved; + /// The given [selection] was added to the composer. - void onSelectionAdded(NonPrimarySelection selection); + void onSelectionAdded(NonPrimarySelection selection) { + _onSelectionAdded?.call(selection); + } /// An existing selection was changed to the new [selection]. - void onSelectionChanged(NonPrimarySelection selection); + void onSelectionChanged(NonPrimarySelection selection) { + _onSelectionChanged?.call(selection); + } /// The selection with the given [id] was removed from the document. - void onSelectionRemoved(String id); + void onSelectionRemoved(String id) { + _onSelectionRemoved?.call(id); + } } /// Holds preferences about user input, to be used for the diff --git a/super_editor/lib/src/default_editor/layout_single_column/_styler_user_selection.dart b/super_editor/lib/src/default_editor/layout_single_column/_styler_user_selection.dart index 0a9ed0159..dfc86497a 100644 --- a/super_editor/lib/src/default_editor/layout_single_column/_styler_user_selection.dart +++ b/super_editor/lib/src/default_editor/layout_single_column/_styler_user_selection.dart @@ -26,6 +26,11 @@ class SingleColumnLayoutSelectionStyler extends SingleColumnLayoutStylePhase { _nonPrimarySelectionStyler = nonPrimarySelectionStyler { // Our styles need to be re-applied whenever the document selection changes. _composer.selectionNotifier.addListener(markDirty); + _composer.addNonPrimarySelectionListener(NonPrimarySelectionListener( + onSelectionAdded: (_) => markDirty(), + onSelectionChanged: (_) => markDirty(), + onSelectionRemoved: (_) => markDirty(), + )); } @override @@ -46,14 +51,18 @@ class SingleColumnLayoutSelectionStyler extends SingleColumnLayoutStylePhase { } _shouldDocumentShowCaret = newValue; - editorLayoutLog.fine("Change to 'document should show caret': $_shouldDocumentShowCaret"); + editorLayoutLog.fine( + "Change to 'document should show caret': $_shouldDocumentShowCaret"); markDirty(); } @override - SingleColumnLayoutViewModel style(Document document, SingleColumnLayoutViewModel viewModel) { - editorLayoutLog.info("(Re)calculating selection view model for document layout"); - editorLayoutLog.fine("Applying selection to components: ${_composer.selection}"); + SingleColumnLayoutViewModel style( + Document document, SingleColumnLayoutViewModel viewModel) { + editorLayoutLog + .info("(Re)calculating selection view model for document layout"); + editorLayoutLog + .fine("Applying selection to components: ${_composer.selection}"); return SingleColumnLayoutViewModel( padding: viewModel.padding, componentViewModels: [ @@ -63,7 +72,8 @@ class SingleColumnLayoutSelectionStyler extends SingleColumnLayoutStylePhase { ); } - SingleColumnLayoutComponentViewModel _applySelection(SingleColumnLayoutComponentViewModel viewModel) { + SingleColumnLayoutComponentViewModel _applySelection( + SingleColumnLayoutComponentViewModel viewModel) { final documentSelection = _composer.selection; final node = _document.getNodeById(viewModel.nodeId)!; @@ -86,8 +96,10 @@ class SingleColumnLayoutSelectionStyler extends SingleColumnLayoutStylePhase { // into atomic transactions (#423) selectedNodes = []; } - nodeSelection = - _computeNodeSelection(documentSelection: documentSelection, selectedNodes: selectedNodes, node: node); + nodeSelection = _computeNodeSelection( + documentSelection: documentSelection, + selectedNodes: selectedNodes, + node: node); } editorLayoutLog.fine("Node selection (${node.id}): $nodeSelection"); @@ -97,14 +109,18 @@ class SingleColumnLayoutSelectionStyler extends SingleColumnLayoutStylePhase { node: node, ); - final textSelection = nodeSelection == null || nodeSelection.nodeSelection is! TextSelection - ? null - : nodeSelection.nodeSelection as TextSelection; - if (nodeSelection != null && nodeSelection.nodeSelection is! TextSelection) { + final textSelection = + nodeSelection == null || nodeSelection.nodeSelection is! TextSelection + ? null + : nodeSelection.nodeSelection as TextSelection; + if (nodeSelection != null && + nodeSelection.nodeSelection is! TextSelection) { editorLayoutLog.shout( 'ERROR: Building a paragraph component but the selection is not a TextSelection. Node: ${node.id}, Selection: ${nodeSelection.nodeSelection}'); } - final showCaret = _shouldDocumentShowCaret && nodeSelection != null ? nodeSelection.isExtent : false; + final showCaret = _shouldDocumentShowCaret && nodeSelection != null + ? nodeSelection.isExtent + : false; // final highlightWhenEmpty = // nodeSelection == null ? false : nodeSelection.highlightWhenEmpty && _selectionStyles.highlightEmptyTextBlocks; @@ -125,19 +141,31 @@ class SingleColumnLayoutSelectionStyler extends SingleColumnLayoutStylePhase { } } if (viewModel is ImageComponentViewModel) { - final styledSelections = _computeStyledSelectionsForUpstreamDownstreamNodes(composer: _composer, node: node); + final styledSelections = + _computeStyledSelectionsForUpstreamDownstreamNodes( + composer: _composer, node: node); viewModel ..styledSelections = styledSelections - ..caret = _shouldDocumentShowCaret && styledSelections.isNotEmpty && styledSelections.last.selection.isCollapsed ? styledSelections.last.selection.extent : null + ..caret = _shouldDocumentShowCaret && + styledSelections.isNotEmpty && + styledSelections.last.selection.isCollapsed + ? styledSelections.last.selection.extent + : null ..caretColor = _selectionStyles.caretColor; } if (viewModel is HorizontalRuleComponentViewModel) { - final styledSelections = _computeStyledSelectionsForUpstreamDownstreamNodes(composer: _composer, node: node); + final styledSelections = + _computeStyledSelectionsForUpstreamDownstreamNodes( + composer: _composer, node: node); viewModel ..styledSelections = styledSelections - ..caret = _shouldDocumentShowCaret && styledSelections.isNotEmpty && styledSelections.last.selection.isCollapsed ? styledSelections.last.selection.extent : null + ..caret = _shouldDocumentShowCaret && + styledSelections.isNotEmpty && + styledSelections.last.selection.isCollapsed + ? styledSelections.last.selection.extent + : null ..caretColor = _selectionStyles.caretColor; } @@ -176,9 +204,11 @@ class SingleColumnLayoutSelectionStyler extends SingleColumnLayoutStylePhase { ); if (nodeSelection?.nodeSelection is TextNodeSelection) { - final textNodeSelection = nodeSelection!.nodeSelection as TextNodeSelection; - final textSelection = - TextSelection(baseOffset: textNodeSelection.baseOffset, extentOffset: textNodeSelection.extentOffset); + final textNodeSelection = + nodeSelection!.nodeSelection as TextNodeSelection; + final textSelection = TextSelection( + baseOffset: textNodeSelection.baseOffset, + extentOffset: textNodeSelection.extentOffset); styledSelections.add(StyledSelection( textSelection, @@ -210,13 +240,17 @@ class SingleColumnLayoutSelectionStyler extends SingleColumnLayoutStylePhase { selectedNodes = []; } - final nodeSelection = - _computeNodeSelection(documentSelection: documentSelection, selectedNodes: selectedNodes, node: node); + final nodeSelection = _computeNodeSelection( + documentSelection: documentSelection, + selectedNodes: selectedNodes, + node: node); if (nodeSelection?.nodeSelection is TextNodeSelection) { - final textNodeSelection = nodeSelection!.nodeSelection as TextNodeSelection; - final textSelection = - TextSelection(baseOffset: textNodeSelection.baseOffset, extentOffset: textNodeSelection.extentOffset); + final textNodeSelection = + nodeSelection!.nodeSelection as TextNodeSelection; + final textSelection = TextSelection( + baseOffset: textNodeSelection.baseOffset, + extentOffset: textNodeSelection.extentOffset); styledSelections.add(StyledSelection( textSelection, @@ -232,11 +266,13 @@ class SingleColumnLayoutSelectionStyler extends SingleColumnLayoutStylePhase { return styledSelections; } - List> _computeStyledSelectionsForUpstreamDownstreamNodes({ + List> + _computeStyledSelectionsForUpstreamDownstreamNodes({ required DocumentComposer composer, required DocumentNode node, }) { - final styledSelections = >[]; + final styledSelections = + >[]; for (final nonPrimarySelection in _composer.getAllNonPrimarySelections()) { late List selectedNodes; @@ -264,7 +300,8 @@ class SingleColumnLayoutSelectionStyler extends SingleColumnLayoutStylePhase { ); if (nodeSelection?.nodeSelection is UpstreamDownstreamNodeSelection) { - final upstreamDownstreamSelection = nodeSelection!.nodeSelection as UpstreamDownstreamNodeSelection; + final upstreamDownstreamSelection = + nodeSelection!.nodeSelection as UpstreamDownstreamNodeSelection; styledSelections.add(StyledSelection( upstreamDownstreamSelection, @@ -296,11 +333,14 @@ class SingleColumnLayoutSelectionStyler extends SingleColumnLayoutStylePhase { selectedNodes = []; } - final nodeSelection = - _computeNodeSelection(documentSelection: documentSelection, selectedNodes: selectedNodes, node: node); + final nodeSelection = _computeNodeSelection( + documentSelection: documentSelection, + selectedNodes: selectedNodes, + node: node); if (nodeSelection?.nodeSelection is UpstreamDownstreamNodeSelection) { - final upstreamDownstreamSelection = nodeSelection!.nodeSelection as UpstreamDownstreamNodeSelection; + final upstreamDownstreamSelection = + nodeSelection!.nodeSelection as UpstreamDownstreamNodeSelection; styledSelections.add(StyledSelection( upstreamDownstreamSelection, @@ -344,7 +384,8 @@ class SingleColumnLayoutSelectionStyler extends SingleColumnLayoutStylePhase { final extentNodePosition = documentSelection.extent.nodePosition; late NodeSelection? nodeSelection; try { - nodeSelection = node.computeSelection(base: baseNodePosition, extent: extentNodePosition); + nodeSelection = node.computeSelection( + base: baseNodePosition, extent: extentNodePosition); } catch (exception) { // This situation can happen in the moment between a document change and // a corresponding selection change. For example: deleting an image and @@ -371,7 +412,9 @@ class SingleColumnLayoutSelectionStyler extends SingleColumnLayoutStylePhase { editorLayoutLog.finer(' - ${node.id}'); } - if (selectedNodes.firstWhereOrNull((selectedNode) => selectedNode.id == node.id) == null) { + if (selectedNodes + .firstWhereOrNull((selectedNode) => selectedNode.id == node.id) == + null) { // The document selection does not contain the node we're interested in. Return. editorLayoutLog.finer(' - this node is not in the selection'); return null; @@ -386,8 +429,11 @@ class SingleColumnLayoutSelectionStyler extends SingleColumnLayoutStylePhase { return DocumentNodeSelection( nodeId: node.id, nodeSelection: node.computeSelection( - base: isBase ? documentSelection.base.nodePosition : node.endPosition, - extent: isBase ? node.endPosition : documentSelection.extent.nodePosition, + base: + isBase ? documentSelection.base.nodePosition : node.endPosition, + extent: isBase + ? node.endPosition + : documentSelection.extent.nodePosition, ), isBase: isBase, isExtent: !isBase, @@ -403,14 +449,17 @@ class SingleColumnLayoutSelectionStyler extends SingleColumnLayoutStylePhase { nodeId: node.id, nodeSelection: node.computeSelection( base: isBase ? node.beginningPosition : node.beginningPosition, - extent: isBase ? documentSelection.base.nodePosition : documentSelection.extent.nodePosition, + extent: isBase + ? documentSelection.base.nodePosition + : documentSelection.extent.nodePosition, ), isBase: isBase, isExtent: !isBase, highlightWhenEmpty: isBase, ); } else { - editorLayoutLog.finer(' - this node is fully selected within the selection'); + editorLayoutLog + .finer(' - this node is fully selected within the selection'); // Multiple nodes are selected and this node is neither the top // or the bottom node, therefore this entire node is selected. return DocumentNodeSelection( @@ -429,4 +478,5 @@ class SingleColumnLayoutSelectionStyler extends SingleColumnLayoutStylePhase { /// Function called to configure [SelectionStyles] for a given [nonPrimarySelection]. /// /// If you don't want to display anything for this selection, return `null`. -typedef NonPrimarySelectionStyler = SelectionStyles? Function(NonPrimarySelection nonPrimarySelection); +typedef NonPrimarySelectionStyler = SelectionStyles? Function( + NonPrimarySelection nonPrimarySelection);