Merge pull request #8548 from atom/ns-improve-mouse-based-autoscroll

Smoothly autoscroll the editor when selecting with the mouse
This commit is contained in:
Nathan Sobo
2015-08-28 13:46:01 -06:00
4 changed files with 109 additions and 38 deletions

View File

@@ -1850,6 +1850,40 @@ describe "TextEditorComponent", ->
nextAnimationFrame()
expect(editor.getSelectedScreenRange()).toEqual [[2, 4], [10, 0]]
it "autoscrolls when the cursor exceeds the boundaries of the editor", ->
wrapperNode.style.height = '100px'
wrapperNode.style.width = '100px'
component.measureDimensions()
nextAnimationFrame()
expect(editor.getScrollTop()).toBe(0)
expect(editor.getScrollLeft()).toBe(0)
linesNode.dispatchEvent(buildMouseEvent('mousedown', {clientX: 0, clientY: 0}, which: 1))
linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: 150, clientY: 50}, which: 1))
nextAnimationFrame()
expect(editor.getScrollTop()).toBe(0)
expect(editor.getScrollLeft()).toBeGreaterThan(0)
linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: 150, clientY: 150}, which: 1))
nextAnimationFrame()
expect(editor.getScrollTop()).toBeGreaterThan(0)
previousScrollTop = editor.getScrollTop()
previousScrollLeft = editor.getScrollLeft()
linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: -20, clientY: 50}, which: 1))
nextAnimationFrame()
expect(editor.getScrollTop()).toBe(previousScrollTop)
expect(editor.getScrollLeft()).toBeLessThan(previousScrollLeft)
linesNode.dispatchEvent(buildMouseEvent('mousemove', {clientX: -20, clientY: -20}, which: 1))
nextAnimationFrame()
expect(editor.getScrollTop()).toBeLessThan(previousScrollTop)
it "stops selecting if the mouse is dragged into the dev tools", ->
linesNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenPosition([2, 4]), which: 1))
linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([6, 8]), which: 1))
@@ -1928,7 +1962,7 @@ describe "TextEditorComponent", ->
linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([11, 11]), which: 1))
nextAnimationFrame()
expect(editor.getSelectedScreenRange()).toEqual [[5, 6], [11, 13]]
expect(editor.getSelectedScreenRange()).toEqual [[5, 6], [12, 2]]
maximalScrollTop = editor.getScrollTop()
@@ -1955,7 +1989,7 @@ describe "TextEditorComponent", ->
linesNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenPosition([11, 11]), which: 1))
nextAnimationFrame()
expect(editor.getSelectedScreenRange()).toEqual [[5, 0], [12, 0]]
expect(editor.getSelectedScreenRange()).toEqual [[5, 0], [12, 2]]
maximalScrollTop = editor.getScrollTop()
@@ -2052,14 +2086,15 @@ describe "TextEditorComponent", ->
nextAnimationFrame()
expect(editor.getLastSelection().isReversed()).toBe false
it "autoscrolls to the cursor position, but not the entire selected range", ->
it "autoscrolls when the cursor exceeds the top or bottom of the editor", ->
wrapperNode.style.height = 6 * lineHeightInPixels + 'px'
component.measureDimensions()
nextAnimationFrame()
expect(editor.getScrollTop()).toBe 0
gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2)))
gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6)))
gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(8)))
nextAnimationFrame()
expect(editor.getScrollTop()).toBeGreaterThan 0
@@ -2069,6 +2104,10 @@ describe "TextEditorComponent", ->
nextAnimationFrame()
expect(editor.getScrollTop()).toBe maxScrollTop
gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(2)))
nextAnimationFrame()
expect(editor.getScrollTop()).toBeLessThan maxScrollTop
it "stops selecting if a textInput event occurs during the drag", ->
gutterNode.dispatchEvent(buildMouseEvent('mousedown', clientCoordinatesForScreenRowInGutter(2)))
gutterNode.dispatchEvent(buildMouseEvent('mousemove', clientCoordinatesForScreenRowInGutter(6)))

View File

