株式会社ライブキャストロゴ 株式会社ライブキャスト

flashcast:フリーで働くITエンジニア集団のブログ: google翻訳APIを使ったAIRアプリを作る!(翻訳部分の実装編)の続きです。

前回まで、クリップボードを監視して値が変わったときに、翻訳するところまでを実装しました。ただ、これだとクリップボードにコピーした時にAPIをIコールするため、

要件の

翻訳元の文章は、インターネットを流れるので、ユーザが意図しない翻訳は極力避ける。

flashcast:フリーで働くITエンジニア集団のブログ: google翻訳APIを使ったAIRアプリを作る!より引用。

を満たさない結果となります。

なので、翻訳処理を実行する前に、ユーザの簡単なアクションを待つような仕組みにしたいと思います。

  • クリップボードの値が変わったときに、WindowsのタスクトレイやMacのドックのアイコンをアニメーションさせる。
  • アニメーションしている間にアイコンがクリック(ユーザの簡単なアクション)されたことをトリガーに翻訳処理を実行する。
  • アイコンがクリックされた後は、アイコンのアニメーションを停止する。

ということで、今回は、WindowsのタスクトレイやMacのドックのアイコンをアニメーションさせ、ユーザのアクションを待って翻訳をするところまでを実装したいと思います。

アニメーションさせるところは、以前書いた2つの記事で使ったロジックを流用します。

この2つを合体させると、あたかも、アイコンが3D風にアニメーションしているように見えます。

dock1dock2dock3

次に、アイコンクリック時の処理についてです。

アイコンクリック時に発生するイベントは、WindowsのタスクトレイとMacのドックで違いがあります。タスクトレイの場合は、普通にMouseEventのClickで良いのですが、

ドックアイコンがクリックされると、NativeApplication オブジェクトから invoke イベントが送出されます。アプリケーションが実行されていない場合は、システムによってアプリケーションが起動されます。それ以外の場合は、invoke イベントは実行中のアプリケーションインスタンスに送られます。

Adobe AIR * タスクバーアイコンより引用。

ドックアイコンの場合は、InvokeEventのInvokeになります。

ということで、ここもタスクトレイがサポートされているか、ドックがサポートされているかを判断して、イベントハンドラを登録することになります。

それを踏まえると、

package {
	import flash.desktop.NativeApplication;
	import flash.display.BitmapData;
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.InvokeEvent;
	import flash.events.MouseEvent;
	import flash.geom.Matrix;
	import flash.geom.Rectangle;

	import mx.core.BitmapAsset;

	public class IconManager extends EventDispatcher
	{
		[Embed(source="assets/translator_icon16.png")]
		private var icon16:Class;
		[Embed(source="assets/translator_icon128.png")]
		private var icon128:Class;
		private var newIconBitmap:BitmapData;           // アイコンくるくる用

		private var degrees:int;
		private var scales:int;
		private var edge:int;

		private var isAnimate:Boolean = false;
		private var isDecrement:Boolean = false;
		private var isReverse:Boolean = false;

		public function IconManager():void {
		}

		public function initIconManager():void {
			initIcon();
		}

		private function initIcon():void {
			if (NativeApplication.supportsSystemTrayIcon) {
				// タスクトレイアイコン
				newIconBitmap = (new icon16() as BitmapAsset).bitmapData;
				NativeApplication.nativeApplication.icon.addEventListener(MouseEvent.CLICK, onIconClick);
				isAnimate = true;
				edge = 16;
			}
			else if (NativeApplication.supportsDockIcon) {
				// ドックアイコン
				newIconBitmap = (new icon128() as BitmapAsset).bitmapData;
				NativeApplication.nativeApplication.addEventListener(InvokeEvent.INVOKE, onIconClick);
				edge = 128;
			}
			else {
				NativeApplication.nativeApplication.exit();
			}

			setIcon(newIconBitmap);
		}

		private function setIcon(bitmap:BitmapData):void {
			NativeApplication.nativeApplication.icon.bitmaps = [bitmap];
		}

		private function onIconClick(event:Event):void {
			if (isAnimate) {
				dispatchEvent(new TranslatorSampleEvent(TranslatorSampleEvent.ICON_CLICK));

				if (NativeApplication.supportsSystemTrayIcon) {
					setIcon(newIconBitmap);
				}
			}
			else {
				isAnimate = true;
			}
		}

		public function iconAnimate(event:Event):void {
			var iconBitmap:BitmapData;
			var newIconBitmap:BitmapData;
			var rectangle:Rectangle;

			degrees++;

			if (isDecrement) {
				scales--;
			}
			else {
				scales++;
			}

			iconBitmap = new BitmapData(edge, edge, true, 0x00000000);
			newIconBitmap = (((edge == 128) ? new icon128() : new icon16()) as BitmapAsset).bitmapData;
			rectangle = new Rectangle(0, 0, edge, edge);

			setIcon(moveBitmap(iconBitmap, newIconBitmap, rectangle, scales, degrees, isReverse));

			if (scales == edge || scales == 0) {
				if (scales == edge) {
					isDecrement = true;
					isReverse = !isReverse;
				}
				else if (scales == 0) {
					isDecrement = false;
				}
			}

			if (degrees == 360) {
				degrees = 0;
			}
		}

		private function moveBitmap(iconBitmap:BitmapData, newIconBitmap:BitmapData,
			rectangle:Rectangle, scales:int, degrees:int, isReverse:Boolean):BitmapData {
			var matrixRotate:Matrix;
			var matrixScaling:Matrix;
			var radian:Number = degrees/180*Math.PI;

			if (isReverse) {
				var tempBitmap:BitmapData = new BitmapData(edge, edge, true, 0x00000000);
				var tempMatrix:Matrix = new Matrix();
				var tempRectangle:Rectangle = new Rectangle(0, 0, edge, edge);
				tempMatrix.scale(-1, 1);
				tempMatrix.translate(edge, 0);
				tempBitmap.draw(newIconBitmap, tempMatrix, null, null, tempRectangle);
				newIconBitmap = tempBitmap.clone();
			}

			matrixScaling = new Matrix();
			var sx:Number;
			var sy:Number;
			var tx:Number;
			var ty:Number;
			sx = (edge-scales)/edge;
			sy = 1;
			tx = edge/2-(sx*edge/2);
			ty = 0;
			matrixScaling.scale(sx, sy);
			matrixScaling.translate(tx, ty);

			matrixRotate = new Matrix();
			matrixRotate.rotate(radian);
			matrixRotate.translate(edge/2-(Math.cos(radian)*edge/2-Math.sin(radian)*edge/2),
				edge/2-(Math.sin(radian)*edge/2+Math.cos(radian)*edge/2));

			matrixScaling.concat(matrixRotate);
			iconBitmap.draw(newIconBitmap, matrixScaling, null, null, rectangle);

			return iconBitmap;
		}

	}
}

