TortoiseHgのカスタムツールバーを設定する方法

Koonies2012-06-11

TortoiseHg 2.4.1がリリースされました。で、1つ前のバージョン2.4で追加されたカスタムツールバーはどうやって設定するのかずっと気になっていましたがようやく分かったのでメモを兼ねて、書いておきます。

設定方法

[tortoisehg-tools]
# Execute a mercurial command. These _MUST_ start with "hg"
# Note that we can use any built-in TortoiseHg icon
update_to_tip.command = hg update tip
update_to_tip.tooltip = Update to tip
update_to_tip.icon = hg-update

http://7ps.ch/2012/05/19/tortoisehg-custom-toolba/

上記のように設定ファイルへ[tortoisehg-tools]という項目を追加し、それに続けてコマンドの設定を書きます。それで書けたらソフトを再起動します。
.command は必須で、.tooltip.icon はオプション(無くても可)ということです。ちなみに上記の引用元のサイトの記述例は

誤:update_to_tip.ico = hg-update
正:update_to_tip.icon = hg-update

という風にnが抜けているためアイコンが正しく設定できていないようです。
それとアイコンの設定にはicoファイルのフルパスの他、hg-updateのように内蔵アイコンも設定可能みたいです。これ以外にも下記のアイコンが使える模様。
(右上のスクリーンショットはコレの一覧)

application-exit
document-new
edit-cut
edit-find
go-down
go-jump
go-next
go-previous
go-up
hg-annotate
hg-archive
hg-clone
hg-commit
hg-grep
hg-incoming
hg-log
hg-merge
hg-outgoing
hg-pull
hg-push
hg-qguard
hg-qpop-all
hg-qpop
hg-qpush-all
hg-qpush
hg-rename
hg-status
hg-tag
hg-update
mail-forward
process-stop
tasktab-refresh
thg-console
thg-log-load-all
thg-mq
thg-qreorder
thg-reporegistry
thg-sync
view-filter
view-refresh

さいごに

どうやって設定するんだろ、と1ヶ月もモヤモヤしてたのがスッキリしました。
よかったら参考にしてください。それでは。

TortoiseHgでAmendを使うと2つ前のコミットをやり直せる

Koonies2012-05-11

TortoiseHgはバージョン管理ソフトMercurialWindows用フロントエンドで、つい先日にリリースされた最新版v2.4でAmendというコマンドが追加されました。Amendを使えば、お手軽に直前のコミットをやり直すことが可能になります。
以前のバージョンでもロールバックして、コミットし直せば同様のことが可能だった訳ですが、実際の操作手順としてはAmendを使った方がコミットのメッセージをコピーする必要もないため地味に便利です。リリースされてからまだ数日ですが、既に何度もお世話になってます。
さてタイトルにも書いた「2つ前」の修正の話。
これまでは「1つ前」の修正ならロールバック、それ以上前の修正をやりたい場合はMQなどのエクステンションを使う必要がありました。
最新版では「1つ前」の修正ならロールバックまたはAmendのどちらでもOKになりました。という事はロールバックして、更にAmendすれば「2つ前」を修正できる??という疑問が湧いてきますよね。それで試してみたら、本当に出来ました

手順

1. まずはロールバック
2. 直前の変更点をシェルフで待避
3. コミットの画面にする
4. コミットボタンの隣の▼をクリックし、"Amend current revision"をクリック*1
5. するとボタン表示がコミット→Amendに変わり、「2つ前」のコミットメッセージが表示されるので、修正してAmendボタンをクリック
6. シェルフで待避した変更点を戻し、あとは普通にコミットをやり直す。

さいごに

Mercurialでも同様のことが出来そうな気がしますね。試してないけど。それでは。

*1:ただしAmendはMQエクステンションが有効でないと使用できないみたいなので注意!

keyhacでCapsLockをモディファイアキー定義して無理矢理使う

