OpenStepが戻ってきた

私は長い間、NeXTSTEP/OPENSTEP/iOSのソフトウェア開発をしてきて、NeXTが作ったOpenStep APIを使い込んできた。

当初はObjective-Cで書かれたフレームワークだったが、AppleによってSwiftで書き替えられた。ただし、ランタイムにはObjective-Cは残っているようだ。

NeXTSTEPの後継OSとして、MacOSが作られた。そしてプラグラミングモデルはOpenStepが採用され、iOSにまでその範囲が拡張された。

私がやっているのは、それとは別のフォークだ。

オープンソースのLinuxの上に、オープンソースのSwiftで、公開されたOpenStep API仕様に準拠したフレームワークを作った。
Objective-C ランタイムのない環境なので、一部、Objective-C時代に身につけたテクニックを使って、動的なメッセージングを行う仕組みを追加している。
また、一部は現代的に、あるいはLinux環境に合わせて、仕様変更している。

これで、NeXTからAppleという流れとは別系統の、NeXTからZIKUUへの流れができたことになる。

ひとまず、デスクトップアプリケーションをOpenStepのプログラミングモデルで開発できる足場を築くことができたので、ここからはZIKUUのシステムを次々にSwiftで書き換えながら、長年の夢でもある、これまで存在しなかったContextual Computingという新しい概念のシステムモデルを作っていくことになる。このフレームワークはその道具になる。

私のところに、OpenStepが戻ってきた。

15年間、小さな一歩を積み上げて、周辺の仕組みを作ってきた末に、このフレームワークの初版完成に至った。

感慨深い。


この初版に含まれる主要なコンポーネント

これはView関連クラスの系譜図だ。OpenStep仕様には含まれないものもあるし、継承関係が違うものもある。

以下、検証用のサンプルアプリのスクリーンショットでそれぞれに言及する。

MenuとMenuBar

OpenStep APIとはクラス構成が異なる。
メニュー、サブメニュー、ショートカットキー、Target/Actionに対応している。

Control

ボタン、トグルボタン、チェックボック、スイッチ、ポップアップメニューなどの、主要なコントロール。

スライダー、ステッパー、セグメントコントロール。
ステッパーとセグメントコントロールはOpenStep APIには定義されていない。

Panel

モーダル/モードレスのパネル、ファイル保存パネル、ファイルオープンパネル。
ファイル操作用のパネルはNeXTSTEP同様にBrowserでファイルシステムをナビゲートする仕組みだ。

Text

テキストフィールドとテキストビュー。
IMEのサポートもできているので、日本語入力が可能だ。
テキストフィールドには、Swift Foundationにあるフォーマッターを設定できるようになっている。
現時点ではAttributedStringに対応していないので、テキストビューはプレーンテキストのみ編集できる。

Image

ImageViewとImageRepの実装。
現時点ではBitmapImageRepにのみ対応。

Container

枠線で囲った領域を表現するボックスと線で仕切られたスプリットビュー。
ボックスのラベルは、上下左右のどこにでも表示できるようにした。

TableView

これはOpenStep API仕様外だが、NeXTSTEPのAppKitに含まれていたテーブルビュー。
EOFに含まれていたDisplayGroupも追加した。

Browser

階層を持つデータを表示するためのブラウザー。

Form

ラベル配置に自由度をもたせたフォーム。


Interface Builder相当のアプリケーションがないので、現時点ではコンポーネントをコードを書いてつなげる必要がある。下のコードは、このサンプルアプリケーションの main のもの。
SwiftでiOS/MacOSのアプリケーションを開発している方なら、「ああ、あれか」と感じるのではないかと思う。

import ZIKUU
import Application
import Core
import Foundation


// アプリケーション共有インスタンス
let app = Application.shared

// アプリケーションプロフィール
let profile = ApplicationProfile.desktop(
    windowSystem: .auto,
    decoration: preferredWindowStyle(),
    renderer: .cairo
)

guard app.configure(profile) else {
    fatalError("Failed to configure WidgetDemo application")
}

// X11バックエンドまたはWaylandバックエンドと接続する
guard app.installDesktopBackend() else {
    fatalError("Failed to install desktop backend")
}

// サンプルコントローラー
let controlsController = ControlsController()
let valuesController = ValuesController()
let textController = TextController()
let imagesController = ImagesController()
let containersController = ContainersController()
let tableController = TableController()
let browserController = BrowserController()
let formController = FormController()

// メインウィンドウ生成
let window = Window()
window.title = "ZIKUU WidgetDemo"
window.initialSize = (width: 800, height: 800)


