LauncherEX でサブモードを変更するときに入力エリアが空だったらサブモードを循環させる

以下の環境で動作確認しました。

LauncherEX は、デフォルトで Ctrl-. に Shortcut_ChangeSubMode() がバインドされています。
この関数は、コマンド入力エリアの文字列を OPTION_EX_SUBMODE にセットします。
これを、コマンド入力エリアが空のときは、サブモードのリストで現在のモードの次の位置にあるモードを、OPTION_EX_SUBMODE にセットするようにします。
現在のサブモードがリストの末尾だったり、リストにない場合は、リストの先頭にあるモードをセットします。現在のサブモードがリストにない場合は、現在のモードをリストの末尾に追加してからモードを変更します。
クラス LauncherModeEX のメソッド Shortcut_ChangeSubMode を置き換えます。
'_OPTION_EX_SUBMODE_LIST' というオプションを追加します。数値にするとカブる可能性があるので、文字列にしてあります。
このオプションには、サブモードのリストをセットします。リストには、サブモードのファイル名から拡張子を除いた名前をセットします。
デフォルトでは『CraftLaunchEx のあるディレクトリパス\extension\clmode_lex\clsubmode_*.py』のパターンにマッチするファイルがセットされています。

コード

config.py などに記述します。

## LauncherEX
import clmode_lex # `from clconst import *' より前に記述する


from clapi import *
from clconst import *


import cloption

cloption._set_option_func_table['_OPTION_EX_SUBMODE_LIST'] \
    = cloption._SetOption_Simple
cloption._get_option_func_table['_OPTION_EX_SUBMODE_LIST'] \
    = cloption._GetOption_Simple

import glob
import os.path

SetOption('_OPTION_EX_SUBMODE_LIST', [
    os.path.splitext(os.path.split(x)[1])[0] for x
    in glob.glob('%s*.py'
                 % os.path.join(GetAppDir(),
                                r'extension\clmode_lex\clsubmode_'))
])

def _LauncherModeEXShortcutChangeSubMode(self):
    u"""サブモードを変更します"""
    import clcore
    from clmode_lex import OPTION_EX_SUBMODE

    s = GetValue()
    if s:
        SetOption(OPTION_EX_SUBMODE, s)
        SetValue('')
    else:
        lst = GetOption('_OPTION_EX_SUBMODE_LIST')
        submode = GetOption(OPTION_EX_SUBMODE)
        try:
            idx = (lst.index(submode) + 1) % len(lst)
        except ValueError:
            lst.append(submode)
            idx = 0
        SetOption(OPTION_EX_SUBMODE, lst[idx])
    clcore.RaiseNextWindow()
clmode_lex.clLauncherEX.LauncherModeEX.Shortcut_ChangeSubMode \
    = _LauncherModeEXShortcutChangeSubMode

以下のコードは、この関数をテストするためにシャレで作ったサブモード『保冷所』です。
アメリカの TV シリーズ『CSI:マイアミ』の登場人物であるホレイショ・ケインの顔文字『(`●ω●´)』に、台詞を言わせます。
台詞は、ホレイショ名ゼリフTOP10 から引用しました。

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

u"""clModeLEXSubModeHoracio - 保冷所

This script is sub mode for LauncherEX.
Horacio keeps on speaking his lines while CraftLaunchEx is unfocused.
"""

version = '0.0.0.0'


import sys

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

del sys, major, platform # Not for export


import ctypes

class c_tchar(ctypes._SimpleCData):
    if winNT5OrLater:
        _type_ = 'u' # c_wchar
    else:
        _type_ = 'c' # c_char


from ctypes.wintypes import BYTE, LONG

# WinUser.h
WM_GETFONT = 0x0031

if winNT5OrLater:
    SendMessage = ctypes.windll.user32.SendMessageW
else:
    SendMessage = ctypes.windll.user32.SendMessageA
GetDlgItem = ctypes.windll.user32.GetDlgItem
GetSystemMetrics = ctypes.windll.user32.GetSystemMetrics
GetDC = ctypes.windll.user32.GetDC


# WinGDI.h
LF_FACESIZE = 32
LOGPIXELSY = 90

class LOGFONT(ctypes.Structure):
    _fields_ = [
        ('lfHeight',         LONG),
        ('lfWidth',          LONG),
        ('lfEscapement',     LONG),
        ('lfOrientation',    LONG),
        ('lfWeight',         LONG),
        ('lfItalic',         BYTE),
        ('lfUnderline',      BYTE),
        ('lfStrikeOut',      BYTE),
        ('lfCharSet',        BYTE),
        ('lfOutPrecision',   BYTE),
        ('lfClipPrecision',  BYTE),
        ('lfQuality',        BYTE),
        ('lfPitchAndFamily', BYTE),
        ('lfFaceName',       c_tchar*LF_FACESIZE)
    ]

GetDeviceCaps = ctypes.windll.gdi32.GetDeviceCaps
if winNT5OrLater:
    GetObject = ctypes.windll.gdi32.GetObjectW
else:
    GetObject = ctypes.windll.gdi32.GetObjectA


from clapi import *
from clconst import *


format = u'(`●ω●´)<%s'
oneLiners = [
    u'俺は決してお前を許さない。必ずお前を塀の中へブチ込んでやる。分かったか?',
    u'我々 CSI は決して…そう、決してあきらめない。',
    u'スピードル、お前のおかげだ。',
    u'もう行け、いいな? 元気でな…さあ。',
    u'それが俺の仕事だ。俺は悪を始末する。',
    u'お前の人生を滅茶苦茶にしてやってもいいんだぞ。',
    u'復讐は正義ではない、犯罪で正義はなし得ない。',
    u'必要ない。この現場が語ってくれる。',
    u'お前みたいな蛆虫を一生ムショにブチ込んでやること、それが俺の究極のスリルだ!',
    u'私を…敵にしない方がいい。'
]
interval = 200
atRandom = False
repeatCount = 1
charNum = 11
fontName = u'MS Pゴシック'
minWidth = 0
position = None # (x, y)
fgColor = None # (R, G, B)
bgColor = None # (R, G, B)


