JXA」タグアーカイブ

JXA Arrow key part2

Visual Studio Code のメニューをよく見たら矢印キーがあるヤン!
つまり矢印キー等の割り当ては可能ってことだ、もっと探そう。

xcode – How to set function keys as key equivalents programmatically – Stack Overflow

こんなページを見つけた。

Function-Key Unicodes – AppKit | Apple Developer Documentation

ふむふむ。
これでメニューに矢印キーを割り当てできるかも。

// Error
let a = $.NSLeftArrowFunctionKey;
let left = $.NSString.stringWithCharactersLength(a, 1);

駄目だ。

いやまて、ようするに UFT-16LE 文字列にすればいいんだろ?
JavaScript 文字列はそのまんま UTF-16LE じゃないか。
そう思って。

let item = $.NSMenuItem.new;
item.keyEquivalent = "←";

できた、マジか!

いやコレだと英語圏の人達が困る。
NSLeftArrowFunctionKey をなんとか変換できないものか。
って、無理に Objective-c の関数に拘る必要は無かった。

let item = $.NSMenuItem.new;
item.keyEquivalent = String.fromCharCode($.NSLeftArrowFunctionKey);

よし、これでメニューバーが寂しいことにならずにすむ。
keyEquivalentModifierMask に 0 を入れれば ⌘ は消せるのね。

しかし先頭や最後のページに移動はどうしよう?
mac には HOME, END キーが無いんだよなぁ。
TextEdit.app に合わせて command+← だと辻褄が合わなくなるし。

JXA Arrow key

次は改ページ用に space や arrow キーイベントの取得だ。
ようするに改ページなんかの割り当て。

comipoli オリジナルは元々 Eye of GNOME に合わせる目的があった。
今回は macOS なので何に合わせる、って何も無いんだなこれが。

左右矢印キーで改ページはガンガンオンラインや GANMA 等で使われている。
ただ comipoli オリジナルは space キーが一番便利なのでこればかり使う。
おかげで Eye of GNOME で間違えて space キーを、、、は置いておいて。

結局はオリジナルと同様にすることにした。
もちろんフルスクリーンは ommand+control+f に変えるよ。

cocoa – NSMenuItem KeyEquivalent ” “(space) bug – Stack Overflow

space を割り当てる手段は簡単に見つかったけど。
arrow を割り当てる手段は見つからない。

objective c – Using arrow keys in cocoa? – Stack Overflow

NSView から拾うしかなさそう。
macOS 使いはメニューからホットキーを覚えるはずだけどしかたがない。
GtkShortcutWindow みたいなものを自作するかな。

