Gjs unzip

筆者の blog ネタは最近 JavaScript ばかり。
なのに自作アプリの comipoli は Python3 製というのは変だ。

と随分前から思っていたけど、やはり Gjs で作り替えしようと考えた。
PyGObject 製だったのは zipfile モジュールを使っていたからで。

Gjs で作り替えとなるとその代替を考えないと。
cbr で unrar を使っているように unzip を使えばいいだけ、なんだけど。

unzip -Z FILENAME

コマンドでアーカイブの詳細は得られる。
最後の半角空白以降を抜き出せばいいかな。

いやまて、ファイル名に半角空白があった場合はどうなる?
ちゃっと実験用アーカイブを作って実験。

そうなるか、最後の半角空白という手は使えない。
色々試したけど 53 文字以降を取り出せば手持ちファイルは全部イケるようだ。
バージョンによって変わるかもだが、ISF 区切りの 9 番目以降という手もあるし。

ファイルは – で始まっているからコレの見分けは簡単だね。
unzip コマンドの最後に抜き出すファイル名を書けばソレを展開してくれる。
ということでこんなサンプルコードになりました。

#!/usr/bin/gjs

const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;

const archive = "test.zip";

let sp = Gio.Subprocess.new(["unzip", "-Z", archive], Gio.SubprocessFlags.STDOUT_PIPE);
let istream = sp.get_stdout_pipe();
let dstream = new Gio.DataInputStream({
    base_stream: istream
});
dstream.read_line_async(GLib.PRIORITY_DEFAULT, null, async_callback);

function async_callback(source_object, ares) {
    let [line, len] = source_object.read_line_finish_utf8(ares);
    if (line == null) {
        mainloop.quit();
    } else {
        if (line.startsWith('-')) {
            let name = line.slice(53);
            if (GLib.Regex.match_simple("\.js$", name, GLib.RegexCompileFlags.CASELESS, 0)) {
                Gio.Subprocess.new(["unzip", archive, name], Gio.SubprocessFlags.STDOUT_SILENCE);
            }
        }
        source_object.read_line_async(GLib.PRIORITY_DEFAULT, null, async_callback);
    }
}

let mainloop = new GLib.MainLoop(null, false);
mainloop.run();

read_line_async はメインループが必要だから面倒臭いなぁ。
for で回したほうが簡単だけど非同期にしたかったので。
GMainLoop はプロパティが無いのでこの引数指定にするらしい。

てゆーか、やっと上手くいったぞ。
実は随分前から試していて上手くいかなかっただけなんですけど。

Clutter Constraint

Clutter のドキュメントの中で Constraint がよく解らない。
Google 翻訳によると「制約」、っていったい何の制約なのだろう?
検索検索。

Clutter の Constraint (制約条件) – ふとしの日記

Gjs で動かないって new キーワードの場合引数はプロパティを JSON 指定ですし。
JSON 引数に書き換えたら普通に動いた、多分今は自身で解っていると思うけど。
五年前のこの当事はみんな手探り状態だったししかたがないね。

Clutter.SnapConstraint

えっとつまり。
Constraint ってアクションを他の Actor と連動させるって解釈でいいのかな?
この名前が不適切なのか、実は英語ではコレで合っているということなのかシラネ!

#!/usr/bin/gjs

const Gtk = imports.gi.Gtk;
const GtkClutter = imports.gi.GtkClutter;
const Clutter = imports.gi.Clutter;
const Lang = imports.lang;

const ConstraintWin = new Lang.Class({
    Name: 'ConstraintWin',
    Extends: Gtk.ApplicationWindow,

    _init: function(app) {
        this.parent({
            application: app
        });
        this.red = new Clutter.Actor({
            background_color: Clutter.Color.new(255, 0, 0, 255),
            x: 25,
            y: 25,
            width: 50,
            height: 50,
            reactive: true
        });
        this.blue = new Clutter.Actor({
            background_color: Clutter.Color.new(0, 0, 255, 255),
            width: 50,
            height: 50,
        });
        // DnD
        this.red.add_action(new Clutter.DragAction());
        // Constraint
        let bind = new Clutter.BindConstraint({
	        coordinate: Clutter.BindCoordinate.POSITION,
	        offset: 50,
            source: this.red
        });
        this.blue.add_constraint(bind);
        // Embed
        let embed = new GtkClutter.Embed();
        let stage = embed.get_stage();
        stage.add_child(this.red);
        stage.add_child(this.blue);
        this.add(embed);
        this.show_all();
    }
});

// init
GtkClutter.init(null);

let app = new Gtk.Application();
app.connect("activate", function() {
    new ConstraintWin(app);
});
app.run(null);

で赤いブロックをドラッグする。
うん、見事にオフセットされたまま連動してドラッグされるんだね。

ClutterAlignConstraint は Totem のボタン類みたいな用途で使えそう。

#!/usr/bin/gjs

