Libre Office Calcに変更を加えようと格闘した話

はじめに

大学で「大規模ソフトウェアを手探る」という授業がありました。

ソースコードが何十万行もあるような実用的なソフトウェアをいじってみることで、普段やっている演習レベルのプログラミングとのギャップを少しでも埋めてやろう、という趣旨の授業です。

僕のグループではLibre Office Calcを手探ってみたので、その格闘の過程(と少しでも新たに得た知識)をここで報告します。(結局明確な変更は施せませんでしたが…)

 

Libre Office Calcとは

 Libre Office Calcとは、様々なOSで利用できる、オープンソースのオフィススイート(Microsoft Officeみたいなやつ)です。Windows以外にも、MacLinux上でも使えます。Calcはその中の表計算ソフトで、要はExcelに該当するものです。

 

動機・やろうとしたこと

実験のレポートを書くときに、グラフを書くためにgnuplotをよく使います。このときにcsv(カンマ区切り)形式のファイルを使いたいわけです。

現行のCalcでは、確かにcsv形式のファイルを出力することはできるのですが、あくまでページ単位でしか出力できません。これは不便です。必要なデータの書かれている範囲を選択して、それをコピー&ペーストでcsv形式に出力できたらとても便利です。効率的なレポート作成に貢献できるはずです。ちなみに現行のCalcでコピペを行うと、タブ区切りになってしまいます。

そういうわけで、僕のグループではCalcにcsvコピペ機能を追加することを目標とすることになりました。(そのときにはこんなに大変な苦労をするとは思いもせず…)

Calcでは、範囲を選択して右クリックするとCopyとかCutとかPasteとかの項目の入ったポップアップメニューが表示されますので、そこにCSV-Copyという項目を追加することを目指します。

 

ビルド

まずはダウンロード。

Libre Officeの公式ホームページ

https://ja.libreoffice.org/download/libreoffice-fresh/

から、バージョン5.0.2.2のソースコードlibreoffice-5.0.2.2.tar.xzをダウンロードしました。解凍すると、大量のファイルとディレクトリが現れます。

 

次にコンパイル

configure→make→make install の順で行います。

configureのコマンド

$ CFLAGS="-O0 -g" CXXFLAGS="-O0 -g" ./configure --prefix="インストール先のパス名”

calcのソースコードにはcだけでなくc++も使われているので、c++に関するオプションも指定するのを忘れないように。でないとmakeやり直しになります。

ところで、いきなりconfigureしても通りません。apt-cache search と apt-get installを地道に繰り返してもキリがありませんので、

$ sudo apt-get build-dep libreoffice

 で、必要なパッケージを自動的にインストールしましょう。

makeには4、5時間かかります。

 

とりあえず起動

 とりあえずgdb上で動かしてみます。といってもファイルが多すぎてどれを実行すればいいのか迷いました。/core/lib/libreoffice/program 内にたくさんのそれらしきファイルがあります。僕たちははじめ、oosplashを実行してみたのですが、これは起動してウィンドウを開くだけのダミーのファイルっぽいです。これをいじっても全然先に進めません。

結局、正しいのは/core/lib/libreoffice/program/soffice.bin のようです。mainにブレークポイントを打ってrunすると、main.cというファイルのSAL_IMPLEMENT_MAINという関数の冒頭で止まります。1回nextすると、Libre Officeが起動し、ウィンドウが表示されます。ちなみに、runするときに何もオプションを付けないと、Libre Officeのどのソフトを使うか選ぶウィンドウが表示されます。そうでなくて、run --calcとすると、始めからCalcのウィンドウが開きます。

 

探索開始~ひたすらgrep~

 さて、起動は出来ましたが、CalcのようなGUIを用いたソフトではイベントループが起こっているので、gdbのnextとstepで普通にステップ実行していてもあまり意味がありません。

なので、変えたいと思っている箇所に関係のありそうな文字列をgrepで検索し、その中から当たりをつけて、そこにブレークポイントを打って探っていくという方針を採ることにしました。例えば、右クリックで表示されるポップアップメニューの項目名(CopyとかPasteとか)ですね。また、Libre OfficeはGUIツールキットとしてgtkというものを用いているようなので、gtkについて調べて、関係のありそうなgtkの関数名でもgrepをかけます。

ただ、このgrepの作業もかなり時間がかかります。少しでも検索の効率を良くするために、検索対象のファイルを*.cと*.cxxと*.hに絞りましょう。

