Can I enable scrolling with middle-button-drag in OS X?

I have a mouse with three buttons but no wheel.

In OS X, is there any way (perhaps with addon software) that would allow me to use my third button for scrolling by holding it and moving the mouse?

8 Answers

I did it with Hammerspoon with the following configuration script inspired by this thread:

Steps:

  • Install Hammerspoon
  • Click its menu icon and select Open Config
  • Paste the following lua script into the configuration:

    -- HANDLE SCROLLING WITH MOUSE BUTTON PRESSED
    local scrollMouseButton = 2
    local deferred = false
    overrideOtherMouseDown = hs.eventtap.new({ hs.eventtap.event.types.otherMouseDown }, function(e) -- print("down") local pressedMouseButton = e:getProperty(hs.eventtap.event.properties['mouseEventButtonNumber']) if scrollMouseButton == pressedMouseButton then deferred = true return true end
    end)
    overrideOtherMouseUp = hs.eventtap.new({ hs.eventtap.event.types.otherMouseUp }, function(e) -- print("up") local pressedMouseButton = e:getProperty(hs.eventtap.event.properties['mouseEventButtonNumber']) if scrollMouseButton == pressedMouseButton then if (deferred) then overrideOtherMouseDown:stop() overrideOtherMouseUp:stop() hs.eventtap.otherClick(e:location(), pressedMouseButton) overrideOtherMouseDown:start() overrideOtherMouseUp:start() return true end return false end return false
    end)
    local oldmousepos = {}
    local scrollmult = -4 -- negative multiplier makes mouse work like traditional scrollwheel
    dragOtherToScroll = hs.eventtap.new({ hs.eventtap.event.types.otherMouseDragged }, function(e) local pressedMouseButton = e:getProperty(hs.eventtap.event.properties['mouseEventButtonNumber']) -- print ("pressed mouse " .. pressedMouseButton) if scrollMouseButton == pressedMouseButton then -- print("scroll"); deferred = false oldmousepos = hs.mouse.getAbsolutePosition() local dx = e:getProperty(hs.eventtap.event.properties['mouseEventDeltaX']) local dy = e:getProperty(hs.eventtap.event.properties['mouseEventDeltaY']) local scroll = hs.eventtap.event.newScrollEvent({-dx * scrollmult, dy * scrollmult},{},'pixel') -- put the mouse back hs.mouse.setAbsolutePosition(oldmousepos) return true, {scroll} else return false, {} end
    end)
    overrideOtherMouseDown:start()
    overrideOtherMouseUp:start()
    dragOtherToScroll:start()
5

Smart Scroll does what you are looking for, with its 'Grab Scroll' feature. Assign it to 'Button 3 (Middle)' and dragging on both axes will work in apps such as browsers (Chrome), Terminal, Adobe Photoshop, and Finder - no app I've tried hasn't worked with it (using the 4.0 betas up and up). It has a free trial.

Screenshot of Smart Scroll

2

