LibreOfficeのCSVコピペに挑戦した

はじめに

学校の演習の一環で、オープンソースLibreOfficeをいじってみました。 CSVでのコピペ機能を追加しようとしたんですが、残念ながら実装はできませんでしたm(__)m。 しかし、今後LibreOfficeを改良する人の手助けになるように、活動報告を書いておきます。

授業って何?

『大規模ソフトウェアを手探る』という演習で、詳しくはこちらに書いております。 大規模ソフトウェアを手探る 3週間位で、オープンソースの改良をしようという趣旨です。僕ら以外のチームでは、InkscapeFirefoxなどをいじっていました。

LibreOfficeとは?

LibreOfficeとはMicroSoft Officeオープンソース版のようなもので、文章作成や表計算などに使用されます。オープンソースといえど、機能的には有料のOfficeと遜色がありません。僕らは今回はLibreOfficeの中で、表計算を担当する「Calc」をいじりました。OfficeでExcelにあたるソフトです。

動機

グラフを簡単に書くためです。実験中にCalcでデータ集計をするのですが、その後実験レポートを書く際にgnuplotを使います。gnuplotでデータを扱うためには、CSVファイルに変換する必要があります。ただ、現在CSV変換の範囲はシート全体のみです。

f:id:K_Watanabe:20151126062831p:plain

例えばドラッグで選択範囲を指定してコピーし、適当なエディタにペーストすれば自動的にカンマが挿入されてCSVファイルが完成、とはなりません。 そのため、これまで実験レポートを書く際には、いちいちCalcのシートを新規作成し、そこにグラフにしたい範囲のデータのみコピペして、シートごとCSVファイルでエクスポートしていました。これだと不便なので、以下のようにコピーandペーストで簡単にCSVファイルを作成できる機能の追加に挑戦しました。

つまり目指したのは、選択範囲を指定して

f:id:K_Watanabe:20151126064255p:plain

貼り付けるとカンマ挿入でCSVファイルを生成する という機能の実現です。

f:id:K_Watanabe:20151126064249p:plain

LibreOfficeのビルド

1回目のビルド

まずはじめにLibreOfficeのビルドをしないといけないのですが、これに手こずりました。。。configureファイルを実行するのですが、パッケージが足りないとエラーが出て、apt-getで追加するという作業を何度も繰り返しました。ただ、もう少し簡単にする方法はあって、後で調べてみると、

$ apt-get build-dep package

とすれば、package(今回はlibreoffice)をビルドするために必要な関連パッケージを自動でインストールしてくれます。ぜひこれを使いましょう。 configureが終わって、makeに入ったら、1回のmakeに5時間くらいかかりました。。。make中のパソコンの排熱ヤバかったです。。。 これでビルド終わり、ようやくLibreOfficeを手探れる!と思いましたが以下のような問題が発生しました。

ブレークポイントが打てないといっても、一部のソースコードだけだったのであまり気にせず開発を進めていました。ただ、開発始まって5日目にペアの人とgdbの挙動が違うことに気づきました。mainでブレークポイント打った後に、stepで中に進んだ1行目から別々のソースコードになる始末。TAさんたちとも相談しましたが、結局makeやり直すことにしました。

ビルドのやり直し

正常にビルドできなかった原因を突き止めると、CXXFLAGSを設定していなかったのが原因らしいです。CXXFLAGSとは何かという話なのですが、C++コンパイラのオプション指定のことです。もともとconfigure時に以下のように指定していました。

$ CFLAGS="-O0 -g" ./configure --prefix=hoge install