*.cと*.cxxと*.hの中から文字列hogeを含むものを検索したければ、

$ find . -regextype posix-egrep -regex ".*\.(cxx|c|h)" | xargs grep hoge

とします。つまり、findコマンドで、名前に*.cまたは*.hまたは*.cxxを含むファイルをまず検索し、その結果をパイプでgrepに渡すということです。

 また、どうやら/coreの直下にあるscというディレクトリがcalcに関連するディレクトリのようなので、このディレクトリ内でgrepすれば十分なはずです。

と、ここまでgrepについて書いてきましたが、実はOpenGrokという非常に便利なものがあります(http://opengrok.libreoffice.org)。これは検索機能付きのソースコードブラウザで、ブラウザ上でソースコードを見たり、grepのように文字列を検索することもできます。Libre Officeのような巨大なソースコードに対して検索をかけるときは、こちらの方がずっと効率がいいそうです。(僕たちはしばらくしてから知りました…)

さて、gtkについていろいろ調べまして、button_press_eventだとかgtk_menu_popupだとかgtk_menu_item_newだとか、右クリックやポップアップメニューに関連するらしい関数名でgrepをかけてみたのですが、先ほどの方法である程度検索を効率化してもまだまだ大量に出てきたり、あるいはびっくりするほど出てくる数が少なかったりして、で、実際にブレークポイントを打って分け入ってみてもあまり有効な手がかりは得られませんでした…。結局この後方針転換をして、ある程度までは変更したい部分に迫れた感があったのですが、その周辺ではこの時検索していたような文字列は一切見当たりませんでした。今思うとgtkは、GUI、つまり画面上に何かを表示する機能に関わるものなので、僕たちがコードをいじりたかった部分と比べると表層の部分に近すぎたのかもしれません。単純にgtkに関する調べ学習が足りなかっただけかもしれませんが。

 

ブレークポイントを打ちまくる

というわけで、grepだけやっていても埒が明かなそうなので、別のアプローチをしてみることにしました。

先程も書いたように、Libre Officeはイベントループしています。なので、ある関数にブレークポイントを打っておいて、その上でその関数が呼び出されるような動作を(GUIの画面上で)行えば、そのブレークポイントが発動してそこで止まります。

そこで、1.まず大量のブレークポイントを打ち、2.次にGUI上でいろいろ動かしてみて様子を見る、という方針を採ることにしました。たくさん打ったブレークポイントの中に、右クリックした時にヒットするものがあれば、それが右クリック時に呼び出される関数だということです。

とはいえ、ここでブレークポイントを張るための当たりをつけるためにはgrepをやらないといけないんですけどね。

ちなみに、runしてまずSAL_IMPLEMENT_MAINで止まった状態でブレークポイントを打とうとすると、

No source file named "ブレークポイントを張ろうとしたファイル名".
Make breakpoint pending on future shared library load? (y or [n])

という文が表示されることがありますが、とりあえず気にせずyとして大丈夫(なはず)です。このようにしてブレークポイントを打っても、該当の動作が行われればちゃんと発動して止まります。

さて、grepで当たりをつけた/core/sc/source/ui/view/gridwin.cxx内に大量のブレークポイントを張ると、マウス移動で発動するもの、コマンド入力で呼び出されるもの、などいろいろ見つかり、結局、

Breakpoint 26, ScGridWindow::SelectForContextMenu (this=0x1909970,    rPosPixel=..., nCellX=4, nCellY=7)   
at /home/denjo/Downloads/libreoffice-5.0.2.2/sc/source/ui/view/gridwin.cxx:3247

 というブレークポイントが右クリック時に発動することが分かりました。

ここからしばらくnextで進んでいくと、同じファイル内の

SfxDispatcher::ExecutePopup( 0, this, &aMenuPos );

という関数に行き着きます。怪しげです。

さらにさらにnextとstepを繰り返し、Excuteという名のつく関数を見つけるたびに中に入り込んでいきますと、dispatch.cxxだとかmnumgr.cxxというファイルを経てmenu.cxxというファイル内の、

sal_uInt16 PopupMenu::Execute(vcl::Window* pExecWindow, const Point& rPopupPos)
{
    return Execute( pExecWindow, Rectangle( rPopupPos, rPopupPos ), PopupMenuFlags::ExecuteDown );
}

という部分に行き着きました。近づいている感じは確かにあるのですが、このまま更に入って行くと、どうやらポップアップメニューの”表示”に関わる部分にどんどん入っていってしまうようでした。なので、”メニュー項目を追加する”という僕達の変更の目標を考えると、このまま更に分け入る意味はあまりなさそうです。

そういう方針を考えて、OpenGrok(http://opengrok.libreoffice.org)でSID_PASTE_ONLY_VALUEを調べたところ、/core/sc/source/ui/src/popup.srcというファイルが出てきまして、見てみるとここでポップアップメニューの各項目について定義しているようなのです。*.cでも*.hでも*.cxxでもなくて、*.srcなんですね。。さっきまでのgrep検索では引っかからないわけです。そういうことを考えても、やはり大規模なソースコードに対して検索をかけるときにはgrepよりもOpenGrokの方が適しているんじゃないかと思います。
この/core/sc/source/ui/src/popup.srcというファイルの中には、

  MenuItem
  {
       Identifier = SID_CUT ;
       HelpId = CMD_SID_CUT ;
       Text [ en-US ] = "Cu~t" ;
  };

 のような塊がいくつも羅列されているところがありまして、ここでポップアップメニューの各項目を定義していると思われます。そこで試しに、

  MenuItem
  {
       Identifier = SID_COPYCSV ;
       HelpId = CMD_SID_COPYCSV ;
       Text [ en-US ] = "~CopyCSV" ;
  };

 という宣言を追加して、ビルドしてみたのですが、特にポップアップメニューの項目が増えたりはしませんでした…。

まあ、宣言を追加するだけでなくて、それを表示する処理に受け渡すところにも変更を加えなくてはいけないんでしょうね…。というか、考えてみると、このままでは最終的にはCOPYCSVが具体的にどういう処理をするのかを一から書かなくてはいけなくて、それは結構大変そうです。

それで、今までは「ポップアップメニューにCOPYCSVの項目を新たに追加する」ことを目標にして進めてきましたが、「既存のCOPYの内容をいじってタブ区切りをカンマ区切りに変更する」事のほうが簡単にできそうです。というか、前者は闇が深そうです…。

そういうわけで、ここに来て目標を後者に変更することになりました。

 

方針転換~既存のCOPYをいじる~

さてさて、タブ区切りをカンマ区切りに変更したいわけなので、「タブで区切るよ」と書いてあるのがどこなのか、をまずは探す必要があります。

先ほど見たpopup.srcの中で、SID_COPYという識別子で処理をポップアップメニューの側に引き渡しているようなので、このSID_COPYで検索をかけて当たりをつけ、またブレークポイントを大量に打ちます。すると、コピーするときに以下のブレークポイントが発動することが分かりました。

Breakpoint 3, ScCellShell::ExecuteEdit (this=0x18960d0, rReq=...)
   at /home/denjo/Downloads/libreoffice-5.0.2.2/sc/source/ui/view/cellsh1.cxx:1242

1回nextすると、CopyToClipという名前のいかにもな関数があるので、中に入ってみます。

「ひとつひとつのセルの内容の集合→ひとつなぎの文」という変換が行われている部分がこのCopyToClipの中にあるはず、という助言を受け、まずクリップボードについて調べて見ると、

1.クリップボードとは、複数のアプリケーションからアクセスが可能な、一種の共有メモリである。

2.あるアプリケーションからデータがコピーされるとき、複数の形式でコピーが行われる。例えばWordでクリップボードへのコピーを行うと、テキストのみのデータや、文字修飾に関する情報など複数の形式でコピーが行われる。そしてペースト時に、ペーストが行われる先のアプリケーションによって、それらの複数の形式を使い分ける。

( http://www.atmarkit.co.jp/fwin2k/win2ktips/103clipbook/103clipbook.html より)

ということが分かりました。これを踏まえて、CopyToClipの中を進んでいくと、ScTransferObjというクラスが見つかりました。これは、Sc、つまりCalcのデータ形式を、何らかのObjectに変換しているのではないか、、、と考えまして、このScTransferObjのメンバ関数を見てみると、

class ScTransferObj : public TransferableHelper
{
(中略)
ScTransferObj( ScDocument* pClipDoc, const TransferableObjectDescriptor& rDesc );
virtual ~ScTransferObj();
virtual void AddSupportedFormats() SAL_OVERRIDE;
virtual bool GetData( const css::datatransfer::DataFlavor& rFlavor, const OUString& rDestDoc ) SAL_OVERRIDE;
virtual bool WriteObject( tools::SvRef<SotStorageStream>& rxOStm, void* pUserObject, SotClipboardFormatId nUserObjectId,
const ::com::sun::star::datatransfer::DataFlavor& rFlavor ) SAL_OVERRIDE;
virtual void ObjectReleased() SAL_OVERRIDE;
virtual void DragFinished( sal_Int8 nDropAction ) SAL_OVERRIDE;
ScDocument* GetDocument() { return pDoc; } // owned by ScTransferObj
(後略)

 となっています。

この中で怪しげな、WriteObjectにブレークポイントを張ってみますが、これはコピー時には発動しませんでした。他にもいくつかのメンバ関数ブレークポイントを張ってみましたが、どれもコピー時には呼ばれませんでした。

CopyToClipの中で、そのような形式の変換を行っていそうなところは他に見当たらなかったのですが…。

 この後もいろいろ手探ってはみたのですが、核心の部分にたどり着くことはできず、結局、明確な変更は施せませんでした…。

 

クリップボードの仕組みについて

ちなみに、最後に先生から、クリップボードの仕組みについて考えてみるようにと助言をいただきました。いったいどこまでがコピーする側の仕事なのか、もしかしたらCalcでコピーを行った時点では、クリップボード上にはただの配列としてコピーされているに過ぎず、ペーストを行う側で適当な貼り付け形式への変換を行っているとしたら…。そうだとしたら、そもそも「タブで区切るよ」という宣言はそもそもCalcのソースコード上にはないのかも知れません。

というわけで、再びクリップボードについていろいろ調べてみたのですが、どうやらただの配列としてコピーしているのではなさそうです。というか、よく考えてみたら、さっき引用させてもらったwebページの一節にそれに関連することが書かれてたんですね。

再び引用↓

2.あるアプリケーションからデータがコピーされるとき、複数の形式でコピーが行われる。例えばWordでクリップボードへのコピーを行うと、テキストのみのデータや、文字修飾に関する情報など複数の形式でコピーが行われる。そしてペースト時に、ペーストが行われる先のアプリケーションによって、それらの複数の形式を使い分ける。

( http://www.atmarkit.co.jp/fwin2k/win2ktips/103clipbook/103clipbook.html より)

つまり、どうやらペーストする側では、複数の形式でコピーされたデータの中から、「一番情報損失の少ない形式のものを選んで取り出し」ているだけのようなんですね。

調べてみると、今クリップボードに書かれているデータのフォーマット一覧とそれぞれの中身を見られるソフトがあるようでして、

試しに「クリップ見え窓」(http://www.officedaytime.com/clipmm/) というソフトを使って、Libre Office Calcからコピーを行ったクリップボードの中身を見てみると、

"DataObject"
"Star Embed Source (XML)"
"Star Object Descriptor (XML)"
"GDIMetaFile"
CF_ENHMETAFILE
CF_METAFILEPICT
"PNG"
CF_DIB
"Windows Bitmap"
"HTML (HyperText Markup Language)"
"HTML Format"
CF_SYLK
"Link"
CF_DIF
CF_UNICODETEXT
CF_TEXT
"Rich Text Format"
"Ole Private Data"
CF_LOCALE
CF_OEMTEXT
CF_BITMAP
CF_DIBV5

という、実に22種類もの形式でコピーされていました。画像形式や、html形式までありますが、例えばCF_TEXT形式の中身を見てみると、タブ区切りのテキスト形式になっていたので、やはりコピーを行うCalcの側でタブ区切りを行っているようですね…。ちなみに同じタブ区切りのテキスト形式でも何種類かあるようです。これら22種類の中のテキスト形式のフォーマット名で検索をかければ道が開ける…かも。クリップボードって結構奥が深いんですね…。

 

まとめ

クリップボードについて得られた知識を考えると、実は核心に到達するにはまだまだ長い道のりなのかもしれません…。結局明確な成果は残せませんでしたが、実用的なソフトウェアのソースコードがいかに複雑なのかを身をもって実感することができましたし、分からないことにぶち当たったときの"ググり力"の大切さも感じることができたので、少しでも得るものはあったかなと思っています。この記事が、今後Libre Officeに挑もうとする他の誰かの役に立つことがあれば幸いです。。