keyhacの話。ある2chのスレを見ていたら、keyhacでCtrlとCapsLockキーを入れ替えようとしたが無理だった、というやりとりを見かけました。
CapsLockというキーは他のキーとは違いDownのイベントは発生するけど、Upは発生しない(ドライバがマスクしているらしい)ため、Ctrlと入れ替えた場合、押されっぱなしの状態になるとか。
試しにkeyhacで内部ログON*1にして、CtrlとCapsキーを押してみると

# Ctrlを押して離した場合

IN  : D-LCtrl
TRU : D-LCtrl
IN  : U-LCtrl
TRU : U-LCtrl
IN  : O-LCtrl

# CapsLockを押して離した場合

IN  : D-(240)
TRU : D-(240)

という風にCapsLockは「U-(240)」が出力されていません。だから入れ替えは無理、か。確かにこれは無理だなと思う反面、出来ないと言われると、何とかしたくなるのが人情。
で、色々考えた末、入れ替えは無理でもユーザー定義のモディファイアキー*2としてなら使えるかもと思い、書いてみました。

■ コード

# config.py

from keyhac import *


def configure(keymap):

    ## 関数実行時にモディファイアの状態をリセットするデコレータ
    def reset_modifier(func):
        import functools

        @functools.wraps(func)
        def _reset_modifier(*args, **kw):
            # モディファイアの状態を無理矢理リセット
            keymap.modifier = 0
            # 関数実行
            return func(*args, **kw)
        return _reset_modifier

    ## JobQueue/JobItem でサブスレッド処理にするデコレータ
    def job_queue(func):
        import functools

        @functools.wraps(func)
        def _job_queue(*args, **kw):

            num_items = JobQueue.defaultQueue().numItems()
            if num_items:   # 処理待ちアイテムがある場合は、その数を表示
                print u"JobQueue.defaultQueue().numItems() :", num_items

            def __job_queue_1(job_item):
                return func(*args, **kw)

            def __job_queue_2(job_item):
                # print "job_queue : ", func.__name__, args, kw
                pass

            job_item = JobItem(__job_queue_1, __job_queue_2)
            JobQueue.defaultQueue().enqueue(job_item)

        return _job_queue

    ## 一定時間経過後にモディファイアの状態をリセット
    @job_queue
    def auto_reset_modifier():
        import time
        WAIT_TIME = 0.5  # 秒
        time.sleep(WAIT_TIME)
        # モディファイアの状態を無理矢理リセット
        # if keymap.modifier: print keymap.modifier
        keymap.modifier = 0

    @reset_modifier
    def minimize():
        keymap.getTopLevelWindow().minimize()

    # どのウインドウにフォーカスがあっても効くキーマップ
    keymap_global = keymap.defineWindowKeymap()

    # ユーザモディファイアキーの定義:CapsLock(240) --> U1
    keymap.defineModifier("(240)", "U1")

    # 単体押しは一定時間後にモディファイアの状態をリセット
    # keymap_global["U1"] = auto_reset_modifier     # これだとキー表記エラー
    keymap_global["(240)"] = auto_reset_modifier

    # Caps + a : ウィンドウ最小化
    keymap_global["U1-a"] = minimize

関数定義で@reset_modifierのデコレータを使うと、関数の実行時にモディファイアキーの状態をリセットします。これによりCapsLockキーの押されっぱなしを防止しています。
これで一応モディファイアっぽく使えます。ただしタイトルに「無理矢理」と書いたように同時押しでなくても

CapsLockを押す → 離す → (0.5秒以内に)Aを押す

でも認識されてしまいます。また、CapsLockを押したままA、Aと2回押すと、2回目のAは同時押しではないAになってしまう等の欠点があります。

■ さいごに

日頃からCapsLockキーって押しやすい場所にあるのに全然使ってないなと思っている方はどうぞ。

*1:トレイアイコンを右クリック→内部ログONをクリック

*2:Ctrl・Shift・Alt・Winキーのような装飾キー

keyhacでテキストエディタのF1キーに任意のヘルプファイルのキーワード表示に関連づける

またまたkeyhacでのカスタマイズネタ。テキストエディタでプログラムを書いていると、そのプログラム言語のヘルプファイルが見たいってことがしばしばあります。そんなときに役立つ設定です。

■ 使い方

