JXA」タグアーカイブ

JXA NSMenu

今回は JXA でメニューバーとキーボードショートカット。
GNOME は取っ払ってしまったけど macOS ではまだ必須だもんな。

JXAでメニューバーアプリ – Qiita

またデリゲートの登録か。。。。。
action に普通の関数を入れてみたりしたけど動かなかった。
従うしかないな。

それと前回 JavaScript 文字列を NSString に変換していなかった。
アルファベットしか使わないならそれでもいいんだけどね。

ということで前回のコードに書き足しと修正を行なって。
ついでに osacompile でコンパイル実行してみる。

#!/usr/bin/osascript

// osacompile -l JavaScript -o MenuTest.app jxa_menu.js

ObjC.import("Cocoa");

class ButtonWindow {
    constructor(app) {
        ObjC.registerSubclass({
            name: "WinDelegate",
            protocols: ["NSWindowDelegate"],
            methods: {
                "windowWillClose:": {
                    types: ["void", ["id"]],
                    implementation: (notification)=> {
                        app.terminate(0);
                    }
                }
            }
        });
        ObjC.registerSubclass({
            name: "AppDelegate",
            methods: {
                "onButtonClicked:": {
                    types: ["void", ["id"]],
                    implementation: (button)=> {
                        this.window.title = $("Click!");
                    }
                }
            }
        });
        let appDelegate = $.AppDelegate.new;
        // NSButton
        this.button = $.NSButton.alloc.initWithFrame($.NSMakeRect(50, 5, 200, 30));
        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, 300, 50),
            $.NSTitledWindowMask
            | $.NSClosableWindowMask
            | $.NSMiniaturizableWindowMask
            | $.NSResizableWindowMask,
            $.NSBackingStoreBuffered,
            false
        );
        this.window.title = $("Title");
        this.window.orderFrontRegardless;
        this.window.delegate = $.WinDelegate.new;
        this.window.contentView.addSubview(this.button);
        this.window.makeKeyAndOrderFront(this.window);
    }
    changeTitle() {
        this.window.title = $("Menu !");
    }
}

function run(argv) {
    const app = $.NSApplication.sharedApplication;
    const window = new ButtonWindow(app);
    app.setActivationPolicy($.NSApplicationActivationPolicyRegular);
    ObjC.registerSubclass({
        name: "MenuAction",
        methods: {
            "changeTitle:": {
                types: ["void", ["id"]],
                implementation: (sender)=> {
                    window.changeTitle();
                }
            }
        }
    });
    app.mainMenu = function() {
        function nm(title, action, key, target) {
            let item = $.NSMenuItem.new;
            if (target) item.target = target;
            item.title = $(title);
            item.action = action;
            item.keyEquivalent = $(key);
            return item;
        }
        // main
        const mainMenu = $.NSMenu.new;
        // Menubar
        const itemApp  = $.NSMenuItem.new;
        const itemFile = $.NSMenuItem.new;
        mainMenu.addItem(itemApp);
        mainMenu.addItem(itemFile);
        // Drop Down Menu
        const menuApp  = $.NSMenu.alloc.initWithTitle($("Comipoli"));
        const menuFile  = $.NSMenu.alloc.initWithTitle($("File"));
        itemApp.submenu  = menuApp;
        itemFile.submenu = menuFile;
        // Action
        menuApp.addItem(nm("Quit", "terminate:", "q", null));
        //  
        let ac = $.MenuAction.new;
        menuFile.addItem(nm("Change", "changeTitle:", "w", ac));
        //
        return mainMenu;
    }();
    app.run;
}

メニューやキーボードショートカットが動作しているのが解る。

コンパイルするとアプリ名になってくれるようだ。
js のまま実行するとアプリ名が osascript になる。
osacompile はシバンを付けたままでも実行できるようです。

何故かスクリプトエディタ.app からはエラーでコンパイルできなかった。
こんなショボいエディタを使っている人はいないだろうからどうでもいいけど。

とりあえず JXA でアプリを作る雛形はこんなもんでいいかな。

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 だらけになってもーたけど。

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 でアプリにすることもできるはず。

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

jxa doShellScript

Gjs のまとめが全然進まないので久々に macOS で jxa でも。

そういえばテンプレートリテラルって doShellScript で使えるの?
可能なら if 文とか for を利用して複雑なコマンドも使えて便利そうだ。

#!/usr/bin/osascript
 
let app = Application.currentApplication();
app.includeStandardAdditions = true;
let res = app.doShellScript(`if [[ $PWD = $HOME ]]; then
    echo ホームです
else
    echo ホームではありません
fi`);
console.log(res);

使えるジャン。

コレを使えばあの有名な Finder で隠しファイル表示切り替えとかを拡張で一発だな。
いや筆者は普段端末で ls -al を使っているんだけーが。
拡張スクリプトならそっちのが便利そう、ってどんなコマンドだっけ?

Finderで隠しファイルを一時的に表示する(キーボードショートカット) – Qiita

command+shift+. で今はイケるんかい!
拡張を作る必要が無かったよ。
しかしコレは知らない人が多そうだ、覚書ページに追記しとこう。

JavaScript Template literals (Here Document)

さて Fedora 26 での Gjs は何か進展があるのかな?
って下記の更新適用はいつだか知らないんだけど。

gjs – GNOME JavaScript/Spidermonkey bindings

あれ、この Template literals ってなんていうか…
こういうのをヒアドキュメントというんじゃないの?

少なくとも WikiPedia では Python の DocString すらヒアドキュメント扱いだぞぃ。
筆写としては「Python のソレは流石に別物!」と言いたいが、色々な解釈があるなぁと。

テンプレート文字列 – JavaScript | MDN

Gjs の機能ではなく ES6 定義か、それなら Node.js てか V8 でも使えるだろう。
うん、どう見てもどう考えてもどう突っ込まれてもヒアドキュメントだよね。

文法とデータ型 – JavaScript | MDN

なのに上記ではヒアドキュメント未対応ですとハッキリ記述、なんだかなぁ。
しっかり定義してくださいよ Mozilla さん。

#!/usr/bin/gjs

let val = "backquote";

let heredoc = `JavaScript
${val}
Test`;

print(heredoc);

バッククォートです、シングルクォートと間違えないでね。
これは便利、プラス記号で文字列の合体って作る側は「読み難いコード」でしかない。
つか Gedit は普通に色分けするんだ、知らなかった。

Safari も対応ってことで JXA でも使えるようだ。
vscode もしっかり色分け。

ちなみに jjs ではバッククォートの時点でエラーになる。
そりゃ今でも let 宣言にすら対応していないのですし。
現状 JAVA 界隈でもまったく使われていないようだし jjs ってガン無視でいいかも。