const Gtk = imports.gi.Gtk;
const GtkClutter = imports.gi.GtkClutter;
const Clutter = imports.gi.Clutter;
const Lang = imports.lang;

const TotemLikeWin = new Lang.Class({
    Name: 'TotemLikeWin',
    Extends: Gtk.ApplicationWindow,

    _init: function(app) {
        this.parent({
            application: app
        });
        // Embed
        let embed = new GtkClutter.Embed();
        let stage = embed.get_stage();
        //
        this.red = new Clutter.Actor({
            background_color: Clutter.Color.new(255, 0, 0, 255),
            width: 200,
            height: 50,
            reactive: true
        });
        stage.add_child(this.red);
        // Constraint
        let x = new Clutter.AlignConstraint({
	        align_axis: Clutter.AlignAxis.X_AXIS,
	        factor: 0.5,
            source: stage
        });
        let y = new Clutter.AlignConstraint({
	        align_axis: Clutter.AlignAxis.Y_AXIS,
	        factor: 0.8,
            source: stage
        });
        this.red.add_constraint(x);
        this.red.add_constraint(y);
        //
        this.add(embed);
        this.show_all();
    }
});

// init
GtkClutter.init(null);

let app = new Gtk.Application();
app.connect("activate", function() {
    new TotemLikeWin(app);
});
app.run(null);

いくらリサイズしても常に同じ位置に貼り付ける動作がこんなにアッサリ!
多分こういうのが本来の使い方だと思う。

Clutter Animation

ClutterActor はアニメーション機能を内蔵している。

clutter_actor_save_easing_state # pause
clutter_actor_set_easing_duration # time
# move, resize, opacity, etc…
clutter_actor_restore_easing_state # start

たったコレだけで様々な変更がアニメーションになって動く。
ということでサンプルコード、クリック毎に Actor が入れ替わります。

#!/usr/bin/gjs

const Gtk = imports.gi.Gtk;
const GtkClutter = imports.gi.GtkClutter;
const Clutter = imports.gi.Clutter;
const Lang = imports.lang;

const AnimateWin = new Lang.Class({
    Name: 'AnimateWin',
    Extends: Gtk.ApplicationWindow,

    _init: function(app) {
        this.parent({
            application: app
        });
        this.red = new Clutter.Actor({
            background_color: Clutter.Color.new(255, 0, 0, 255),
            x: 25,
            y: 25,
            width: 50,
            height: 50
        });
        this.blue = new Clutter.Actor({
            background_color: Clutter.Color.new(0, 0, 255, 255),
            x: 75,
            y: 75,
            width: 50,
            height: 50
        });
        // action
        let click = new Clutter.ClickAction();
        click.connect("clicked", Lang.bind(this, function() {
            // pause
            this.red.save_easing_state();
            this.blue.save_easing_state();
            // animation time (default 250)
            this.red.set_easing_duration(2500);
            // move
            let [x, y] = this.red.get_position();
            let [x2, y2] = this.blue.get_position();
            this.red.set_position(x2, y2);
            this.blue.set_position(x, y);
            // play
            this.red.restore_easing_state();
            this.blue.restore_easing_state();
        }));
        // Embed
        let embed = new GtkClutter.Embed();
        let stage = embed.get_stage();
        stage.add_child(this.red);
        stage.add_child(this.blue);
        stage.add_action(click);
        this.add(embed);
        this.show_all();
    }
});

// init
GtkClutter.init(null);

let app = new Gtk.Application();
app.connect("activate", function() {
    new AnimateWin(app);
});
app.run(null);

コレだけだと使いどころがあんまりなさそうだよね。
しかし、ClutterImage の resize が超滑らかになるメリットがあった!
ということで comipoli に早速採用、永遠に実験用アプリ…

JXA NSWindow

今回は JXA でウインドウを作ってみる。

実はウチの PyGObject Tips ページのアクセス数で解るんだけどさ。
ウインドウを作るのページだけブッチギリで多いのよ、初心者ってそんなもんだ。
なんだかなぁ、プログラミングが面白いのはその先からだと思うんですけど。
ということで画像も表示してみる。

検索すると色々な実装があるけど結果は全部同じだね。
それなら簡潔で理解が早い人が多いっぽい実装を選んでみよう。

でも気に入らないのは大半の人がウインドウを閉じると終了するコードだ。
おい macOS だぞ、メニューバーに command+Q は必須だろ?
それと無意味な即時実行多すぎ、多分よく解っていないんだろうけど。

NSApp.servicesMenu in JXA ? GitHub

なんだよ、command+Q メニューはこんなにアッサリ実装できるんかい。
まさかこんなところでバックコーテーションを使うとは。
alloc.init だけなら new メソッドでいいみたい、ふむふむ。

しかし一回しか使わない関数なら無名関数の即時実行でいいのに。
即時実行ってそういう場合に使うと思うんだが。
ということで、こんなサンプルコードになりました。