このようなロジックになります。
ただ、

invoke イベントはアプリケーションが起動されたときに NativeApplication オブジェクトによって必ず送出されますが、それ以外のときにも送出されることがあります。例えば、ユーザーがアプリケーションに関連付けられたファイルをアクティブ化すると、実行中のアプリケーションは追加の InvokeEvent を受け取ります。

InvokeEvent – ActionScript 3.0 言語およびコンポーネントリファレンスより引用。

ということで、ドックアイコン対応の場合、起動されたときも同イベントが発生してしまうため、起動時のイベントかどうかを判断するようにしています(62行目)。

また、アイコンをクリックされたことをメインのロジックに伝える必要がありますので、以前作成したカスタムイベントをdispatchするようにしています。

dispatchEvent(new TranslatorSampleEvent(TranslatorSampleEvent.ICON_CLICK));

このイベントをハンドリングして、アイコンをアニメーションさせますので、メインのロジックは、以下のようになります。

package
{
	import flash.events.Event;

	import mx.binding.utils.ChangeWatcher;
	import mx.core.Application;
	import mx.events.PropertyChangeEvent;

	public class TranslatorSample5Base extends Application
	{
		private var imanager:IconManager;
		private var cmanager:ClipboardManager;
		private var gmanager:TranslateManager;

		private var gmodel:TranslateModel;

		public function TranslatorSample5Base():void {
		}

		public function initApp():void {
			gmodel = new TranslateModel();

			imanager = new IconManager();
			cmanager = new ClipboardManager();
			gmanager = new TranslateManager();

			imanager.initIconManager();
			cmanager.initClipboardManager(gmodel);
			gmanager.initTranslateManager();

			ChangeWatcher.watch(gmodel, '_original', onClipboardChangeHandler);

			imanager.addEventListener(TranslatorSampleEvent.ICON_CLICK, onIconClick);
		}

		private function onClipboardChangeHandler(event:PropertyChangeEvent):void {
			if ((event.oldValue as String) != "") {
				this.addEventListener(Event.ENTER_FRAME, imanager.iconAnimate);
			}
		}

		private function onIconClick(event:TranslatorSampleEvent):void {
			this.removeEventListener(Event.ENTER_FRAME, imanager.iconAnimate);
			gmanager.Translate("1.0", gmodel.Original, "en", "ja");
		}
	}
}

いつものように、実行結果はとりあえずデバックトレースです。

sample5

やっぱり、わかりづらいですね。
それはともかく、これで、「ユーザが意図しない翻訳を極力避ける」という、要件を満たすことができました!

サンプルソース

サンプルソースをダウンロードできるようにしておきます。