アイコンをその場で合成しながらアニメーション

SWTを使ったアプリケーションでアニメーションをさせるためのコード片を公開しておきます。(ほとんど自分のためにですが。)肝はThread#sleep()でフレーム秒間を調整したらだめで、Display#timerExec(int,Runnable)を使って再帰的に画像合成と描画を行うようにしておくことです。この考え方はDraw2dを使ったアプリケーションでももちろん有効です。

  private static final class AnimationListener implements Listener {

    // WindowsとLinuxで画像の位置が変なので、調整するための定数を宣言
    private static final int PLATFORM_DELTA_X;
    private static final int PLATFORM_DELTA_Y;
    static {
      if("gtk".equals(SWT.getPlatform())){
        PLATFORM_DELTA_X = 1;
        PLATFORM_DELTA_Y = 3;
      }else{
        PLATFORM_DELTA_X = 0;
        PLATFORM_DELTA_Y = 0;      
      }
    }
    
    private int animationIndex = 0;
    private ImageData[] animationImages;
    protected int per10msec;
    private Control control;

    private AnimationListener(Control control) {
      this.control = control;
      // アニメーションの画像をロードする
      ImageLoader loader = new ImageLoader();
      InputStream stream = this.getClass().getResourceAsStream("loading.gif");
      animationImages = loader.load(stream);
    }

    public void handleEvent(final Event event) {
      if (event.item instanceof TreeItem) {
        TreeItem item = (TreeItem) event.item;
        Rectangle rect = item.getImageBounds(0);
        // elementはアニメーションの終了条件を判断する。
        // SWTでは各Controlはdata領域にモデルを格納できる。SWTプラットフォームからはdata領域は触らない。
        Object element = item.getData();
        if (element instanceof INotificationService) {
          INotificationService service = (INotificationService) element;
          Image image = item.getImage();
          doAnimation(control, item, rect, image, service);
          // 終了後の画像描画
          if(image != null && !event.gc.isDisposed()) {
            event.gc.drawImage(image, rect.x + PLATFORM_DELTA_X,rect.y + PLATFORM_DELTA_Y);
          }
        }
      }
    }

    private void doAnimation(final Control tree,
        final TreeItem item, final Rectangle rect, final Image image,final INotificationService service) {
      // アニメーション用のRunnableインスタンス
      Runnable animation = new Runnable() {
        public void run() {
          if(item.isDisposed()) return;
          GC imageGc = new GC(tree);
          final Image animationImage = getAnimationImage(image.getImageData());
          if(animationImage != null) {
            imageGc.drawImage(animationImage, rect.x + PLATFORM_DELTA_X,rect.y + PLATFORM_DELTA_Y);
            animationImage.dispose();
          }            
          if(service.isLoading()){
            // ここが肝。再帰的にRunnableを呼び出す 
            tree.getDisplay().timerExec(per10msec,this);
          }else{
            imageGc.drawImage(image,rect.x + PLATFORM_DELTA_X,rect.y + PLATFORM_DELTA_Y);
          }
          imageGc.dispose();
        }
      };
      if(service.isLoading()){
        tree.getDisplay().asyncExec(animation);
      }else{
        GC imageGc = new GC(tree);
        imageGc.drawImage(image,rect.x + PLATFORM_DELTA_X,rect.y + PLATFORM_DELTA_Y);
        imageGc.dispose();
      }
    }

    private Image getAnimationImage(final ImageData baseImage){
      //画像合成のためのクラス。めんどいので匿名に。
      CompositeImageDescriptor icon = new CompositeImageDescriptor(){
        @Override
        protected void drawCompositeImage(int width, int height) {
          // 合成するための元の画像を書き出す
          drawImage(baseImage,0,0);
          // 合成するアニメーションの画像を取得する
          ImageData imageData = animationImages[animationIndex];
          animationIndex = animationIndex == animationImages.length - 1 ? 0 : animationIndex + 1;
          drawImage(imageData, 0, 0);
          // アニメーションのフレーム秒間を計算する
          per10msec = imageData.delayTime * 10;
        }

        @Override
        protected Point getSize() {
          return new Point(16,16);
        }
      };
      return icon.createImage();
    }
  }  
  
  ViewLabelProvider(TreeViewer viewer){
    Control tree = viewer.getControl();
    // 描画時のListenerとして登録する
    tree.addListener(SWT.PaintItem, new AnimationListener(tree));
  }

Percs 0.4.0 リリース

3ヶ月ほど開きましたが、Percs 0.4.0をリリースしました。
更新サイト

https://eclipse-study.svn.sourceforge.net/svnroot/eclipse-study/PercsProject/trunk/org.kompiro.readviewer.site