使い方はカンタン。テキストエディタ上で調べたい単語を選択しF1を押します。
そうすれば設定しておいたchm形式のヘルプファイルからキーワード検索し、その項目を表示します。

■ コード

chm_viewer.py

まずは↓の部分をchm_viewer.pyという名前保存し、extensionフォルダに置いてください。

# chm_viewer.py
# http://d.hatena.ne.jp/pipehead/20071121/1195597559 のCraftLaunchEx用コードをほぼそのまま利用

# Windows 2000/XP では Unicode 版 (HtmlHelpW) を使用し、2000/XP 以外では ANSI 版 (HtmlHelpA) を使用します。
def _IsWinNT5OrLater():
    import sys

    (major, platform) = sys.getwindowsversion()[0:4:3]
    # VER_PLATFORM_WIN32_NT: 2
    return ((platform == 2) and (major >= 5))

winNT5OrLater = _IsWinNT5OrLater()


def _T(string):
    if isinstance(string, str):
        string = unicode(string, 'utf-8')
    if not winNT5OrLater:
        return string.encode('mbcs')
    return string


import ctypes

class c_tchar_p(ctypes._SimpleCData):
    if winNT5OrLater:
        _type_ = 'Z' # c_wchar_p
    else:
        _type_ = 'z' # c_char_p


# WinUser.h
GetDesktopWindow = ctypes.windll.user32.GetDesktopWindow


# HtmlHelp.h
HH_DISPLAY_TOC    = 0x0001
HH_KEYWORD_LOOKUP = 0x000D
HH_CLOSE_ALL      = 0x0012


from ctypes.wintypes import BOOL

class HH_AKLINK(ctypes.Structure):
    _fields_ = [
        ('cbStruct',     ctypes.c_int), # sizeof this structure
        ('fReserved',    BOOL),         # must be FALSE (really!)
        ('pszKeywords',  c_tchar_p),    # semi-colon separated keywords
        ('pszUrl',       c_tchar_p),    # URL to jump to if no keywords found (may be NULL)
        ('pszMsgText',   c_tchar_p),    # Message text to display in MessageBox if pszUrl is NULL and no keyword match
        ('pszMsgTitle',  c_tchar_p),    # Message text to display in MessageBox if pszUrl is NULL and no keyword match
        ('pszWindow',    c_tchar_p),    # Window to display URL in
        ('fIndexOnFail', BOOL)          # Displays index if keyword lookup fails.
    ]


if winNT5OrLater:
    HtmlHelp = ctypes.windll.LoadLibrary('hhctrl.ocx').HtmlHelpW
else:
    HtmlHelp = ctypes.windll.LoadLibrary('hhctrl.ocx').HtmlHelpA


def HtmlHelpDisplayTOC(chmPath, data=None):
    u"""指定されたヘルプウィンドウでヘルプトピックを開く

    引数  : chmPath - 文字列 - コンパイル済みヘルプまたはコンパイル済みヘルプ
            ファイル中のトピック
    引数  : data - 数値 - コンパイル済みヘルプファイル中のトピックへのポインタ
    戻り値: ヘルプウィンドウのハンドルを返す"""
    return HtmlHelp(GetDesktopWindow(), _T(chmPath), HH_DISPLAY_TOC, data)

def HtmlHelpKeywordLookup(chmPath, kwd):
    u"""コンパイル済みヘルプファイルからキーワードを検索する

    引数  : chmPath - 文字列 - コンパイル済みヘルプファイル
    引数  : kwd - 文字列 -  検索するキーワード
            複数の項目はセミコロン `;' で区切る
    戻り値: ヘルプウィンドウのハンドルを返す"""
    aklnk = HH_AKLINK(ctypes.sizeof(HH_AKLINK), False, _T(kwd), None, None,
                      None, None, True)
    return HtmlHelp(GetDesktopWindow(), _T(chmPath), HH_KEYWORD_LOOKUP,
                    ctypes.byref(aklnk))

def HtmlHelpCloseAll():
    u"""呼び出しプログラムによって開かれたヘルプウィンドウをすべて閉じる

    戻り値: なし"""
    HtmlHelp(None, None, HH_CLOSE_ALL, 0)
