エクステンションを使ってTortoiseHgを改造する

**これは[TortoiseHg Advent Calender 2012](http://connpass.com/event/1504/) ８日目の記事です**
**昨日は、 @cointossさんの
[TortoiseHgを使ってファイル登録しコミットしよう - cointoss.log](http://cointoss.hatenablog.com/touch/entry/2012/12/07/235900) でした**

Mercurialでは、エクステンションを導入することによって新たにコマンドを追加したり、動作をカスタマイズしたりすることができます。

これは本来Mercurial本体を対象とした仕組みなんですが、TortoiseHg実行時にもエクステンションは読み込まれるので、実はTortoiseHgの動作をカスタマイズすることも出来るんですね。

もちろん、明確なカスタマイズインフラが用意されている訳ではないのですが、そこは独自言語パイソンさんの気安さで、読み込まれてしまえば既存のクラスを書き換えたり追加したり、たいていのことは出来るわけです。

という訳で、例えばこんなことができるよ、というご紹介も兼ねて、
##thg-itarize エクステンション
ってのを作ってみました。
面倒なのでソースそのまま貼ります。

```python:itarize.py
# -*- coding: utf-8 -*-
import os
import sys

def uisetup(ui):
    if not os.path.basename(sys.argv[0]).startswith('thg'):
        # TortoiseHg以外から読み込まれた場合は何もしない
        return

    from mercurial import util
    from PyQt4 import QtGui, QtCore
    from types import MethodType

    class Config(object):
        """設定値を保持する。画像の読み込みも受け持つ"""
        def __init__(self, ui):
            self.ui = ui

        @util.propertycache
        def default_image(self):
            path = self.ui.config('itarize', 'image.default')
            return self.get_pixmap(path, "white")

        @util.propertycache
        def clicked_image(self):
            path = self.ui.config('itarize', 'image.clicked')
            return self.get_pixmap(path, "white") or self.default_image

        @util.propertycache
        def opacity(self):
            try:
                val = float(self.ui.config('itarize', 'opacity', "1"))
                return min(max(val, 0), 1)
            except ValueError:
                return 0.5

        @util.propertycache
        def autoscale(self):
            val = self.ui.config('itarize', 'autoscale', 'true').lower()
            return val in ('1', 'yes', 'true')

        def get_pixmap(self, path, mask_color):
            if not path:
                return None
            img = QtGui.QImage(path)
            p = QtGui.QPixmap.fromImage(img)
            return p

    def Itarate(cls):
        """指定されたクラスに装飾を施す。clsはQWidgetから派生している必要がある"""
        conf = Config(ui)

        def get_image_rect(widget, img):
            vp = widget.viewport()
            ww, wh = vp.width(), vp.height()
            iw, ih = img.width(), img.height()
            if (ww < iw or wh < ih) and conf.autoscale:
                scale = min(float(ww) / iw, float(wh) / ih)
                iw *= scale
                ih *= scale
            return QtCore.QRect(ww - iw, wh - ih, iw, ih)

        cls_init = cls.__init__

        def __init__(self, *args, **kwargs):
            cls_init(self, *args, **kwargs)
            self.img = conf.default_image
            if hasattr(self, "verticalScrollBar"):
                self.verticalScrollBar().valueChanged.connect(
                    self.viewport().update
                )
            if hasattr(self, "horizontalScrollBar"):
                self.horizontalScrollBar().valueChanged.connect(
                    self.viewport().update
                )

        def paintEvent(self, event, org=cls.paintEvent):
            org(self, event)

            if not self.img:
                return
            vp = self.viewport()
            p = QtGui.QPainter(vp)
            p.setRenderHints(QtGui.QPainter.SmoothPixmapTransform
                             | QtGui.QPainter.Antialiasing)
            p.setOpacity(conf.opacity)
            rc = get_image_rect(self, self.img)
            p.drawPixmap(rc, self.img)
            p.end()

        def mousePressEvent(self, event, org=cls.mousePressEvent):
            org(self, event)
            if event.buttons() == QtCore.Qt.LeftButton:
                rc = get_image_rect(self, self.img)
                if rc.left() <= event.x() and rc.top() <= event.y():
                    self.img = conf.clicked_image
                    self.viewport().update()

        def mouseReleaseEvent(self, event, org=cls.mouseReleaseEvent):
            org(self, event)
            if getattr(self, "img", None) != conf.default_image:
                self.img = conf.default_image
                self.viewport().update()

        cls.__init__ = MethodType(__init__, None, cls)
        cls.paintEvent = MethodType(paintEvent, None, cls)
        cls.mousePressEvent = MethodType(mousePressEvent, None, cls)
        cls.mouseReleaseEvent = MethodType(mouseReleaseEvent, None, cls)

    # AnnotateViewを装飾
    from tortoisehg.hgqt.fileview import AnnotateView
    Itarate(AnnotateView)
```

###何ができるの？
####こうなります
![thg-itarize](http://gyazo.com/e97406c7af2f6123f2b655c6793e699e.png)
高階ことり [(c)mzp (CC-by-SA)]

**これでつらい残業時間も頑張れますね :)**

###使い方
1. あなたの嫁画像を用意します。リアル嫁でも２次嫁でも猫でもお好きなものを。透過png形式がいいと思います。
2. Mercurial.iniやhgrcに、設定を追加します。

```conf:hgrc
[extensions]
itarize = itarize.pyのパス

[itarize]
image.default = 画像ファイルのパス
```

ファイルが見えねえよ！ジャマだよ！という愛の足らない人は、`opacity = 0.5`とか指定するといいと思います。
![thg-itarize-opacity](http://gyazo.com/0090e86b278fde3d28e59bde68f9ea7d.png)

`image.clicked = 別の画像`という指定を追加すると、クリックしたときに別の画像が表示されるようになります。
クリックした座標によって違うリアクションを返せるようにとかすると実用度()が上がるんですが、変態Advent Calender行きになりかねないので自重しました。

ちなみに、最後の２行をちょっと書き換えると、別のところに出てきます。例えば

```python
    from tortoisehg.hgqt.repoview import HgRepoView
    Itarate(HgRepoView)
```

だと、こっちに
![repoview-itarize](http://gyazo.com/aa6eb63255d06969ea2ea89d53be43c7.png)

###解説
といっても、解説するほどのことはやってないですが。

* 普通のエクステンションと違って、TortoiseHgに対してしか効果をもたないものなので、最初にTortoiseHgからの起動かどうかをチェックして、違ったら何もしないようにしています。

* PyQtのウィジェットは、paintEventメソッドをオーバーライドすることによって描画処理をカスタマイズすることができます。
  このエクステンションでは、TortoiseHgのファイル表示ウィジェットであるAnnotateViewのpaintEventの処理を置き換えて、画像のオーバーレイ処理を追加してる訳ですね。

* このサンプルではあまり使ってませんが、シグナルハンドラ(いわゆるイベントハンドラのことです)をうまく使えば、工夫次第でいろいろなカスタマイズが比較的安全に実現できると思います。

##まとめ
* エクステンションでTortoiseHgに対して色々悪さするのも、なかなか楽しいです。
* **ことりちゃんかわいいよことりちゃん**

それでは、Have a happy Mercurial life !

※ことりちゃんって誰？ って方は、こちらをどうぞ -> http://mzp.hatenablog.com/entry/2012/12/04/212043