import clmode
import clcore
import random

class SubModeHoracio(clmode.BaseMode):

    def __init__(self, prevMode):
        clmode.BaseMode.__init__(self)
        self.__PrevMode = prevMode
        # identifier of the edit control: 1000
        self.__PrevFont = _GetFont(GetDlgItem(GetHandle(), 1000))
        self.__PrevMinWidth = GetOption(OPTION_EDIT_MIN_WIDTH)
        self.__PrevFGColor = GetOption(OPTION_FG_COLOR)
        self.__Format = format
        self.__OneLiners = oneLiners
        self.__OneLinersIndex = -1
        self.__Interval = interval
        self.__Random = atRandom
        self.__RepeatCount = repeatCount if repeatCount > 0 else 1
        self.__Counter = self.__RepeatCount
        self.__CharNum = charNum if charNum > 0 else 1
        self.__CharIndex = -self.__CharNum
        self.__MaxCharIndex = -self.__CharNum
        self.__FontName = fontName
        self.__MinWidth = minWidth
        self.__Position = position
        self.__FGColor = fgColor
        self.__BGColor = bgColor

    def OnGetControl(self):
        if self.__FontName:
            SetFont(self.__FontName, *self.__PrevFont[1:])
        self.__SetMinWidth()
        self.__SetPosition()
        if self.__FGColor:
            SetFgColor(*self.__FGColor)
        if self.__BGColor:
            SetBgColor(*self.__BGColor)
        SetStatusIndicator(u'保')
        AddTimerHandler(self.__Interval, self.OnTimer)
        self.OnTimer()

    def OnLoseControl(self):
        RemoveTimerHandler(self.OnTimer)
        SetValue('')
        SetFont(*self.__PrevFont)
        SetOption(OPTION_EDIT_MIN_WIDTH, self.__PrevMinWidth)

    def OnActivate(self, event):
        clmode.BaseMode.OnActivate(self, event)
        if event.active:
            SetPos(*GetOption(OPTION_ORIGINAL_POSITION))
            SetFgColor(*self.__PrevFGColor)
            PopMode()
            clmode.Top().OnActivate(event)

    def OnTimer(self):
        if not IsActive():
            self.__Speak()

    def __SetMinWidth(self):
        if self.__MinWidth:
            n = self.__MinWidth
        else:
            hwnd = GetHandle()
            hEdit = GetDlgItem(hwnd, 1000)
            lFont = _GetLogFont(hEdit)
            if lFont:
                (left, right) = GetWindowRect(hEdit)[0:3:2]
                n = (max([clcore.Edit_GetTextWidth(x) for x
                          in [self.__Format % x[:self.__CharNum] for x
                              in self.__OneLiners]])
                     + int(-lFont.lfHeight * 1.5)
                     + left + GetWindowRect(hwnd)[2] - right
                     + GetSystemMetrics(SM_CXEDGE) * 2)
            else:
                n = 0
        SetOption(OPTION_EDIT_MIN_WIDTH, n)

    def __SetPosition(self):
        if self.__Position:
            (x, y) = self.__Position
        else:
            (x, y) = clcore.GetSystemParametersInfo(SPI_GETWORKAREA)[0:4:3]
            (top, bottom) = GetWindowRect(GetHandle())[1:4:2]
            y += top - bottom
        SetPos(x, y)

    def __SetNextOneLinersIndex(self):
        if self.__Counter < self.__RepeatCount:
            self.__Counter += 1
        else:
            if self.__Random:
                try:
                    self.__OneLinersIndex = random.choice([
                        i for i in xrange(0, len(self.__OneLiners))
                        if i != self.__OneLinersIndex
                    ])
                except IndexError:
                    self.__OneLinersIndex = 0
            else:
                self.__OneLinersIndex = \
                    (self.__OneLinersIndex + 1) % len(self.__OneLiners)
            self.__Counter = 1

    def __Speak(self):
        if self.__CharIndex >= self.__MaxCharIndex:
            self.__SetNextOneLinersIndex()
            self.__CharIndex = -self.__CharNum
            self.__MaxCharIndex = len(self.__OneLiners[self.__OneLinersIndex])
        self.__CharIndex += 1
        if self.__CharIndex < 0:
            start = 0
        else:
            start = self.__CharIndex
        end = self.__CharIndex + self.__CharNum
        if self.__OneLiners[self.__OneLinersIndex][start:end].endswith(' '):
            end += 1
            self.__CharIndex += 1
        SetValue(self.__Format
                 % self.__OneLiners[self.__OneLinersIndex][start:end])


def OnSubmode():
    PushMode(SubModeHoracio(clmode.Top()))


from ctypes.wintypes import HGDIOBJ, SIZE

def _GetLogFont(hwnd):
    hFont = SendMessage(hwnd, WM_GETFONT, 0, 0)
    if not hFont:
        return
    lFont = LOGFONT()
    if GetObject(hFont, ctypes.sizeof(LOGFONT), ctypes.byref(lFont)):
        return lFont

def _GetFont(hwnd):
    lFont = _GetLogFont(hwnd)
    if lFont:
        faceName = lFont.lfFaceName
        if isinstance(faceName, str):
            faceName = unicode(string, 'mbcs')
        size = int(float(-lFont.lfHeight)
                   / GetDeviceCaps(GetDC(hwnd), LOGPIXELSY)
                   * 72)
        return (faceName, size, lFont.lfWeight, lFont.lfItalic)