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

2008/03/11

CentOS 4.6 で、ZABBIX 1.4.4

社内のサーバ監視に、ZABBIX日本語サイト)を使用しているのですが、ZABBIX 1.4 では、「WEB monitoring」というWebサイトを簡単に監視できる機能が搭載されたようなので、この機能を使いたいがため、1.1.3 からアップデートしてみました。


New in ZABBIX 1.4 FEATURES日本語サイト


  • Installation Wizard

  • Installation Wizard automatically checks pre-requisites, database connectivity and generates configuration file for WEB front end.




  • Support of new database engines

  • Support of SQLite has been implemented. It allows use of ZABBIX in embedded environments.




  • WEB interface improvements

  • Speed and usability of WEB interface has been improved very much.




  • New notification methods

  • Native support of Jabber messaging has been introduced.




  • Distributed monitoring

  • ZABBIX distributed monitoring is made for complex environments consisting of different locations.
    ZABBIX supports monitoring of unlimited number of nodes. Centralized configuration allows easy configuration of all nodes from a single location.




  • Auto-discovery

  • ZABBIX distributed monitoring module allows easy deployment of ZABBIX systems. The discovery support IP ranges, service checks, agent and SNMP checks for efficient auto-discovery.




  • Many-to-many template linkage

  • More flexible host-template linkage saves time and make configuration of hosts more flexible and straight forward.




  • Database watchdog

  • ZABBIX server will automatically warn group of users if database is down and continues normal operations when database is back.




  • WEB monitoring

  • WEB monitoring module allows flexible and easy monitoringof availability and performanceof WEB sites and WEB based applications. It supports passing of GET and POST variables.




  • XML data import/export

  • New XML data import and export functionality is an excellent way of sharing templates, hosts configuration and items/triggers related information.




  • Support of Windows Vista

  • ZABBIX Windows agent supports Windows Vista, both 32 and 64 bit versions.




  • More flexible actions

  • Multiple operations (notifications, script execution) per action are supported. Choice of action calculation algorithm was introduced.




  • Server-side external checks

  • Server-side external checks can be used to introduce custom checks executed on ZABBIX server side.




  • New user permission schema

  • Old user permission schema is no longer support. It was replaced by new more efficient, yet simple, schema working on level of user groups and host groups.




  • Support of hysteresis

  • ZABBIX support use of different trigger expressions for going to ON and OFF states.




  • Support of slide show

  • Several screens can be grouped into a slide show for better presentation.




  • ZABBIX server can spread load across several servers

  • Groups of server side processes (discoverer, poller, HTTP poller, trapper, etc) can be located on different physical servers for better performance and availability.




  • Other improvements

  • See Release Notes for a complete list of improvements.




CentOS 4.6 の環境で、いくつか躓いたところがあったので、メモです。


CURLのバージョン


Web Monitoring機能を有効にするには、--with-libcurlオプションを付けてインストールします。


$ ./configure --enable-server --with-mysql --with-net-snmp --with-libcurl

すると、次のようなエラーが出て止まってしまいます。


checking for libcurl >= version 7.13.1... no
configure: error: Not found Curl library

CentOS 4.x では、curlのバージョンは7.12なのでダメなようです。

いろいろ調べてみると、結局、curlを自身でバージョンアップするしかないようです。

以下のようにして解決です。


centosinstall

$ cd ~/src/
$ wget ftp://ftp.planetmirror.com/pub/curl/libcurl4-devel-7.16.2-1.i386.rpm
$ wget ftp://ftp.planetmirror.com/pub/curl/libcurl4-7.16.2-1.i386.rpm
$ sudo yum install openssl096b
$ sudo yum remove curl-devel
$ sudo rpm -i libcurl4*


PHP4


ZABBIXのフロントエンドはPHPなのですが、やっとインストールが成功して管理画面にアクセスすると以下のエラーが発生。


PHP Parse error: parse error, unexpected T_STATIC, expecting T_OLD_FUNCTION or T_FUNCTION or T_VAR or '}' in /u02/zabbix/web/include/copt.lib.php on line 112

調べてみると、PHP5用のコードになっているのが原因のようです。

以下のようにして解決です。


Blank web install - ZABBIX Forums

includes/copt.inc.php を修正し、'static function' を 'function' にすべて置き換えます。


$ vi includes/copt.inc.php
:%s/static function/function/g


これで、無事にアップデートすることができました。

2008/03/09

RubyでGoogle Data APIs

Googleが提供するさまざまなサービスには、Google Data APIsを利用して、データの取得や更新を行うことができます。


現在、以下のサービスへのAPIが提供されています。



JavaScriptからSpreadsheetsを操作できないかと調べてみたのですが、JavaScript client libraryは、現在、Google Calendar と Bloggerのみのようです。


そこで、Rubyのライブラリを探してみると、gdata-rubyというライブラリがありました。
ただ、このライブラリは現在開発がストップしているらしく、SpreadsheetとBloggerのみに対応しているようです。


というわけで、せっかくなのでRubyでGoogle Spreadsheets data APIを試してみました。


ruby-gdataのインストール


gem install GData

書き込みテスト


#!/usr/bin/env ruby

require 'gdata/spreadsheet'

gdata_user = 'xxx@gmail.com'
gdata_pass = 'xxx'
gs_key = 'xxx'

gs = GData::Spreadsheet.new(gs_key)
gs.authenticate(gdata_user, gdata_pass)

gs.add_to_cell 'sin(0.2)'

残念ながら、使用感はいまいちでした。他によいライブラリはないのでしょうか・・・?