Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions src/Album-Tests/AlbTextEditorTest.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -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 [

Expand Down Expand Up @@ -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
]
7 changes: 7 additions & 0 deletions src/Album/AlbTextCrappyEventHandlerAttribute.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 39 additions & 9 deletions src/Album/AlbTextEditor.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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' }
Expand Down Expand Up @@ -800,20 +831,19 @@ 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.
secondaryCursors := OrderedCollection new.
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"
Expand Down
57 changes: 2 additions & 55 deletions src/Album/AlbTextEditorCursorAddCommand.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -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
]
10 changes: 10 additions & 0 deletions src/Album/AlbTextEventHandlerAttribute.class.st
Original file line number Diff line number Diff line change
Expand Up @@ -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 [

Expand Down