diff --git a/src/Album-Tests/AlbTextEditorTest.class.st b/src/Album-Tests/AlbTextEditorTest.class.st index 5957a97..ca8d74f 100644 --- a/src/Album-Tests/AlbTextEditorTest.class.st +++ b/src/Album-Tests/AlbTextEditorTest.class.st @@ -10,12 +10,102 @@ Class { #category : #'Album-Tests-TextEditor' } +{ #category : #'tests - cursor attributes' } +AlbTextEditorTest >> addCursorAttributeLoggingIn: aCollection to: aText [ + "Add to the 13 to 23 part of aText a cursor attribute that logs in aCollection when the cursor enters and leaves it" + + (aText from: 13 to: 23) + onCursorEnter: [ :anEditor :anEvent :aFrom :aTo | aCollection add: #enter -> (aFrom to: aTo) ] + leave: [ :anEditor :anEvent :aFrom :aTo | aCollection add: #leave -> (aFrom to: aTo) ] +] + +{ #category : #'tests - cursor attributes' } +AlbTextEditorTest >> editorTextWithCursorAttributeLoggingIn: aCollection [ + "Give the editor a text with a cursor attribute logging in aCollection, and forget the initial leave sent when the text is set" + + textEditor text: (self textWithCursorAttributeLoggingIn: aCollection). + aCollection removeAll +] + { #category : #initialization } AlbTextEditorTest >> setUp [ textEditor := AlbTextEditor new ] +{ #category : #'tests - cursor attributes' } +AlbTextEditorTest >> testCursorAttributeAddedToEditorTextFiresEnterAction [ + + | log | + log := OrderedCollection new. + textEditor text: 'Hello World- click here' asRopedText. + self addCursorAttributeLoggingIn: log to: textEditor text. + textEditor moveCursorTo: 15. + self assert: log asArray equals: { (#enter -> (13 to: 23)) } +] + +{ #category : #'tests - cursor attributes' } +AlbTextEditorTest >> testCursorAttributeDoesNotFireWhenCursorStaysOutside [ + + | log | + log := OrderedCollection new. + self editorTextWithCursorAttributeLoggingIn: log. + textEditor moveCursorTo: 2. + textEditor moveCursorTo: 5. + self assert: log isEmpty +] + +{ #category : #'tests - cursor attributes' } +AlbTextEditorTest >> testCursorAttributeFiresEnterActionOnceWhileCursorStaysInside [ + + | log | + log := OrderedCollection new. + self editorTextWithCursorAttributeLoggingIn: log. + textEditor moveCursorTo: 15. + textEditor moveCursorTo: 18. + self assert: log asArray equals: { (#enter -> (13 to: 23)) } +] + +{ #category : #'tests - cursor attributes' } +AlbTextEditorTest >> testCursorAttributeInInitialTextFiresEnterAction [ + + | log | + log := OrderedCollection new. + self editorTextWithCursorAttributeLoggingIn: log. + textEditor moveCursorTo: 15. + self assert: log asArray equals: { (#enter -> (13 to: 23)) } +] + +{ #category : #'tests - cursor attributes' } +AlbTextEditorTest >> testCursorAttributeInInitialTextFiresLeaveAction [ + + | log | + log := OrderedCollection new. + self editorTextWithCursorAttributeLoggingIn: log. + textEditor moveCursorTo: 15. + textEditor moveCursorTo: 2. + self assert: log asArray equals: { + (#enter -> (13 to: 23)). + (#leave -> (13 to: 23)) } +] + +{ #category : #'tests - cursor attributes' } +AlbTextEditorTest >> testSettingTextFiresLeaveActionOfCursorAttribute [ + + | log | + log := OrderedCollection new. + textEditor text: (self textWithCursorAttributeLoggingIn: log). + self assert: log asArray equals: { (#leave -> (13 to: 23)) } +] + +{ #category : #'tests - cursor attributes' } +AlbTextEditorTest >> testTextWithoutCursorAttributeHasNoEventHandlerAttribute [ + + textEditor text: (self textWithCursorAttributeLoggingIn: OrderedCollection new). + textEditor text: 'Hello' asRopedText. + self deny: textEditor hasEventHandlerAttribute +] + { #category : #tests } AlbTextEditorTest >> test_emptyEditor [ @@ -83,3 +173,13 @@ AlbTextEditorTest >> test_textFromStringWithNonPrintableCharacter [ "non printable character test" self should: [textEditor checkAndSetText: Character backspace asString] raise: Error. ] + +{ #category : #'tests - cursor attributes' } +AlbTextEditorTest >> textWithCursorAttributeLoggingIn: aCollection [ + "Return the text 'Hello World- click here' whose 'click here' part (13 to 23) logs in aCollection when the cursor enters and leaves it" + + | text | + text := 'Hello World- click here' asRopedText. + self addCursorAttributeLoggingIn: aCollection to: text. + ^ text +] diff --git a/src/Album/AlbTextCrappyEventHandlerAttribute.class.st b/src/Album/AlbTextCrappyEventHandlerAttribute.class.st index e96c46b..99105b0 100644 --- a/src/Album/AlbTextCrappyEventHandlerAttribute.class.st +++ b/src/Album/AlbTextCrappyEventHandlerAttribute.class.st @@ -29,6 +29,13 @@ AlbTextCrappyEventHandlerAttribute >> mayHaveExternalReferences [ ^ true ] +{ #category : #registration } +AlbTextCrappyEventHandlerAttribute >> registerInEditor: aTextEditor [ + "Called by aTextEditor when I am part of its text. I receive the cursor events directly through #handleEvent:, so I have nothing to hand over" + + +] + { #category : #comparing } AlbTextCrappyEventHandlerAttribute >> type [ ^ self diff --git a/src/Album/AlbTextEditor.class.st b/src/Album/AlbTextEditor.class.st index efcbef9..2d44390 100644 --- a/src/Album/AlbTextEditor.class.st +++ b/src/Album/AlbTextEditor.class.st @@ -447,6 +447,28 @@ AlbTextEditor >> notifyAttributesWithinCursor: aCollectionOfAttributes [ (eachAttribute wantsEvent: anEvent) ifTrue: [ eachAttribute handleEvent: anEvent ] ] ] ] +{ #category : #'event handler attributes' } +AlbTextEditor >> notifyCursorAttributesOfCursorPositions [ + "Send an enter event to each event handler attribute of my text that contains one of my cursors, and a leave event to the others. Cursor attributes ignore repeated enter or leave events" + + | intervals | + intervals := IdentityDictionary new. + text findAttributesSuchThat: [ :eachAttribute | eachAttribute isEventHandler ] indicesDo: [ :aStart :anEnd :anAttribute | + | previous | + previous := intervals at: anAttribute ifAbsent: [ aStart to: anEnd ]. + intervals at: anAttribute put: ((previous first min: aStart) to: (previous last max: anEnd)) ]. + + intervals keysAndValuesDo: [ :anAttribute :anInterval | + | anEvent | + anEvent := (self cursors anySatisfy: [ :eachCursor | eachCursor position between: anInterval first and: anInterval last ]) + ifTrue: [ AlbTextCursorAttributeEnterEvent new ] + ifFalse: [ AlbTextCursorAttributeLeaveEvent new ]. + anEvent + textEditor: self; + interval: anInterval. + (anAttribute wantsEvent: anEvent) ifTrue: [ anAttribute handleEvent: anEvent ] ] +] + { #category : #'event firing' } AlbTextEditor >> notifyDeletedFrom: aFromIndex to: aToIndex [ self dispatchEvent: (AlbTextDeletedEvent new @@ -756,12 +778,21 @@ AlbTextEditor >> redo [ ] { #category : #'event handler attributes' } -AlbTextEditor >> registerEventHandlerAttribute: anEventHandlerattribute from: aFromIndex to: aToIndex [ - | eventHandlers | +AlbTextEditor >> registerEventHandlerAttribute: anEventHandlerAttribute from: aFromIndex to: aToIndex [ + "Remember that my text has event handler attributes, so that cursor moves notify them, and let the attribute register itself" + hasEventHandlerAttribute := true. - eventHandlers := anEventHandlerattribute eventDispatcher handlers. - anEventHandlerattribute eventDispatcher removeEventHandlers. - eventHandlers do: [ :eachHandler | self addEventHandler: (BlEventForwarder forwardTo: eachHandler from: anEventHandlerattribute) ] + anEventHandlerAttribute registerInEditor: self +] + +{ #category : #'event handler attributes' } +AlbTextEditor >> registerEventHandlerAttributesOfText [ + "Register the event handler attributes already present in my text, so that they are notified as if they had been added after the text was set" + + hasEventHandlerAttribute := false. + text + findAttributesSuchThat: [ :eachAttribute | eachAttribute isEventHandler ] + indicesDo: [ :aStart :anEnd :anAttribute | self registerEventHandlerAttribute: anAttribute from: aStart to: anEnd ] ] { #category : #'api - shortcuts' } @@ -800,8 +831,8 @@ AlbTextEditor >> restoreState: aTextEditorMemento [ { #category : #accessing } AlbTextEditor >> restoreText: aText [ - "before changing the text we must unsubscribe from existing one" + text ifNotNil: [ text unsubscribe: self ]. self history setUpToStart. cursor := self defaultCursor. @@ -809,11 +840,10 @@ AlbTextEditor >> restoreText: aText [ text := aText. " then subscribe to internal text state change (insertion/deletion/attribute change) " self initializeText: text. + self registerEventHandlerAttributesOfText. "Replacing text instance means that we can not guarantee structural integrity of existing cache and have to rebuild it invalidating the whole underlying model structure" - segmentCollector - ifNil: [ segmentCollector := self newSegmentCollector ] - ifNotNil: [ segmentCollector resetCache ]. + segmentCollector ifNil: [ segmentCollector := self newSegmentCollector ] ifNotNil: [ segmentCollector resetCache ]. segmentCollector text: text. self onTextReplaced. "notify UI about structural update" diff --git a/src/Album/AlbTextEditorCursorAddCommand.class.st b/src/Album/AlbTextEditorCursorAddCommand.class.st index 3cbf64b..1108624 100644 --- a/src/Album/AlbTextEditorCursorAddCommand.class.st +++ b/src/Album/AlbTextEditorCursorAddCommand.class.st @@ -42,61 +42,8 @@ AlbTextEditorCursorAddCommand >> newHolderEvent [ { #category : #executing } AlbTextEditorCursorAddCommand >> processAttributesOn: aTextEditor [ + "Tell the event handler attributes of the text whether the cursors now enter or leave them" - | anEditorText thePotentialHandlerTextInterval theEventHandlingAttributes eventHandlingAttributesOutside eventHandlingAttributesInside | - self flag: 'what a crap!'. aTextEditor hasEventHandlerAttribute ifFalse: [ ^ self ]. - anEditorText := aTextEditor text. - self cursor position > anEditorText size ifTrue: [ ^ self ]. - self cursor position <= 0 ifTrue: [ ^ self ]. - - "only chars to the left from the cursor handle cursor events" - thePotentialHandlerTextInterval := self cursor position to: self cursor position. - - theEventHandlingAttributes := ((thePotentialHandlerTextInterval - flatCollect: [ :eachTextIndex | anEditorText attributesAt: eachTextIndex ]) - select: [ :eachAttribute | eachAttribute isEventHandler ]) asSet. - - theEventHandlingAttributes - ifEmpty: [ ^ self ]. - - eventHandlingAttributesOutside := OrderedCollection new. - eventHandlingAttributesInside := OrderedCollection new. - - anEditorText - findAttributes: theEventHandlingAttributes - indicesDo: [ :eachEventHandlerStart :eachEventHandlerEnd :eachEventHandlerAttribute | - (aTextEditor cursor position between: eachEventHandlerStart and: eachEventHandlerEnd) - ifTrue: [ - eventHandlingAttributesInside - add: eachEventHandlerAttribute -> - (eachEventHandlerStart to: eachEventHandlerEnd) ] - ifFalse: [ - eventHandlingAttributesOutside - add: eachEventHandlerAttribute -> - (eachEventHandlerStart to: eachEventHandlerEnd) ] ]. - - eventHandlingAttributesOutside do: [ :eachEventHandlerAttributeAndInterval | - | eachAttribute eachInterval anEvent | - eachAttribute := eachEventHandlerAttributeAndInterval key. - eachInterval := eachEventHandlerAttributeAndInterval value. - anEvent := - AlbTextCursorAttributeLeaveEvent new - textEditor: aTextEditor; - interval: eachInterval; - yourself. - (eachAttribute wantsEvent: anEvent) ifTrue: [ - eachAttribute handleEvent: anEvent ] ]. - - eventHandlingAttributesInside do: [ :eachEventHandlerAttributeAndInterval | - | eachAttribute eachInterval anEvent | - eachAttribute := eachEventHandlerAttributeAndInterval key. - eachInterval := eachEventHandlerAttributeAndInterval value. - anEvent := - AlbTextCursorAttributeEnterEvent new - textEditor: aTextEditor; - interval: eachInterval; - yourself. - (eachAttribute wantsEvent: anEvent) ifTrue: [ - eachAttribute handleEvent: anEvent ] ] + aTextEditor notifyCursorAttributesOfCursorPositions ] diff --git a/src/Album/AlbTextEventHandlerAttribute.class.st b/src/Album/AlbTextEventHandlerAttribute.class.st index d698f0d..835da42 100644 --- a/src/Album/AlbTextEventHandlerAttribute.class.st +++ b/src/Album/AlbTextEventHandlerAttribute.class.st @@ -38,6 +38,16 @@ AlbTextEventHandlerAttribute >> isEventHandler [ ^ true ] +{ #category : #registration } +AlbTextEventHandlerAttribute >> registerInEditor: aTextEditor [ + "Called by aTextEditor when I am part of its text. Move my event handlers to aTextEditor so that the events it receives are forwarded to them" + + | eventHandlers | + eventHandlers := self eventDispatcher handlers. + self eventDispatcher removeEventHandlers. + eventHandlers do: [ :eachHandler | aTextEditor addEventHandler: (BlEventForwarder forwardTo: eachHandler from: self) ] +] + { #category : #'api - event managing' } AlbTextEventHandlerAttribute >> wantsEvent: aBlEvent [