// Saveメニューのターゲット
final class SaveActionTarget: Monas {
    static let saveSelector = Selector("save")

    static func registerActions() throws {
        try registerMethod(
            saveSelector,
            signature: MethodSignature.signature(
                argumentTypes: [],
                returnType: MethodType(Void.self)
            ),
            handler: { _, _ in
                print("SAVE ACTION EXECUTED")
                return ()
            }
        )
    }
}

try SaveActionTarget.registerActions()
let saveActionTarget = SaveActionTarget()

// メニューの構築
let mainMenu = Menu(title: "Main Menu")

let fileMenu = Menu(title: "File")
fileMenu.addItem(MenuItem(title: "New"))
fileMenu.addItem(MenuItem(title: "Open…"))

let recentMenu = Menu(title: "Open Recent")
recentMenu.addItem(MenuItem(title: "Document A"))
recentMenu.addItem(MenuItem(title: "Document B"))
recentMenu.addItem(MenuItem(title: "Document C"))

let recentItem = MenuItem(title: "Open Recent")
recentItem.submenu = recentMenu
fileMenu.addItem(recentItem)

let saveItem = MenuItem(title: "Save")
saveItem.keyEquivalent = "s"
saveItem.keyModifiers = [.control]
saveItem.action = SaveActionTarget.saveSelector
saveItem.target = saveActionTarget
fileMenu.addItem(saveItem)

fileMenu.addItem(MenuItem.separator())
fileMenu.addItem(MenuItem(title: "Close"))

let fileItem = MenuItem(title: "File")
fileItem.submenu = fileMenu
mainMenu.addItem(fileItem)

let editMenu = Menu(title: "Edit")
editMenu.addItem(MenuItem(title: "Undo"))
editMenu.addItem(MenuItem(title: "Redo"))
editMenu.addItem(MenuItem.separator())
editMenu.addItem(MenuItem(title: "Cut"))
editMenu.addItem(MenuItem(title: "Copy"))
editMenu.addItem(MenuItem(title: "Paste"))

let editItem = MenuItem(title: "Edit")
editItem.submenu = editMenu
mainMenu.addItem(editItem)

window.menuBarView = MenuBarView(menu: mainMenu, frame: Rect(x: 0, y: 0, width: 800, height: 28))

// ビューの配置
let rootView = View(frame: Rect(x: 0, y: 0, width: 800, height: 800))

let tabView = TabView(frame: rootView.bounds)
tabView.autoresizingMask = [.flexibleWidth, .flexibleHeight]

tabView.addTabViewItem(TabViewItem(label: "Controls", view: controlsController.view))
tabView.addTabViewItem(TabViewItem(label: "Values", view: valuesController.view))
tabView.addTabViewItem(TabViewItem(label: "Text", view: textController.view))
tabView.addTabViewItem(TabViewItem(label: "Images", view: imagesController.view))
tabView.addTabViewItem(TabViewItem(label: "Containers", view: containersController.view))
tabView.addTabViewItem(TabViewItem(label: "Table", view: tableController.view))
tabView.addTabViewItem(TabViewItem(label: "Browser", view: browserController.view))
tabView.addTabViewItem(TabViewItem(label: "Form", view: formController.view))

rootView.addSubview(tabView)
window.contentView = rootView

// ウィンドウの表示
window.makeKeyAndOrderFront(nil)

// ランループ開始
app.run()

// ウィンドウスタイル構成
func preferredWindowStyle() -> WindowDecorationStyle {
    let arguments = CommandLine.arguments

    if let index = arguments.firstIndex(of: "--window-style"),
       arguments.indices.contains(index + 1) {
        return parseWindowStyle(arguments[index + 1])
    }

    if let argument = arguments.first(where: {
        $0.hasPrefix("--window-style=")
    }) {
        let value = String(
            argument.dropFirst("--window-style=".count)
        )
        return parseWindowStyle(value)
    }

    if let value = ProcessInfo.processInfo.environment[
        "ZIKUU_WINDOW_STYLE"
    ] {
        return parseWindowStyle(value)
    }

    return .zikuu
}

func parseWindowStyle(
    _ value: String
) -> WindowDecorationStyle {
    switch value.lowercased() {
    case "system":
        return .system

    case "zikuu":
        return .zikuu

    default:
        return .zikuu
    }
}

太字の部分がプログラマーが書くアプリケーションコードで、それ以外は決まり文句みたいなもの。
XXXXXControllerクラスは、別のソースファイルにあり、それぞれは100〜200行程度のコード。

「OpenStepが戻ってきた」への1件のフィードバック

コメントする