Skip to content

Commit

Permalink
Add independent scrolling setting when mouse wheeling over the minimap
Browse files Browse the repository at this point in the history
This allow to browse a file quickly and pinpoint a location to jump to
from the minimap.

Closes #414
  • Loading branch information
abe33 committed Mar 6, 2016
1 parent 54780a4 commit 376b0b7
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 27 deletions.
11 changes: 11 additions & 0 deletions lib/config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,17 @@
"default": true,
"description": "Whether to offset the minimap canvas when scrolling to keep the scroll smooth. When `true` the minimap canvas will be offseted, resulting in a smoother scroll, but with the side-effect of a blurry minimap when the canvas is placed between pixels. When `false` the canvas will always stay at the same position, and will never look blurry, but the scroll will appear more jagged."
},
"independentMinimapScroll": {
"type": "boolean",
"title": "Independent Minimap Scroll On Mouse Wheel Events",
"default": false,
"description": "When enabled, using the mouse wheel over the Minimap will make it scroll independently of the text editor. The Minimap will still sync with the editor whenever the editor is scrolled, but it will no longer relay the mouse wheel events to the editor."
},
"scrollSensitivity": {
"type": "number",
"default": 0.5,
"description": "The scrolling speed when the `Independent Minimap Scroll On Mouse Wheel Events` setting is enabled."
},
"createPluginInDevMode":{
"type":"boolean",
"default":false
Expand Down
12 changes: 9 additions & 3 deletions lib/minimap-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,9 @@ export default class MinimapElement {

this.subscriptions.add(this.subscribeTo(this, {
'mousewheel': (e) => {
if (!this.standAlone) { this.relayMousewheelEvent(e) }
if (!this.standAlone) {
this.relayMousewheelEvent(e)
}
}
}))

Expand Down Expand Up @@ -801,7 +803,7 @@ export default class MinimapElement {
if (this.scrollIndicator != null) {
let minimapScreenHeight = minimap.getScreenHeight()
let indicatorHeight = minimapScreenHeight * (minimapScreenHeight / minimap.getHeight())
let indicatorScroll = (minimapScreenHeight - indicatorHeight) * minimap.getCapedTextEditorScrollRatio()
let indicatorScroll = (minimapScreenHeight - indicatorHeight) * minimap.getScrollRatio()

if (SPEC_MODE) {
this.applyStyles(this.scrollIndicator, {
Expand Down Expand Up @@ -1021,7 +1023,11 @@ export default class MinimapElement {
* @access private
*/
relayMousewheelEvent (e) {
this.getTextEditorElement().component.onMouseWheel(e)
if (this.minimap.scrollIndependentlyOnMouseWheel()) {
this.minimap.onMouseWheel(e)
} else {
this.getTextEditorElement().component.onMouseWheel(e)
}
}

/**
Expand Down
104 changes: 85 additions & 19 deletions lib/minimap.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,16 +182,14 @@ export default class Minimap {
this.adapter = new LegacyAdater(this.textEditor)
}

if (this.standAlone) {
/**
* When in stand-alone mode, a Minimap doesn't scroll and will use this
* value instead.
*
* @type {number}
* @access private
*/
this.scrollTop = 0
}
/**
* When in stand-alone or independent scrolling mode, this value can be used
* instead of the computed scroll.
*
* @type {number}
* @access private
*/
this.scrollTop = 0

const subs = this.subscriptions
let configSubscription = this.subscribeToConfig()
Expand All @@ -208,6 +206,7 @@ export default class Minimap {

subs.add(this.adapter.onDidChangeScrollTop(() => {
if (!this.standAlone) {
this.updateScrollTop()
this.emitter.emit('did-change-scroll-top', this)
}
}))
Expand Down Expand Up @@ -366,22 +365,33 @@ export default class Minimap {
}))
subs.add(atom.config.observe('minimap.charHeight', opts, (configCharHeight) => {
this.configCharHeight = configCharHeight
this.updateScrollTop()
this.emitter.emit('did-change-config')
}))
subs.add(atom.config.observe('minimap.charWidth', opts, (configCharWidth) => {
this.configCharWidth = configCharWidth
this.updateScrollTop()
this.emitter.emit('did-change-config')
}))
subs.add(atom.config.observe('minimap.interline', opts, (configInterline) => {
this.configInterline = configInterline
this.updateScrollTop()
this.emitter.emit('did-change-config')
}))
subs.add(atom.config.observe('minimap.independentMinimapScroll', opts, (independentMinimapScroll) => {
this.independentMinimapScroll = independentMinimapScroll
this.updateScrollTop()
}))
subs.add(atom.config.observe('minimap.scrollSensitivity', opts, (scrollSensitivity) => {
this.scrollSensitivity = scrollSensitivity
}))
// cdprr is shorthand for configDevicePixelRatioRounding
subs.add(atom.config.observe(
'minimap.devicePixelRatioRounding',
opts,
(cdprr) => {
this.configDevicePixelRatioRounding = cdprr
this.updateScrollTop()
this.emitter.emit('did-change-config')
}
))
Expand Down Expand Up @@ -597,6 +607,7 @@ export default class Minimap {
setScreenHeightAndWidth (height, width) {
this.height = height
this.width = width
this.updateScrollTop()
}

/**
Expand Down Expand Up @@ -763,6 +774,13 @@ export default class Minimap {
)
}

/**
* Returns true when the `independentMinimapScroll` setting have been enabled.
*
* @return {boolean} whether the minimap can scroll independently
*/
scrollIndependentlyOnMouseWheel () { return this.independentMinimapScroll }

/**
* Returns the current scroll of the Minimap.
*
Expand All @@ -772,13 +790,9 @@ export default class Minimap {
* @return {number} the scroll top of the Minimap
*/
getScrollTop () {
if (this.standAlone) {
return this.scrollTop
} else {
return Math.abs(
this.getCapedTextEditorScrollRatio() * this.getMaxScrollTop()
)
}
return this.standAlone || this.independentMinimapScroll
? this.scrollTop
: this.getScrollTopFromEditor()
}

/**
Expand All @@ -788,12 +802,46 @@ export default class Minimap {
* @emits {did-change-scroll-top} if the Minimap's stand-alone mode is enabled
*/
setScrollTop (scrollTop) {
this.scrollTop = scrollTop
if (this.standAlone) {
this.scrollTop = Math.max(0, Math.min(this.getMaxScrollTop(), scrollTop))

if (this.standAlone || this.independentMinimapScroll) {
this.emitter.emit('did-change-scroll-top', this)
}
}

/**
* Returns the minimap scroll as a ration between 0 and 1.
*
* @return {number} the minimap scroll ratio
*/
getScrollRatio () {
return this.getScrollTop() / this.getMaxScrollTop()
}

/**
* Updates the scroll top value with the one computed from the text editor
* when the minimap is in the independent scrolling mode.
*
* @access private
*/
updateScrollTop () {
if (this.independentMinimapScroll) {
this.setScrollTop(this.getScrollTopFromEditor())
this.emitter.emit('did-change-scroll-top', this)
}
}

/**
* Returns the scroll top as computed from the text editor scroll top.
*
* @return {number} the computed scroll top value
*/
getScrollTopFromEditor () {
return Math.abs(
this.getCapedTextEditorScrollRatio() * this.getMaxScrollTop()
)
}

/**
* Returns the maximum scroll value of the Minimap.
*
Expand All @@ -810,6 +858,24 @@ export default class Minimap {
*/
canScroll () { return this.getMaxScrollTop() > 0 }

/**
* Updates the minimap scroll top value using a mouse event when the
* independent scrolling mode is enabled
*
* @param {MouseEvent} event the mouse wheel event
* @access private
*/
onMouseWheel (event) {
if (!this.canScroll()) { return }

const {wheelDeltaY} = event
const previousScrollTop = this.getScrollTop()
const updatedScrollTop = previousScrollTop - Math.round(wheelDeltaY * this.scrollSensitivity)

event.preventDefault()
this.setScrollTop(updatedScrollTop)
}

/**
* Delegates to `TextEditor#getMarker`.
*
Expand Down
17 changes: 15 additions & 2 deletions spec/helpers/events.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,15 @@ function mouseEvent (type, properties) {
}
}

return new MouseEvent(type, properties)
const e = new MouseEvent(type, properties)

for (let k in properties) {
if (e[k] !== properties[k]) {
e[k] = properties[k]
}
}

return e
}

function touchEvent (type, touches) {
Expand Down Expand Up @@ -75,7 +83,12 @@ module.exports = {objectCenterCoordinates, mouseEvent}
})

module.exports.mousewheel = function (obj, deltaX = 0, deltaY = 0) {
obj.dispatchEvent(mouseEvent('mousewheel', {deltaX, deltaY}))
obj.dispatchEvent(mouseEvent('mousewheel', {
deltaX,
deltaY,
wheelDeltaX: deltaX,
wheelDeltaY: deltaY
}))
}

;['touchstart', 'touchmove', 'touchend'].forEach((key) => {
Expand Down
37 changes: 34 additions & 3 deletions spec/minimap-element-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -473,15 +473,46 @@ describe('MinimapElement', () => {
})

describe('using the mouse scrollwheel over the minimap', () => {
beforeEach(() => {
it('relays the events to the editor view', () => {
spyOn(editorElement.component.presenter, 'setScrollTop').andCallFake(() => {})

mousewheel(minimapElement, 0, 15)
})

it('relays the events to the editor view', () => {
expect(editorElement.component.presenter.setScrollTop).toHaveBeenCalled()
})

describe('when the independentMinimapScroll setting is true', () => {
let previousScrollTop

beforeEach(() => {
atom.config.set('minimap.independentMinimapScroll', true)
atom.config.set('minimap.scrollSensitivity', 0.5)

spyOn(editorElement.component.presenter, 'setScrollTop').andCallFake(() => {})

previousScrollTop = minimap.getScrollTop()

mousewheel(minimapElement, 0, -15)
})

it('does not relay the events to the editor', () => {
expect(editorElement.component.presenter.setScrollTop).not.toHaveBeenCalled()
})

it('scrolls the minimap instead', () => {
expect(minimap.getScrollTop()).not.toEqual(previousScrollTop)
})

it('clamp the minimap scroll into the legit bounds', () => {
mousewheel(minimapElement, 0, -100000)

expect(minimap.getScrollTop()).toEqual(minimap.getMaxScrollTop())

mousewheel(minimapElement, 0, 100000)

expect(minimap.getScrollTop()).toEqual(0)
})
})
})

describe('middle clicking the minimap', () => {
Expand Down
28 changes: 28 additions & 0 deletions spec/minimap-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,33 @@ describe('Minimap', () => {
})
})

describe('when independentMinimapScroll is true', () => {
let editorScrollRatio
beforeEach(() => {
editor.setText(largeSample)
editorElement.setScrollTop(1000)
editorScrollRatio = editorElement.getScrollTop() / (editorElement.getScrollHeight() - editorElement.getHeight())

atom.config.set('minimap.independentMinimapScroll', true)
})

it('ignores the scroll computed from the editor and return the one of the minimap instead', () => {
expect(minimap.getScrollTop()).toEqual(editorScrollRatio * minimap.getMaxScrollTop())

minimap.setScrollTop(200)

expect(minimap.getScrollTop()).toEqual(200)
})

describe('scrolling the editor', () => {
it('changes the minimap scroll top', () => {
editorElement.setScrollTop(2000)

expect(minimap.getScrollTop()).not.toEqual(editorScrollRatio * minimap.getMaxScrollTop())
})
})
})

// ######## ######## ###### #######
// ## ## ## ## ## ## ##
// ## ## ## ## ## ##
Expand Down Expand Up @@ -579,6 +606,7 @@ describe('Stand alone minimap', () => {
it('has a scroll top that is not bound to the text editor', () => {
let scrollSpy = jasmine.createSpy('didScroll')
minimap.onDidChangeScrollTop(scrollSpy)
minimap.setScreenHeightAndWidth(100, 100)

editor.setText(largeSample)
editorElement.setScrollTop(1000)
Expand Down

0 comments on commit 376b0b7

Please sign in to comment.