初マクロ

帰省して、やることがないのでマクロを作ってみた。
SVNのCommitマクロを作るつもりだったが、実家からはリポジトリにアクセスできないのでテスト不能
ローカルにリポジトリを作るのも面倒なので休み明けかな。

作ったのはショートカットを開くマクロ。
jFD2はショートカットの作成・削除が簡単で良いのだが、開く時の操作性がDFに比べるといまいちなので、それを改善するのが目標。

機能:ショートカットを開く
(1) 起動した時にショートカットフォルダーにあるショートカットを一覧表示。
(2) その段階でインクリメンタルサーチ(Migemoが使えればMigemo)できる*1
(3) Enterキーで選択したショートカットを開く。
(4) Shift+Enterキーで選択したショートカットを新しいタブで開く。

Swingでちょっとはまった。実家ではネット環境が貧弱なので調べるのも大変。
Groovyではインターフェイスの実装が楽でいいなぁ。KeyAdapterを使わなくてもKeyListenerの実装が簡単にできる。

import com.nullfish.lib.vfs.VFS
import com.nullfish.lib.keymap.KeyStrokeMap
import com.nullfish.app.jfd2.ui.container2.NumberedJFD2
import com.nullfish.app.jfd2.ui.container2.JFD2TitleUpdater
import com.nullfish.app.jfd2.ui.container2.ContainerPosition
import com.nullfish.app.jfd2.util.IncrementalSearcher
import com.nullfish.app.jfd2.util.WildCardUtil
import com.nullfish.app.jfd2.util.MigemoInfo
import org.monazilla.migemo.Migemo
import java.awt.event.KeyListener
import java.awt.event.KeyEvent
import java.awt.event.InputEvent
import java.awt.event.KeyAdapter
import javax.swing.KeyStroke

files=[]
shortcutDir=(String)jfd.getCommonConfigulation().getParam("shortcut_dir", "")
new File(shortcutDir).list().each{
  m=(it=~/(.+)\.jfdlnk$/)
  if(m.size()>0) files<<=m[0][1]
}
if(files.size()<1) return
files=files.sort()
dlg=new groovy.swing.SwingBuilder().dialog(title:"Open shortcut"){
  box(axis:javax.swing.BoxLayout.Y_AXIS){
    scrollPane(){
      ls=list(listData:files,selectionMode:javax.swing.ListSelectionModel.SINGLE_SELECTION,selectedIndex:0)
    }
    panel(){
      label(text:"Search:")
      word=label()
    }
    panel(){
      label(text:"Enter:Open, Shift+Enter:NewTab, ESC:Cancel, Chars:Search")
    }
  }
}

dlg.pack()
dlg.setLocationRelativeTo(null)
dlg.setVisible(true)

ls.addKeyListener([keyPressed:{e->
  setIndex={pos->
    ls.selectedIndex=pos
    ls.ensureIndexIsVisible(pos)
    ls.updateUI()
  }
  searchWord={
    wd=MigemoInfo.usesMigemo()?Migemo.lookup(word.text):"^"+WildCardUtil.wildCard2Regex(word.text).toLowerCase()
    pattern=wd.bitwiseNegate()
    for(i in 0..files.size()-1){
      if(files[i].toLowerCase()=~pattern){
        setIndex(i)
        break
      }
    }
  }

  switch(KeyStrokeMap.getKeyStrokeForEvent(e)){
  case KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0):
  case KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, InputEvent.SHIFT_DOWN_MASK):
    file=new File(new File(shortcutDir),files[ls.selectedIndex]+".jfdlnk")
    newPath=new String(file.readBytes())
    newFile = VFS.getInstance(jfd).getFile(newPath)
    if(newFile != null){
      if(e.modifiersEx&KeyEvent.SHIFT_DOWN_MASK){
        newJFD = new NumberedJFD2()
        newJFD.init(jfd.jFDOwner.configDirectory)
        jfd.jFDOwner.addComponent(newJFD, ContainerPosition.MAIN_PANEL, new JFD2TitleUpdater(newJFD))
        newJFD.model.setDirectoryAsynchIfNecessary(newFile, newFile.getParent(), newJFD)
      }else{
        jfd.model.setDirectoryAsynchIfNecessary(newFile, newFile.parent, jfd)
      }
    }
  case KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0):
    dlg.dispose()
    break
  case KeyStroke.getKeyStroke(KeyEvent.VK_BACK_SPACE, 0):
  case KeyStroke.getKeyStroke(KeyEvent.VK_H, InputEvent.CTRL_DOWN_MASK):
    switch(word.text.size()){
    case 0:break
    case 1:
      word.text=""
      setIndex(0)
      break
    default:
      word.text=word.text.getAt(0..-2)
      searchWord()
    }
    break
  default:
    c=Character.toUpperCase(e.getKeyChar()).toString()
    if(word.text.size()<30&&IncrementalSearcher.ACCEPTABLE_CHARS.indexOf(c)>=0){
      word.text+=(e.modifiersEx&KeyEvent.SHIFT_DOWN_MASK) ? c:e.getKeyChar().toString()
      searchWord()
    }
  }
}] as KeyListener)