#!/usr/bin/osascript

ObjC.import("Cocoa");

const imgePath = $("/Users/sasakima-nao/Pictures/[シクシクおよよ]三嶋ゆらら(SSR).jpg");

/**
 * Contents
 */
let image = $.NSImage.alloc.initWithContentsOfFile(imgePath);
if (image.isNil()) {
    console.log("Image Not Found.");
}
let imageView = $.NSImageView.alloc.initWithFrame(
    $.NSMakeRect(0, 0, 320, 400)
);
imageView.setImageScaling($.NSScaleToFit);
imageView.setImage(image);

/**
 * Window
 */
let window = $.NSWindow.alloc.initWithContentRectStyleMaskBackingDefer(
    $.NSMakeRect(0, 0, 320, 400),
    $.NSTitledWindowMask
    | $.NSClosableWindowMask
    | $.NSMiniaturizableWindowMask,
    //| $.NSResizableWindowMask,
    $.NSBackingStoreBuffered,
    false
);
window.title = $("[シクシクおよよ]三嶋ゆらら(SSR)");
window.center;
window.orderFrontRegardless;
window.contentView.addSubview(imageView);

/**
 * Application
 */
let app = $.NSApplication.sharedApplication;
app.setActivationPolicy($.NSApplicationActivationPolicyRegular);
app.mainMenu = function() {
    
    function newMenu(title, action, key) {
       return $.NSMenuItem.alloc.initWithTitleActionKeyEquivalent(title, action, key);
    }
    
    const appName = "Test Window";
    
    const mainMenu = $.NSMenu.new;
    const itemApp  = $.NSMenuItem.new;
    const menuApp  = $.NSMenu.new;
    itemApp.submenu  = menuApp;
    mainMenu.addItem(itemApp);
    menuApp.addItem(newMenu(`Quit ${appName}`, 'terminate:', 'q') );

    return mainMenu;
}();
app.run;

やばい、ゆららちゃんがこんなにカワイイとは!
つい炭酸全部使ってフルマカロン取っ、、、、、いやそれはどうでもよくて。

macOS アプリって Application と Window に何も関連性が無いんだね。
あぁだから macOS では Window を全部閉じてもアプリは終了しないのか。

GTK+, WPF, VCL 等は全部 Application class が Window を管理している。
で、管理する Window が無くなったら Application が終了する、という流れ。
mac は本当に独特なんだなと。

しかしメソッドに括弧が無いのは相変わらず慣れない。
app.run にすら不要って意味ワカンネ!

run JXA in Visual Studio Code

Visual Studio Code (以下 Code)で新たな発見。

他のエディタと同じファイルを開いて他側にて上書き保存。
すると即座に Code は全自動で読み直ししてくれる、凄い!
更に開いているファイルを削除すると全自動でタブを閉じてくれる、凄い!

正直迷惑な場合が多いのですけど…

筆者は macOS で日本語が入力できない Gedit の代用で使っている。
alt(option)+上下矢印キーで行入れ替え
fn+左矢印キー(Home) でインデントの先頭へ移動
shift+tab で選択していなくてもインデント戻し
が可能なエディタをこの2つしか知らないのでしかたがない。

ウザいだけのインテリセンスを無効にして騙し騙し使っていたり。
問題は F5 デバックだ。
そもそも F5 にしている理由は Visual Studio に合わせたのですし。
Gedit で Python スクリプトを debug – L’Isola di Niente

Code で F5 を叩くと、なんで node.js 専用なんだよ!
筆者は JXA しかやるつもりは無い、代用手段は無いのか?

特集:Visual Studio Code早分かりガイド:Visual Studio Codeの使い勝手をよくするツール (1/5) – @IT

あるみたい。
ということで F5 に割り付けしてみよう!

run task をコマンドパレットに打ち込んでも出ない、あれ?
って「タスク」と日本語で打ち込まないといけなくなったようで。
shift+command+B は普通に使えるようだ、って覚えにくいよ。

tasks.json はディレクトリ毎に作られるようで。
とにかく以下のように書き換える。

{
    // See https://go.microsoft.com/fwlink/?LinkId=733558
    // for the documentation about the tasks.json format
    "version": "0.1.0",
    "command": "osascript",
    "isShellCommand": true,
    "args": ["${file}"],
    "showOutput": "always"
}

${file} 変数はココで使えるかどうか見つからないけど実験をかねて。
んで、キーボードショートカットの f5 を上書き。

// 既定値を上書きするには、このファイル内にキー バインドを挿入します
[
    {
        "key": "f5",
        "command": "workbench.action.tasks.build"
    }
]

ここまでやって f5 を叩いてみる。

おぉコレは!
と思ったけどこの機能はファイル単体で開いた時では使えないのね。
使うには毎回 [フォルダを開く] をやらなきゃいけないみたい。
うーん…