LoginSignup
1
1

More than 5 years have passed since last update.

LibreOfficeCalcに関数候補表示機能を付けるまで 第6回 関数候補・説明表示機能を実装する

Last updated at Posted at 2015-11-25

前回のまとめ

でっかい進捗を産んだ。

キー入力について

偶然見つけたコマンド

ソースコードをぼーっと眺めていると、こんな関数を発見した。

/sc/source/ui/app/inputhdl.cxx
bool ScInputHandler::KeyInput( const KeyEvent& rKEvt, bool bStartEdit /* = false */ )
{
    if (!bOptLoaded)
    {
        bAutoComplete = SC_MOD()->GetAppOptions().GetAutoComplete();
        bOptLoaded = true;
    }

    vcl::KeyCode aCode = rKEvt.GetKeyCode();
    sal_uInt16 nModi  = aCode.GetModifier();
    bool bShift   = aCode.IsShift();
    bool bControl = aCode.IsMod1();
    bool bAlt     = aCode.IsMod2();
    sal_uInt16 nCode  = aCode.GetCode();
    sal_Unicode nChar = rKEvt.GetCharCode();

    if (bAlt && !bControl && nCode != KEY_RETURN)
        // Alt-Return and Alt-Ctrl-* are accepted. Everything else with ALT are not.
        return false;

    if (!bControl && nCode == KEY_TAB)
    {
        // Normal TAB moves the cursor right.
        EnterHandler();

        if (pActiveViewSh)
            pActiveViewSh->FindNextUnprot( bShift );
        return true;
    }

    bool bInputLine = ( eMode==SC_INPUT_TOP );

    bool bUsed = false;
    bool bSkip = false;
    bool bDoEnter = false;

    switch ( nCode )
    {
        case KEY_RETURN:
// (中略)
        case KEY_TAB:
            if (bControl && !bAlt)
            {
                if (pFormulaData && nTipVisible && miAutoPosFormula != pFormulaData->end())
                {
                    // Iterate
                    NextFormulaEntry( bShift );
                    bUsed = true;
                }
                else if (pColumnData && bUseTab && miAutoPosColumn != pColumnData->end())
                {
                    // Iterate through AutoInput entries
                    NextAutoEntry( bShift );
                    bUsed = true;
                }
            }
            break;
        case KEY_ESCAPE:
// (後略)

こ、これじゃん!!!まさにこれじゃん!!!
欲しかった「関数の候補を別の候補に切り替える機能」は、Ctrl+TABでできるらしい。いやいやそのコマンドわかりづらすぎるでしょ。

Ctrl+Tabを使えるようにする

これを使って求めている関数候補を一覧で表示するやつをつくる。とその前に、三度目のfindText()修正。いまのままだと関数はイテレータを無視して全候補を返しているので、「候補を次に進める」という操作ができない。そこで、「与えられたイテレータの次要素から始めて全検索し、最後まで行ったら最初に戻り、与えられたイテレータまで来たら終了する」ようにfindTextAll()関数を修正する。

/sc/source/ui/app/inputhdl.cxx
// added
ScTypedCaseStrSet::const_iterator findTextAll(
    const ScTypedCaseStrSet& rDataSet, ScTypedCaseStrSet::const_iterator itPos,
    const OUString& rStart, ::std::vector< OUString > &rResultVec, bool bBack)
{
    rResultVec.clear(); // clear contents

    int nCount = 0;
    ScTypedCaseStrSet::const_iterator retit = rDataSet.begin();
    if (bBack) // Backwards
    {
        ScTypedCaseStrSet::const_reverse_iterator it = rDataSet.rbegin(), itEnd = rDataSet.rend();
        if (itPos != rDataSet.end())
        {
            size_t nPos = std::distance(rDataSet.begin(), itPos);
            size_t nRPos = rDataSet.size() - 1 - nPos;
            std::advance(it, nRPos);
            itEnd = it;
            ++it;
        }

        for (; ; ++it) {
            if(it == itEnd)
                break;
            if(it == rDataSet.rend()) // go to the first if reach the end
                it = rDataSet.rbegin();

            const ScTypedStrData& rData = *it;
            if (rData.GetStringType() == ScTypedStrData::Value)
                // skip values
                continue;

            if (!ScGlobal::GetpTransliteration()->isMatch(rStart, rData.GetString()))
                // not a match
                continue;

            rResultVec.push_back(rData.GetString()); // set the match data
            if(nCount == 0)
                retit = it.base(); // convert the reverse iterator back to iterator.
            nCount ++;
            if(nCount >= findnumber)
                return retit;
        }
    }
    else // Forwards
    {
        ScTypedCaseStrSet::const_iterator it = rDataSet.begin(), itEnd = rDataSet.end();
        if (itPos != rDataSet.end())
        {
            it = itPos;
            itEnd = it;
            ++it;
        }

        for (; ; ++it)
        {
            if(it == itEnd)
                break;
            if(it == rDataSet.end()) // go to the first if reach the end
                it = rDataSet.begin();

            const ScTypedStrData& rData = *it;
            if (rData.GetStringType() == ScTypedStrData::Value)
                // skip values
                continue;

            if (!ScGlobal::GetpTransliteration()->isMatch(rStart, rData.GetString()))
                // not a match
                continue;

            rResultVec.push_back(rData.GetString()); // set the match data
            if(nCount == 0)
                retit = it; // remember first match iterator
            nCount ++;
            if(nCount >= findnumber)
                return retit;            
        }
    }

    if(nCount > 0) // at least one function has matched
        return retit;
    return rDataSet.end(); // no matching text found
}
// end added

さーどうかなー…?

Screenshot from 2015-11-09 14:38:31.png
この状態で、Ctrl+Tabを押すと、
Screenshot from 2015-11-09 14:38:47.png
この状態になる。
「IFERROR(),IFNA(), IMABS(), [次の関数]()」ってなってほしいんだけど…あれ?なんか違うかも!

どうやら、NextFormulaEntry()関数(Ctrl+Tabをおしたときに呼ばれる関数)内でも似たような処理をしており、その部分も書き換えなきゃいけないらしい。統一してよ〜。

/sc/source/ui/app/inputhdl.cxx
void ScInputHandler::NextFormulaEntry( bool bBack )
{
    EditView* pActiveView = pTopView ? pTopView : pTableView;
    if ( pActiveView && pFormulaData )
    {
// exchange
//        OUString aNew;
//        ScTypedCaseStrSet::const_iterator itNew = findText(*pFormulaData, miAutoPosFormula, aAutoSearch, aNew, bBack);
//        if (itNew != pFormulaData->end())
//        {
//            miAutoPosFormula = itNew;
//            if (aNew[aNew.getLength()-1] == cParenthesesReplacement)
//                aNew = aNew.copy( 0, aNew.getLength()-1) + "()";
//            ShowTip(aNew); // Display a quick help
//        }
// exchange for
        ::std::vector<OUString> aNewVec;
        ScTypedCaseStrSet::const_iterator itNew = findTextAll(*pFormulaData, miAutoPosFormula, aAutoSearch, aNewVec, bBack);
        if (itNew != pFormulaData->end())
        {
            miAutoPosFormula = itNew;
            OUString tipStr;
            ::std::vector<OUString>::iterator itStr = aNewVec.begin();
            int maxFindNumber = 4;
            for( ; itStr != aNewVec.end(); ++itStr ) {
                if(itStr != aNewVec.begin())
                    tipStr = tipStr.copy(0, tipStr.getLength()) + ", ";
                tipStr = tipStr.copy(0, tipStr.getLength()) + (*itStr).copy(0, (*itStr).getLength()-1); // tipStr += *itStr without last character
                if ((*itStr)[(*itStr).getLength()-1] == cParenthesesReplacement) {
                    tipStr = tipStr.copy(0, tipStr.getLength()) + "()";
                } else {
                    tipStr = tipStr.copy(0, (*itStr).getLength()) + (*itStr).copy((*itStr).getLength()-1, (*itStr).getLength());
                }
                if(--maxFindNumber <= 0)
                    break;
            }
            ShowTip( tipStr );
        }
// end exchange
    }

    // For Tab we always call HideCursor first
    if (pActiveView)
        pActiveView->ShowCursor();
}

で、やってみたけど、どうもいまいち。たまに3つしか表示されなくなるし(4つ候補があるはずなのに3つしか出ない時がある)、Ctrl+Shift+TABの逆戻りが効かない。gdbで原因を探すぞー。
よくわからないけど、一番後ろと一番最初をつなげる部分で不具合があるっぽい。
イテレータのbegin()関数は最初の要素を返すのだが、end()関数は最後の要素の次を返す(while(itr != end())とか書けるようにするため)。なので、一周回るときはend()とbegin()を同一視することになる。しかも、while(itr != end())と書くためには、一周回るときの最初と最後を一致させておかなければならない(わかりづらい!)。
がんばってるけど微妙にうまくいかない…。

/sc/source/ui/app/inputhdl.cxx
// added
ScTypedCaseStrSet::const_iterator findTextAll(
    const ScTypedCaseStrSet& rDataSet, ScTypedCaseStrSet::const_iterator itPos,
    const OUString& rStart, ::std::vector< OUString > &rResultVec, bool bBack)
{
    rResultVec.clear(); // clear contents

    int nCount = 0;
    ScTypedCaseStrSet::const_iterator retit = rDataSet.begin();
    if (bBack) // Backwards
    {
        ScTypedCaseStrSet::const_reverse_iterator it, itEnd;
        it = rDataSet.rbegin();
        size_t nPos = std::distance(rDataSet.begin(), itPos);
        size_t nRPos = rDataSet.size() - 1 - nPos;
        std::advance(it, nRPos);
        if(it == rDataSet.rend())
            it = rDataSet.rbegin();
        itEnd = it;
        bool isFirstTime = true;

        while(it != itEnd || isFirstTime) {
            ++it;
            if(it == rDataSet.rend()) // go to the first if reach the end
                it = rDataSet.rbegin();

            if(isFirstTime)
                isFirstTime = false;
            const ScTypedStrData& rData = *it;
            if (rData.GetStringType() == ScTypedStrData::Value)
                // skip values
                continue;

            if (!ScGlobal::GetpTransliteration()->isMatch(rStart, rData.GetString()))
                // not a match
                continue;

            rResultVec.push_back(rData.GetString()); // set the match data
            if(nCount == 0)
                retit = it.base(); // convert the reverse iterator back to iterator.
            ++nCount;
        }
    }
    else // Forwards
    {
        ScTypedCaseStrSet::const_iterator it, itEnd;
        it = itPos;
        if(it == rDataSet.end())
            it = rDataSet.begin();
        itEnd = it;
        bool isFirstTime = true;

        while(it != itEnd || isFirstTime) {
            ++it;
            if(it == rDataSet.end()) // go to the first if reach the end
                it = rDataSet.begin();

            if(isFirstTime)
                isFirstTime = false;
            const ScTypedStrData& rData = *it;
            if (rData.GetStringType() == ScTypedStrData::Value)
                // skip values
                continue;

            if (!ScGlobal::GetpTransliteration()->isMatch(rStart, rData.GetString()))
                // not a match
                continue;

            rResultVec.push_back(rData.GetString()); // set the match data
            if(nCount == 0)
                retit = it; // remember first match iterator
            ++nCount;
        }
    }

    if(nCount > 0) // at least one function has matched
        return retit;
    return rDataSet.end(); // no matching text found
}
// end added

イテレータの初期値はitPos。最終値もitPos。ループ内でインクリメントするたびにend()になってないか確かめる。うまくいきそうだけど、やっぱりCtrl+Shift+Tabが効かない…。

/sc/source/ui/app/inputhdl.cxx
       it = rDataSet.rbegin();
       size_t nPos = std::distance(rDataSet.begin(), itPos);
       size_t nRPos = rDataSet.size() - 1 - nPos;
       std::advance(it, nRPos);

症状だけ見ると、この部分がめっちゃ怪しいんだよなー。
たとえばrDataSetが{sum, sumif, sumifs}だったとして、itPosがsumifを指してたとする。rbegin()はsumifsを指す。distance()はThe number of increments needed to go from first to lastを返すので、この場合は1を返すはず。だからnPosは1、nRPosは1。で、itはadvance(it, 1)でsumifを指すようになる。
合ってるよね…?うーん。
じゃあ、itPosがrDataSet.end()を指してたとしたら。
さっきと同じく、itはsumifsを指す。nPosは3になる。なので、nRPos=-1。
-1!?原因これでは!?

advance()関数は、第二引数が負の時どうなるのか。
http://en.cppreference.com/w/cpp/iterator/advance

std::advance
Increments given iterator it by n elements.
If n is negative, the iterator is decremented. In this case, InputIt must meet the requirements ofBidirectionalIterator, otherwise the behavior is undefined.

デクリメント(--)さえ定義されていれば、普通に前に戻ってくれるらしい。でも今回の場合、itはrbegin()にあるから、前に戻ることはできない。ここでおかしなことになってたんだな。
というわけで、advance()に渡る値が負だったらitをrend()から動かすように修正(何度も書くが、ループしてるのでbegin()=end())。

/sc/source/ui/app/inputhdl.cxx
        ScTypedCaseStrSet::const_reverse_iterator it, itEnd;
        it = rDataSet.rbegin();
        size_t nPos = std::distance(rDataSet.begin(), itPos);
        size_t nRPos = rDataSet.size() - 1 - nPos;
        if(nRpos < 0)
            it = rDataSet.rend();
        std::advance(it, nRPos);
        if(it == rDataSet.rend())
            it = rDataSet.rbegin();
        itEnd = it;
        bool isFirstTime = true;

makeしてたら、こんなwarningが出た。

shell
/sc/source/ui/app/inputhdl.cxx:187:20: warning: comparison of unsigned expression < 0 is always false [-Wtype-limits]
         if(nRPos < 0)
                    ^

あ、unsigned!!!やっちまった!!!size_tはunsignedだった(0以上しかサポートしないクラスだった)のか!
ってことは、これまで何度もunsigned型に負の数を入れてオーバーフローを起こしてたのか…これはやばいな。
再び修正。

/sc/source/ui/app/inputhdl.cxx
        ScTypedCaseStrSet::const_reverse_iterator it, itEnd;
        if(itPos == rDataSet.end()) {
            it = rDataSet.rend();
            --it;
            itEnd = it;
        } else {
            it = rDataSet.rbegin();
            size_t nPos = std::distance(rDataSet.begin(), itPos);
            size_t nRPos = rDataSet.size() - 1 - nPos; // if itPos == rDataSet.end(), then nRPos = -1
            std::advance(it, nRPos);
            if(it == rDataSet.rend())
                it = rDataSet.rbegin();
            itEnd = it;
        }
        bool isFirstTime = true;

きちんと例外処理を行うようにした。これで大丈夫かな?
が、ダメ…! なんでや!

gdbで見てみた。まずは、 上記のit,itEndの初期化が終わったところ。

gdb
(gdb) p *itPos
$13 = (const ScTypedStrData &) @0x1c79e70: {maStrValue = {
    pData = 0x7fffa61d5e90}, mfValue = 0, 
  meStrType = ScTypedStrData::Standard, mbIsDate = false}
(gdb) p *it
$14 = (const ScTypedStrData &) @0x1c79e70: {maStrValue = {
    pData = 0x7fffa61d5e90}, mfValue = 0, 
  meStrType = ScTypedStrData::Standard, mbIsDate = false}

ちゃんと*it=*itPosになってる。
で、retit=itを実行した直後。

gdb
(gdb) p *it
$15 = (const ScTypedStrData &) @0x1c79ca0: {maStrValue = {
    pData = 0x7fffa61e7190}, mfValue = 0, 
  meStrType = ScTypedStrData::Standard, mbIsDate = false}

*itPosとは異なる値。で、ループを抜けた後。

gdb
(gdb) p *retit
$16 = (const ScTypedStrData &) @0x1c79e70: {maStrValue = {
    pData = 0x7fffa61d5e90}, mfValue = 0, 
  meStrType = ScTypedStrData::Standard, mbIsDate = false}

*retit=*itPos。どうやら、イテレータのコピーに失敗してるらしい…。深いコピーはしてくれないのか。
とか思いつつ該当箇所を見てみたら、こうなってた。

/sc/source/ui/app/inputhdl.cxx
            rResultVec.push_back(rData.GetString()); // set the match data
            if(nCount == 0)
                retit = it.base(); // convert the reverse iterator back to iterator.
            ++nCount;

it.base()、結局なにする関数かよくわかってなかったやつだ…。
こう書き換えてみる。

/sc/source/ui/app/inputhdl.cxx
            rResultVec.push_back(rData.GetString()); // set the match data
            if(nCount == 0) { // convert the reverse iterator back to iterator.
                // actually we want to do "retit = it;".
                retit = rDataSet.begin();
                size_t nRPos = std::distance(rDataSet.rbegin(), it);
                size_t nPos = rDataSet.size() - 1 - nRPos;
                std::advance(retit, nPos);
            }
            ++nCount;

これで実行してみると…。で、できたー!!!

関数の説明を出す

関数候補だけじゃなくて、せっかくならその関数の説明も出したい。

/sc/source/core/data/funcdesc.cxx
OUString ScFuncDesc::getDescription() const
{
    OUString sRet;
    if ( pFuncDesc )
        sRet = *pFuncDesc;
    return sRet;
}

これを呼べば、なんかうまくいきそう。

inputhdl.cxxに関数ShowDescriptionTip()を追加してみた。とりあえず、テスト的に、関数を表示しただけ。

/sc/source/ui/app/inputhdl.cxx
// added
void ScInputHandler::ShowDescriptionTip( const OUString& rFuncName )
{
    ShowTipBelow ( rFuncName );
}
// end added

で、これをUseFormulaData()で読み込む。

ShowDescriptionTip ( aNewVec[0] );

あとちゃんとヘッダファイルも書き換えておく。

Screenshot from 2015-11-25 14:26:55.png
よし、問題なし。

/sc/source/ui/app/inputhdl.cxx
// added
void ScInputHandler::ShowDescriptionTip( const OUString& rFuncName )
{
    FormulaHelper aHelper(ScGlobal::GetStarCalcFunctionMgr());
    sal_Int32 nLeftParentPos = lcl_MatchParenthesis( rFuncName, rFuncName.getLength()-1 );
    sal_Int32 nNextFStart = aHelper.GetFunctionStart( rFuncName, nLeftParentPos, true);
    const IFunctionDescription* ppFDesc;
    ::std::vector< OUString> aArgs;
    if( aHelper.GetNextFunc( rFuncName, false, nNextFStart, NULL, &ppFDesc, &aArgs ) )
    {
        if( !ppFDesc->getFunctionName().isEmpty() )
        {
            sal_Int32 nArgPos = aHelper.GetArgStart( rFuncName, nNextFStart, 0 );
            sal_uInt16 nArgs = static_cast<sal_uInt16>(ppFDesc->getParameterCount());
            OUString aFuncName( ppFDesc->getDescription() );
            ShowTipBelow ( aFuncName );
        }
    }                  
}
// end added

これで動くかな?と思ったが、結局うまくいかず。うーん。

GetNextFunc()の中身がこちら。

/formula/source/ui/dlg/FormulaHelper.cxx
bool FormulaHelper::GetNextFunc( const OUString&  rFormula,
                                     bool             bBack,
                                     sal_Int32&       rFStart,   // Input and output
                                     sal_Int32*       pFEnd,     // = NULL
                                     const IFunctionDescription**   ppFDesc,   // = NULL
                                     ::std::vector< OUString>*   pArgs )  const // = NULL
{
    sal_Int32  nOldStart = rFStart;
    OUString   aFname;

    rFStart = GetFunctionStart( rFormula, rFStart, bBack, ppFDesc ? &aFname : NULL );
    bool bFound  = ( rFStart != FUNC_NOTFOUND );

    if ( bFound )
    {
        if ( pFEnd )
            *pFEnd = GetFunctionEnd( rFormula, rFStart );

        if ( ppFDesc )
        {
            *ppFDesc = NULL;
            const OUString sTemp( aFname );
            const sal_uInt32 nCategoryCount = m_pFunctionManager->getCount();
            for(sal_uInt32 j= 0; j < nCategoryCount && !*ppFDesc; ++j)
            {
                const IFunctionCategory* pCategory = m_pFunctionManager->getCategory(j);
                const sal_uInt32 nCount = pCategory->getCount();
                for(sal_uInt32 i = 0 ; i < nCount; ++i)
                {
                    const IFunctionDescription* pCurrent = pCategory->getFunction(i);
                    if ( pCurrent->getFunctionName().equalsIgnoreAsciiCase(sTemp) )
                    {
                        *ppFDesc = pCurrent;
                        break;
                    }
                }// for(sal_uInt32 i = 0 ; i < nCount; ++i)
            }
            if ( *ppFDesc && pArgs )
            {
                GetArgStrings( *pArgs,rFormula, rFStart, static_cast<sal_uInt16>((*ppFDesc)->getParameterCount() ));
            }
            else
            {
                static OEmptyFunctionDescription s_aFunctionDescription;
                *ppFDesc = &s_aFunctionDescription;
            }
        }
    }
    else
        rFStart = nOldStart;

    return bFound;
}

GetFunctionStart()GetFunctionEnd()は、文字列から関数っぽい文字を探しだしてその場所を返す関数。こいつが曲者で、一文字目から関数っぽい文字だった場合0ではなく1を返してしまう上に、最後の文字が関数っぽい文字だった場合65535などという頭のおかしい値を返してくる。そこで、最初と最後に意味のない文字をつける(最初に=、最後に()を付けることでセル内の文字列っぽく仕立てあげる)。

/sc/source/ui/app/inputhdl.cxx
// added
void ScInputHandler::ShowDescTip( OUString funcName )
{
    FormulaHelper aHelper(ScGlobal::GetStarCalcFunctionMgr());
    sal_Int32 nNextFStart = 0;
    const IFunctionDescription* ppFDesc;
    ::std::vector< OUString> aArgs;
    OUString eqPlusFuncName = "=" + funcName.copy(0, funcName.getLength());
    if( aHelper.GetNextFunc( eqPlusFuncName, false, nNextFStart, NULL, &ppFDesc, &aArgs ) ) 
    {
        if( !ppFDesc->getFunctionName().isEmpty() )
        {
            OUString aFuncDesc( ppFDesc->getDescription());
            ShowTipBelow( aFuncDesc );
        }
    }
}
// end added
/sc/source/ui/app/inputhdl.cxx
void ScInputHandler::UseFormulaData()
{
// (中略)
                    OUString tipStr;
                    OUString funcNameStr;
                    OUString discFuncNameStr;
                    ::std::vector<OUString>::iterator itStr = aNewVec.begin();
                    int maxFindNumber = 4;
                    for( ; itStr != aNewVec.end(); ++itStr ) {
                        funcNameStr = (*itStr).copy(0, (*itStr).getLength()-1);
                        if ((*itStr)[(*itStr).getLength()-1] == cParenthesesReplacement) {
                            funcNameStr = funcNameStr.copy(0, funcNameStr.getLength()) + "()";
                        } else {
                            funcNameStr = funcNameStr.copy(0, funcNameStr.getLength()) + (*itStr).copy((*itStr).getLength()-1, (*itStr).getLength());
                        }
                        if(itStr != aNewVec.begin()) {
                            tipStr = tipStr.copy(0, tipStr.getLength()) + ", ";
                        } else {
                            discFuncNameStr = funcNameStr;
                        }
                        tipStr = tipStr.copy(0, tipStr.getLength()) + funcNameStr.copy(0, funcNameStr.getLength());
                        if(--maxFindNumber <= 0)
                            break;
                    }
                    ShowTip( tipStr );
                    ShowDescTip( discFuncNameStr );
                    aAutoSearch = aText;
// (中略)

さあ、実行してみると…
Screenshot from 2015-11-10 20:15:48.png
おっ!?
ここで、Ctrl+Tabを押して…
Screenshot from 2015-11-10 20:16:08.png

うまくできた!

バックスペースを押しても候補が消えないようにする

バックスペースを押すと選択肢が消えるのは非常に使いにくいので、修正する。

/sc/source/ui/app/inputhdl.cxx
bool ScInputHandler::KeyInput( const KeyEvent& rKEvt, bool bStartEdit /* = false */ )
{
// (中略)
}

この関数の一行目にブレークポイントを張り、バックスペースを押した時にどういう挙動をするか確かめてみる。

/sc/source/ui/app/inputhdl.cxx
    // Only execute cursor keys if already in EditMode
    // E.g. due to Shift-Ctrl-PageDn (not defined as an accelerator)
    bool bCursorKey = EditEngine::DoesKeyMoveCursor(rKEvt);
    bool bInsKey = ( nCode == KEY_INSERT && !nModi ); // Treat Insert like Cursorkeys
    if ( !bUsed && !bSkip && ( bDoEnter || EditEngine::DoesKeyChangeText(rKEvt) ||
                    ( eMode != SC_INPUT_NONE && ( bCursorKey || bInsKey ) ) ) )
    {
        HideTip();
        HideTipBelow();

        if (bSelIsRef)
        {
            RemoveSelection();
            bSelIsRef = false;
        }

このif文に入った。bUsedbSkipbDoEnterbCursorKeybInsKeyはいずれもfalse。つまりbackspace時はEditEngine::DoesKeyChangeText(rKEvt)のみがtrueらしい。
つまりはこのif文内でバックスペースかどうかをなんとかして判定して、バックスペースのときだけ

HideTip();
HideTipBelow();

が起動しないようにすれば万事解決。

/editeng/source/editeng/editeng.cxx
bool EditEngine::DoesKeyChangeText( const KeyEvent& rKeyEvent )
{
    bool bDoesChange = false;

    KeyFuncType eFunc = rKeyEvent.GetKeyCode().GetFunction();
    if ( eFunc != KeyFuncType::DONTKNOW )
    {
        switch ( eFunc )
        {
            case KeyFuncType::UNDO:
            case KeyFuncType::REDO:
            case KeyFuncType::CUT:
            case KeyFuncType::PASTE: bDoesChange = true;
            break;
            default:    // is then possibly edited below.
                        eFunc = KeyFuncType::DONTKNOW;
        }
    }
    if ( eFunc == KeyFuncType::DONTKNOW )
    {
        switch ( rKeyEvent.GetKeyCode().GetCode() )
        {
            case KEY_DELETE:
            case KEY_BACKSPACE: bDoesChange = true;
            break;
            case KEY_RETURN:
            case KEY_TAB:
            {
                if ( !rKeyEvent.GetKeyCode().IsMod1() && !rKeyEvent.GetKeyCode().IsMod2() )
                    bDoesChange = true;
            }
            break;
            default:
            {
                bDoesChange = IsSimpleCharInput( rKeyEvent );
            }
        }
    }
    return bDoesChange;
}

この部分が大事。

switch ( rKeyEvent.GetKeyCode().GetCode() )
        {
            case KEY_DELETE:
            case KEY_BACKSPACE: bDoesChange = true;

これを利用して、バックスペースかどうかを判定する。

/editeng/source/editeng/editeng.cxx
    // Only execute cursor keys if already in EditMode
    // E.g. due to Shift-Ctrl-PageDn (not defined as an accelerator)
    bool bCursorKey = EditEngine::DoesKeyMoveCursor(rKEvt);
    bool bInsKey = ( nCode == KEY_INSERT && !nModi ); // Treat Insert like Cursorkeys
    if ( !bUsed && !bSkip && ( bDoEnter || EditEngine::DoesKeyChangeText(rKEvt) ||
                    ( eMode != SC_INPUT_NONE && ( bCursorKey || bInsKey ) ) ) )
    {
// exchange
//        HideTip();
//        HideTipBelow();
// exchange to
        if( rKeyEvent.GetKeyCode().GetCode() != KEY_BACKSPACE ) {
            HideTip();
            HideTipBelow();
        }            
// end exchange
        if (bSelIsRef)
        {
            RemoveSelection();
            bSelIsRef = false;
        }

これで、バックスペースを押しても候補や説明は消えないはず。
→と、思ったけどダメだった。

/sc/source/ui/app/inputhdl.cxx
                // When the selection is changed manually or an opening parenthesis
                // is typed, stop overwriting parentheses
                if ( bUsed && nChar == '(' )
                    ResetAutoPar();

                if ( KEY_INSERT == nCode )
                {
                    SfxViewFrame* pViewFrm = SfxViewFrame::Current();
                    if (pViewFrm)
                        pViewFrm->GetBindings().Invalidate( SID_ATTR_INSERT );
                }
                if( bUsed && bFormulaMode && ( bCursorKey || bInsKey || nCode == KEY_DELETE || nCode == KEY_BACKSPACE ) )
                {
                    ShowTipCursor();
                }

KeyInput()関数の奥底でShowTipCursor()関数が呼び出されていて、その冒頭で

/sc/source/ui/app/inputhdl.cxx
void ScInputHandler::ShowTipCursor()
{
    HideTip();
    HideTipBelow();
    EditView* pActiveView = pTopView ? pTopView : pTableView;

このように、HideTip()が呼ばれている。
うーむ、なんでShowTipCursorを呼ぶ必要があるんだろう?試しにこれを消してみる。

/sc/source/ui/app/inputhdl.cxx
                // When the selection is changed manually or an opening parenthesis
                // is typed, stop overwriting parentheses
                if ( bUsed && nChar == '(' )
                    ResetAutoPar();

                if ( KEY_INSERT == nCode )
                {
                    SfxViewFrame* pViewFrm = SfxViewFrame::Current();
                    if (pViewFrm)
                        pViewFrm->GetBindings().Invalidate( SID_ATTR_INSERT );
                }
// exchange
//                if( bUsed && bFormulaMode && ( bCursorKey || bInsKey || nCode == KEY_DELETE || nCode == KEY_BACKSPACE ) )
//                {
//                    ShowTipCursor();
//                }
// exchange to
                if( bUsed && bFormulaMode && ( bCursorKey || bInsKey || nCode == KEY_DELETE ) )
                {
                    ShowTipCursor();
                }
// end exchange

実行すると…
Screenshot from 2015-11-12 13:59:00.png
Screenshot from 2015-11-12 13:59:58.png
お、消えない!!
Screenshot from 2015-11-12 14:00:08.png
あ、あれ?
もうifは予測できないはずなのに??
Screenshot from 2015-11-12 14:00:16.png
ああー…。
残念、中身を更新してくれないっぽい。

というわけで、UseFormulaData()関数を呼んで無理やり更新させてみる。

/sc/source/ui/app/inputhdl.cxx
                // When the selection is changed manually or an opening parenthesis
                // is typed, stop overwriting parentheses
                if ( bUsed && nChar == '(' )
                    ResetAutoPar();

                if ( KEY_INSERT == nCode )
                {
                    SfxViewFrame* pViewFrm = SfxViewFrame::Current();
                    if (pViewFrm)
                        pViewFrm->GetBindings().Invalidate( SID_ATTR_INSERT );
                }
// exchange
//                if( bUsed && bFormulaMode && ( bCursorKey || bInsKey || nCode == KEY_DELETE || nCode == KEY_BACKSPACE ) )
//                {
//                    ShowTipCursor();
//                }
// exchange to
                if( bUsed && bFormulaMode && ( bCursorKey || bInsKey || nCode == KEY_DELETE ) )
                {
                    ShowTipCursor();
                }
                if( bUsed && bFormulaMode && nCode == KEY_BACKSPACE )
                {
                     UseFormulaData();
                }
// end exchange

しかし、うまくいかない。
今度は、最初のHideTip()のコメントアウトを外してからUseFormulaData()を呼び出してみる。
Screenshot from 2015-11-12 15:33:56.png
↓Del
Screenshot from 2015-11-12 15:35:10.png
↓Del
Screenshot from 2015-11-12 15:35:17.png
↓Del
Screenshot from 2015-11-12 15:35:26.png
できたー!!

引数の説明を表示

GetArgmentDescription()などという関数があるので、せっかくだから使ってみる。

/sc/inc/funcdesc.hxx
    /**
      Returns description of parameter at given position

      @param _nPos
      Position of the parameter

      @return   OUString description of the parameter
    */
    virtual OUString getParameterDescription(sal_uInt32 _nPos) const SAL_OVERRIDE ;

これがその関数。検索を掛けてみたが、あまり使われてないみたい。

/sc/inc/funcdesc.hxx
void ScInputHandler::ShowArgumentsTip( const OUString& rParagraph, OUString& rSelText, const ESelection& rSel,
        bool bTryFirstSel )
{
(中略)
                if( !ppFDesc->getFunctionName().isEmpty() )
                {
                    sal_Int32 nArgPos = aHelper.GetArgStart( rSelText, nNextFStart, 0 );
                    sal_uInt16 nArgs = static_cast<sal_uInt16>(ppFDesc->getParameterCount());
                    OUString aFuncName( ppFDesc->getFunctionName() + "(");
                    OUString aNew;
                    ScTypedCaseStrSet::const_iterator it =
                        findText(*pFormulaDataPara, pFormulaDataPara->end(), aFuncName, aNew, false);
                    if (it != pFormulaDataPara->end())
                    {
                        bool bFlag = false;
                        sal_uInt16 nActive = 0;
                        for( sal_uInt16 i=0; i < nArgs; i++ )
                        {
                            sal_Int32 nLength = aArgs[i].getLength();
                            if( nArgPos <= rSelText.getLength()-1 )
                            {
                                nActive = i+1;
                                bFlag = true;
                            }
                            nArgPos+=nLength+1;
                        }
                        if( bFlag )
                        {
// (中略)
                            if (nStartPosition > 0)
                            {
                                OUStringBuffer aBuf;
                                aBuf.append(aNew.copy(0, nStartPosition));
                                aBuf.append(static_cast<sal_Unicode>(0x25BA));
                                aBuf.append(aNew.copy(nStartPosition));
                                aNew = aBuf.makeStringAndClear();
// exchange
//                                ShowTipBelow( aNew );
// exchange to
                                ShowTip( aNew );
                                ShowTipBelow( ppFDesc->getParameterDescription(nActive-1) );
// end exchange
                                bFound = true;
                            }
                        }
                        else
                        {
// exchange
//                            ShowTipBelow( aNew );
// exchange to
                            ShowTip( aNew );
                            ShowTipBelow( ppFDesc->getParameterDescription(nActive-1) );
// end exchange
                            bFound = true;
                        }

これでうまくいった。
Screenshot from 2015-11-25 14:46:03.png

今回のまとめ

inputhdr.cxxに自分のやりたかったことが全て入っていたので、簡単に実装することができた。
結局、

  • 関数候補をたくさん表示
  • 関数の説明を表示
  • 関数の引数の説明を表示
  • 上記のツールチップがバックスペースを押しても消えないように改良

を実装した。

次回予告

せっかく作ったので、LibreOfficeにコミットしてreviewを受けてみる。

リンク

LibreOfficeCalcに関数候補表示機能を付けるまで

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1