2008/03/28

Rails と AIR で、付箋紙アプリ

Stickynotes

Rails の最新は 2.0.2 ですし、AIR は正式版の 1.0 が公開されました。

情報はある程度追ってはいるものの、やはり実際に試してみないとなかなか身につきません。


というわけで、以前から気になっていた、[Think IT] 第1回:付箋紙アプリケーションを作ろう!を参考に、Ruby on RailsとAIRによるデスクトップ付箋紙アプリケーションを作ってみました。


Adobe AIR のインストールはこちらから。


サンプル付箋紙アプリをお試しいただく場合は、こちらから。

ちなみに、このアプリはユーザ管理はしておりません。ので、大変ソーシャルな付箋アプリです(汗


Download Stickynotes AIR


基本的な動作は、[Think IT] 第1回:付箋紙アプリケーションを作ろう!と、Ruby on RailsとAdobe AIRでデスクトップアプリを作る - Pokeal.COMをベースに、なんとなくタスクトレイアイコンも使ってみました。ついでに、常に前面表示も可能です。


Railsに関しては、実質、次の2行のみです。これでRestfulなバックエンドアプリのできあがりです・・。


 $ ruby script/generate scaffold sticky body:text x:float y:float width:integer height:integer
$ rake db:migrate

なお、Railsアプリは、先日紹介した、Herokuで構築しています。


AIRでは、透過するTextFieldを作るのにちょっと苦労しました。
ポイントは、blendModeをLAYERにしたSpriteでした。


dynamic textfield alpha problem - kirupaForum

I am not sure if this is quite what you are looking for but here is a solution I found for adjusting alpha on a TextField object.


// create an empty sprite
var rect:Sprite = new Sprite();
rect.blendMode = BlendMode.LAYER;

addChild(rect);

var txtField:TextField = new TextField();
txtField.txtColor = 0x000000;
txtField.alpha = .5;
txtField.appendText("SOME TEXT");

rect.addChild(txtField);

the key is to set the blendMode property on the sprite to LAYER.



以下、参考までにソースコードです。


Menu.mxml


<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" width="300" height="200" creationComplete="onCreationComplete();" closing="closing(event);">
<mx:Script>
<![CDATA[
import mx.core.BitmapAsset;

[Embed(source="icons/broken-16x16.png")]
private var icon16:Class;

private var stickies:Array = [];

private function create():void {
setStatus(">> Create new sticky");

var sticky:Sticky = new Sticky();

sticky.setAlwaysInFront(isAlwaysInFront.selected);
sticky.save();
sticky.show();
stickies.push(sticky);

}

private function onCreationComplete():void {
setStatus(">> Initializing...");

// is supports system tray icon
if (NativeApplication.supportsSystemTrayIcon) {

var images:Array = [];
images.push((new icon16() as BitmapAsset).bitmapData);
nativeApplication.icon.bitmaps = images;

var systemTrayIcon:SystemTrayIcon = (nativeApplication.icon as SystemTrayIcon);

systemTrayIcon.tooltip = "Stickynotes";

var nativeMenu:NativeMenu = new NativeMenu();
var menuItemNew:NativeMenuItem = new NativeMenuItem("New Stickynote");
menuItemNew.addEventListener(Event.SELECT, function(e:Event):void {
create();
});
var menuItemReload:NativeMenuItem = new NativeMenuItem("Reload Stickynotes");
menuItemReload.addEventListener(Event.SELECT, function(e:Event):void {
load();
});
var menuItemRestore:NativeMenuItem = new NativeMenuItem("Restore window");
menuItemRestore.addEventListener(Event.SELECT, function(e:Event):void {
restore();
});
var menuItemExit:NativeMenuItem = new NativeMenuItem("Exit");
menuItemExit.addEventListener(Event.SELECT, function(e:Event):void {
exit();
});
nativeMenu.addItem(menuItemNew);
nativeMenu.addItem(menuItemReload);
nativeMenu.addItem(menuItemRestore);
nativeMenu.addItem(menuItemExit);
systemTrayIcon.menu = nativeMenu;

}

load();
}

private function load():void {
setStatus(">> Loading from http://stickynotes.heroku.com ...");

closing(null);

var request:URLRequest = new URLRequest("http://stickynotes.heroku.com/stickies.xml");
request.method = 'GET';
var loader:URLLoader = new URLLoader();
loader.addEventListener(Event.COMPLETE, function(e:Event):void {
messages.text += e.target.data + "\n";

var xml:XML = new XML(e.target.data);
for each (var element:Object in xml.sticky) {
var sticky:Sticky = new Sticky();
sticky.id = element.id;
sticky.editor.text = element.body;
sticky.window.x = element.x * Capabilities.screenResolutionX;
sticky.window.y = element.y * Capabilities.screenResolutionY;
sticky.window.width = element.width;
sticky.window.height = element.height;
sticky.setAlwaysInFront(isAlwaysInFront.selected);
sticky.updateStatus();
sticky.show();

stickies.push(sticky);
}

clearStatus();
});
loader.load(request);
}

private function setStatus(s:String):void {
this.status = s;
}
private function clearStatus():void {
this.status = "";
}

private function closing(e:Event):void {
for each (var sticky:Sticky in stickies) {
if (sticky) {
sticky.window.close();
}
}
stickies = [];
}

private function saveAll():void {
setStatus(">> Save stickies");

for each(var sticky:Sticky in stickies) {
sticky.save();
}
}

private function onChangeHandle():void {
for each (var sticky:Sticky in stickies) {
sticky.setAlwaysInFront(isAlwaysInFront.selected);
}
}

]]>
</mx:Script>
<mx:Button left="10" top="10" label="New" id="new_btn" click="create();" />
<mx:Button left="60" top="10" label="Reload" id="load_btn" click="load();" />
<mx:Button right="10" top="10" label="Save" id="save_btn" click="saveAll();" />
<mx:CheckBox left="10" top="40" label="always in front" id="isAlwaysInFront" change="onChangeHandle();" />
<mx:TextArea right="10" top="70" left="10" bottom="10" id="messages" />
</mx:WindowedApplication>


Sticky.as


package {
import flash.text.*;
import flash.display.*;
import flash.events.*;
import flash.system.*;
import flash.net.*;
import mx.controls.*;

public class Sticky {
public var window:NativeWindow; // sticky window
public var editor:TextField; // sticky edit area
public var id:Number; // primary key
private var button:SimpleButton = new SimpleButton(); // cloase button
private var resizeHandle:SimpleButton = new SimpleButton(); // resize handle
private var x:Number; // sticky x
private var y:Number; // sticky y
private var height:Number; // sticky height
private var width:Number; // sticky width
private var body:String; // sticky body
private var sprite:Sprite = new Sprite();
private static var RESIZE_HANDLE_SIZE:int = 20;

/* create sticky window */
public function Sticky():void {
var initOptions:NativeWindowInitOptions = new NativeWindowInitOptions();
initOptions.systemChrome = NativeWindowSystemChrome.NONE;
initOptions.transparent = true;
initOptions.type = NativeWindowType.LIGHTWEIGHT;

window = new NativeWindow(initOptions);
window.alwaysInFront = false;
window.stage.align = StageAlign.TOP_LEFT;
window.stage.scaleMode = StageScaleMode.NO_SCALE;

// layer for transparent editor
sprite.blendMode = BlendMode.LAYER;
window.stage.addChild(sprite);

// for edit area
editor = new TextField();
editor.x = editor.y = 0;
editor.selectable = true;
editor.border = false;
editor.type = TextFieldType.INPUT;
editor.multiline = true;
editor.background = true;
editor.wordWrap = true;
editor.backgroundColor = 0xE6E082;
editor.alpha = 1;
sprite.addChild(editor);

// window position of center
window.x = 0.5 * Capabilities.screenResolutionX - 300 * 0.5
window.y = 0.5 * Capabilities.screenResolutionY - 100 * 0.5

// size for window and edit area
window.width = editor.width = 300;
window.height = editor.height = 100;

// resize for window and edit area
window.stage.addEventListener(Event.RESIZE, function(e:Event):void {
resized();
});

// move and resize
window.stage.addEventListener(MouseEvent.MOUSE_DOWN, function(e:MouseEvent):void {
var x:Number = e.stageX;
var y:Number = e.stageY;
if (y > window.height - RESIZE_HANDLE_SIZE && x > window.width - RESIZE_HANDLE_SIZE) {
//window.startResize(NativeWindowResize.BOTTOM_RIGHT);
} else {
window.startMove();
}
});

window.stage.addEventListener(MouseEvent.MOUSE_UP, function(e:MouseEvent):void {
if (isChanged()) {
save();
}
});

editor.addEventListener(FocusEvent.FOCUS_OUT, function(e:FocusEvent):void {
if (isChanged()) {
save();
}
});

} // end of function Sticky

// show window
public function show():void {
// create close button
button.x = window.width - 15;
button.y = 5;
button.upState = createBox(0xE6E082, 10, 0.3);
button.overState = createBox(0xE6E082, 10, 1);
button.downState = createBox(0xCCCCCC, 10, 1);
button.hitTestState = button.upState;
button.addEventListener(MouseEvent.CLICK, function(e:MouseEvent):void {
var request:URLRequest = new URLRequest("http://stickynotes.heroku.com/stickies/" + id + ".xml");
request.method = 'DELETE';
var loader:URLLoader = new URLLoader();
loader.load(request);
window.close();
});
window.stage.addChild(button);

// create resize handle
resizeHandle.x = window.width - 15;
resizeHandle.y = window.height - 15;
resizeHandle.upState = createResizeHandle(0xE6E082, 10, 0.3);
resizeHandle.overState = createResizeHandle(0xE6E082, 10, 1);
resizeHandle.downState = createResizeHandle(0xCCCCCC, 10, 1);
resizeHandle.hitTestState = button.upState;
resizeHandle.addEventListener(MouseEvent.MOUSE_DOWN, function(e:MouseEvent):void {
window.startResize(NativeWindowResize.BOTTOM_RIGHT);
});
window.stage.addChild(resizeHandle);

window.visible = true;
}

// call after window resized
public function resized():void {
editor.width = window.width;
editor.height = window.height;
button.x = window.width - 15;
button.y = 5;
resizeHandle.x = window.width - 15;
resizeHandle.y = window.height - 15;
}

// save sticky
public function save():void {
// create URL of sticky
var request:URLRequest
if (!id) {
request = new URLRequest("http://stickynotes.heroku.com/stickies.xml");
request.method = 'POST';
} else {
request = new URLRequest("http://stickynotes.heroku.com/stickies/" + id + ".xml");
request.method = 'PUT';
}

// create paramater for edit
var variables:URLVariables = new URLVariables();
variables['sticky[x]'] = window.x / Capabilities.screenResolutionX;
variables['sticky[y]'] = window.y / Capabilities.screenResolutionY;
variables['sticky[width]'] = window.width;
variables['sticky[height]'] = window.height;
variables['sticky[body]'] = editor.text;
request.data = variables;

// send
var loader:URLLoader = new URLLoader();
loader.addEventListener(Event.COMPLETE, function(e:Event):void {
var xml:XML = new XML(e.target.data);
if (!id) {
id = xml.id;
}
updateStatus();
});
loader.load(request);
}

public function setAlwaysInFront(value:Boolean):void {
window.alwaysInFront = value;
if (value) {
editor.alpha = 0.85;
} else {
editor.alpha = 1;
}
}

public function updateStatus():void {
// sticky status
x = window.x;
y = window.y;
width = window.width;
height = window.height;
body = editor.text;
}

/* private methods ***********/

// close button
private function createBox(color:uint, radius:Number, vis:Number):Shape {
var xShape:Shape = new Shape();
xShape.graphics.lineStyle(1, 0x000000);
xShape.graphics.beginFill(color);
xShape.graphics.drawRect(0, 0, radius, radius);
xShape.graphics.moveTo(0, radius);
xShape.graphics.lineTo(radius, 0);
xShape.graphics.moveTo(radius, radius);
xShape.graphics.lineTo(0, 0);
xShape.graphics.endFill();
xShape.alpha = vis;
return xShape;
}

private function createResizeHandle(color:uint, radius:Number, vis:Number):Shape {
var xShape:Shape = new Shape();
xShape.graphics.lineStyle(1, 0x000000);
xShape.graphics.beginFill(color);
xShape.graphics.moveTo(0, radius);
xShape.graphics.lineTo(radius, 0);
xShape.graphics.lineTo(radius, radius);
xShape.graphics.lineTo(0, radius);
xShape.graphics.endFill();
xShape.alpha = vis;
return xShape;
}

// is paramater changed
private function isChanged():Boolean {
return (x != window.x || y != window.y ||
width != window.width || height != window.height ||
body != editor.text);
}

} // end of class Sticky

} // end of package

2 件のコメント:

いけちょ さんのコメント...

現在AIRで付箋紙アプリの作成をしている「いけちょ」といいます。

もしよろしければ、ウィンドウの作成あたりのコードの一部を拝借したいのですが、よろしいでしょうか?

一応AIRのお勉強のため、PHP+AIRでオンラインにデータを置いておける付箋紙アプリの開発を目指しています。

naoki さんのコメント...

> いけちょさん
コードの使用は問題ありません。参考になれば幸いです。
アプリ開発、頑張ってください!