Smooze does that, among other things. (I'm the developer)

What differentiate it from other suggestions is the ability to use it in every mac app while still identify links, for example. (in case you use your middle button drag to grab and throw but still want that a middle button click will act as a middle button)

With Smooze is more like grab-drag-throw than grab-drag. The release effects the momentum and animation of the scroll, similar to the iPhone scroll.

enter image description here

1

There's a very nice open source app called Karabiner which will do this and a whole lot more (Keyboard and mouse remapping etc). See this question for some examples. Also, for certain manufacturers, they supply custom control software, which may allow for improved/modified functionality (e.g. Logitech Control Center).

As mentioned in the comments below, that whilst a new version of 'Karabiner Elements' has been released for MacOS Sierra (10.12) onwards, it only provides for keyboard based remapping so far - so currently mouse remapping can't be done with it.

However Hammerspoon is another free open source tool that may be used, amongst many other things, to remap the keys on the mouse (and/or keyboard) to different functions. You'll need to install the tool and supply it with some appropriate config - see examples here for mouse remapping.

To check which event types and mouseEventButtonNumbers are being generated by your device you can run this (just copy/paste the 4 lines into the console) in the Hammerspoon console (Use reload config to stop it):

hs.eventtap.new({"all"},function(e)
print(e,"mouseEventButtonNumber:",
e:getProperty(hs.eventtap.event.properties['mouseEventButtonNumber']))
end):start()

Note: If you've installed the Logitech Control Centre (LCC) tools - it grabs the events directly from the Logitech devices using their installed kernel module so Hammerspoon can't see them. You'll need to uninstall LCC if you want to remap the mouse buttons using Hammerspoon.

5

+1 for Hammerspoon and a script, a normal mouse/trackball drives me mad on a Mac.

I wrote one to scroll while middle mouse button is pressed down - the further you move the mouse the faster it will scroll.

Click still works like a normal click with a 5 pixel dead-zone so you don't have to keep the mouse perfectly still between pressing and releasing the wheel.

------------------------------------------------------------------------------------------
-- AUTOSCROLL WITH MOUSE WHEEL BUTTON
-- timginter @ GitHub
------------------------------------------------------------------------------------------
-- id of mouse wheel button
local mouseScrollButtonId = 2
-- scroll speed and direction config
local scrollSpeedMultiplier = 0.1
local scrollSpeedSquareAcceleration = true
local reverseVerticalScrollDirection = false
local mouseScrollTimerDelay = 0.01
-- circle config
local mouseScrollCircleRad = 10
local mouseScrollCircleDeadZone = 5
------------------------------------------------------------------------------------------
local mouseScrollCircle = nil
local mouseScrollTimer = nil
local mouseScrollStartPos = 0
local mouseScrollDragPosX = nil
local mouseScrollDragPosY = nil
overrideScrollMouseDown = hs.eventtap.new({ hs.eventtap.event.types.otherMouseDown }, function(e) -- uncomment line below to see the ID of pressed button --print(e:getProperty(hs.eventtap.event.properties['mouseEventButtonNumber'])) if e:getProperty(hs.eventtap.event.properties['mouseEventButtonNumber']) == mouseScrollButtonId then -- remove circle if exists if mouseScrollCircle then mouseScrollCircle:delete() mouseScrollCircle = nil end -- stop timer if running if mouseScrollTimer then mouseScrollTimer:stop() mouseScrollTimer = nil end -- save mouse coordinates mouseScrollStartPos = hs.mouse.getAbsolutePosition() mouseScrollDragPosX = mouseScrollStartPos.x mouseScrollDragPosY = mouseScrollStartPos.y -- start scroll timer mouseScrollTimer = hs.timer.doAfter(mouseScrollTimerDelay, mouseScrollTimerFunction) -- don't send scroll button down event return true end
end)
overrideScrollMouseUp = hs.eventtap.new({ hs.eventtap.event.types.otherMouseUp }, function(e) if e:getProperty(hs.eventtap.event.properties['mouseEventButtonNumber']) == mouseScrollButtonId then -- send original button up event if released within 'mouseScrollCircleDeadZone' pixels of original position and scroll circle doesn't exist mouseScrollPos = hs.mouse.getAbsolutePosition() xDiff = math.abs(mouseScrollPos.x - mouseScrollStartPos.x) yDiff = math.abs(mouseScrollPos.y - mouseScrollStartPos.y) if (xDiff < mouseScrollCircleDeadZone and yDiff < mouseScrollCircleDeadZone) and not mouseScrollCircle then -- disable scroll mouse override overrideScrollMouseDown:stop() overrideScrollMouseUp:stop() -- send scroll mouse click hs.eventtap.otherClick(e:location(), mouseScrollButtonId) -- re-enable scroll mouse override overrideScrollMouseDown:start() overrideScrollMouseUp:start() end -- remove circle if exists if mouseScrollCircle then mouseScrollCircle:delete() mouseScrollCircle = nil end -- stop timer if running if mouseScrollTimer then mouseScrollTimer:stop() mouseScrollTimer = nil end -- don't send scroll button up event return true end
end)
overrideScrollMouseDrag = hs.eventtap.new({ hs.eventtap.event.types.otherMouseDragged }, function(e) -- sanity check if mouseScrollDragPosX == nil or mouseScrollDragPosY == nil then return true end -- update mouse coordinates mouseScrollDragPosX = mouseScrollDragPosX + e:getProperty(hs.eventtap.event.properties['mouseEventDeltaX']) mouseScrollDragPosY = mouseScrollDragPosY + e:getProperty(hs.eventtap.event.properties['mouseEventDeltaY']) -- don't send scroll button drag event return true
end)
function mouseScrollTimerFunction() -- sanity check if mouseScrollDragPosX ~= nil and mouseScrollDragPosY ~= nil then -- get cursor position difference from original click xDiff = math.abs(mouseScrollDragPosX - mouseScrollStartPos.x) yDiff = math.abs(mouseScrollDragPosY - mouseScrollStartPos.y) -- draw circle if not yet drawn and cursor moved more than 'mouseScrollCircleDeadZone' pixels if mouseScrollCircle == nil and (xDiff > mouseScrollCircleDeadZone or yDiff > mouseScrollCircleDeadZone) then mouseScrollCircle = hs.drawing.circle(hs.geometry.rect(mouseScrollStartPos.x - mouseScrollCircleRad, mouseScrollStartPos.y - mouseScrollCircleRad, mouseScrollCircleRad * 2, mouseScrollCircleRad * 2)) mouseScrollCircle:setStrokeColor({["red"]=0.3, ["green"]=0.3, ["blue"]=0.3, ["alpha"]=1}) mouseScrollCircle:setFill(false) mouseScrollCircle:setStrokeWidth(1) mouseScrollCircle:show() end -- send scroll event if cursor moved more than circle's radius if xDiff > mouseScrollCircleRad or yDiff > mouseScrollCircleRad then -- get real xDiff and yDiff deltaX = mouseScrollDragPosX - mouseScrollStartPos.x deltaY = mouseScrollDragPosY - mouseScrollStartPos.y -- use 'scrollSpeedMultiplier' deltaX = deltaX * scrollSpeedMultiplier deltaY = deltaY * scrollSpeedMultiplier -- square for better scroll acceleration if scrollSpeedSquareAcceleration then -- mod to keep negative values deltaXDirMod = 1 deltaYDirMod = 1 if deltaX < 0 then deltaXDirMod = -1 end if deltaY < 0 then deltaYDirMod = -1 end deltaX = deltaX * deltaX * deltaXDirMod deltaY = deltaY * deltaY * deltaYDirMod end -- math.ceil / math.floor - scroll event accepts only integers deltaXRounding = math.ceil deltaYRounding = math.ceil if deltaX < 0 then deltaXRounding = math.floor end if deltaY < 0 then deltaYRounding = math.floor end deltaX = deltaXRounding(deltaX) deltaY = deltaYRounding(deltaY) -- reverse Y scroll if 'reverseVerticalScrollDirection' set to true if reverseVerticalScrollDirection then deltaY = deltaY * -1 end -- send scroll event hs.eventtap.event.newScrollEvent({-deltaX, deltaY}, {}, 'pixel'):post() end end -- restart timer mouseScrollTimer = hs.timer.doAfter(mouseScrollTimerDelay, mouseScrollTimerFunction)
end
-- start override functions
overrideScrollMouseDown:start()
overrideScrollMouseUp:start()
overrideScrollMouseDrag:start()
------------------------------------------------------------------------------------------
-- END OF AUTOSCROLL WITH MOUSE WHEEL BUTTON
------------------------------------------------------------------------------------------
5

It depends on the software - for example, Firefox supports it, while Google Chrome does not.

Currently there is no software to enable such feature system-wide in OS X, sadly.

1

I used Better Touch Tool to assign Ctrl+middle-click to PgUp, and Option+Middle-click to PgDown. It's free, excellent software, and works well.

Karabiner-Elements has this implemented.

Install it, then go to this custom complex_modifications page and import the "Change mouse motion to scroll" rule. enter image description here

1

Your Answer

Sign up or log in

Sign up using Google Sign up using Facebook Sign up using Email and Password

Post as a guest

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

You Might Also Like