From 376b0b7230974aadb73d6609ee35c7897d13ce27 Mon Sep 17 00:00:00 2001 From: abe33 Date: Sun, 6 Mar 2016 22:42:12 +0100 Subject: [PATCH] Add independent scrolling setting when mouse wheeling over the minimap This allow to browse a file quickly and pinpoint a location to jump to from the minimap. Closes #414 --- lib/config-schema.json | 11 ++++ lib/minimap-element.js | 12 +++- lib/minimap.js | 104 ++++++++++++++++++++++++++++------- spec/helpers/events.js | 17 +++++- spec/minimap-element-spec.js | 37 ++++++++++++- spec/minimap-spec.js | 28 ++++++++++ 6 files changed, 182 insertions(+), 27 deletions(-) diff --git a/lib/config-schema.json b/lib/config-schema.json index 0c765eee..93fd004f 100644 --- a/lib/config-schema.json +++ b/lib/config-schema.json @@ -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 diff --git a/lib/minimap-element.js b/lib/minimap-element.js index 451286a9..b948f334 100644 --- a/lib/minimap-element.js +++ b/lib/minimap-element.js @@ -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) + } } })) @@ -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, { @@ -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) + } } /** diff --git a/lib/minimap.js b/lib/minimap.js index a63cdb5c..b5a3e217 100644 --- a/lib/minimap.js +++ b/lib/minimap.js @@ -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() @@ -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) } })) @@ -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') } )) @@ -597,6 +607,7 @@ export default class Minimap { setScreenHeightAndWidth (height, width) { this.height = height this.width = width + this.updateScrollTop() } /** @@ -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. * @@ -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() } /** @@ -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. * @@ -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`. * diff --git a/spec/helpers/events.js b/spec/helpers/events.js index 8a33a494..25b07e03 100644 --- a/spec/helpers/events.js +++ b/spec/helpers/events.js @@ -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) { @@ -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) => { diff --git a/spec/minimap-element-spec.js b/spec/minimap-element-spec.js index c9d7b74f..fc161017 100644 --- a/spec/minimap-element-spec.js +++ b/spec/minimap-element-spec.js @@ -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', () => { diff --git a/spec/minimap-spec.js b/spec/minimap-spec.js index f90bbb62..5381410c 100644 --- a/spec/minimap-spec.js +++ b/spec/minimap-spec.js @@ -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()) + }) + }) + }) + // ######## ######## ###### ####### // ## ## ## ## ## ## ## // ## ## ## ## ## ## @@ -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)