JXA NSButton

JXA からの Objective-c バインディングを再勉強。

Building OS X Apps with JavaScript by Tyler Gaw

今回はボタンを押した時のハンドラ指定でも。
target にインスタンス化したデリゲートを指定。
action にハンドラ名を文字列にて指定。
でいいみたい、何だか変な指定方法だけど従うしかない。

ところで現在の JXA は ES6 が使えるので var より let のほうがいい。
ただ全部グローバル変数だとコードが長くなった時にワケワカメになるのよね。
やっぱり class にしたいなぁ。

ObjC.import("Cocoa");
class ButtonWindow extends $.NSWindow {
}
//TypeError: The value of the superclass's
//prototype property is not an object. (-2700)

駄目だ。

ぶっちゃけ Cocoa の API はこう継承するメリットは無さそうだし。
this.window みたいな感じで class にくっつけておけばいいかなと。

それと忘れていたけどアプリにするなら起動パラメーターも得ないと。
まあ少しづつやっていくつもり。

#!/usr/bin/osascript

ObjC.import("Cocoa");

class ButtonWindow {
    constructor(app) {
        ObjC.registerSubclass({
            name: 'WinDelegate',
            protocols: ['NSWindowDelegate'],
            methods: {
                'windowWillClose:': {
                    types: ['void', ['id']],
                    implementation: function(notification) {
                        return app.terminate(0);
                    }
                }
            }
        });
        ObjC.registerSubclass({
            name: 'AppDelegate',
            methods: {
                'onButtonClicked': {
                    types: ['void', ['id']],
                    implementation: (button)=> {
                        // parameter ?
                        button.title = "Click!Click!Click!Click!Click!";
                        // this ?
                        this.window.title = "Click!";
                    }
                }
            }
        });
        let appDelegate = $.AppDelegate.new;
        // NSButton
        this.button = $.NSButton.alloc.initWithFrame($.NSMakeRect(10, 10, 150, 50));
        this.button.title = "Click!";
        this.button.bezelStyle = $.NSRoundedBezelStyle;
        this.button.buttonType = $.NSMomentaryLightButton;
        this.button.target = appDelegate;
        this.button.action = "onButtonClicked";
        // NSWindow
        this.window = $.NSWindow.alloc.initWithContentRectStyleMaskBackingDefer(
            $.NSMakeRect(0, 0, 320, 120),
            $.NSTitledWindowMask
            | $.NSClosableWindowMask
            | $.NSMiniaturizableWindowMask
            | $.NSResizableWindowMask,
            $.NSBackingStoreBuffered,
            false
        );
        this.window.title = $("JXA NSButton");
        this.window.orderFrontRegardless;
        this.window.delegate = $.WinDelegate.new;
        this.window.contentView.addSubview(this.button);
        this.window.makeKeyAndOrderFront(this.window);
    }
}

function run(argv) {
    let app = $.NSApplication.sharedApplication;
    app.setActivationPolicy($.NSApplicationActivationPolicyRegular);
    app.mainMenu = function() {
        function newMenu(title, action, key) {
            return $.NSMenuItem.alloc.initWithTitleActionKeyEquivalent(title, action, key);
        }
        const mainMenu = $.NSMenu.alloc.initWithTitle("Test");
        const itemApp  = $.NSMenuItem.new;
        const menuApp  = $.NSMenu.alloc.initWithTitle("Test2");
        itemApp.submenu  = menuApp;
        mainMenu.addItem(itemApp);
        menuApp.addItem(newMenu('Quit', 'terminate:', 'q') );
        return mainMenu;
    }();
    let window = new ButtonWindow(app);
    app.run;
}

ボタンをクリックするとタイトルバーとボタンの文字が変わります。
よしハンドラ指定方法はコレでいいようだ。

で、やはり絶対位置配置なのでボタン文字列のはみ出しが起こります。
macOS って見た目は洗練されているけど API は古臭いな。

constructor(app) の app ポインタは保持されるようです。
app.terminate(0) をどうしようかと思ったけど試してみるもんです。

class と run を使ったらいかにもアプリのコードって感じになった。
アロー関数で this が保持され一般的な class のように使える。
やっぱり class にしたほうが理解しやすい、this だらけになってもーたけど。

Visual Studio Code 2018

Visual Studio Code がなんだか変だ。
無効にしたはずの迷惑機能が復活している、設定が変わったか?
やり直ししよう。