・ config.py

で、↓ がconfig.pyの設定部分で、キーの割り当てとヘルプファイルのパス設定などを行います。
下記の例ではメモ帳F1を押すとPythonのヘルプファイル(ダウンロードファイル一覧 - Python Japanese Environment - OSDN)を表示します。

# config.py

from keyhac import *

def configure(keymap):

    ## 選択文字列をヘルプファイル(*.chm)から検索
    def select_word_help():
        def _select_word_help():
            # ヘルプファイルのフルパス
            chm_path = r"C:\Program Files\craftware\keyhac\Python26-DocJa1.chm"

            before = getClipboardText()

            # 選択文字列をコピー
            copy_key = "C-c"
            keymap.command_InputKey(copy_key)()

            maxcnt = 10
            while maxcnt > 0:
                maxcnt -= 1

                # クリップボードから文字列取り込み
                select_word = getClipboardText()
                if select_word != before:
                    break

                # クリップボードの反映待ち
                import time
                time.sleep(0.1)

            if select_word:
                import chm_viewer
                chm_viewer.HtmlHelpKeywordLookup(chm_path, select_word)

            # クリップボードを元の状態に戻して終わり
            setClipboardText(before)

        # ホットキー経由での関数呼び出し
        keymap.callHotKey(_select_word_help)

    # メモ帳のキーマップ
    keymap_notepad = keymap.defineWindowKeymap(exe_name=u"notepad.exe")

    keymap_notepad["F1"] = select_word_help

■ さいごに

この前コードを書いていたら、ふとVisual Studioみたいに普段使いのエディタでもヘルプ表示ができたらいいのになァと思い立ち、ちょっと調べてみたら、ほぼそのまま流用可能なこちらのエントリを見つけたので、設定してみました。よければどうぞ。

keyhacでWindows7のエアロスナップをマネマネ。

Windows7から導入されたエアロスナップ。要は画面左・右半分の最大化がマウス操作で簡単に実行できる機能です。ご存知ない方はこの30秒の動画を見てください。すごく便利な機能なんですが、僕の普段使いのPCはXP。当然エアロスナップなんてありません。
だったらkeyhacの出番です。本家にない上半分・下半分も出来ちゃいます。ついでに移動、リサイズも紹介しときます。

■ コード

from keyhac import *

## 関数を返す関数にするデコレータ
def ret_func(func):
    import functools

    @functools.wraps(func)
    def _ret_func(*args, **kw):
        @functools.wraps(func)
        def __ret_func():
            return func(*args, **kw)
        return __ret_func
    return _ret_func