*1:自分の使い方だとMigemoは切った方が良さそう

Groovyの実験

きっかけはjFD2のGroovyマクロで外部コマンドの実行方法を調べたこと。
当初はJavaでの方法をそのままGroovyに持ってきたのだが、ネットでみたサンプルに驚愕!

"ls -l".execute().in.eachLine{println it}

なんですかこれ?
すぐには理解できなかったが調べてみると、Stringクラスに"execute"メソッドを追加して、ProcessクラスにはInputStreamを取得できるプロパティ"in"を追加しているらしい。やりたい放題だなGroovy。

このコードを見て、気になったことを実験*1
○実験その1
プロパティと書いたが、実際はgetIn()メソッドが追加されているだけらしい。
例えば既存のクラスで

sv=button.getVisible()
button.setVisible(true)

なんて書いていたのを

sv=button.visible
button.visible=true

なんて変えても、同じように動作する。
そこで疑問。
Javaのクラスが

public class Foo{
  public String foo;
  private String bar;
  public Foo(){
    foo="foo";
    bar="bar";
  }
  public String getFoo(){return bar;}
}

だった時にGroovyで

foo=new Foo()
println foo.getFoo()    // ここは"bar"だよね
println foo.foo         // ここは?

とした時どうなるか?
・・結果。
"bar"でした。
"getXXX()"の代わりに"xXX"は安心して使えるということですね。
ちなみに上記のJavaクラスFooからgetFooメソッドを削除すると、ちゃんとpublic変数のfooを参照してくれます。
○実験その2
既存クラスへのメソッドの追加はどうやるか。
Rubyなら簡単だけどJavaじゃできないはず?Groovyでなぜできる?ってのは、このさい追求しないことにする。できるもんはできる。で、やってみた。
まずJavaクラスを用意

public class Foo{
  private String foo;
  public Foo(){
    foo="foo";
  }
}

んで、Groovy

Foo.metaClass.getFoo << {
  return foo;
}
foo=new Foo()
println foo.getFoo()  // foo.fooでも同じ

あっさりJavaのprivateフィールド読めてます。
privateメソッドもラップするメソッドを定義すれば問題なく呼べるでしょう。
それでいいのか?
Java的には問題あるかもしれんけど、まあ便利だからいいんじゃね。

*1:検証環境はGroovy 1.5.0

Subversion対応マクロ(update)

Subversionのファイル管理のために、わざわざEclipseを立ち上げるってのも何か間違ってる気がする。
ファイル管理はファイラーの役目、ってことでSubversion対応のマクロを少しずつ作ってみる。
まずはupdateから。
外部コマンドのsvnを呼んでるので、Windowsならsvn.exeにパスが通っている必要がある。

ダイアログを作るのにgroovyのSwingBuilderを使ったけど、これはいい!!
コンポーネントの関係が見やすいしレイアウトしやすい。
Swingを使うのは初めて(awtもほとんど忘れた)なので、希望のレイアウト(BoxLayout)を探すのにちょっと手間取ったけど。