{
    // ドットを打っても候補が表示されないように
    "editor.suggestOnTriggerCharacters": false,
     // クイック候補の無効化
    "editor.quickSuggestions": {
        "other": false,
        "comments": false,
        "strings": false
    },
    // かわりに tab キー保管を有効化
    "editor.tabCompletion": true,
    // 自動閉じ括弧保管にイライラする
    "editor.autoClosingBrackets": false,
    // モードラインのほうがいい!
    "editor.detectIndentation": false,
    //
    // これで Gedit と同じになる、後は好み
    // ちな Welcome ページは便利なので活用
    //
    // 右端で折り返す
    "editor.wordWrap": "on",
    // 全部タブで開く
    "window.openFilesInNewWindow": "off",
    // 再起動で直近のファイルを開かない
    "window.restoreWindows": "none",
    // ミニマップなんてウザい
    "editor.minimap.enabled": false,
    // 行の強調表示もウザい
    "editor.renderLineHighlight": "none",
}

ほんと mac 版 Gedit で日本語入力ができればこんなことしなくてもいいのに。
コード保管万歳な人は絶対にプログラミングなんてやっていないだろ!

でも Welcome ページって使ってみると便利だよね。
macOS は基本的にファイラーは使わないって方向ですし。
mac が使いづらいって人はファイルをダブルクリックで思考停止していると思う。
アプリで全部管理できればファイラーなんていらないもんね。

更に
task.json のバージョンを変更しようとしたら一部に波線が。

波線の上にカーソルを合わせるとヘルプがポップアップ。
相変わらずやたら親切で笑えるよ。

Tasks in Visual Studio Code

{
    // See https://go.microsoft.com/fwlink/?LinkId=733558
    // for the documentation about the tasks.json format
    "version": "2.0.0",
    "tasks": [
        {
            "label": "JXA Start",
            "type": "shell",
            "command": "osascript",
            "args": [
                "${file}"
            ],
            "group": {
                "kind": "build",
                "isDefault": true
            },
            "presentation": {
                "reveal": "always",
                "panel": "new"
            }
        }
    ]
}

ものすごく多機能になったけどものすごく面倒臭いよ。
いや単機能でいいなら tasks の配列に入れなくてもいいんだけど。

筆者はアプリの設定をやりたいんじゃない、JXA を書きたいんだぃ!
ほんとシンプルだけど強力な Gedit みたくにはできないものか。

Comipoli for macOS

Safari からのアクセスが微妙に増えている気がする。
筆者は Fedora メインで mac はサブなので mac ネタなんてほとんど無いのに。
でもそういえば macOS も bash だし fedora よりユーザーは当然多いし。

Comipoli の macOS 版を JXA で作る計画は一応あるんだけど。
cbz を Finder でサムネイル表示できない macOS で意味あるんだろうか?
てか筆者は Cocoa の知識はほとんど無い。
しかも Objective-c から JavaScript へ変換という情報がほぼ無いことが必要。

そもそも NSWindow の配置はレイアウタなのか絶対位置なのかさえ知らない。
よし解らないなら実験だ、NSImageView を 2 つ置いて並べてみよう。

#!/usr/bin/osascript

ObjC.import("Cocoa");

const P_LEFT = $("/Users/sasakima-nao/Pictures/三嶋ゆらら.jpg");
const P_RIGHT = $("/Users/sasakima-nao/Pictures/優木苗.jpg");

ObjC.registerSubclass({
    name: 'WinDelegate',
    //superclass: 'NSObject',
    protocols: ['NSWindowDelegate'],
    methods: {
        // メソッド名は合体しない
        'windowDidResize:': {
            types: ['void', ['id']],
            implementation: function(aNotification) {
                fitImage();
            }
        },
        'windowWillClose:': {
            types: ['void', ['id']],
            implementation: function(notification) {
                return app.terminate(0);
            }
        }
    }
});

/**
 * func
 */
var fitImage = ()=> {
    let aw = window.frame.size.width;
    let ah = window.frame.size.height;
    let iw = leftView.image.size.width;
    let ih = leftView.image.size.height;
    // left
    let f = leftView.frame;
    f.size = $.NSMakeSize(aw/2, ah);
    leftView.frame = f;
    // right
    f = rightView.frame;
    f.size = $.NSMakeSize(aw/2, ah);
    f.origin = $.CGPointMake(aw/2, 0);
    rightView.frame = f;
}
var createImageView = (path)=> { 
    let image = $.NSImage.alloc.initWithContentsOfFile(path);
    let view = $.NSImageView.new;
    view.setImageScaling($.NSScaleToFit);
    view.setImage(image);
    return view;
}

