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

JXA NSToolbar (HeaderBar)

Cocoa にて GNOME アプリの GtkHeaderBar を再現したい。
ようするに Safari のタイトルバーみたいにボタンを置きたい。
GNOME は完全に専用 widget だけど Cocoa ではどうやっているのだ?

調べてみるとアレは実は単なる NSToolbar だ。
配置してタイトルバーを消す設定にすると勝手にタイトルバー化。

よしやってみよう、と探しても Xcode で扱う手段しか見つからない。
えっと Xcode は否定しないけど、皆 Xcode の使い方ばかり覚える方向になっていない?
Visual Studio 使いをソレで笑っていたけど macOS 屋もたいして変わらなかった。

NSToolbarDelegate – AppKit | Apple Developer Documentation

つぎはぎだらけの情報で作り方をまとめると。
NSToolbarDelegate は必須。
Configuring a Toolbar の項目は全部 override 必須。
NSToolbarItem は NSToolbarDelegate 内で作る。
NSToolbarItem の view には NSButton, NSTextField 等を指定できる。
NSLabel なんて無いから NSTextField を使う。
右寄せや中央配置には NSToolbarFlexibleSpaceItemIdentifier を使う。
NSToolbarDelegate の関数名は合体せずそのまんま書く。

実際の作り方はコードで。

#!/usr/bin/osascript

ObjC.import("Cocoa");

ObjC.registerSubclass({
    name: "ToolbarDelegate",
    protocols: ["NSToolbarDelegate"],
    methods: {
        "toolbarAllowedItemIdentifiers:":{
            types: ["id", ["id"]],
            implementation: (toolbar)=> {
                return $([]);
            }
        },
        "toolbarSelectableItemIdentifiers:":{
            types: ["id", ["id"]],
            implementation: (toolbar)=> {
                return $([]);
            }
        },
        "toolbarDefaultItemIdentifiers:":{
            types: ["id", ["id"]],
            implementation: (toolbar)=> {
                console.log("toolbar Default");
                return $.NSArray.arrayWithObjects(
                    $("OPEN"),
                    $.NSToolbarFlexibleSpaceItemIdentifier,
                    $("TITLE"),
                    $.NSToolbarFlexibleSpaceItemIdentifier,
                    $("LR"),
                    $("RIGHT")
                );
            }
        },
        "toolbar:itemForItemIdentifier:willBeInsertedIntoToolbar:": {
            //types: ["id", ["id", "id", "bool"]],
            implementation: (toolbar, itemIdentifier, flag)=> {
                console.log(itemIdentifier.js);
                let item = $.NSToolbarItem.alloc.initWithItemIdentifier(itemIdentifier);
                if (itemIdentifier.isEqualToString($("OPEN"))) {
                    item.view = openButton;
                } else if (itemIdentifier.isEqualToString($("TITLE"))) {
                    item.view = label;
                } else if (itemIdentifier.isEqualToString($("LR"))) {
                    item.view = lrButton;
                } else if (itemIdentifier.isEqualToString($("RIGHT"))) {
                    item.view = thumbButton;
                }
                return item;
            }
        }
    }
});

// buttons Delegate
ObjC.registerSubclass({
    name: "ToolButtonDelegate",
    methods: {
        "onToolButtonClicked": {
            types: ["void", ["id"]],
            implementation: (button)=> {
                label.stringValue = button.title;
            }
        }
    }
});
let appDelegate = $.ToolButtonDelegate.new;

/**
 * ToolBar Item
 */
// open
let openButton = $.NSButton.alloc.initWithFrame($.NSMakeRect(10, 10, 60, 40));
openButton.title = "Open";
openButton.bezelStyle = $.NSRoundedBezelStyle;
openButton.target = appDelegate;
openButton.action = "onToolButtonClicked";
// title
label = $.NSTextField.alloc.initWithFrame($.NSMakeRect(10, 10, 300, 20));
label.drawsBackground = false;
//label.bordered = false;
label.editable = false;
label.selectable = false;
label.stringValue = "Title";
// L/R
let lrButton = $.NSButton.alloc.initWithFrame($.NSMakeRect(10, 10, 60, 40));
lrButton.title = "L<-R";
lrButton.bezelStyle = $.NSRoundedBezelStyle;
lrButton.buttonType = $.NSPushOnPushOffButton;
lrButton.target = appDelegate;
lrButton.action = "onToolButtonClicked";
// thumbnail
let thumbButton = $.NSButton.alloc.initWithFrame($.NSMakeRect(10, 10, 60, 40));
thumbButton.image = $.NSImage.imageNamed($.NSImageNameIconViewTemplate);
thumbButton.bezelStyle = $.NSRoundedBezelStyle;
thumbButton.target = appDelegate;
thumbButton.action = "onToolButtonClicked";

/**
 * ToolBar
 */
let toolbar = $.NSToolbar.alloc.initWithIdentifier($("ToolbarTest"));
//toolbar.allowsUserCustomization = true;
//toolbar.autosavesConfiguration = true;
toolbar.delegate = $.ToolbarDelegate.new;