import javax.swing.BoxLayout
import com.nullfish.app.jfd2.ext_command.CommandExecuter
dlg=new groovy.swing.SwingBuilder().dialog(title:"svn update ...doing"){
  box(axis:BoxLayout.Y_AXIS){
    scrollPane(){
      ta=textArea(rows:15,columns:60)
    }
    btn=button(text:"OK",visible:false,actionPerformed:{dlg.dispose()})
  }
}
dlg.pack()
dlg.locationRelativeTo=null
dlg.visible=true
model=jfd.model
files = model.markedFiles
if(files == null || files.length == 0){
  files = [model.selectedFile]
}
cmd=files.inject("svn update "){s,f->s+='"'+f.name+'" '}
ta.append(cmd+"\n")
CommandExecuter.instance
  .exec(cmd,CommandExecuter.USE_APP_SHELL,new File(model.currentDirectory.absolutePath))
  .in.eachLine{ta.append(it+"\n")}
dlg.title="svn update ...done"
btn.visible=true
btn.requestFocusInWindow()

マクロいろいろ

(1) 2chに張ったマクロ。
新しいタブを開く時にフォルダーにカーソルがあったら、そのフォルダーを開く。
DFのShift+Enterの動作に近い。

import com.nullfish.app.jfd2.ui.container2.NumberedJFD2
import com.nullfish.app.jfd2.ui.container2.JFD2TitleUpdater
import com.nullfish.app.jfd2.ui.container2.ContainerPosition
owner = jfd.getJFDOwner()
if(owner==null) return
model = jfd.getModel()
selectedFile = model.getSelectedFile()
newDir = selectedFile.isDirectory() ? selectedFile:model.getCurrentDirectory()
newJFD = new NumberedJFD2()
newJFD.init(owner.getConfigDirectory())
owner.addComponent(newJFD, ContainerPosition.MAIN_PANEL, new JFD2TitleUpdater(newJFD))
newJFD.getModel().setDirectoryAsynchIfNecessary(newDir, newDir.getParent(), newJFD)

(2) 長いから2chに張ってないマクロ。
1画面モードから2画面に縦分割。アクティブでないタブをサブウィンドウに移送。
コピー先を決めるのに便利。

import com.nullfish.app.jfd2.ui.container2.JFD2TitleUpdater
import com.nullfish.app.jfd2.ui.container2.NumberedJFD2
import com.nullfish.app.jfd2.ui.container2.ContainerPosition

owner=jfd.getJFDOwner()
if(owner==null) return
mainPanel=owner.getContentPane().getComponent(0)
subPanel=owner.getContentPane().getComponent(1)
tc0=mainPanel.getTabCount()
tc1=subPanel.getTabCount()
if(tc0+tc1<2) return
owner.getContentPane().getLayout().setHorizontal(true)
if(tc1==0){  // 1画面
  colCount=(int)(jfd.getColumnCount()/2)
  if(colCount==0) colCount=1
  for (ee in mainPanel.getComponents()) {
    e=ee.getComponent()
    e.setColumnCount(colCount)
  }
}
if(tc0==1){  // 次画面無し
  colCount=(int)(jfd.getColumnCount()*2)
  jfd.setColumnCount(colCount)
  for (ee in subPanel.getComponents()) {
    e=ee.getComponent()
    owner.removeComponent(e)
    e.setColumnCount(colCount)
    owner.addComponent(e, ContainerPosition.MAIN_PANEL,new JFD2TitleUpdater((NumberedJFD2)e))
  }
}else{
  for (ee in mainPanel.getComponents()) {
    e=ee.getComponent()
    if(e.getComponent()==jfd) continue
    owner.removeComponent(e)
    owner.addComponent(e, ContainerPosition.SUB_PANEL,new JFD2TitleUpdater((NumberedJFD2)e))
    break
  }
}
owner.removeComponent(jfd)
owner.addComponent(jfd, ContainerPosition.MAIN_PANEL,new JFD2TitleUpdater((NumberedJFD2)jfd))

コードをpreタグで囲んでいたのに、確認したら括弧が重なった部分が、はてな記法の脚注と判断されてしまった・・。
preタグの中はそのまま出してくれ。
仕方が無くはてなのヘルプを参照。"スーパーpre記法"ってのを発見。コードの言語タイプを指定するとキーワードの色分けとかもしてくれる。言語にはgroovyも指定できるのは偉い"とかも見てくれる">*1