def configure(keymap):

    ## 10pixel単位のウインドウのリサイズ(最大最小のときは何もしない)
    #
    @ret_func
    def resize_window(direction):
        if not keymap.getTopLevelWindow().isMaximized():
            i = "LRUD".find(direction)
            dx, dy = ((-10, 0),  # L
                      (+10, 0),  # R
                      (0, -10),  # U
                      (0, +10)   # D
                     )[i]

            wnd = keymap.getTopLevelWindow()
            rect = list(wnd.getRect())
            rect[2] += dx
            rect[3] += dy
            wnd.setRect(rect)

    ## 10pixel単位のウインドウの移動(最大最小のときは何もしない)
    #
    @ret_func
    def move_window(direction):
        if not keymap.getTopLevelWindow().isMaximized():
            i = "LRUD".find(direction)
            dx, dy = ((-10, 0),  # L
                      (+10, 0),  # R
                      (0, -10),  # U
                      (0, +10)   # D
                     )[i]

            # keyhac_keymap.py 定義の関数を利用
            keymap.command_MoveWindow(dx, dy)()

    ## ウィンドウを画面端まで移動(最大最小のときは何もしない)
    #
    @ret_func
    def move_window_monitoredge(direction):
        if not keymap.getTopLevelWindow().isMaximized():
            i = "LURD".find(direction)
            # keyhac_keymap.py 定義の関数を利用
            keymap.command_MoveWindow_MonitorEdge(i)()

    ## 画面半分の切り替え(Windows7のエアロスナップ風)
    #
    @ret_func
    def toggle_aero_snap(direction):
        wnd = keymap.getTopLevelWindow()
        if wnd.isMaximized():
            # 元に戻す
            wnd.restore()
        else:
            # 最大化
            wnd.maximize()

            # ウィンドウのサイズを取得
            rect = list(wnd.getRect())

            # ウィンドウの幅/高さの半分
            width_50 = (rect[0] + rect[2]) / 2
            height50 = (rect[1] + rect[3]) / 2

            # 入力された方向に応じて対応する座標を幅/高さの半分で上書き
            i = "RDLU".find(direction)
            rect[i] = (width_50,  # R
                       height50,  # D
                       width_50,  # L
                       height50   # U
                      )[i]

            # ウィンドウの新しいサイズを反映
            wnd.setRect(rect)

    # どのウインドウにフォーカスがあっても効くキーマップ
    keymap_global = keymap.defineWindowKeymap()

    ## ウィンドウ移動系
    for arrow_key in ("Left", "Right", "Up", "Down"):
        c = arrow_key[:1]

        # Win+←→↑↓ : 10pixel単位のウインドウの移動
        keymap_global["W-" + arrow_key] = move_window(c)
        # Win+Ctrl+←→↑↓ : 画面の端まで移動
        keymap_global["W-C-" + arrow_key] = move_window_monitoredge(c)
        # Win+Shift+←→↑↓ : 10pixel単位のウインドウのリサイズ
        keymap_global["W-S-" + arrow_key] = resize_window(c)
        # Win+Alt+←→↑↓ : Windows7のエアロスナップ風
        keymap_global["W-A-" + arrow_key] = toggle_aero_snap(c)

■ 軽〜く説明

Win+←→↑↓ 10pixel単位のウインドウの移動
Win+Ctrl+←→↑↓ 画面の端まで移動
Win+Shift+←→↑↓ 10pixel単位のウインドウのリサイズ
Win+Alt+←→↑↓ Windows7のエアロスナップ風

■ 参考リンク

keyhacで[半角/全角]キーの1回・2回押しでIME OFF・ONを切り替える

またまたkeyhacでのカスタマイズネタについて。IMEのON/OFFはいつまで経っても大して使い勝手が良くならない。どうゆうことかと言うと文字をタイプし始めてから「あ!半角のままだった……」と気づきIMEをONにして漢字入力する。またその逆も。
これは[半角/全角]キーがトグルスイッチになっているためで、いまどちらの状態にあるのかを把握せずにON/OFFできるようになれば大幅に打ち損じが減るんじゃないかと思いついた。つまり……

★ 現状

IME OFFの状態でキーを押すとIMEONONのときに押すとOFFになる。

★ 変更後

1回ポンっと押すとIMEをOFF、ダブルクリックのようにタタンっと2回連続押しだとIMEがONになる。
これをkeyhacで実現するのが、以下のスクリプトです。

■ コード

from keyhac import *