ObjC.registerSubclass({
    name: "ComipoliView",
    superclass: "NSView",
    methods: {
        "acceptsFirstResponder": ()=> {
            return true;
        },
        "keyDown:": (event)=> {
            switch(event.keyCode) {
            case 126: // up
            case 125: // down
            case 124: // right
            case 123: // left
            case 49:  // space
            case 51:  // delete
                console.log(event.keyCode);
                break;
            default:
                //console.log(event.keyCode);
                break;
            }
        }
    }
}

ただメニューバーが寂しいことに。

JXA NSOpenPanel

JXA で「開く」ダイアログを使いたい。
Cocoa の NSOpenPanel というものを使うらしい。

OpenおよびSaveパネルの使用

手段はアッサリ見つかったけど、使うと以下が stderror に出力される。

objc[672]: Class FIFinderSyncExtensionHost is implemented in both /System/Library/PrivateFrameworks/FinderKit.framework/Versions/A/FinderKit (0x7fffb49ddb68) and /System/Library/PrivateFrameworks/FileProvider.framework/OverrideBundles/FinderSyncCollaborationFileProviderOverride.bundle/Contents/MacOS/FinderSyncCollaborationFileProviderOverride (0x109f37cd8). One of the two will be used. Which one is undefined.

どちらの kit を使うかはっきりせぇ!ということらしい。
調べると AppKit 版と sandbox 版があるらしい。
Overview の所を Google 翻訳にて。

NSOpenPanel – AppKit | Apple Developer Documentation

一応、sandbox については以下が解りやすい。
iOS アプリは全部コレだし。

新・OS X ハッキング!(37) これから必須のセキュリティモデル「サンドボックス」 | マイナビニュース

とにかくはっきりせぇ!と言われても手段がわからない。

App Sandbox in Depth

真ん中あたりの Open and Save Dialog Behavior with App Sandbox を見つける。
えっと、このコードだと sandbox で動かせないって警告なのかな。
って JXA はデフォルトにて sandbox で動いているの?
無効にしようと振り分け手段を探しても Xcode からの手段しか出てこない。。。。。

ってよく考えたら GUI アプリなら stderror 関係ないジャン!
以上 sandbox についてのお勉強でした、macOS メンドクサ!

ということで、無視することにして。

ObjC.registerSubclass({
    name: "MenuAction",
    methods: {
        "openFile:": {
            types: ["void", ["id"]],
            implementation: (sender)=> {
                let panel = $.NSOpenPanel.openPanel;
                panel.canChooseDirectories = true;
                //panel.message = $("指定すると TitleBar が現れる");
                panel.allowsMultipleSelection = false;
                panel.allowedFileTypes = $(["cbz","zip"]);
                // sandbox @ Error
                //panel.beginSheetModalForWindowCompletionHandler(window, (res)=> {
                panel.beginWithCompletionHandler( (res)=> {
                    if (res == $.NSFileHandlingPanelOKButton) {
                        let url = panel.URLs.objectAtIndex(0);
                        window.setPath(url.path.js);
                    }
                });
            }
        }
    }
});

これで動く。
上記 stderror を吐くけど気にしない!

beginSheetModalForWindowCompletionHandler は動かない。
sandbox なので NSWindow を参照できない、でいいのかな?
間違えていたらごめんチャイ。

動きました、筆者の凡ミスコードのせいだった。

JXA drawRect

すっかり JXA のネタばかりになってしまった。
だって面白いんだもん、昔 PyGtk を始めた頃を思い出す。
知らなかったフレームワークを使いこなせるようになるとガンガン捗る。
プログラミングの面白さは言語ではなく GTK+, Cocoa 等だよね。

ところで NSImageView を使わなくても画像が表示できることが解った。
てか DnD を実装するのにコイツがあるとチト困るので変えることにした。

ただ手段として NSView の drawRect を override する必要がある。
以前ドン詰まりしたけどもう一度手段を探しまくること 8 時間w

OS X 10.10 Release Notes

なんだよ、公式の一番下にあるじゃん!

プロパティに id を指定できるのか。
それなら NSImage をコッチで保持すれば計算もやりやすくなる。
300 行近くなったので今回は抜き出しコードで。

ObjC.registerSubclass({
    name: "ComipoliView",
    superclass: "NSView",
    properties: {
        firstPage: "id", // NSImage
        secondPage: "id",
        LtoR: "bool",
        spread: "bool"
    },
    methods: {
        "drawRect:": function(rect) {
            if (!this.firstPage.isNil()) {
                let aw = rect.size.width;
                let ah = rect.size.height
                let w = this.firstPage.size.width;
                let h = this.firstPage.size.height;
                // Horizontal?
                if (w - h > 0 || !this.spread) {
                    // Single Page
                    let width, height, x, y = 0;
                    if (aw * h > ah * w) {
                        width = w * ah / h;
                        height = ah;
                        x = (aw - width) / 2;
                        y = 0;
                    } else {
                        width = aw;
                        height = h * aw / w;
                        x = 0;
                        y = (ah - height) / 2;
                    }
                    let r1 = $.NSMakeRect(x, y, width, height);
                    this.firstPage.drawInRect(r1);
                } else {
                    if (!this.secondPage.isNil()) {
                        let left = ah * w / h;
                        if (this.LtoR) {
                            let r1 = $.NSMakeRect(aw / 2 - left, 0, left, ah);
                            this.firstPage.drawInRect(r1);
                            //
                            let w2 = this.secondPage.size.width;
                            let h2 = this.secondPage.size.height;
                            let right = ah * w2 / h2;
                            let r2 = $.NSMakeRect(aw / 2, 0, right, ah);
                            this.secondPage.drawInRect(r2);
                        } else {
                            let r1 = $.NSMakeRect(aw / 2, 0, left, ah);
                            this.firstPage.drawInRect(r1);
                            //
                            let w2 = this.secondPage.size.width;
                            let h2 = this.secondPage.size.height;
                            let right = ah * w2 / h2;
                            let r2 = $.NSMakeRect(aw / 2 - right, 0, right, ah);
                            this.secondPage.drawInRect(r2);
                        }
                    }
                }
            }
        }
    }
}

// etc...

this.comipoliView = $.ComipoliView.new;
this.comipoliView.LtoR = false;
this.comipoliView.spread = true;
this.window.contentView.addSubview(this.comipoliView);

サブクラス内でアスペクト比計算まで完結するコードのできあがり。
注意点はアロー関数を使うと this が変わってしまうってとこだけ。
DnD もこれで簡単に実装できたし完成も近いかな。
バックアップを兼ねてソースも置いておこう、なんか久々。
20180304.tar.gz

次は何を実装するかな、プログラミングが面白すぎる。
おかげで GF(仮) を全然やっていないw
今回はむったん取れるのに、なんかもういいや。。。。。

JXA NSTask

Comipoli では JXA で標準出力から画像データを得る必要がある。
doShellScript が使えるから簡単だね、と思ったら…

こいつだと 1 ライン分(一行目)しか得られないようだ。
てか画像データを直接受け取っても JavaScript にはバイナリのデータ型が無い。
NSData として受け取る必要がある。

JXA で UNIX コマンドを使う方法は cookbook にあるんだけど。
Getting the Application Instance ? JXA-Cookbook/JXA-Cookbook Wiki ? GitHub

NSTask の launch は現在非推奨になっているんだなぁこれが。
NSTask – Foundation | Apple Developer Documentation

Swift では Process.run() という新しい手段ができている。
NSTask Sample for Swift ? GitHub

で、Objective-c, JXA では Process class は使えないと。
つまり Objective-c に NSProcess が追加されるまで launch を使うしかない。

ところで NSTask.launchPath にはフルパスを!と皆書いているけど。
/usr/bin/env を入れればいいと思うのは筆者だけ?
パスが通っていればどこにあってもいいように Python のシバンに使うよね。

てなことで、ちょっと長いけど。

#!/usr/bin/osascript

// osacompile -l JavaScript -o Comipoli.app comipoli.js

ObjC.import("Cocoa");

const PATH = "/Users/sasakima-nao/Documents/もっと GF.cbz";
const PICEXT = /\.(jpe?g|png|gif)$/i;

class ComipoliArchive {
    constructor() {
        this.status = 0;
        this.namelist = [];
    }
    getData(arr) {
        let task = $.NSTask.new;
        let pipe = $.NSPipe.pipe;
        task.standardOutput = pipe;
        task.launchPath = "/usr/bin/env"; // Deprecated
        task.arguments = arr;
        task.launch; // Deprecated
        //task.waitUntilExit; // No!
        let data = pipe.fileHandleForReading.readDataToEndOfFile;
        return data;
    }
    getString(arr) {
        let data = this.getData(arr);
        let str = $.NSString.alloc.initWithDataEncoding(data, $.NSUTF8StringEncoding);
        return str.js;
    }
    newArchive(path, is_unrar, is_7za) {
        this.path = path;
        if (/\.(cbz|zip)$/i.test(path)) {
            this.status = 0;
            this.namelist = [];
            let output = this.getString(["unzip", "-Z", path]);
            let list = output.split("\n");
            for (let line of list) {
                if (line.startsWith("-")) {
                    let name = line.slice(53);
                    if (PICEXT.test(name)) {
                        this.namelist.push(name);
                    }
                }
            }
            this.namelist.sort();
        }
        else {
            return false;
        }
        return true;
    }
    _zipEscape(str) {
        const ESCAPE = "[]*?!^-\\";
        let res = "";
        for (let s of str) {
            if (ESCAPE.includes(s)) res += "\\";
            res += s;
        }
        return res;
    }
    getItem(num) {
        let name = this._zipEscape(this.namelist[num]);
        let data = this.getData(["unzip", "-pj", this.path, name]);
        return $.NSImage.alloc.initWithData(data);
    }
    getLength() {
        return this.namelist.length;
    }
}

class ComipoliWindow {
    constructor(app) {
        ObjC.registerSubclass({
            name: "WinDelegate",
            protocols: ["NSWindowDelegate"],
            methods: {
                "windowDidResize:": {
                    types: ["void", ["id"]],
                    implementation: (aNotification)=> {
                        this.fitImage();
                    }
                },
                "windowWillClose:": {
                    types: ["void", ["id"]],
                    implementation: (notification)=> {
                        app.terminate(0);
                    }
                }
            }
        });
        // var
        this.num = 0;
        this.window = $.NSWindow.alloc.initWithContentRectStyleMaskBackingDefer(
            $.NSMakeRect(0, 0, 600, 500),
            $.NSTitledWindowMask
            | $.NSClosableWindowMask
            | $.NSMiniaturizableWindowMask
            | $.NSResizableWindowMask,
            $.NSBackingStoreBuffered,
            false
        );
        // NSImageView
        this.firstpage = $.NSImageView.new;
        this.firstpage.setImageScaling($.NSScaleToFit);
        this.secondpage = $.NSImageView.new;
        this.secondpage.setImageScaling($.NSScaleToFit);
        this.window.contentView.addSubview(this.firstpage);
        this.window.contentView.addSubview(this.secondpage);
        // set
        this.archive = new ComipoliArchive();
        this.archive.newArchive(PATH, false, false);
        this.nextPage();
        //   
        this.window.title = $("Title");
        this.window.orderFrontRegardless;
        this.window.delegate = $.WinDelegate.new;
        this.window.makeKeyAndOrderFront(this.window);
        this.fitImage();
    }
    nextPage() {
        let item = this.archive.getItem(this.num);
        this.firstpage.setImage(item);
        //
        item = this.archive.getItem(this.num + 1); 
        this.secondpage.setImage(item);
        this.num += 2;
    }
    fitImage() {
        let aw = this.window.contentView.frame.size.width;
        let ah = this.window.contentView.frame.size.height;
        let iw = this.firstpage.image.size.width;
        let ih = this.secondpage.image.size.height;
        // left
        let f = this.secondpage.frame;
        f.size = $.NSMakeSize(aw/2, ah);
        this.secondpage.frame = f;
        // right
        f = this.firstpage.frame;
        f.size = $.NSMakeSize(aw/2, ah);
        f.origin = $.CGPointMake(aw/2, 0);
        this.firstpage.frame = f;
    }
}

function run(argv) {
    const app = $.NSApplication.sharedApplication;
    const window = new ComipoliWindow(app);
    app.setActivationPolicy($.NSApplicationActivationPolicyRegular);
    ObjC.registerSubclass({
        name: "MenuAction",
        methods: {
            "nextPage:": {
                types: ["void", ["id"]],
                implementation: (sender)=> {
                    window.nextPage();
                }
            }
        }
    });
    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("Next Page", "nextPage:", "p", ac));
        //
        return mainMenu;
    }();
    app.run;
}

できた。

command+P でページめくりもできます。
意外に完成は早くなりそう。