pythonでキーバインド管理クラス

現在制作中のオリジナルエディタで使用しているキーバインド管理用クラス、だいたいまとまってきたので、
ご紹介します。


以下のクラス(クラス名はちょっと変かもしれませんけど。。)は、コンストラクタが呼び出された時に
引数bindDictで渡された情報を内部用の情報に展開して保持します。

簡単な例をあげると、

「control + aのキーの入力があったら、funcを呼び出す」

といった設定を保持しています。


このクラスから情報を取り出すためには、入力されたキーの情報を指定してget_key_handler()関数を
呼び出します。

たとえば、

handled, func = manager.get_key_handler('a')とか

handled, func = manager.get_key_handler('a')とか

といった感じで、キーの組み合わせを表す文字列を渡して、それに対応する関数を取得します。
詳しくは下記の関数コメントを参照してください。

あと、このクラスは複合キーにも対応させてあります。
どういうことかというと、emacsのようにC-x, C-cといった感じで複数のキーが押された場合に実行する関数も登録できます。

get_key_handler()関数は1度の呼び出しにつき、キーの組み合わせを表す文字列を1つしか指定できません。
その場合、例えば次のような場合には、

control + x → control + cの順でキーが押された場合に実行される関数を取得する

このようにします。

handled, func = manager.get_key_handler('c')
handled, func = manager.get_key_handler('x')

上記のように呼び出すと、戻り値はそれぞれ

(True, None)
(True, function)

となります。
つまり、キーが入力されるたびに、それらを文字列に変換してget_key_handler()を呼び出し、
関数が取得できた時点でそれを呼び出せばいいわけです。

# -*- coding: utf-8 -*-

import types