今回のリリースではRCP版も作成してみました。
RCP版配布元
https://sourceforge.net/project/showfiles.php?group_id=207441
主な改良点は

  • 更新を通知するポップアップを邪魔にならない程度にいい感じにしてみました。

  • JRE 1.5でも動作するようにしました。
  • LInux上のクライアントから各ノードのポップアップを見やすくしました。

(Windows版はフォントが大きすぎて見にくいかも)

  • 読み込んでいる最中はアニメーションをさせてみました。(プログレスバーが動かなくなる不具合あり。)

多分チケットなどで変更点を管理すべきでしょうが、欲しくて作っているものなので、1.0まではそのまま特にリリースノートを作らずにいきたいと思います。個人的には結構やりたいネタが溜まっている(例えばPOP3に対応したり、設定した時間で通知したり)ので、それらのサービスをどんどん追加していきます。ただ、ふと立ち止まって考えると世の中RSSとAtomだけで結構情報がとれるとれる。ちょっとしたサイトだったらJSONでやりとりしているデータをこそっと持ってくるだけでまた色々やれちゃったり。最後のは微妙か…。でも面白いですね。

Percs 0.3.0リリース

一番大きいのは複数のサービスに対応したことですね。後は細かなバグフィックス等。

更新サイト

https://eclipse-study.svn.sourceforge.net/svnroot/eclipse-study/PercsProject/trunk/org.kompiro.readviewer.site

インターフェースと拡張ポイントを実装するだけでいろんなサービスをView上に表示できます。手始めにGmailを実装してみた。URLとか入力しないでもGmailのIDとパスワードがあれば、最新のメールを通知してくれます。ただ、まだログイン画面をすっ飛ばす実装はできてません。
でもまだパスワードの暗号化をしていません。それを実現できると結構手軽に色々拡張できそうです。Viewの表示もまだまだ微妙だなぁ。やること盛りだくさん。
ところで画像はGnome ScreenShotで撮ってみたんですが、タイトルバーとかないのでさみしい感じですね。

Gmailに認証後にメッセージの詳細を取得する

Gmailはメッセージの一覧をAtomで配信している。なので、メッセージのIDが埋め込まれたURLはそこから取得することができるが、そのままURLをブラウザに渡すとまだ認証されていないため、認証画面が出てくる。どうにかこうにかしてこの画面をすっ飛ばしたいなと思って書いたのが次のコード。例のごとくJUnit4です。で、CookieManagerを使いたかったので、Java6使ってます。もしあれだったらApache CommonsのHttpClientあたりを使うようにすればJava1.4くらいでも動きそうですな。それとHTMLのパースにJericho HTML Parser、ウィジェットにはSWTを使ってます。

public class LearningGmailConnection {
  private String email = "******";
  private String passwd = "*****";
  private String messageId = "******";
  
  private static String loginformId = "gaia_loginform";
  private static String gmailLoginURL = "https://www.google.com/accounts/ServiceLogin?service=mail&passive=true&rm=false&continue=http%3A%2F%2Fmail.google.com%2Fmail%2F%3Fui%3Dhtml%26zy%3Dl&ltmpl=default&ltmplcache=2";

  @Test
  public void learningGmailURL() throws Exception {
    CookieManager manager = new CookieManager();
    manager.setCookiePolicy(CookiePolicy.ACCEPT_ALL);
    CookieHandler.setDefault(manager);

    URL url = new URL(gmailLoginURL);
    Source source = new Source(url);
    List<?> forms = source.findAllElements(Tag.FORM);
    for (Iterator<?> itr = forms.iterator(); itr.hasNext();) {
      Element form = (Element) itr.next();
      if (loginformId.equals(form.getAttributeValue("id"))) {
        authenticationToGmail(form);
        
      }
    }
        String script = makeCookies(manager);
    url = new URL(messageId);
    showBrowser(url,script);
  }


  private void authenticationToGmail(Element form)
      throws MalformedURLException, IOException, ProtocolException {
    URL action = new URL(form.getAttributeValue("action"));
    HttpURLConnection con = (HttpURLConnection) action
        .openConnection();
    con.setRequestMethod("POST");

    List<?> tags = form.findAllElements(Tag.INPUT);
    
    con.setDoOutput(true);
    DataOutputStream outStream = new DataOutputStream(con
        .getOutputStream());
    String params = buildParameters(tags);
    byte[] bytes = params.getBytes();
    outStream.write(bytes, 0, bytes.length);
    con.connect();
    BufferedReader br = new BufferedReader(new InputStreamReader(
        con.getInputStream()));
    while (br.readLine() != null){}
    con.disconnect();
  }