@@ -190,7 +190,7 @@ class Selection extends Model
# position.
#
# * `position` An instance of {Point}, with a given `row` and `column`.
selectToScreenPosition: (position) ->
selectToScreenPosition: (position, options) ->
position = Point.fromObject(position)
@modifySelection =>
@@ -200,12 +200,12 @@ class Selection extends Model
else
@marker.setScreenRange([@initialScreenRange.start, position], reversed: false)
else
@cursor.setScreenPosition(position)
@cursor.setScreenPosition(position, options)
if @linewise
@expandOverLine()
@expandOverLine(options)
else if @wordwise
@expandOverWord()
@expandOverWord(options)
# Public: Selects the text from the current cursor position to a given buffer
# position.
@@ -311,28 +311,28 @@ class Selection extends Model
# Public: Modifies the selection to encompass the current word.
#
# Returns a {Range}.
selectWord: ->
options = {}
selectWord: (options={}) ->
options.wordRegex = /[\t ]*/ if @cursor.isSurroundedByWhitespace()
if @cursor.isBetweenWordAndNonWord()
options.includeNonWordCharacters = false
@setBufferRange(@cursor.getCurrentWordBufferRange(options))
@setBufferRange(@cursor.getCurrentWordBufferRange(options), options)
@wordwise = true
@initialScreenRange = @getScreenRange()
# Public: Expands the newest selection to include the entire word on which
# the cursors rests.
expandOverWord: ->
expandOverWord: (options) ->
@setBufferRange(@getBufferRange().union(@cursor.getCurrentWordBufferRange()), autoscroll: false)
@cursor.autoscroll()
@cursor.autoscroll() if options?.autoscroll ? true
# Public: Selects an entire line in the buffer.
#
# * `row` The line {Number} to select (default: the row of the cursor).
selectLine: (row=@cursor.getBufferPosition().row) ->
selectLine: (row, options) ->
row ?= @cursor.getBufferPosition().row
range = @editor.bufferRangeForBufferRow(row, includeNewline: true)
@setBufferRange(@getBufferRange().union(range), autoscroll: true)
@setBufferRange(@getBufferRange().union(range), options)
@linewise = true
@wordwise = false
@initialScreenRange = @getScreenRange()
@@ -341,10 +341,10 @@ class Selection extends Model
# the cursor currently rests.
#
# It also includes the newline character.
expandOverLine: ->
expandOverLine: (options) ->
range = @getBufferRange().union(@cursor.getCurrentLineBufferRange(includeNewline: true))
@setBufferRange(range, autoscroll: false)
@cursor.autoscroll()
@cursor.autoscroll() if options?.autoscroll ? true
###
Section: Modifying the selected text

View File