#
#
class KeymapMgr:
    '''
    キーバインドを管理するマネージャクラス。
    このクラスでは、キーの組み合わせと、ハンドラ関数を管理する。
    情報は以下の形式の辞書として管理される。    

    辞書:
        (1)単一キーの場合:
           キー: キーの組み合わせを表すタプル。
                 (Controlキーフラグ, Mod1キーフラグ, Mod2キーフラグ,
                 Mod3キーフラグ, Mod4キーフラグ, Mod5キーフラグ, キー文字列)
           値  : ハンドラ関数
        (2)複合キーの場合:
           入れ子の辞書となる。
           例)「<control>a + <control>b --> functionを実行」の場合
             {"<control>"a: {"<control>b": function}}
           キー: キーの組み合わせを表すタプル。(単一キーの場合と同じ)

    複合キー用の関数に対応するため、get_key_handler()が呼び出された際、
    関数の取得に至らなかった場合には、self.pendingに部分的にマッチングした
    辞書が記憶され、次にget_key_handler()が呼び出された時には、self.pendingの
    辞書がマッチング対象の辞書となる。
    self.pendingは、入力されたキーのマッチングが終わった際(関数が見付かった、
    見付からなかったにかかわらず)にクリアされる。
    '''
    MOD_KEY_CONTROL = '<control>'
    MOD_KEY_MOD1 = '<mod1>'
    MOD_KEY_MOD2 = '<mod2>'
    MOD_KEY_MOD3 = '<mod3>'
    MOD_KEY_MOD4 = '<mod4>'
    MOD_KEY_MOD5 = '<mod5>'
    
    def __init__(self, bindDict):
        '''
        初期化
        '''
        self.keyMap = {}
        self.pending = None
        
        if bindDict == None:
            return

        for wName, keyDict in bindDict.items():
            self.keyMap[wName] = {}
            for keyDef, funcInfo in keyDict.items():
                defType = type(keyDef)
                # キー定義部が文字列の場合の処理
                if defType == types.StringType:
                    bindKey = self.__str_to_bindkey(keyDef)
                    if bindKey == None:
                        continue
                    self.keyMap[wName][bindKey] = funcInfo
                    
                # キー定義部がシーケンスの場合の処理(複号キー対応)
                # 複合キーの場合には、シーケンスとして指定されることを
                # 期待している
                # 例えば('<control>a', '<control>b')
                elif defType == types.TupleType or defType == types.ListType:
                    parent = self.keyMap[wName]
                    lastIdx = len(keyDef) - 1
                    # 入れ子の辞書を作成する
                    for idx, kd in enumerate(keyDef):
                        bindKey = self.__str_to_bindkey(kd)
                        if bindKey == None:
                            continue
                        if idx == lastIdx:
                            parent[bindKey] = funcInfo
                        else:
                            if bindKey not in parent:
                                parent[bindKey] = {}                            
                            parent = parent[bindKey]

    def get_key_handler(self, wName, keyStr):
        '''
        キーハンドラ(関数)を取得
        複合キー用関数(複数のキーが入力されてはじめて実行される関数)
        が登録されている場合には、入力が確定するまでは戻り値として
        (True, None)つまり、「キーが登録されているが、関数はない」という
        戻り値を返す。入力が確定すると(True, func)を返す。

        wName:
            ウィジェット名文字列
        keyStr:
            キー文字列
            例)"<control><shift><alt>a"など
            下記の指定に関しては同じ結果を取得する。
            ・"<control><mod1>a"
            ・"<mod1><control>a"

            つまり、モディファイアキーの順序に影響されない。

        RETURN:
            タプル(キー入力がハンドリングされたかどうかのフラグ,
                   キーに割り当てられている関数)

            ハンドリングされたかどうかのフラグ
                True -> 該当するキーが登録されている
                False-> 該当するキーが登録されていない

            キー文字列に割り当てられている関数があればそれを返す。
            そうでない場合にはNone。
        '''
        if self.keyMap == None:
            return False, None
        if wName == None or len(wName) == 0:
            return False, None
        if keyStr == None or len(keyStr) == 0:
            return False, None

        # マッチング途中の辞書がある場合には、それをマッチング
        # 対象に。そうでない場合には、自身が保持しているトップレベル
        # の辞書をマッチング対象にする。
        keyDict = self.pending
        if keyDict == None:
            if wName not in self.keyMap:
                self.pending = None
                return False, None
            keyDict = self.keyMap[wName]

        # キー文字列を内部用のマッチングキーに変換
        mapKey = self.__str_to_bindkey(keyStr)
        if mapKey not in keyDict:
            self.pending = None
            return False, None

        dictVal = keyDict[mapKey]
        if type(dictVal) == types.DictType:
            # マッチしたキーの値が辞書だった場合、複合キー用の設定。
            # マッチング途中の辞書として覚えておく。            
            self.pending = dictVal
            return True, None
        else:
            # マッチング完了。
            # 処理関数を返す
            self.pending = None
            return True, dictVal[0]

    def __str_to_bindkey(self, keyStr):
        '''
        キー文字列を内部保持用の辞書キーへ変換する。
        
        keyStr:
            キー文字列
            例)"<control><mod1>a"など

        RETURN:
            タプル。
            (Controlキーフラグ, Mod1キーフラグ, ..., Mod5キーフラグ,  キー文字列)
            keyStr中に<Control>, <Mod1><Mod2><Mod3><Mod4><Mod5>が含まれる場合、
            該当する要素にTrueが設定される。
        '''
        if keyStr == None or len(keyStr) == 0:
            return

        # mapkeyはモディファイアキー用フラグリスト
        # [control, Mod1, Mod2, Mod3, Mod4, Mod5]のならびになっており、
        # 該当するモディファイアキーが押されている要素がTrueに設定される
        mapKey = [False, False, False, False, False, False]
        modStrs = [self.MOD_KEY_CONTROL,
                   self.MOD_KEY_MOD1,
                   self.MOD_KEY_MOD2,
                   self.MOD_KEY_MOD3,
                   self.MOD_KEY_MOD4,
                   self.MOD_KEY_MOD5]
        # フラグを設定
        for idx, mod in enumerate(modStrs):
            if mod in keyStr:
                mapKey[idx] = True
                keyStr = keyStr.replace(mod, '')

        # フラグリストの末尾にモディファイア以外のキーコードを追加し、
        # タプルとして返す。
        mapKey.append(keyStr)
        return tuple(mapKey)
#
#
if __name__ == '__main__':
    def on_control_a():
        print 'on_control_a'

    def on_control_shift_a():
        print 'on_control_shift_a'

    def on_control_shift_alt_a():
        print 'on_control_shift_alt_a'

    def on_x_c():
        print 'x_c'

    def on_x_k():
        print 'x_k'

    keyconfigs = {'aaaa': {'<control>a': (on_control_a, None),
                           '<control>A': (on_control_shift_a, None),
                           ('<control>x', '<control>c'):(on_x_c, None),
                           ('<control>x', '<control>k'): (on_x_k, None),}}
    
    mgr = KeymapMgr(keyconfigs)
    print mgr.keyMap

    print mgr.get_key_handler('aaaa', '<control>x')
    print mgr.get_key_handler('aaaa', '<control>c')
    print mgr.get_key_handler('aaaa', '<control>x')
    print mgr.get_key_handler('aaaa', '<control>k')

    print mgr.get_key_handler('aaaa', '<control>a')
    print mgr.get_key_handler('aaaa', '<control>A')
    print mgr.get_key_handler('aaaa', '<control><alt>A')
    print mgr.get_key_handler('aaaa', '<alt><control>A')