/**
 * Window
 */
let window = $.NSWindow.alloc.initWithContentRectStyleMaskBackingDefer(
    $.NSMakeRect(0, 0, 600, 400),
    $.NSTitledWindowMask
    | $.NSClosableWindowMask
    | $.NSMiniaturizableWindowMask
    | $.NSResizableWindowMask,
    $.NSBackingStoreBuffered,
    false
);
window.orderFrontRegardless;
// Like a GtkHeaderBar
window.titleVisibility = 1;
window.toolbar = toolbar;

/**
 * Application
 */
let app = $.NSApplication.sharedApplication;
app.setActivationPolicy($.NSApplicationActivationPolicyRegular);
app.mainMenu = function() {
    const mainMenu = $.NSMenu.new;
    const itemApp  = $.NSMenuItem.new;
    const menuApp  = $.NSMenu.new;
    itemApp.submenu  = menuApp;
    mainMenu.addItem(itemApp);
    menuApp.addItem($.NSMenuItem.alloc.initWithTitleActionKeyEquivalent("Quit", "terminate:", "q") );
    return mainMenu;
}();
app.run;

LR ボタンを右に移したけどほぼ同じタイトルバーのできあがり。
JXA だけで作れることは証明できた。

toolbar:itemForItemIdentifier:willBeInsertedIntoToolbar:
の types はコレでいいと思うんだけど何故かエラー。
コメントアウトしても動くのでこれでいいや。

allowsUserCustomization 等を true にするとカスタムできる。
ツールバーを指二本タップで出る奴、って筆者は今まで知らなかったぞ!

だって mac ではボタンなんてほとんど使わないモン。
Dock ですら邪魔なので全部消して一番小さく設定しているくらい。

Toolbar は何のアプリかを一眼で見分ける目印、って感じ。
TextEdit.app と見た目が同じじゃつまんないもんね。

JXA NSUserDefaults

GUI アプリの再起動で前回終了した位置と大きさを再現したい。
Cocoa は NSUserDefaults という超便利なものを用意しているようだ。

EZ-NET: NSUserDefaults でアプリケーションの設定値を管理する : Objective-C プログラミング

まて、JXA で js のまま動かして値を保存するとどうなるんだ?
com.apple.osascript.plist に保存されてしまった、あーあやっぱり。

Windowの位置やサイズを前回終了時の状態に復元する – Xcode coding memo

debug は当然 js のまま行うので上記の超簡単手段は使えないなぁ。
osacompile 後はアプリ名になると思うけど、多分。
つか com.apple.***.plist みたいなのばかりだし誰もやっていないんじゃないの?

NSUserDefaults – Foundation | Apple Developer Documentation

initWithSuiteName という関数があるじゃないの。
これで js のままとコンパイル後で同じ設定が読み書きできるはず。
com.*** のほうがカッコイイし。

// Read
let defaults = $.NSUserDefaults.alloc.initWithSuiteName("com.sasakima.comipoli");
let aw = defaults.floatForKey("width");
if (aw == 0) aw = 400;
let ah = defaults.floatForKey("height");
if (ah == 0) ah = 400;
let x = defaults.floatForKey("x");
let y = defaults.floatForKey("y");
this.window = $.NSWindow.alloc.initWithContentRectStyleMaskBackingDefer(
    $.NSMakeRect(x, y, aw, ah),
//etc...

// Write
let defaults = $.NSUserDefaults.alloc.initWithSuiteName("com.sasakima.comipoli");
defaults.setFloatForKey(this.window.frame.size.width, "width");
defaults.setFloatForKey(this.window.frame.size.height, "height");
defaults.setFloatForKey(this.window.frame.origin.x, "x");
defaults.setFloatForKey(this.window.frame.origin.y, "y");
defaults.synchronize;

イケた。

floatForKey は plist が存在しない場合はゼロが戻るだけみたい。
NSRect で保存する手段もあるみたいだけど plist が存在しない場合にチト困る。

ところで NSApp という定義でアプリケーションが参照できるのね。
sharedApplication の戻り値を得る必要は無かった。

function run(argv) {
    $.NSApplication.sharedApplication;
    // etc...
    $.NSApp.run;
}

んで。
キーボード操作は暫定で以下のように、space, delete でも同様に動くように。
space を使うので装飾は opthion キーしか選択肢が無くなったことは秘密。

icns を作る方法は以下のサイトを参考に、手抜きで画像一つだけでもイケた。
icnsファイルの作り方(Mac) – 2番煎じMEMO

現在ダイアログやもう一つウインドウを出す手段に苦しんでいる。
コレができないと同一ディレクトリ巡回やサムネイル選択が作れない。
あと何やったか忘れた、ここまでのバックアップ。
comimac-0.0.1.tar.gz

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 を参照できない、でいいのかな?
間違えていたらごめんチャイ。

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