prefixとはインストール先のこと。普通だと/usr/binとか/opt以下とかにインストールされるのですが、今回はソースコードを変えるので、通常のディレクトリとは違う方がよく、prefixでインストール先のディレクトリを指定する。 CFLAGSはCXXFLAGSと同様にCコンパイラのオプションです。"-O0 -g"ですが、-O0が最適化のオプションです。通常コンパイラは高速化のために、意味が変わらない程度にソースコードの順番を入れ替えてコンパイルするのですが、ブレークポイントで追うときにあっちこっち言ってしまうと困ります。そこで-O0とすれば、最適化をせずにコンパイルしてくれます。 この指定をCFLAGSだけでなく、CXXFLAGSに対してもする必要がありました。 またさらに調べてみると、CXXFLAGSとか一つ一つ指定するの面倒とういうことで、「--enable-dbgutil」というオプションをつければ、最適化とかオプションをデバッグ用にしてくれるらしいです。 他にも原因があったらしく、prefix指定で「~」は使えず、$HOMEを使わないといけないらしいです。 それで結局以下のようにconfigureからやり直しました。

/autogen.sh --with-lang="ja" --prefix=hoge --enable-dbgutil

成功でしたね。

開発の方針

ということで、ようやくLibreOfficeの手探りがスタートしました。まずは右クリックで呼び出されるポップアップメニューを変えようということになりました。現在はコピーだけなんですが、CSVコピー欄を加えることを目標にしました。開発の方針は以下の通り

  • find で全探索した後、grepでキーワードと思われる語句で検索をかけた。例えば今回はSID_COPY
  • 検索で引っかかった部分にブレークポイントを打つ
  • コピーする際にブレークポイントが引っかかれば、そこを調べる。

すると、以下の部分が引っかかりました。

CopyToClip

[/sc/source/ui/view/cellsh1.cxx:1261]

            //  Clipboard

    case SID_COPY:              // for graphs in DrawShell
        {
            WaitObject aWait( GetViewData()->GetDialogParent() );
            __pTabViewShell->CopyToClip( NULL, false, false, true );__
            rReq.Done();
            GetViewData()->SetPasteMode( (ScPasteFlags) (SC_PASTE_MODE | SC_PASTE_BORDER) );
            pTabViewShell->ShowCursor();
            pTabViewShell->UpdateCopySourceOverlay();
        }
        break;

多分CopyToClip関数が怪しいらしいので、これを見てみよう。 sc/source/ui/view/viewfun3.cxx:156

//      C O P Y

bool ScViewFunc::CopyToClip( ScDocument* pClipDoc, bool bCut, bool bApi, bool bIncludeObjects, bool bStopEdit )
{
    ScRange aRange;
    ScMarkType eMarkType = GetViewData().GetSimpleArea( aRange );
    ScMarkData& rMark = GetViewData().GetMarkData();
    bool bDone = false;

    if ( eMarkType == SC_MARK_SIMPLE || eMarkType == SC_MARK_SIMPLE_FILTERED )
    {
       ScRangeList aRangeList;
       aRangeList.Append( aRange );
       bDone = CopyToClip( pClipDoc, aRangeList, bCut, bApi, bIncludeObjects, bStopEdit, false );←最初これを呼び出す
    }
    else if (eMarkType == SC_MARK_MULTI)
    {
        ScRangeList aRangeList;
        rMark.MarkToSimple();
        rMark.FillRangeListWithMarks(&aRangeList, false);
        bDone = CopyToClip( pClipDoc, aRangeList, bCut, bApi, bIncludeObjects, bStopEdit, false );
    }
    else
    {
        if (!bApi)
            ErrorMessage(STR_NOMULTISELECT);
    }

    return bDone;
}

再帰的にCopyToClipが呼び出されており、ソースコードが結構長くて複雑だった。ざっと見て手がかりになりそうな箇所が見つからなかったので、一時解読を断念した。

タブを探す

クリップボードにコピーよりも狭い範囲でgrepすれば、真に見つけたいソースコードに辿り着ける確率が高くなるはず。今回は「タブをカンマに切り替え」れば良いので、コピーではなくタブ関連でgrepしてブレークポイントを打つという方針に変えた。よって、\tとかで全探索して、ブレークポイントを打ち、コピー時に呼び出される関数を調べると、以下が呼び出された。