@@ -395,16 +395,16 @@ class TextEditorComponent
if cursorAtScreenPosition and @editor.hasMultipleCursors()
cursorAtScreenPosition.destroy()
else
@editor.addCursorAtScreenPosition(screenPosition)
@editor.addCursorAtScreenPosition(screenPosition, autoscroll: false)
else
@editor.setCursorScreenPosition(screenPosition)
@editor.setCursorScreenPosition(screenPosition, autoscroll: false)
when 2
@editor.getLastSelection().selectWord()
@editor.getLastSelection().selectWord(autoscroll: false)
when 3
@editor.getLastSelection().selectLine()
@editor.getLastSelection().selectLine(null, autoscroll: false)
@handleDragUntilMouseUp (screenPosition) =>
@editor.selectToScreenPosition(screenPosition, true)
@editor.selectToScreenPosition(screenPosition, suppressSelectionMerge: true, autoscroll: false)
onLineNumberGutterMouseDown: (event) =>
return unless event.button is 0 # only handle the left mouse button
@@ -422,14 +422,14 @@ class TextEditorComponent
clickedScreenRow = @screenPositionForMouseEvent(event).row
clickedBufferRow = @editor.bufferRowForScreenRow(clickedScreenRow)
initialScreenRange = @editor.screenRangeForBufferRange([[clickedBufferRow, 0], [clickedBufferRow + 1, 0]])
@editor.setSelectedScreenRange(initialScreenRange, preserveFolds: true)
@editor.setSelectedScreenRange(initialScreenRange, preserveFolds: true, autoscroll: false)
@handleGutterDrag(initialScreenRange)
onGutterMetaClick: (event) =>
clickedScreenRow = @screenPositionForMouseEvent(event).row
clickedBufferRow = @editor.bufferRowForScreenRow(clickedScreenRow)
initialScreenRange = @editor.screenRangeForBufferRange([[clickedBufferRow, 0], [clickedBufferRow + 1, 0]])
@editor.addSelectionForScreenRange(initialScreenRange, preserveFolds: true)
@editor.addSelectionForScreenRange(initialScreenRange, preserveFolds: true, autoscroll: false)
@handleGutterDrag(initialScreenRange)
onGutterShiftClick: (event) =>
@@ -439,9 +439,9 @@ class TextEditorComponent
clickedLineScreenRange = @editor.screenRangeForBufferRange([[clickedBufferRow, 0], [clickedBufferRow + 1, 0]])
if clickedScreenRow < tailScreenPosition.row
@editor.selectToScreenPosition(clickedLineScreenRange.start, true)
@editor.selectToScreenPosition(clickedLineScreenRange.start, suppressSelectionMerge: true, autoscroll: false)
else
@editor.selectToScreenPosition(clickedLineScreenRange.end, true)
@editor.selectToScreenPosition(clickedLineScreenRange.end, suppressSelectionMerge: true, autoscroll: false)
@handleGutterDrag(new Range(tailScreenPosition, tailScreenPosition))
@@ -456,7 +456,6 @@ class TextEditorComponent
endPosition = [dragRow + 1, 0]
screenRange = new Range(endPosition, endPosition).union(initialRange)
@editor.getLastSelection().setScreenRange(screenRange, reversed: false, autoscroll: false, preserveFolds: true)
@editor.getLastCursor().autoscroll()
onStylesheetsChanged: (styleElement) =>
return unless @performedInitialMeasurement
@@ -512,7 +511,9 @@ class TextEditorComponent
animationLoop = =>
@requestAnimationFrame =>
if dragging and @mounted
screenPosition = @screenPositionForMouseEvent(lastMousePosition)
linesClientRect = @linesComponent.getDomNode().getBoundingClientRect()
autoscroll(lastMousePosition, linesClientRect)
screenPosition = @screenPositionForMouseEvent(lastMousePosition, linesClientRect)
dragHandler(screenPosition)
animationLoop()
else if not @mounted
@@ -541,7 +542,33 @@ class TextEditorComponent
dragging = false
window.removeEventListener('mousemove', onMouseMove)
window.removeEventListener('mouseup', onMouseUp)
willInsertTextSubscription.dispose()
disposables.dispose()
autoscroll = (mouseClientPosition) =>
editorClientRect = @domNode.getBoundingClientRect()
if mouseClientPosition.clientY < editorClientRect.top
mouseYDelta = editorClientRect.top - mouseClientPosition.clientY
yDirection = -1
else if mouseClientPosition.clientY > editorClientRect.bottom
mouseYDelta = mouseClientPosition.clientY - editorClientRect.bottom
yDirection = 1
if mouseClientPosition.clientX < editorClientRect.left
mouseXDelta = editorClientRect.left - mouseClientPosition.clientX
xDirection = -1
else if mouseClientPosition.clientX > editorClientRect.right
mouseXDelta = mouseClientPosition.clientX - editorClientRect.right
xDirection = 1
if mouseYDelta?
@presenter.setScrollTop(@presenter.getScrollTop() + yDirection * scaleScrollDelta(mouseYDelta))
if mouseXDelta?
@presenter.setScrollLeft(@presenter.getScrollLeft() + xDirection * scaleScrollDelta(mouseXDelta))
scaleScrollDelta = (scrollDelta) ->
Math.pow(scrollDelta / 2, 3) / 280
pasteSelectionClipboard = (event) =>
if event?.which is 2 and process.platform is 'linux'
@@ -550,7 +577,9 @@ class TextEditorComponent
window.addEventListener('mousemove', onMouseMove)
window.addEventListener('mouseup', onMouseUp)
willInsertTextSubscription = @editor.onWillInsertText(onMouseUp)
disposables = new CompositeDisposable
disposables.add(@editor.onWillInsertText(onMouseUp))
disposables.add(@editor.onDidDestroy(stopDragging))
isVisible: ->
@domNode.offsetHeight > 0 or @domNode.offsetWidth > 0
@@ -748,17 +777,20 @@ class TextEditorComponent
if scrollSensitivity = parseInt(scrollSensitivity)
@scrollSensitivity = Math.abs(scrollSensitivity) / 100
screenPositionForMouseEvent: (event) ->
pixelPosition = @pixelPositionForMouseEvent(event)
screenPositionForMouseEvent: (event, linesClientRect) ->
pixelPosition = @pixelPositionForMouseEvent(event, linesClientRect)
@editor.screenPositionForPixelPosition(pixelPosition)
pixelPositionForMouseEvent: (event) ->
pixelPositionForMouseEvent: (event, linesClientRect) ->
{clientX, clientY} = event
linesClientRect = @linesComponent.getDomNode().getBoundingClientRect()
linesClientRect ?= @linesComponent.getDomNode().getBoundingClientRect()
top = clientY - linesClientRect.top + @presenter.scrollTop
left = clientX - linesClientRect.left + @presenter.scrollLeft
{top, left}
bottom = linesClientRect.top + @presenter.scrollTop + linesClientRect.height - clientY
right = linesClientRect.left + @presenter.scrollLeft + linesClientRect.width - clientX
{top, left, bottom, right}
getModel: ->
@editor

View File

@@ -1943,10 +1943,10 @@ class TextEditor extends Model
# This method may merge selections that end up intesecting.
#
# * `position` An instance of {Point}, with a given `row` and `column`.
selectToScreenPosition: (position, suppressMerge) ->
selectToScreenPosition: (position, options) ->
lastSelection = @getLastSelection()
lastSelection.selectToScreenPosition(position)
unless suppressMerge
lastSelection.selectToScreenPosition(position, options)
unless options?.suppressSelectionMerge
@mergeIntersectingSelections(reversed: lastSelection.isReversed())
# Essential: Move the cursor of each selection one character upward while