16
5
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?
16
5
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?
10
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.
4
I did it with Hammerspoon with the following configuration script inspired by this thread: https://github.com/tekezo/Karabiner/issues/814#issuecomment-337643019
Steps:
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()
2
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.
1Unfortunately Karabiner no longer works with modern OSX. There is a 'Karabiner Elements' now that does, but it has half the functionality of the original, and this is one of the things it can't do. – Nathan Hornby – 2017-10-30T10:19:52.937
1Yes it is currently limited so I've updated my answer to add another solution. – Pierz – 2017-10-30T10:36:14.530
Hammerspoon is the solution I landed on yesterday, so good suggestion! :) For some reason I couldn't get it to bind to one of the mouse buttons, but mapping it to ctrl+cmd seemed to work fine. – Nathan Hornby – 2017-10-31T11:19:17.263
1I've added another edit as I had this problem when I had LCC installed but uninstalling it fixed it (once I'd worked which buttons generated which mouseEventButtonNumber - on my Marble Mouse left mini-button is 3 and the right one is 4). – Pierz – 2017-11-01T12:32:52.517
I suspected that might be the issue! Thanks for the confirmation, I'll sort that out when I get the chance. – Nathan Hornby – 2017-11-02T13:12:04.867
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.
1
+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.floor - scroll event accepts only integers
deltaX = math.floor(deltaX)
deltaY = math.floor(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
------------------------------------------------------------------------------------------
Killer feature! Thanks so much, exactly what I was looking for. One bug however deltaX = deltaY * -1
should be deltaY = deltaY * -1
and I commented out deltaX = deltaX * -1
because I didn't want X axis inverted. – Tyler – 2019-11-14T20:54:10.737
Thanks for spotting the typo. I rewrote it a bit and changed the option to reverse vertical scrolling only – TIM – 2019-11-26T15:38:37.033
1
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.
Maybe it wasn't compatible with Chrome way back in 2011, but certainly in 2014 after no doubt quite a few revisions, Smart Scroll's 'Grab Scroll' works smoothly with Chrome and Opera, I can confirm. I think it is OS-wide too as it works in Finder, Adobe Photoshop, and even Terminal. So I think your data is out of date! :) – None – 2014-06-24T07:00:15.643
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.
just tried it and it works beautifully. – im_chc – 2019-05-11T09:16:47.633
since I prefer to make the Y-scroll the other way round, I've changed the lua code a little bit: change "dy" in the line "local scroll = hs.eventtap.event.newScrollEvent({-dx * scrollmult, dy * scrollmult},{},'pixel')" to negative (so it would be "-dy * scrollmult") – im_chc – 2019-05-11T09:26:22.493