Androidアプリにも便利なEclipseプラグイン開発とは
ヤフー松田です。
Eclipseは数多くのプラグインで構成されている統合開発環境であり、Androidアプリの開発者にとっても馴染みのあるものと思います。各々のプラグインはEclipseで開発することができるようになっており、さらに既存のプラグインの利用や拡張が可能です。
そこで本稿では、Android Development Toolkit(ADT)に関連したプラグイン開発の紹介をしたいと思います。サンプルプラグインはEclipse 4.3、ADT 22.6.3の環境で開発を行っています。
今回作成するもの
どうしてもLogCatの本文だけをコピーしたかったので、今回はLogCatからテキストを抽出する簡単なプラグインを作成します。
以下の二つの機能を実装します。
- LogCatの行選択時、対応する本文を表示するビューを提供する
- LogCatのコンテキストメニューに本文コピー用のメニューを追加する
本稿では主にビュー機能について説明したいと思います。
本プラグインはEclipseの更新マネージャーに、
「http://www.geocities.jp/plugin_update/site.xml」を指定することでインストール可能です。
プラグインプロジェクトを作成する
まずは、「新規プロジェクト」→「プラグイン開発」→「プラグイン・プロジェクト」から、プラグイン開発用のプロジェクトを用意します。
「アクティベーター(プラグインのライフ・サイクルを管理するJavaクラス)の生成」と「このプラグインをUIに追加」のオプションにチェックをつけておきます。
plugin.xmlを専用のマニフェストエディターで開き、LogCatViewが含まれるcom.android.ide.eclipse.ddmsと、テキスト表示用のビューワーを利用するためのorg.eclipse.jface.textをプラグインの依存関係に追加します。
Eclipseの画面構成
開発に入る前にEclipseの画面構成について確認します。Eclipseにはその構成に応じたインターフェイスが用意されており、そのルートはワークベンチ(IWorkbench)と呼ばれます。
ワークベンチはひとつ以上のワークベンチウィンドウ(IWorkbenchWindow)を持っており、またウィンドウはひとつ以上のワークベンチページ(IWorkbenchPage)を持っています。
各ページはひとつ以上のワークベンチパート(IWorkbenchPart)を持っており、ビュー(IViewPart)かエディター(IEditorPart)に属します。
これらのパートの配置をまとめたものがパースペクティブです。
拡張ポイントの追加
プラグイン開発では、あらかじめ定義された拡張ポイントに対して拡張を行います。
今回は、独自のビューの追加とEclipseの初期化時にLogCatの検出準備を行うので、org.eclipse.ui.viewsとorg.ecilpse.ui.startupの拡張をそれぞれ追加します。追加した要素を右クリックし、「新規」から子要素を追加します。
追加された項目を選択し、各種属性や実装クラスの設定を行います。
ここで、ビューの実装クラスはorg.eclipse.ui.IViewPartを実装する必要があり、通常ViewPartを継承することになります。また、startupの実装クラスはorg.eclipse.ui.IStartupを実装する必要があります。
public class LogCatExtractorView extends ViewPart {
//このビューのID。外部からこのビューを探したり、開いたりする際に利用する
public static final String PLUGIN_ID = "jp.ym.eclipse.logcatextractor.view"; //$NON-NLS-1$
//テキストを表示するためのUIコントロール
private TextViewer mTextViewer;
@Override
public void createPartControl(Composite parent) {
//複数行表示、縦スクロール、折り返し、枠線つきで初期化
mTextViewer = new TextViewer(parent, SWT.MULTI | SWT.V_SCROLL | SWT.WRAP | SWT.BORDER);
}
@Override
public void setFocus() {
if (mTextViewer != null) {
//フォーカスを内部のコントロールに渡す
mTextViewer.getControl().setFocus();
}
}
//渡したテキストをUIに反映する
public void setText(String text) {
if (mTextViewer != null) {
mTextViewer.setDocument(new Document(text));
}
}
}
createPartControl()でビューのUIを構築し、setFocus()でビュー内のUIにフォーカスを割り当てます。外部からテキストを設定するためのメソッドも用意しました。これでビューは完成です。
続いて、プラグインのライフサイクルを管理するクラスにIStartupを実装します。Eclipseの初期化時にワークベンチパートの各種イベントを監視するリスナーをワークベンチページに追加しています。
public class Activator extends AbstractUIPlugin implements IStartup {
// プラグインID
public static final String PLUGIN_ID = "jp.ym.eclipse.ui.LogCatExtractor"; //$NON-NLS-1$
// プラグインのインスタンス
private static Activator sPlugin;
// パートをイベントを監視するためのリスナー。
private IPartListener mPartListener;
public Activator() {
}
@Override
public void earlyStartup() {
// メインウィンドウのアクティブなワークベンチページを取得
IWorkbenchPage page = PluginUtils.getMainWindowActivePage();
if (page == null) {
return;
}
// ワークベンチパートのイベントを監視するリスナーを生成
mPartListener = new LogCatDetectListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
// 内部のテーブル行選択時にカスタムビューをアクティブ化する
LogCatExtractorUtils.activateLogCatExtractView(e, false);
}
}, new MouseAdapter() {
@Override
public void mouseDoubleClick(MouseEvent e) {
// 内部のテーブル行ダブルクリック時にカスタムビューをアクティブ化する
LogCatExtractorUtils.activateLogCatExtractView(e, true);
}
});
// リスナーを追加
page.addPartListener(mPartListener);
}
// プラグイン開始時に呼び出される
public void start(BundleContext context) throws Exception {
super.start(context);
sPlugin = this;
}
// プラグイン停止時に呼び出される
public void stop(BundleContext context) throws Exception {
sPlugin = null;
super.stop(context);
// 設定したリスナーを除去する
IWorkbenchPage page = PluginUtils.getMainWindowActivePage();
if (page != null && mPartListener != null) {
page.removePartListener(mPartListener);
}
}
public static Activator getDefault() {
return sPlugin;
}
}
PluginUtils.getMainWindowActivePage()は以下のような実装で、アクティブなワークベンチページを取得しています。
public static final IWorkbenchPage getMainWindowActivePage() {
IWorkbench workbench = PlatformUI.getWorkbench();
IWorkbenchWindow[] windows = workbench.getWorkbenchWindows();
if (windows == null || windows.length == 0) {
return null;
}
return windows[0].getActivePage();
}
LogCatDetectListenerはLogCatがアクティブになるタイミングで内部のTableにアクセスし、メニューやリスナーの設定を行っています。 直接ログのテキストにアクセスする方法が見つからなかったため、今回はTableから取得します。
public class LogCatDetectListener extends SelectionAdapter implements IPartListener, Listener {
// LogCat内のテーブル
protected Table mLogCatTable;
// コピー用のメニュー項目
protected MenuItem mLogCatCopyMenu;
// LogCat内のテーブルの行選択時のイベントを監視するためのリスナー
protected SelectionListener mLogCatTableSelectionListener;
// LogCat内のテーブルのマウス操作を監視するためのリスナー
protected MouseListener mLogCatTableMouseListener;
// リスナーを渡すコンストラクタ
public LogCatDetectListener(SelectionListener selectionListener, MouseListener mouseListener) {
mLogCatTableSelectionListener = selectionListener;
mLogCatTableMouseListener = mouseListener;
}
@Override
public void partActivated(IWorkbenchPart part) {
if (part instanceof LogCatView) {
// LogCatビューがアクティブになった時
onLogCatActivated((LogCatView) part);
}
}
@Override
public void partBroughtToTop(IWorkbenchPart part) {
}
@Override
public void partClosed(IWorkbenchPart part) {
if (part instanceof LogCatView) {
// LogCatビューが閉じられる時
onLogCatClosed((LogCatView) part);
}
}
@Override
public void partDeactivated(IWorkbenchPart part) {
}
@Override
public void partOpened(IWorkbenchPart part) {
}
@Override
public void handleEvent(Event event) {
Menu menu;
switch (event.type) {
case SWT.Show:
// 本文をコピーするためのメニュー項目を追加
mLogCatCopyMenu = LogCatExtractorUtils.addMenuItem(mLogCatTable,
Messages.LogCatExtractorView_CopyMenuLabel, SWT.PUSH);
if (mLogCatCopyMenu != null) {
mLogCatCopyMenu.addSelectionListener(this);
}
// リスナーを外しておく
menu = mLogCatTable.getMenu();
if (menu != null) {
menu.removeListener(SWT.Show, this);
}
if (mLogCatTable != null) {
mLogCatTable.removeListener(SWT.MenuDetect, this);
}
break;
case SWT.MenuDetect:
// ポップアップメニューを検出
menu = mLogCatTable.getMenu();
if (menu != null) {
menu.addListener(SWT.Show, this);
}
break;
}
}
@Override
public void widgetSelected(SelectionEvent e) {
// メニュー項目が選択されたとき、クリップボードにコピーを行う
LogCatExtractorUtils.copyLogCatText(mLogCatTable);
}
protected void onLogCatActivated(LogCatView logCatView) {
if (mLogCatTable != null && !mLogCatTable.isDisposed()) {
return;
}
// ログ表示に使用されている内部のテーブルを取得する
mLogCatTable = LogCatExtractorUtils.extractLogCatTable(logCatView);
if (mLogCatTable == null) {
return;
}
// テーブルのポップアップメニュー検出用のリスナーを追加
mLogCatTable.addListener(SWT.MenuDetect, this);
if (mLogCatTableSelectionListener != null) {
// テーブルを選択した際のイベントを処理するリスナーを追加
mLogCatTable.addSelectionListener(mLogCatTableSelectionListener);
}
if (mLogCatTableMouseListener != null) {
// テーブルのマウス操作のイベントを処理するリスナーを追加
mLogCatTable.addMouseListener(mLogCatTableMouseListener);
}
}
//後始末
protected void onLogCatClosed(LogCatView logCatView) {
removeLogCatTableListeners();
removeLogCatMenuListeners();
}
protected void removeLogCatTableListeners() {
if (mLogCatTable == null) {
return;
}
if (mLogCatTableSelectionListener != null) {
mLogCatTable.removeSelectionListener(mLogCatTableSelectionListener);
}
if (mLogCatTableMouseListener != null) {
mLogCatTable.removeMouseListener(mLogCatTableMouseListener);
}
mLogCatTable.removeListener(SWT.MenuDetect, this);
mLogCatTable = null;
}
protected void removeLogCatMenuListeners() {
if (mLogCatCopyMenu != null) {
mLogCatCopyMenu.removeSelectionListener(this);
mLogCatCopyMenu = null;
}
}
}
LogCatExtractorUtils.extractLogCatTable()は以下のような実装で、LogCatビューのUIのコンテナであるLogCatPanelからTableを取得しています。
static final Table extractLogCatTable(LogCatView logCatView) {
try {
Field field = LogCatView.class.getDeclaredField("mLogCatPanel"); //$NON-NLS-1$
field.setAccessible(true);
LogCatPanel panel = (LogCatPanel) field.get(logCatView);
field = LogCatPanel.class.getDeclaredField("mTable"); //$NON-NLS-1$
field.setAccessible(true);
return (Table) field.get(panel);
} catch (Exception e) {
return null;
}
}
ここで取得できるTableはSWTの標準テーブルであり、テキストカラムの文字列を取得すれば、LogCatの本文を抽出できます。本プラグインでは以下のような実装で、選択行に応じて連結動作を変更しています。
static final String extractLogCatText(Table table) {
if (table == null) {
return null;
}
int textColumnIndex = table.getColumnCount() - 1;
if (textColumnIndex < 0) {
return null;
}
int selections = table.getSelectionCount();
StringBuilder builder;
switch (selections) {
case 0:
//選択なし
return null;
case 1:
//単一行選択の時は上下の文字も考慮する
int selectedIndex = table.getSelectionIndex();
builder = new StringBuilder();
TableItem item = table.getItem(selectedIndex);
//最初のカラムが空の場合は長い文章の続き
if (item.getText(0).isEmpty()) {
List<String> buffer = new ArrayList<String>();
for (int i = selectedIndex - 1; i >= 0; i--) {
TableItem prevItem = table.getItem(i);
buffer.add(getItemText(prevItem, textColumnIndex, false));
if (!prevItem.getText(0).isEmpty()) {
break;
}
}
Collections.reverse(buffer);
for (String text : buffer) {
builder.append(text);
}
}
int itemCount = table.getItemCount();
builder.append(getItemText(item, textColumnIndex, false));
for (int i = selectedIndex + 1; i < itemCount; i++) {
item = table.getItem(i);
if (!item.getText(0).isEmpty()) {
break;
}
builder.append(getItemText(item, textColumnIndex, true));
}
break;
default:
//複数行選択の時は順番に連結する
builder = new StringBuilder();
TableItem[] items = table.getSelection();
for (int i = 0; i < selections - 1; i++) {
builder.append(getItemText(items[i], textColumnIndex, false));
}
builder.append(getItemText(items[selections - 1], textColumnIndex, true));
break;
}
return builder.toString().trim();
}
static final String getItemText(TableItem item, int index, boolean endLine) {
String text = item.getText(index);
int end = text.length() - 1;
//改行マーク
if (text.codePointAt(end) == 0x23CE) {
if (Character.isWhitespace(text.charAt(end - 1))) {
//半角スペースも余分にある
--end;
}
text = text.substring(0, end);
} else if (!endLine) {
text += System.getProperty("line.separator");//$NON-NLS-1$
}
return text;
}
後は、Activatorで設定したリスナー(テーブルの選択やダブルクリックを監視)が呼び出されたタイミングで先に作成したカスタムビューを取得し、setText()で抽出したテキストを設定するだけです。
LogCatExtractorUtils.activateLogCatExtractView()は以下のような実装です。
static final void activateLogCatExtractView(EventObject e, boolean open) {
if (e == null) {
return;
}
Object source = e.getSource();
if (!(source instanceof Table)) {
return;
}
//テーブルからテキストカラムの文字列を取得する
String text = extractLogCatText((Table) source);
if (text == null) {
return;
}
//カスタムビューを開く
LogCatExtractorView view = ((LogCatExtractorView) PluginUtils.getView(LogCatExtractorView.PLUGIN_ID, open));
if (view != null) {
view.setText(text);
}
}
PluginUtils.getView()では指定のIDを持つビューをアクティブなワークベンチページから取得しています。
public static final IViewPart getView(String pluginId, boolean open) {
IWorkbench workbench = PlatformUI.getWorkbench();
IWorkbenchWindow window = workbench.getActiveWorkbenchWindow();
if (window == null) {
return null;
}
IWorkbenchPage page = window.getActivePage();
if (page == null) {
return null;
}
//アクティブなパートから指定IDのビューを探す
IViewPart view = page.findView(pluginId);
if (view != null) {
if (open) {
//開く場合はアクティブ化
page.activate(view);
}
return view;
}
if (!open) {
return null;
}
try {
//開く場合はIDで探し出して表示
return page.showView(pluginId);
} catch (PartInitException e) {
return null;
}
}
最後にマニフェストエディターからエクスポートして完了です。ここで単一のプラグインが作成され、dropinsフォルダに配置すれば手動インストールができます。
また、フィーチャープロジェクト(プラグインをまとめたもの)と更新サイトプロジェクトを作成し、Webサイトにデプロイすれば(今回の例のように)Eclipseの更新マネージャーを用いてユーザーに配布することも可能です。
おわりに
いかがでしたでしょうか?
Eclipse自身でグラフィカルにプラグインの開発が行え、UIの構築にSWTやJFaceを利用できるため、比較的簡単に導入が行えると思います。
アイデア次第で普段の開発をより効率よく行えるようになるので、皆さんも是非いろいろなプラグイン開発に挑戦してみてください。
CodeIQコード銀行にあなたのコードを預けてみませんか?
- CodeIQコード銀行ではあなたのコードを財産と考えます。
- お預かりいただいたコードは、CodeIQコード銀行がしっかり評価し、フィードバックいたします。
- 当コード銀行にお預けいただいたコードは、企業がみてスカウトをかける可能性があります。
- 転職したい方や将来転職することを考えている方で、今の自分のスキルレベルを知りたい方はぜひ挑戦してみてください。
- 企業からスカウトがきたら困る人は挑戦しないでください。
興味を持った方はこちらからチャレンジを!
ヤフー株式会社に勤務。入社後、Androidアプリ開発に携わる。
現在は写真・動画を気軽にやりとりできるコミュカメラの開発を担当。