Mighty Mouse の調子が悪いので Greasemonkey

MacBook で使っている Mighty Mouse のスクロールボールの調子が悪い。
掃除(http://support.apple.com/kb/HT1537?viewlocale=ja_JP&locale=ja_JP) をしてみてもよくならない。このままではWEBサイトを見ているときに手軽にスクロールできなくて困るので、Greasemonkey でスクロールできるようにしてみた。


動作のイメージとしては Wii のブラウザのように、つまんでスクロールさせるみたいな感じ。

使い方
 1 任意の場所で長めのクリック
 2 マウスカーソルを動かすとその方向へスクロールする
 3 もう一回クリックするとスクロール解除

意外と便利。スクロール問題は解決した。


wiiscroll.user.js

// ==UserScript==
// @name           WiiScroll
// @namespace      http://d.hatena.ne.jp/mohayonao
// @include        *
// ==/UserScript==

(function() {
    // 状態遷移を管理する
    function Observer() {
        this.initialize.apply(this, arguments);
    };
    
    Observer.prototype = {
        initialize: function(name) {
            this.name = name;
        },
        setup: function(config) {
            this.eventListener = new EventListenerList(config.eventListener, this);
            this.onStart = config.onStart || function() {};
            this.onStop  = config.onStop  || function() {};
        },
        start: function(event) {
            this.onStart(this, event);
            this.eventListener.start();
        },
        stop: function(event) {
            this.eventListener.stop();
            this.onStop(this, event);
        },
        move: function(next, event) {
            this.stop(event);
            next.start(event);
        }
    };
    
    // イベントリスナー管理用
    function EventListenerList() {
        this.initialize.apply(this, arguments);
    }
    
    EventListenerList.prototype = {
        initialize: function(list, observer) {
            this.list = [];
            if (!list) return;
            
            for (var type in list) {
                (function (self, listener) {
                    var curriedListener = function(event) {
                        return listener(observer, event);
                    };
                    self.list.push({type: type, listener: curriedListener});
                })(this, list[type]);
            }
        },
        start: function() {
            for(var i = 0; i < this.list.length; i++) {
                var t = this.list[i];
                document.addEventListener(t.type, t.listener, false);
            }
        },
        stop: function() {
            for(var i = 0; i < this.list.length; i++) {
                var t = this.list[i];
                document.removeEventListener(t.type, t.listener, false);
            }
        }
    };
    
    
    function Arrow() {
        this.initialize.apply(this, arguments);
    }
    
    Arrow.prototype = {
        initialize: function(x, y) {
            this.baseX = this.curX = x;
            this.baseY = this.curY = y;
            this.distanceX = 0;
            this.distanceY = 0;
        },
        setPosition: function(x, y) {
            this.curX = x;
            this.curY = y;
            this.distanceX = this.baseX - this.curX;
            this.distanceY = this.baseY - this.curY;
        },
        draw: function(g) {
            g.strokeStyle = '#66f';
            g.fillStyle   = '#66f';
            g.lineWidth = 2;
            
            g.beginPath();
            g.arc(this.baseX, this.baseY, 3, 0, Math.PI * 2, false);
            g.arc(this.curX, this.curY, 3, 0, Math.PI * 2, false);
            g.fill();
            g.closePath();
            
            g.beginPath();
            g.moveTo(this.baseX, this.baseY);
            g.lineTo(this.curX, this.curY);
            g.stroke();
            g.closePath();
        }
    }
    
    
    function Canvas() {
        this.initialize.apply(this, arguments);
    }
    
    Canvas.prototype = {
        initialize: function() {
            this.canvas = document.createElement('canvas');
            
            this.canvas.style.position = "fixed";
            this.canvas.style.left     = 0;
            this.canvas.style.top      = 0;
            this.canvas.style.zIndex   = 100;
            
            this.canvas.width  = window.innerWidth;
            this.canvas.height = window.innerHeight;
            
            document.body.appendChild(this.canvas);
            
            this.context = this.canvas.getContext('2d');
        },
        quit: function() {
            document.body.removeChild(this.canvas);
            delete this;
        },
        clear: function() {
            this.context.clearRect(0, 0, window.innerWidth, window.innerHeight);
        },
        draw: function(obj) {
            if (typeof(obj) == 'object' && !!obj.draw) {
                obj.draw(this.context);
            }
        }
    };
    
    
    function getScrollableElement(elem) {
        if (!elem.tagName) return;
        var overflow = getComputedStyle(elem, '').overflow;
        if (overflow == 'scroll' || overflow == 'auto') {
            if (elem.scrollHeight != elem.offsetHeight ||
                elem.scrollWidth  != elem.offsetWidth     ) {
                return elem;
            }
        }
        return arguments.callee(elem.parentNode);
    }
    
    
    var init = new Observer('init');
    var wait = new Observer('wait');
    var standby = new Observer('standby');
    var monitor = new Observer('monitor');
    
    init.setup({
        eventListener: {
            mousedown: function(self, event) {
                self.move(wait, event);
            }
        }
    });
    
    wait.setup({
        eventListener: {
            mouseup: function(self, event) {
                self.move(init, event);
            },
            mousemove: function(self, event) {
                self.move(init, event);
            }
        },
        onStart: function(self, event) {
            self.timerID = setTimeout(function() {
                self.move(standby, event);
            }, 150)
        },
        onStop: function(self, event) {
            if (self.timerID > 0) {
                clearTimeout(self.timerID);
            }
        }
    });
    
    standby.setup({
        eventListener: {
            mouseup  : function(self, event) {
                self.move(monitor, event);
            },
            mousemove: function(self, event) {
                self.move(init, event);
            }
        }
    });
    
    monitor.setup({
        eventListener: {
            mousedown: function(self, event) {
                self.move(init, event);
            },
            mousemove: function(self, event) {
                self.arrow.setPosition(event.clientX, event.clientY);
                self.canvas.clear();
                self.canvas.draw(self.arrow);
            }
        },
        onStart: function(self, event) {
            self.canvas = new Canvas();
            self.arrow  = new Arrow(event.clientX, event.clientY);
            
            self.canvas.draw(self.arrow);
            
            var target = getScrollableElement(event.target);
            
            self.timerID = setInterval(function() {
                var scrollOffsetX = self.arrow.distanceX / 2;
                var scrollOffsetY = self.arrow.distanceY / 5;
                
                if (!!target) {
                    target.scrollLeft -= scrollOffsetX;
                    target.scrollTop  -= scrollOffsetY;
                } else {
                    scrollBy(-scrollOffsetX, -scrollOffsetY);
                }
                
            }, 150);
        },
        onStop: function(self, event) {
            if (self.timerID > 0) {
                clearInterval(self.timerID);
            }
            self.arrow = null;
            self.canvas.quit();
        }
    });
    
    
    init.start();
})();