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 の関数名は合体せずそのまんま書く。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#!/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 と見た目が同じじゃつまんないもんね。