def configure(keymap):

    ## IMEを切り替える
    #
    #  @param flag      切り替えフラグ(True:IME ON / False:IME OFF)
    #
    def switch_ime(flag):

        # バルーンヘルプを表示する時間(ミリ秒)
        BALLOON_TIMEOUT_MSEC = 500

        # if not flag:
        if flag:
            ime_status = 1
            message = u"[あ]"
        else:
            ime_status = 0
            message = u"[_A]"

        # IMEのON/OFFをセット
        keymap.wnd.setImeStatus(ime_status)
        # IMEの状態をバルーンヘルプで表示
        keymap.popBalloon("ime_status", message, BALLOON_TIMEOUT_MSEC)

    ## キーの1回/2回押しで引数の関数コールを切り替える
    #
    #  @param func      コールする関数
    #
    #  引数の func は1回押しなら func(True)、2回連続押しなら func(False)
    #  でコールされる
    #
    def double_key(func, cache_t={}):

        # 2回連続押し判断の許容間隔(ミリ秒)
        TIMEOUT_MSEC = 500

        func_name = func.__name__

        # 前回時刻
        t0 = 0
        if func_name in cache_t:
            t0 = cache_t[func_name]
        # 現在時刻を保存
        import time
        cache_t[func_name] = time.clock()
        # 前回実行からの経過時間(ミリ秒)
        delta_t = (cache_t[func_name] - t0) * 1000

        # 関数コール
        if delta_t > TIMEOUT_MSEC:
            func(False)     # 1回押し
        else:
            func(True)      # 2回連続押し

    keymap_global = keymap.defineWindowKeymap()

    if 1:   # [半角/全角]
        keymap_global["U-(243)"] = lambda: double_key(switch_ime)  # 押す
        keymap_global["D-(243)"] = lambda: None                    # 離す
        keymap_global["U-(244)"] = lambda: double_key(switch_ime)  # 押す
        keymap_global["D-(244)"] = lambda: None                    # 離す

    if 0:   # [変換]
        keymap_global["S-(28)"] = "(28)"            # Shift+[変換]で再変換
        keymap_global["(28)"] = lambda: double_key(switch_ime)

    if 0:   # [無変換]
        keymap_global["(29)"] = lambda: double_key(switch_ime)

↑ [変換]・[無変換]キーがいい場合は、if 1 に変更してください。

■ さいごに

慣れれば便利だと思うので、よかったら使ってみてください!

■ 参考リンク

keyhacでカラーピッカー(画面上の任意の場所から色コード取得)

20110808232618現在はまっているkeyhac*1Win+Cを押すとマウスカーソル位置の色コードを取得し、クリップボードへコピーするようにしてみました。

■ コード

from keyhac import *

def configure(keymap):

    ## バルーンヘルプ(ツールチップ)を指定の位置に開く
    #
    # keyhac_keymap.py / popBalloon() を流用
    #
    def pop_balloon(name, text, timeout=None, pos=None):
        if pos == None:
            # オフセット量
            OFFSET_X = 0
            OFFSET_Y = -20

            # マウスカーソルの位置を取得
            pos_x, pos_y = pyauto.Input.getCursorPos()
            pos_x += OFFSET_X
            pos_y += OFFSET_Y
        else:
            pos_x, pos_y = pos

        keymap.balloon.setText(pos_x, pos_y, text)

        if keymap.balloon_timer:
            keymap.killTimer(keymap.balloon_timer)
            keymap.balloon_timer = None

        if timeout:
            def onTimerCloseBalloon():
                keymap.closeBalloon(name)
            keymap.balloon_timer = onTimerCloseBalloon
            keymap.setTimer(keymap.balloon_timer, timeout)

        keymap.balloon_name = name

    ## カーソル位置のRGB情報取得
    def color_picker():
        root = pyauto.Window.getDesktop()
        img = root.getImage()

        width, height = img.getSize()
        x, y = pyauto.Input.getCursorPos()

        if not (0 <= x < width and 0 <= y < height):
            rgb = u"範囲外"
        else:
            start = (width * y + x) * 3
            buf = img.getBuffer()[start:start + 3]
            r, g, b = [ord(c) for c in buf]

            if 0:  # 10進表記
                rgb = u"(%d,%d,%d)" % (r, g, b)
            else:  # 16進表記
                hex_24_bit = lambda val: u"#%06X" % val
                rgb = hex_24_bit((0x10000 * r) + (0x100 * g) + b)

        pop_balloon("color_picker", rgb, 1500)
        setClipboardText(rgb)
        print rgb

    keymap_global = keymap.defineWindowKeymap()

    # カーソル位置のRGB情報取得
    keymap_global["W-c"] = color_picker

■ さいごに

カーソル位置の色を取得するAutoHotkeyスクリプト - Hail2uに触発され書いてみました。このリンク先とは違って取得するカラーコードは16進表記になっています。もし10進表記の方がよければコードの中の「if 0:」を「if 1:」へ変更してからお使い下さい。

■ 参考リンク

*1:Autohotkeyみたいなフリーソフトで、Pythonコードで色んなソフトのショートカットキーを設定できる