class ScDelimiterTable
{
public:
        ScDelimiterTable( const OUString& rDelTab )
            :   theDelTab ( rDelTab ),
                __cSep      ( '\t' ),__
                 nCount    ( comphelper::string::getTokenCount(rDelTab, '\t') ),
                nIter     ( 0 )
            {}

    sal_uInt16  GetCode( const OUString& rDelimiter ) const;
    OUString  GetDelimiter( sal_Unicode nCode ) const;

    OUString  FirstDel()  { nIter = 0; return theDelTab.getToken( nIter, cSep ); }
    OUString  NextDel()   { nIter +=2; return theDelTab.getToken( nIter, cSep ); }

これcSep('\t')cSep(',')にすればいいと考え、変えてビルドし直したが、変化ない。タブで調べた他の箇所はコピー時に引っかからなかった。さてどうしよう。。。

クリップボードとは

初心に帰り、クリップボードの仕組みについて調べてみる。以下の性質があるらしい。

貼り付け形式を決めるのは、コピー側ではなく、貼り付けられるエディタ側だったのだ。 つまり、先ほどまでタブをカンマで変換することを目標にしていたが、これは無駄だったのだ。 よって方針転換を余儀なくされる。新しい方針は以下だ。

  • 各セルは多分配列になっている。
  • 配列ではなく、一つの要素に各セルをカンマ区切りで追加する。

よって、再びCopyToClip関数に戻ることになったのだ。

再びCopyToClip

CopyToClip関数は再帰的にCopyToClipを呼び出しているのだが、この構造を整理する。

1.ScViewFunc::CopyToClip 
2.ScViewFunc::CopyToClip
3.ScDocument::CopyToClip
4.ScTable::CopyToClip
5.ScTable::CopyToClip
6.ScColumn::CopyToClip

このように、計6回CopyToClipが呼び出される。なぜ6回も呼び出されるのかというと、シート全体->シート1枚->選択範囲のみ->選択範囲のある行と範囲を狭めているからだ。よって、一番最下層の番目のCopyToClipが怪しい。ソースコードは以下の通り。

void ScColumn::CopyToClip(
     sc::CopyToClipContext& rCxt, SCROW nRow1, SCROW nRow2, ScColumn& rColumn ) const
{
    pAttrArray->CopyArea( nRow1, nRow2, 0, *rColumn.pAttrArray,
                          rCxt.isKeepScenarioFlags() ? (SC_MF_ALL & ~SC_MF_SCENARIO) : SC_MF_ALL );
  
     {
         CopyToClipHandler aFunc(*this, rColumn, rCxt.getBlockPosition(rColumn.nTab, rColumn.nCol), rCxt.isCloneNotes());
         sc::ParseBlock(maCells.begin(), maCells, aFunc, nRow1, nRow2);
     }
    
     {
         __CopyTextAttrToClipHandler aFunc(rColumn.maCellTextAttrs);__
         sc::ParseBlock(maCellTextAttrs.begin(), maCellTextAttrs, aFunc, nRow1, nRow2);
     }
    
    rColumn.CellStorageModified();
}

CopyTextAttrToClipHandler aFunc(rColumn.maCellTextAttrs);など怪しそうで、調べてみたものの手がかりが見つからなかった。一番の手がかりはコピーした文字列をgdb中でprintしながら探すことだが、LibreOfficeは文字列を独自の文字列OUStringで格納していて外部から見れない。それ以外の手がかりは思いつかず、時間もないので諦めた。

まとめ

  • クリップボードの形式を決めるのは貼り付け側
  • ScColumn::CopyToClipが怪しい
  • 文字列はgdb上からは見れないので、これ以上の探索は別の視点が必要

感想

結局何の成果も得られなかったのは、とても悔しいです。多分後もう少しなのですが、徐々にcppやcファイルではなく、iniファイルなどテンプレートなどが増えて、挙動が分からなくなって方針がつかめなくなったのが痛かったです。今後は後に続くだれかに任せたいと思います。

役だったリンク