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

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

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

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 が使えないや。

yield @ Python3 and JavaScript

我が comipoli はどうせ cairo に書き換えるなら Gjs 化してしまおう。
ということで以前 ClutterImage 作成が遅過ぎでお蔵入りしたソースを引っ張りだした。
ES6 仕様に書き換えをしていて yield でハマったので覚書。

#!/usr/bin/env python3

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GLib

class Dlg(Gtk.Dialog):
    def __init__(self):
        Gtk.Dialog.__init__(self, transient_for=None, title="Comipoli Thumbnail")
        self.show_all()
        # idle
        self.iter = self.iter_func()
        GLib.idle_add(self.iter_idle)

    def iter_idle(self):
        try:
            n = next(self.iter)
            print(n)
            return True
        except Exception as e:
            return False

    def iter_func(self):
        for num in range(100):
            yield num

dlg = Dlg()
dlg.run()
dlg.destroy()

PyGObject

#!/usr/bin/gjs

const GObject = imports.gi.GObject;
const GLib = imports.gi.GLib;
const Gtk = imports.gi.Gtk;

var Dlg = GObject.registerClass({
    GTypeName: "Dlg"
}, class Dlg extends Gtk.Dialog {
    _init() {
        super._init({
            transient_for: null,
            title: "Comipoli Thumbnail"
        });
        this.show_all();
        // idle
        this.iter = this.iter_func();
        GLib.idle_add(GLib.PRIORITY_LOW, ()=> {
            let r = this.iter.next();
            if (r.done) return false;
            print(r.value);
            return true;
        });
    }
    * iter_func() {
        for (let i=0; i<100; i++) {
            yield i;
        }
    }
});
Gtk.init(null);
let dlg = new Dlg();
dlg.run();
dlg.destroy();

Gjs

g_idle_add の引数が違うことは置いておいて。
g_idle_add は何も処理を行っていない時に与えられた関数を実行し続ける関数。
false を戻さないかぎり動き続ける。

Python は例外で判別、JavaScript は next メソッドの戻り値で判別。
yield, next の使い方はまったく同じなので混乱してもーたがね。

検索すると個別で next() を呼んでいる人ばかりで困ったよ。
どう考えても yield でそんな使い方はしないと思うんですけど。
複数ファイル処理で一つ読み終わったらとっとと表示みたいな場合が主だよね。

あと JavaScript のジェネレーター関数はアスタリスクが必要。
無名関数にする手段は無いっぽい、JavaScript を筆者が使うようになった切っ掛けなのに。
でも Python には無名関数はいらないな、そんなの Python じゃない。
適材適所ってことですよ、何でもアリはコードを読み辛くするだけだ。

screentone 2D and 3D

コミックブックアーカイブビューアを作っている筆者でありますが。
スクリーントーンの描写が変になる場合があることに困っていた。

Clutter(OpenGL ES) でリサイズしているのが悪いのかな?
と思い GdkPixbuf をリサイズしてからレンダリングで解決した。
よかったよかった、じゃない!もう少し調べよう。

Jトーン 網点(ドットパターン) J-00|スクリーントーン 通販【ジェイトーン・ネットショッピング】

トーンは上記のサンプル画像をお借りしてと。

#!/usr/bin/env python3

import gi
gi.require_version("Gtk", "3.0")
gi.require_version("Clutter", "1.0")
gi.require_version("GtkClutter", "1.0")
from gi.repository import Gtk, Gdk, GdkPixbuf, Clutter, GtkClutter, Cogl

class DrawWindow(Gtk.Window):
    """
        2D and 3D View Check
    """
    def __init__(self):
        Gtk.Window.__init__(self)
        # tone
        self.pixbuf = GdkPixbuf.Pixbuf.new_from_file("tone.jpg")
        # cairo (2D)
        self.da = Gtk.DrawingArea()
        self.da.connect("draw", self.on_draw)
        # OpenGL ES (3D)
        embed = GtkClutter.Embed()
        stage = embed.get_stage()
        stage.set_background_color(Clutter.Color.new(0, 0, 0, 255))
        stage.connect("allocation-changed", self.on_stage_allocation_changed)
        self.actor = Clutter.Actor()
        image = Clutter.Image()
        image.set_data(
            self.pixbuf.get_pixels(),
            Cogl.PixelFormat.RGBA_8888 if self.pixbuf.get_has_alpha() else Cogl.PixelFormat.RGB_888,
            self.pixbuf.get_width(),
            self.pixbuf.get_height(),
            self.pixbuf.get_rowstride()
        )
        self.actor.set_content(image)
        stage.add_child(self.actor)
        # set
        vbox = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0)
        vbox.pack_start(self.da, True, True, 0)
        vbox.pack_start(embed, True, True, 0)
        self.add(vbox)
        self.connect("delete-event", Gtk.main_quit)
        self.show_all()

    def on_stage_allocation_changed(self, actor, box, flags):
        aw = box.x2
        ah = box.y2
        w = self.pixbuf.get_width()
        h = self.pixbuf.get_height()
        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
        self.actor.set_size(width , height)
        self.actor.set_position(x, y)

    def on_draw(self, widget, cr):
        d_width = widget.get_allocated_width()
        d_height = widget.get_allocated_height()
        cr.set_source_rgb(0, 0, 0)
        cr.rectangle(0, 0, d_width, d_height)
        cr.fill()
        p_width = self.pixbuf.get_width()
        p_height = self.pixbuf.get_height()
        if (d_width * p_height) > (d_height * p_width):
            width = p_width * d_height / p_height
            height = d_height
        else:
            width = d_width
            height = p_height * d_width / p_width
        pixbuf = GdkPixbuf.Pixbuf.scale_simple(self.pixbuf, width, height, GdkPixbuf.InterpType.BILINEAR)
        Gdk.cairo_set_source_pixbuf(cr, pixbuf, (d_width-width)/2, (d_height-height)/2)
        cr.paint()

GtkClutter.init()
DrawWindow()
Gtk.main()

動かす。

上半分が cairo で下が Clutter です。
拡大はまったく同じ描写になるけど縮小は全然違うものになることが解る。
なんだよこの 3D 描写。。。。。

画像処理や 3D に詳しいわけではないので細かい解説はできないけど。
単なるラスターデータとして扱う 2D と RGB として扱う 3D の違いだろう。
網点を RGB 色として認識した結果こんな悲惨なことに。

とにかく 3D では小さめな画像かアニメ調のベタ塗りでないとこんなことになる。
スクリーントーンを多用するコミック表示には徹底的に向いていないようです。

Clutter で cairo が使える手段があるのはそういうことか。
cairo で作り替えしよ。