*1:javaと同じかと思ったら、ちゃんと"->"とかも見てくれる

ファイラー

長いことDFというファイラーを愛用してきた。
その機能に大きな不満もなかったけれど航海日誌jFD2というファイラー(と、その2chスレ)を発見し、ここ数日試用している。
Java(Swing)アプリだが結構軽快に動いてくれる。まあこれはNetBeansを使ってるので驚かない。
DFでやっていたことはほとんどできそうだしマクロでカスタマイズもできるので、しばらく使い込んでみるつもり。
DFのキーアサインを体が覚えてしまっているので、jFD2のキーアサインをDF風に修正するかjFD2に慣れるか考え中。その昔はFDを長年使ってたので、すぐ思い出しそうな気もするが。

マクロの言語はGroovy。
JRubyのがいいなぁ、と当初は思ったのだが、実際にマクロを書いてみるとGroovyで良かったと思う。
Javaオブジェクトの操作がほとんどなのでRuby的な書き方で楽をできる局面が少ないし、Javaのソース*1から使える部分をパクってマクロにするのはJavaの文法に似ているGroovyの方が楽。

以下は、2chに張ったマクロ。
"home"という名前で作ったjFD2のショートカットに一発でジャンプする。


import com.nullfish.lib.vfs.VFS;
shortCutDirPath = (String)jfd.getCommonConfigulation().getParam("shortcut_dir", "")
try{
file=new File(new File(shortCutDirPath),"home.jfdlnk")
homePath=new String(file.readBytes())
home = VFS.getInstance(jfd).getFile(homePath)
if(home != null){
jfd.getModel().setDirectoryAsynchIfNecessary(home, home.getParent(), jfd)
}
}catch(Exception e){
}
文字列リテラルは使わないでクラスで定義されている QuickAccessCommand.SHORTCUT_DIR などをつかうべき、とか、ショートカットファイルの構造に依存しているので、構造が変わってもいいように ShortCutFileクラスを使うべき、とか、突っ込みどころはあるけれどマクロは簡単に書いてなんぼなのでコレで良い。

*1:jFD2はソースが公開されている

どう書く?org、コードの評価

アカウント取得で管理人さんの手を煩わせてしまったどう書く?org」。Rubyで何件か投稿したところ、コードをプラス評価をして頂けることも増えてきた。
これまでの投稿は、お題からそのまま凝ったことはしていないので、Rubyをある程度使っている人なら大体同じようなコードになると思う*1
他の言語の方から見ると簡潔でいいコードに見えるのかな。
Haskellで書かれたこれとか、めちゃくちゃかっこいいコードなんだけれど、Haskellでは普通なのかも?

*1:もちろん、もっといいコードを書くRubyHackerも世の中には多いはず

NetBeans6.0リリース

Netbeans6.0がリリースされました。
開発者・関係者の皆さんご苦労様でした。
Rubyの開発環境の選択肢が増えたことを素直に喜びたいと思います。
とりあえずRC1->RC2と引き継いだ設定ファイルをコピーして使っていますが快適です。
まあ、自分の使い方ではRCになってから特に問題なかったので当然ですが。
Rubyがサポートされた始めてのバージョンなので、今後の発展に期待します。


ということで、現バージョンについて細かい希望を数点。
(1) Projectsウィンドウでファイルを右クリックした時のコンテキストメニューにRunとDebugが欲しい。
プロジェクトでなくファイルを実行したい時に、いちいちファイルを開いて、そのコンテキストメニューから"Run File"を選んでました。
最近、Projectsウィンドウでも(Shift+F6)が効くことに気付いたけれど、メニューに合った方が判りやすい。
(2) 直前に行ったRunやDebugを簡単に繰り返せるようにして欲しい。
EclipseのRunボタン(直前のRunを再実行、ヒストリからも実行可能)のような仕組みがあると嬉しい。
(3) エディターでクラスの畳み込み表示を。
メソッドは畳み込んで表示できるけれど、クラスはできません。
Javaでは1クラス1ファイルなのであまり意味がないかもしれないが(でもインナークラスは畳み込みたいな)、Rubyでは複数クラスを書くこともあるし、クラスのテストコードを同じファイルに書く時なども畳み込めると便利。