守破離でいこう! -Let's go with SyuHaRi!-

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

ラベル: ,

automatically translated by Google Translate Hack!

naoki 21:51 | 0 comments |
HaloScan: |