  private String buildParameters(List<?> tags) {
    StringBuilder builder = new StringBuilder(
        "Email=" + email + "&Passwd=" + passwd);
    for (Iterator<?> iitr = tags.iterator(); iitr.hasNext();) {
      Element tag = (Element) iitr.next();
      if ("hidden".equals(tag.getAttributeValue("type"))) {
        String name = tag.getAttributeValue("name");
        String value = tag.getAttributeValue("value");
        builder.append("&");
        builder.append(name);
        builder.append("=");
        builder.append(value);
      }
    }
    return builder.toString();
  }

  private String makeCookies(CookieManager manager) {
    CookieStore store = manager.getCookieStore();
    List<HttpCookie> cookies = store.getCookies();
    StringBuilder scriptBuilder = new StringBuilder();
    for (int i = 0; i < cookies.size(); i++) {
        HttpCookie cookie = cookies.get(i);
        scriptBuilder.append(createCookieScript(cookie));
    }
    String script = scriptBuilder.toString();
    return script;
  }

  private String createCookieScript(HttpCookie cookie) {
    StringBuilder builder = new StringBuilder("document.cookie=\"");
    builder.append(cookie.getName());
    builder.append("=");
    builder.append(cookie.getValue());
    builder.append(";");
    String domain = cookie.getDomain();
    if(domain != null){
      builder.append("domain=");
      builder.append(domain);
      builder.append(";");
    }
    builder.append("\"\n");
    return builder.toString();
  }
  
  private void showBrowser(URL url, String script) {
    showBrowser(url, null, script);
  }


  private void showBrowser(URL url, String html,String script) {
    if(url == null && html == null){
      throw new NullPointerException("url or html, which one is needed.");
    }
    Display display = new Display();
    Shell shell = new Shell(display);
    shell.setLayout(new FillLayout());
    Browser browser = new Browser(shell, SWT.NONE);
    if(script != null){
      browser.execute(script);
    }
    if(url != null){
      browser.setUrl(url.toString());
    }
    if(html != null){
      browser.setText(html);
    }
    shell.open();

    while (!shell.isDisposed()) {
      if (!display.readAndDispatch())
        display.sleep();
    }

    display.dispose();
  }
}

前のエントリで書いたとおり、認証画面をどうにかこうにかしてPOSTすることでクッキーを取得し、それをブラウザに食わせるというやり方をとってみたらすんなりうまくいった。他のサイトでもFormで認証させる場合は同様のやり方でいける。

このコードを見ている限りコードの基盤はオブジェクト指向というよりも構造化なんだよなと思った。構造化されたコードをわかりやすく分類したものがクラスで、そのメッセージの飛ばし具合がオブジェクト指向っていうのがクラスベースオブジェクト指向インスタンスベースオブジェクト指向はそもそもインスタンスがあるよねっていう世界だからJavaには合わないね。脱線。

Percs v0.2.0リリース

といっても開発中なので、更新サイトは例によってリポジトリのtrunkの中にあったりしますが。


更新サイト

https://eclipse-study.svn.sourceforge.net/svnroot/eclipse-study/PercsProject/trunk/org.kompiro.readviewer.site

実はGmailも読めました。

Gmailのフィードは

https://mail.google.com/mail/feed/atom

とですが、ユーザー名とパスワードを指定してvalidationボタンを押してください。フィードを受信後、メッセージをダブルクリックするとブラウザが開きます。最初ログイン画面が表示されますが、ログインしてしまえば2度目以降Eclipseを再起動するまでログイン画面をすっ飛ばして表示してくれる模様です。今はEclipseが用意しているEditorをそのまま使ってますが、適当に使いやすいように改良を加えるつもりです。

ドッグフードを食らう(その2)

ということで引き続き食らっております。ちょっと安定してきました。どういうものかってーとその昔WinBiffというメール通知ソフトがあったでしょ。あれのプラグイン版です。

ポーリングして、一定時間ごとに最新の情報を取得します。で、取得した情報をダブルクリックすると、対象のページを開きます。現在はRSS,atom,rdfの受信のみ対応しています。なので、一応Gmailとか、mixiの更新は検知しますが、認証が必要なので表示はできません。で、一定時間で更新があったものを通知してくれるサービスを作ってます。

WizardPageとサービスを実装するだけで受信したいものを追加できるという構成になってるので、既存のpop3とかnet-newsとかも実装するつもり。(もちろんGmailとか、mixiとかはそれ専用にサービス作って、認証画面をすっ飛ばせる実装にします。)

そもそもEclipseにブラウジング用のプラグインってものがあまりない(RSSOwlくらい??)ので、そのあたりを作りこもうかと。Lingrとか、Twitterも受信可能だよなー。と。受信結果によってリズムパターンを鳴らすところまでが目標。(だからPercsって名前です。)

若干酔ってる日記ですので、乱文ご容赦。