Programming」カテゴリーアーカイブ

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 でページめくりもできます。
意外に完成は早くなりそう。

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

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