/**
 * image
 */
leftView = createImageView(P_LEFT);
rightView = createImageView(P_RIGHT);

/**
 * Window
 */
let window = $.NSWindow.alloc.initWithContentRectStyleMaskBackingDefer(
    $.NSMakeRect(0, 0, 320, 400),
    $.NSTitledWindowMask
    | $.NSClosableWindowMask
    | $.NSMiniaturizableWindowMask
    | $.NSResizableWindowMask,
    $.NSBackingStoreBuffered,
    false
);
window.title = $("comipoli");
//window.center;
window.orderFrontRegardless;
window.delegate = $.WinDelegate.new;
window.contentView.addSubview(leftView);
window.contentView.addSubview(rightView);
window.makeKeyAndOrderFront(window);
fitImage();

/**
 * 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;

class 使いたい…

うん絶対位置配置なんだね。
macOS にも unzip はあるし意外に移植は簡単そうだ。
osacompile でアプリにすることもできるはず。

問題はコード分割が現状できないことなんだが。

Gmail…

google アカウントから「ブロックしました」と度々通知が来るようになった。
何だよ、筆者のアカウントなんかハッキングしてもたいしたもんは無いぞ?

何度も来るのでさすがにおかしいと思い通知の時間を元に色々検証。
って、その通知メールを受信している Sylpheed 自身が原因だった。
imap で既読を付けようとする毎にブロックされているようだ。
3.6 は Gmail が要求するセキュリティに合致していないってことかな。

Sylpheed – 軽快で使いやすいオープンソースのメールソフト

3.7 が出ているけどこの件には関係なさそうだなぁ。
安全性の低いアプリへのアクセスを許可もできるみたいだけど。。。。。
今は全アカウントで imap なので何を使ってもいいし。
他を探してもまたブロックされるかもしれないし。

ということで数年ぶりに Evolution を起動。
こいつはブロックされないようだ、当然だと思うけど。

あぁ、GNOME 純正でないのは KeePassX と google-chrome だけになってしまった。
実際 macOS ではこれに Visual Studio Code が入るだけで使ってるし。

十年くらい前に Linux を使い始めてからずっと使い続けているメーラーなのに。
まさかこんな理由で乗り換えするはめになるとはね。
今後もこんな感じでサードパーティの淘汰みたいな流れなんだろうな。

unzip escape

comipoli を gjs 化するので python3 の zipfile が使えない。
ということで unzip コマンドを使う方法に切り替えるのだが
展開する内部ファイル名にブラケット [, ] があると展開できないと気が付いた。

検索するとアスタリスクが駄目なことは皆気がついているようだ、他にもあるかな?
file-roller の GPL ソースを見るのが一番速い。

file-roller/fr-command-zip.c at master ? GNOME/file-roller ? GitHub

[]*?!^-\

こんなにあった、てか記号全部ではないのね。
駄目文字の前にバックスラッシュを付けるだけだし変換は簡単だね。

var ComipoliArchive = class ComipoliArchive {
    // etc...
    _zipEscape(str) {
        const ESCAPE = "[]*?!^-\\";
        let res = "";
        for (let s of str) {
            if (ESCAPE.includes(s)) res += "\\";
            res += s;
        }
        return res;
    }
    getItem(num) {
        let cmdArray = null;
        let pixbuf = null;
        switch (this.status) {
        case 1:
            cmdArray = ["unrar", "p", "-inul", "-@", "--", this.path, this.namelist[num]];
            break;
        case 2:
            cmdArray = ["7za", "x", "-so", this.path, this.namelist[num]];
            break;
        default:
            //cmdArray = ["unzip", "-pj", this.path, this.namelist[num]];
            cmdArray = ["unzip", "-pj", this.path, this._zipEscape(this.namelist[num])];
        }
        let sp = Gio.Subprocess.new(cmdArray, Gio.SubprocessFlags.STDOUT_PIPE);
        let stream = sp.get_stdout_pipe();
        try {
            pixbuf = GdkPixbuf.Pixbuf.new_from_stream(stream, null);
        }
        catch(e) {
            print("Error: " + this.namelist[num] + "\n" + e);
        }
        stream.close(null);
        return pixbuf;
    }
}

展開できた。

しかし cairo 描写に書き換えたら 3D で動く毎に draw シグナルが…
これでは clutter_actor_save_easing_state が使えないや。