Now, I’m writing Switch Control ebook. It desribe how easy to make GUI for AppleScript with Switch Control.
# Book images are working version. Not finished yet.
This book will be sold at booth.pm.
Now, I’m writing Switch Control ebook. It desribe how easy to make GUI for AppleScript with Switch Control.
# Book images are working version. Not finished yet.
This book will be sold at booth.pm.
Double PDF v2.1をMac App Storeに出すべく準備中です。このバージョンは、あくまでアプリケーションの署名周りの問題を解決するだけのリリースであり、機能的な追加を行うものではありません。
Mac用のオープンソースのディスプレイミラーリング・ツール「mirror-displays」を改変して、Xcode上でコードサインしやすく、かつAppleScriptアプリケーション(Xcode上で記述)から呼び出しやすくしたものです。
ディスプレイのミラーリング表示のOn/Offを行うmirror-displaysを、そのままMac App Storeに申請するアプリケーションの中にバイナリで入れようとしたら、Xcode上のValidate(Mac App Storeにアップロードする前段階の各種妥当性チェック)でひっかかってしまいました。
アプリケーションバンドル中のResourcesフォルダに入れてdo shell scriptで呼ぶという「お気楽」な呼び方が(Code Signの問題で)できなかったわけです。
# コマンドライン・ツールとしてビルドするときにCode Signすればよかったんじゃないか、という話もありますが、いずれ最終的にこの形式にする必要があったので、これでいいんじゃないかと
そこで、コマンドラインから呼び出す形式ではなく、Objective-Cのプログラム「らしい」形式に変更して(ヘッダファイルをゼロから書き起こしました)、AppleScriptから呼び出しやすく変更してみました。配布条件がGPLだったので、ここにmirror-displayまわりのソースと最低限の呼び出し側のAppleScriptアプリケーションのプロジェクトを掲載した次第です。
–> Download Xcode Project’s zip-archive
AppleScript名:AppDelegate.applescript |
— — AppDelegate.applescript — mirrorTest — — Created by Takaaki Naganoya on 2020/04/15. — Copyright © 2020 Takaaki Naganoya. All rights reserved. — script AppDelegate property parent : class "NSObject" — IBOutlets property theWindow : missing value on applicationWillFinishLaunching:aNotification — Insert code here to initialize your application before any files are opened end applicationWillFinishLaunching: on applicationShouldTerminate:sender — Insert code here to do any housekeeping before your application quits return current application’s NSTerminateNow end applicationShouldTerminate: on clicked:sender current application’s mirrorObjC’s alloc()’s init()’s mirror() end clicked: end script |
Mac App StoreアプリケーションをAppleScriptで作ったときに、ファイル保存ダイアログを実装するのにchoose file nameコマンドでは拡張子の処理に問題があります(ユーザーが拡張子まで入力しなかった場合への対処を行うと逆効果に)。
choose file nameコマンド ユーザーが選択、入力したパス:Macintosh HD:Users:me:Desktop:aaaaa
Script側で拡張子を補足:Macintosh HD:Users:me:Desktop:aaaaa.kame --> エラー(Sandbox制限による)
その回避策としてありものを引っ張り出してきて手直ししたNSSavePanelベースのファイル保存ダイアログ表示サブルーチンです。
▲choose file nameコマンドで表示したファイル名入力ダイアログ。ダイアログ上でタグの選択はできるがchoose file nameにはタグ受信のための機能がない。また、拡張子が入力されていなかったらそのまま
▲本Scriptで表示したNSSavePanelによる保存ファイル名入力ダイアログ。タグの選択、指定した拡張子の指定などが行える。ユーザーが拡張子を入力していなくても自動で補える
choose file nameによるダイアログも、NSSavePanelによるダイアログも、見た目は同じで見分けはつきません。
普通にAppleScriptを書いて自分で実行している分にはchoose file nameコマンドでも問題はありません。
AppleScript名:ファイル保存ダイアログ(SavePanel)表示 |
— Original by Shane Stanley — Modified 2020-04-22 by Takaaki Naganoya use AppleScript version "2.4" use scripting additions use framework "Foundation" use framework "AppKit" property savePath : "" property saveTag : {} set savePath to "" set saveTag to {} set aParam to {extList:{"kame"}, saveMsg:"新規ファイルの名前と場所を指定", saveName:"Some name", initialDir:(POSIX path of (path to documents folder))} my performSelectorOnMainThread:"showModalSave:" withObject:(aParam) waitUntilDone:true return {savePath:savePath as string, saveTags:saveTag as list} –> {savePath:"/Users/me/Desktop/Some name.kame", saveTags:{"ブルー", "ヤンキー"}} on showModalSave:sender set tmpExt to extList of sender as list set tmpMsg to saveMsg of sender as string set defName to saveName of sender as string set tmpInit to initialDir of sender as string using terms from scripting additions set startURL to current application’s NSURL’s fileURLWithPath:(tmpInit) end using terms from set thePanel to current application’s NSSavePanel’s savePanel() tell thePanel its setMessage:(tmpMsg) its setAllowedFileTypes:(tmpExt) its setNameFieldStringValue:(defName) its setShowsHiddenFiles:false its setTreatsFilePackagesAsDirectories:false –its setDirectoryURL:startURL–指定しないほうが前回呼び出し時と同じフォルダが表示されて便利 set returnCode to its runModal() as integer end tell if returnCode = (current application’s NSFileHandlingPanelOKButton) as integer then set my savePath to thePanel’s |URL|()’s |path|() if (thePanel’s respondsToSelector:"tagNames") as boolean then set my saveTag to thePanel’s tagNames() end if else — cancel button error -1 end if end showModalSave: |
Xcode上で作成したAppleScriptアプリケーションでDark Modeの検出を行いたいときに、NSAppearance’s currentAppearance()で取得したら、正しくModeの検出が行えませんでした。同じコードをスクリプトエディタ/Script Debugger上で動かした場合には正しくModeの判定が行えているのですが。
そこで、System Eventsの機能を用いてMode判定を行うように処理を書き換えたりしてみたのですが、Mac App Storeに出すアプリケーションでこの処理を記述していたら、これを理由にリジェクトされてしまいました。
仕方なく解決策を探してみたところ、macOS 10.13用に書いたshell scriptによる迂回処理を、そのまま他のOSバージョンでも動かせばよいのではないかと気づき、結局そこに落ち着きました。
AppleScript名:Dark Modeの検出(Xcode上でも正しく判定) |
— – Created by: Takaaki Naganoya – Created on: 2020/04/22 — – Copyright © 2020 Piyomaru Software, All Rights Reserved — use AppleScript version "2.4" — Yosemite (10.10) or later use framework "Foundation" use scripting additions set apRes to retLightOrDark() of me –> true (Dark), false (Light) on retLightOrDark() try set sRes to (do shell script "defaults read -g AppleInterfaceStyle") return (sRes = "Dark") as boolean on error return false end try end retLightOrDark |
ふだん、AppleScriptの処理はOSによる制約の少ない環境で処理を行っています。サンドボックスによる制約がない、あるいはきわめて少ない環境であるといえます。
そうした自由な環境に慣れていると、Sandbox環境下で(Xcode上で)アプリケーションを作成したときに、あっと驚く制約が存在していて驚かされることが多々あります。
Sandbox環境下では、他のアプリケーションを操作するAppleScriptについては、実行専用形式で保存したうえでファイル書き込み不可の状態にしておく必要があります。Scriptのプロパティ(resultもプロパティ)を書き換えられてはいけないので、この措置が必要になります。Sandbox環境ではこのプロパティの書き換えが、自己書き換えと判定され、禁則事項に該当してしまうためです。
ファイルの新規保存時にchoose file nameコマンドで保存先のパス+ファイル名をユーザーに指定させるような処理を行っています。このときに、ファイルの拡張子が指定されていない場合には、choose file nameから返ってきたパスを文字列に変換して、拡張子を文字列で追加するような処理もよく行っています。
これが、Sandbox環境下では禁じ手になります。
最初に遭遇したときには「意味がわからない!」と、イラつきまくりましたが、落ち着いて考えつつ追試を行ってみたところ理解できました。
Sandbox化したアプリケーションでファイル保存を行うためには、Xcode上でentitlementsファイルを編集し、「com.apple.security.files.user-selected.read-write」といったエントリに「YES」を設定しておくことになります。ユーザーが選択したファイルの読み書きを許可するという設定です。
このとき、choose file nameコマンドで入力されなかった拡張子の部分をAppleScript側で勝手に補ってしまうと、「ユーザーが指定したファイル」以外のファイルを処理することになるわけで、(拡張子を文字列操作で補ったようなパスは)ファイル保存することができませんでした。
そこで、NSSavePanelを用いてファイル保存ダイアログを作成する必要に迫られます。事前に補うべき拡張子の情報を渡してダイアログ表示を行うので、ユーザーが拡張子まで入力しなくても、拡張子がついたファイルパスが返ってきます。
ほかにもいろいろありますが、作成するAppleScriptのプログラム全体の数から見ると、Sandbox環境下で動かすAppleScriptはごく一部であるため、あまりノウハウが蓄積されていません。たまーにSandboxの制約に抵触して驚かされるといったところでしょうか。
本Blogにちょくちょく、その作例を掲載している6角形図形を組み合わせた図でアイデアを練るソフトウェア「Kamenoko」をMac App Storeのレビューに送信しました。2作目なので、レビューの概要もわかっており(1発で通ることはないので)前回のように却下に対して24時間即時対応、みたいな体勢にはしていません。しばらくApp Storeの審査とやりとりする必要があることを理解しています(1週間ぐらいでしょうか)。
目下App Store系の審査の担当者の人数が少ないために審査にかかる時間が長くなっているとの噂がありますが、これまでもMac App Storeの審査は時間がかかっていたので(前回1か月かかったし)、そんなもんだと思っています。
このKamenoko v1.0はAppleScriptからのスクリプティングには対応していません。ただ、中身はほとんど(99.9%)AppleScriptで書いてあるので、対応は難しくはありません。
難しくはないものの、このアプリケーションはAppleScriptによって操作してもあまり意味がないので(アイデアを練るためのアプリケーション)、書類の作成とか書類の内容取得、書類の内容変更などが行えるライブラリをアプリケーションに同梱する方向でAppleScriptサポート機能の実装を検討しています。
System Eventsを呼んでいることを問題視されているようです。当初、このアプリケーションでSystem Eventsを呼び出す必要はなかったのですが、Dark Modeの検出がmacOS 10.13でうまく働かなかったので、System EventsでDark Modeの検出を行わせるようにしていました。
Mac App Storeのレビューでは、指摘事項同士の矛盾については考慮されていません。(A)という指摘をされたので(B)という対応をしたら、説明の仕方を変更するだけで実は(A)の状態のままでよかった、ということがままあります。また、アプリケーション全体で複数の指摘項目があったとしても、1回のレビューで指摘されるのはだいたい1項目だけです。
なので、1項目の指摘事項があったので即時修正したとしても、さみだれ式に次々と指摘事項が(枝葉末節の問題から、徐々にセキュリティ系の事項へと)上がってくるものなので、「そういうもんだ」と割り切っていないとやりきれません。
System Eventsに依存しないAppearanceの判定を行わせると、macOS 10.13でDark Modeの検出ができなくなる可能性があり、macOS 10.13でDark Modeの検出ができなくなると、macOS 10.13対応が不適切であるという指摘が未来のタイミングで行われ、結局macOS 10.13対応を行うためにはSystem Eventsを呼ぶしかなかった、という結論に落ち着きそうな感じがとっても嫌な雰囲気です。
ただ、これまで1発目のレビュー(リジェクト)とかいうと、10分ぐらいであっという間に終わっていたので(溢れる「流れ作業」感)、1時間もかかったのは新記録ではないでしょうか。お茶飲みながらの1時間かもしれませんけれども。
のちのちのバージョンでやりたい(実現できるとは言っていない)機能のために宣言しておいたEntitlementsが、アプリケーションの機能に存在していないので却下、と判断されました。
あれ? AppleのWeb側にもXcode上にもそんな設定は(残ってい)ないのですがー(ーー;;;
Cocoaの機能やサードパーティのFrameworkの機能を呼び出して、ディスプレイの回転コントロールを行なったり、解像度の変更を行うなどの制御がAppleScript単体でできるようになってきましたが、ディスプレイの位置関係を定義して変更したいとか凝ったことを言い出すと、さすがにまだまだ市販のツール類を併用したほうが話が早かったりします。
実際に、「システム設定」アプリをGUI Scriptingで強引に操作したり、Objective-Cなどのプログラムを呼び出したとしても、相当に苦労する箇所です。SwitchResXを買って解決できるなら、それで解決すべきだと考えます。最近のシステム設定.appは作りがおかしいので、GUI Scriptingで詳細な設定の変更は行えません。
以前からSwitchResX(16USドル)がAppleScriptからコントロールできることを知っていましたが、どの程度のコントロールが行えるかについては調べたことはありませんでした。どのように使えて、どのようにコントロールできて、どのような点に注意すべきなのかを実際に調べてみました。
SwitchResXの解像度変更機能自体は水準通りだと思います。この手のツールは割といろいろ存在しており、自分はQuickResを利用しています。どちらのツールも画面のオーバースキャン(実解像度よりも大きな解像度の表示)ができます。画面の回転や色数、画面の相対位置の変更、アプリケーションごとに解像度を変更する機能などが本ツールの特徴でしょう。
画面解像度の変更機能について1つ、QuickResを大幅に上回っているものがありました。HiDPI(Retina解像度)の表示サポートです。非Retina解像度のディスプレイであっても、表示ピクセル数を抑えることでHiDPI表示を可能とするもので、表示すること自体にはあまり意味はありませんが、「HiDPI解像度の画面キャプチャが行える」というメリットがあります(非Retina環境でRetina解像度の画面キャプチャが行えるのは、資料作成時にとても便利)。このあたり、Mac miniでちょっと困らされた点でした。 Mac mini 2014(macOS 10.15.4)+HDMI Display(1980x1080)の組み合わせで、OSが標準で提供しているのは960x540解像度におけるHiDPI表示ですが、SwitchResXは1280x800 / 960x600 / 840x525 / 1280x720 / 800x450 などの解像度におけるHiDPI表示もサポートしています。
SwitchResXはメニューバーに常駐して、解像度設定の切り替えが行えるようになっています。設定自体はこのメニューバーから「SwitchResX Preferences…」を選択するか、システム環境設定の「SwitchResX」を選択すると起動される「SwitchResX Control」というGUIアプリケーションから行います。
SwitchResXでは、画面解像度、色(カラー、グレイ)、表示ON/OFF、ミラーリング、メインディスプレイ、画面回転、カラープロファイルなどを設定したディスプレイセットを作成し、このディスプレイセットをメニューやキーボードショートカット、コンテクストメニューから切り替えられるようになっており、その一環としてAppleScriptから同様にディスプレイセットの切り替えが行えるようになっています。
SwitchResXのAppleScript用語辞書は以下のとおりです。
AppleScriptによるコントロールは「SwitchResX Daemon」という不可視アプリケーションプロセスに対して行うようになっており、基本的には各種ディスプレイの状態を取得(Read Only)することと若干の基本的なコマンドの実行と、ユーザーが定義したディスプレイセットの切り替えを行う、という内容です。
AppleScript側からこまかくディスプレイのIDを指定して個別にグレースケール表示を行わせるといったことはできません。逆に、指定したIDのディスプレイの現在の状況(properties)を取得することはできます。そういう感じです。
ディスプレイセットを新規作成したあとで、GUI側から明示的に保存(Command-S)を実行しないと、追加したディスプレイセットは認識されませんでした。ここは、たいへんに注意が必要です(なかなか気づきませんでした)。
ディスプレイの回転や位置関係の変更など、込み入った設定変更を行ったディスプレイセットを作成した場合には、それらを元に戻したディスプレイセットを作成しておく必要があります。restore desktopコマンドで元の状態に戻してくれるのかと思っていたのですが、そういうものではないようです。
こういうツールを使おうかと考えているユーザーは複数のディスプレイをMacにつないで使っているはずなので、本ツールで一気に切り替えたあと、戻す設定が定義されていないと手作業で元に戻すというScripterにとってはたいへんに屈辱的な作業を強いられることになります。
ディスプレイセットには、すべての設定項目が同居できるというわけではなく、特定の設定項目のみが単独で切り替えられるというものもあるようなので、この点も注意が必要です(注意点ばっかりやな)。
AppleScriptのプログラム中からSwitchResXの機能を呼び出して画面構成の変更を自由に行えるわけですが、個人的には「そこまでやる必要があるのか?」という印象です。あと、AppleScript用語辞書にサンプルScript掲載しとけとか画面キャプチャのグラフィックを入れておかないとわかりにくいので、そのあたり改善の余地ありといったところでしょうか。他人のアプリケーションについては、山のように不満点や改善点が出てくるものです。
あと、SwitchResXを操作するAppleScriptをどこから呼び出すことになるのか、については少し興味があります。macOS標準搭載のスクリプトメニューから呼び出すのが一番「ありそう」な利用パターンですが、それ単体で呼び出すと「隣り合ったSwitchResXのメニューから呼び出すのとほぼ等価な内容をスクリプトメニューに入れたAppleScriptから呼び出す」という間抜けな状態になります。
まとまった自動処理を行うのに必要な画面設定(速度重視のために画面解像度を極端に落とすとか、メニュー操作が想定外の動きを行わないように特定の解像度に明示的に変更するとか)を行い、処理本体を実行したうえで、後処理で画面設定を元に戻しておく、といったところでしょうか。
AppleScript名:ディスプレイを数えて、プロパティを取得 |
tell application "SwitchResX Daemon" set dCount to count every display –> 2 repeat with i from 1 to dCount properties of display i –> {built in:false, position:{0, 0}, name:"Cinema HD Display", class:display, brightness:-1.0, id:69501836, mirroring:{}, enabled:true, overscan:false, index:1, current mode:mode 1 of display 1 of application "SwitchResX Daemon", orientation:0, underscan:1.0, current depth:32, display profile:profile "Cinema HD Display" of application "SwitchResX Daemon"} –> {built in:false, position:{-1920, 0}, name:"Cinema HD", class:display, brightness:-1.0, id:69513479, mirroring:{}, enabled:true, overscan:false, index:2, current mode:mode 1 of display 2 of application "SwitchResX Daemon", orientation:0, underscan:1.0, current depth:32, display profile:profile "Cinema HD" of application "SwitchResX Daemon"} end repeat end tell |
▲display set 2だと2行目の「disp set 2 (90 degree rotation)」が指定される
AppleScript名:display set の切り替え |
tell application "SwitchResX Daemon" set dSet to display set 2 apply dSet end tell |
AppleScriptでダイナミックに(動的に)生成したアラートダイアログ上にテキストビューを生成し、そのテキストビュー上にテキスト入力用のキャレットを移動させる(フォーカスする)AppleScriptです。
Xcode上でAppleScriptでGUIアプリケーションを作っていて、文字サイズの大きなテキスト入力フィールドを動的に生成。さらに、テキスト入力フィールドにテキスト入力用のキャレットを移動させようとして、割と手こずっていました。
テキスト入力を受け付けるGUI部品に対して、入力フォーカスを与え、テキスト入力のキャレットが置かれた状態を再現するには、GUIの画面上からはマウスカーソルでクリック操作するとできます。同じ状態をプログラム側から作る場合にはFirstResponderにセットするという処理になります。
この、FirstResponderに指定することは理解でき、Xcode(Interface Builder)上で配置した部品に対して実行する処理は昔からやっています。NSTextFieldに強制的に入力フォーカスを置いた上でバーコードリーダーからのテキスト入力を受け付けるといった処理です。
一方、プログラムから動的に生成したGUI部品に対して、Cocoaが用意しているAPIのどれを用いて、どのように指定すべきなのかで困りました。
親WindowのFirstResponderにしてみたり、アラートウィンドウのFirstResponderにしてみたり、いろいろ試した末に、
parentWin's setInitialFirstResponder:aView
で、Alert DialogのWindowをparentWinに、alert dialog上のテキストビューをaViewに代入した状態で実行して、入力キャレットをテキストビュー上に移動できました。本Xcode Projectは上記の1行の記述の検証を行うためのテストプロジェクトです。
AppleScript名:AppDelegate.applescript |
— — AppDelegate.applescript — dialogCarret — — Created by Takaaki Naganoya on 2020/04/14. — Copyright © 2020 Takaaki Naganoya. All rights reserved. — script AppDelegate property parent : class "NSObject" property |NSURL| : a reference to current application’s |NSURL| property NSFont : a reference to current application’s NSFont property NSView : a reference to current application’s NSView property NSAlert : a reference to current application’s NSAlert property NSColor : a reference to current application’s NSColor property NSTextView : a reference to current application’s NSTextView property NSScrollView : a reference to current application’s NSScrollView property NSRunningApplication : a reference to current application’s NSRunningApplication — IBOutlets property theWindow : missing value property returnCode : 0 property resStr : "" on applicationWillFinishLaunching:aNotification — end applicationWillFinishLaunching: on applicationShouldTerminate:sender return current application’s NSTerminateNow end applicationShouldTerminate: on clicked:sender set tmpStr to "TEST" set paramObj to {myMessage:"Input Text", mySubMessage:"", mes1:(tmpStr), mesWidth:400, mesHeight:200} my performSelectorOnMainThread:"dispTextViewWithAlertdialog:" withObject:paramObj waitUntilDone:true set aResText to (my resStr) as string display dialog aResText end clicked: on dispTextViewWithAlertdialog:paramObj –Receive Parameters set aMainMes to (myMessage of paramObj) as string –Main Message set aSubMes to (mySubMessage of paramObj) as string –Sub Message set mesStr to (mes1 of paramObj) as string –Text Input field 1 Label set aWidth to (mesWidth of paramObj) as integer –TextView width set aHeight to (mesHeight of paramObj) as integer –TextView height set vNum to system attribute "sys2" if vNum > 13 then set apRes to retLightOrDark() of me if apRes = true then set textColor to (current application’s NSColor’s cyanColor()) else set textColor to (current application’s NSColor’s blackColor()) end if else set textColor to (current application’s NSColor’s blackColor()) end if — Create a TextView with Scroll View set aScroll to NSScrollView’s alloc()’s initWithFrame:(current application’s NSMakeRect(0, 0, aWidth, aHeight)) set aView to NSTextView’s alloc()’s initWithFrame:(current application’s NSMakeRect(0, 0, aWidth, aHeight)) aView’s setDelegate:me aView’s setRichText:true aView’s useAllLigatures:true aView’s setTextColor:textColor aView’s setFont:(current application’s NSFont’s systemFontOfSize:90.0) set aColor to current application’s NSColor’s colorWithDeviceRed:0.0 green:0.0 blue:0.0 alpha:0.1 aView’s setBackgroundColor:aColor aView’s setString:mesStr aScroll’s setDocumentView:aView aView’s enclosingScrollView()’s setHasVerticalScroller:true — set up alert set theAlert to NSAlert’s alloc()’s init() tell theAlert its setMessageText:aMainMes its setInformativeText:aSubMes its addButtonWithTitle:"OK" –its addButtonWithTitle:"Cancel" its setAccessoryView:aScroll set parentWin to its |window|() end tell parentWin’s setAlphaValue:0.8 –parentWin’s setOpaque:(false) parentWin’s setLevel:(current application’s NSScreenSaverWindowLevel) parentWin’s setInitialFirstResponder:aView —Place Input Carret to Alert Dialog — show alert in modal loop NSRunningApplication’s currentApplication()’s activateWithOptions:0 my performSelectorOnMainThread:"doModal:" withObject:(theAlert) waitUntilDone:true if (my returnCode as number) = 1001 then — else set my resStr to aView’s |string|() end if end dispTextViewWithAlertdialog: on doModal:aParam set (my returnCode) to aParam’s runModal() end doModal: on retLightOrDark() return true end retLightOrDark end script |
システムフォントの名称を取得するAppleScriptです。
GUIベースのアプリケーションを作っているときに、「もう、無難なフォントなんでもいいからとりあえず指定できるものがあれば指定しておこうよ」という局面はあります。ないフォントを指定するとクラッシュすることもあるので、とりあえずコレ指定しておけば大丈夫だから!
という「安全パイ」のフォントとしてSystem Fontを取得&指定したいという時に書いたScriptでもあります。プログラム内容がつまらない割には、切実なニーズを満たすためのものです。
AppleScript名:システムフォントの名称を取得.scptd |
— – Created by: Takaaki Naganoya – Created on: 2020/04/06 — – Copyright © 2020 Piyomaru Software, All Rights Reserved — use AppleScript version "2.4" — Yosemite (10.10) or later use framework "Foundation" use framework "AppKit" use scripting additions set aFont to current application’s NSFont’s systemFontOfSize:24.0 set aInfo to aFont’s fontDescriptor() set aSize to (aInfo’s pointSize()) as real set aPSName to (aInfo’s postscriptName()) as string –> ".SFNSDisplay" set bFont to current application’s NSFont’s boldSystemFontOfSize:24.0 set bInfo to bFont’s fontDescriptor() set bSize to (bInfo’s pointSize()) as real set bPSName to (bInfo’s postscriptName()) as string –> ".SFNSDisplay-Bold" |
自前でメニューを作ってフォントおよびサイズの選択を行おうとするとめんどくさい(時間がかかるし管理も大変)ので、NSFontPanelを用いてフォントを選択する試作品を作ってみました。
ただし、まだ完全ではなく「1つ前の状態が取得できる」という状態なので、修正が必要です。作成はmacOS 10.14.6+Xcode 11.3.1上で行っています。
AppleScript名:AppDelegate.applescript |
— — AppDelegate.applescript — fontPanel — — Created by Takaaki Naganoya on 2020/03/12. — Copyright © 2020 Takaaki Naganoya. All rights reserved. — script AppDelegate property parent : class "NSObject" — IBOutlets property theWindow : missing value property theField : missing value property aFontNameField : missing value property aFontSizeField : missing value property aFontManager : missing value property font : missing value property aFP : missing value on applicationWillFinishLaunching:aNotification set aFontManager to current application’s NSFontManager’s sharedFontManager() aFontManager’s setAction:"appSpecificChangeFont:" set aFP to current application’s NSFontPanel’s sharedFontPanel() set aFont to current application’s NSFont’s fontWithName:"Helvetica" |size|:16 aFontManager’s setSelectedFont:aFont isMultiple:false theField’s setStringValue:"ぴよまるソフトウエア, Piyomaru Software" end applicationWillFinishLaunching: on applicationShouldTerminate:sender — Insert code here to do any housekeeping before your application quits return current application’s NSTerminateNow end applicationShouldTerminate: on clicked:aSender theWindow’s makeFirstResponder:theField aFP’s makeKeyAndOrderFront:me end clicked: on appSpecificChangeFont:sender set aSelFont to sender’s selectedFont() set aDesc to aSelFont’s fontDescriptor() set aPSID to (aDesc’s postscriptName()) as string set aSize to (aDesc’s pointSize()) as real theField’s setFont:aSelFont aFontNameField’s setStringValue:aPSID aFontSizeField’s setStringValue:(aSize as string) end appSpecificChangeFont: end script |
新型コロナウィルスの蔓延により、リモートワークでビデオ/オーディオチャットを行う機会が増え、音量調整のためのいい方法がないと嘆いている方が多いようなので、便利な方法をご紹介します。
macOSにはアクセシビリティ系(障害者支援)の機能が多々用意されており、その中核をなす「スイッチコントロール」という機能があります。この機能を利用して、画面上に操作可能な機能ボタンを貼り付けておくことができるため、オーディオ系の音量調整機能などをここに割り振って利用すると便利です。
# 業務系のScriptをフローティングパレット表示しておいて、ワンクリックで実行できるようにしておくと、とても便利だと思います
このスイッチコントロールの機能はユーザーごとに自由にカスタマイズすることができ、さらにその内容を書類に書き出して他のユーザーと共有できるようになっています。
それがパネル構成書類です。これはmacOS標準装備の機能「スイッチコントロール」の書類で、ボタン定義データやボタンから呼び出すAppleScriptが内包されています。
–> Download switchControlUserPanels1.ascconfig(12KB)
[Update] English Version included in same panel
このパネル構成書類はmacOS 10.14上で作成し、macOS10.14.6およびmacOS 10.15.4上での動作を確認しています。
Zipアーカイブから展開して、ユーザーの~/Library/Application Support/com.apple.AssistiveControlフォルダに入れてください。macOS 10.15上では展開後.ascconfig書類がフォルダに見えるかもしれませんが、気にせずそのまま上記フォルダに移動してください。
# 「com.apple.AssistiveControl」フォルダがApplication Support内に存在しない場合には作成してください。このフォルダ名を間違えるとOSに認識されないので、ご注意ください
「システム環境設定」を起動。
「アクセシビリティ」を選択。
左の一覧リストの下の方にある「スイッチコントロール」を選択。
「スイッチコントロールを有効にする」にチェックを入れます。この際に、管理者パスワードを求められる場合があります。
フローティング操作パレット「スイッチコントロール」のホーム画面が表示されるので、「カスタム」をクリック。
カスタムの中にさきほど追加した「入出力音量調整」があるので、それをクリック(”Sound Volume Control” is English version)。
これで、フローティングパレットの入出力音量調整パレットが表示されます。
▲SwitchControlの操作方法。macOS標準のGUI体系とは若干異なる
パレット上の各ボタンをクリックするか、あるいはMacのサブディスプレイとしてiPadを接続している場合には、iPadの画面上を指でタップすると各ボタンの機能が実行されます。
スイッチコントロールのフローティングパレットはパレット左上の「x」をクリックすると消去できます。いったん消去しても、ふたたび「システム環境設定」>「アクセシビリティ」>「スイッチコントロール」に戻って、「スイッチコントロールを有効にする」のチェックを入れると表示されます。
iPadをMacのサブディスプレイとして利用するアプリケーションは、Apple純正のSide Car(macOS 10.15標準装備)のほか、サードパーティ製の「duet display」や「AirDisplay」などが有名です。
とくに、サードパーティ製のアプリケーションは、古めのiOSや古めのiPadをサポートしている(Apple純正機能は最新のiPadしかサポートしていない)ため、身の回りに転がっている古いiPadや中古のiPadをMacのタッチ操作コントローラーにお安く利用できるため、とてもおすすめです。
REST API呼び出しに欠かせない部品をアップデートしました。URLキャッシュが効くようになったような気がします。
–> GET method REST API v4.4.1
–> GET method REST API v4.4
–> GET method REST API v4.3
–> GET method REST API v4.1
–> GET method REST API v4
前バージョンではmacOS 10.15上でクラッシュしないかわりにURLキャッシュが効かないという特徴がありました。ただ、edama2さんと協議した結果、macOS 10.15上でクラッシュする原因と考えられていた内容にあまり根拠がないことがわかってきました(人ごとではなく自分のことなのですが)。
可能な範囲でトライアル&エラーで調査を行なったところ、本バージョンのような処理に落ち着きました。AppleScriptからのCocoa利用については明確にドキュメントがAppleから出ているわけではないので、すでに存在するObjective-Cのプログラムの処理を参考にしつつ、Objective-CからAppleScriptへの置き換えが可能かを検討しています。
URLキャッシュについては、(当然のことながら)処理1回目には効きません。なぜか2回目も効きません。3回目とか4回目あたりから効いていることが実感できる感じです(回数ではなく、前回処理時からの経過時間を見ているのかも? GUIアプリケーションに入れて使うと2回目から効いたりします)。URLキャッシュが効いていない場合には1.5秒ぐらい、URLキャッシュが効き出すと0.02秒ぐらいで結果が返ってきています。
macOS 10.13/14/15で検証を行い、繰り返し処理を行ってもクラッシュしないことを確認しています。ただ、動作保証するというレベルではないので(本Blog掲載のScriptすべてそうですが)、問題があったら知らせてください。
ためしに、Xcode上で作成したGUIベースのAppleScriptアプリケーションに本処理を導入したところ、REST APIへの問い合わせがキャッシュされて著しい高速化を実現できました。
AppleScript名:GET method REST API v4.4.2a_wikipedia.scptd |
— Created 2019-05-02 by Takaaki Naganoya — Modified 2020-04-03 by Takaaki Naganoya — 2020 Piyomaru Software use AppleScript version "2.5" use scripting additions use framework "Foundation" use framework "AppKit" property |NSURL| : a reference to current application’s |NSURL| property NSString : a reference to current application’s NSString property NSURLCache : a reference to current application’s NSURLCache property NSURLSession : a reference to current application’s NSURLSession property NSMutableData : a reference to current application’s NSMutableData property NSURLQueryItem : a reference to current application’s NSURLQueryItem property NSOperationQueue : a reference to current application’s NSOperationQueue property NSURLComponents : a reference to current application’s NSURLComponents property NSJSONSerialization : a reference to current application’s NSJSONSerialization property NSMutableDictionary : a reference to current application’s NSMutableDictionary property NSMutableURLRequest : a reference to current application’s NSMutableURLRequest property NSUTF8StringEncoding : a reference to current application’s NSUTF8StringEncoding property NSURLSessionConfiguration : a reference to current application’s NSURLSessionConfiguration property NSURLRequestReturnCacheDataElseLoad : a reference to current application’s NSURLRequestReturnCacheDataElseLoad property retData : missing value property retCode : 0 property retHeaders : 0 property drecF : false property aSession : missing value property aCache : missing value on run set retData to missing value set drecF to false set aSession to missing value set aQueryKeyTitle to "AppleScript" set aRes to getWikiText(aQueryKeyTitle) of me return aRes end run on getWikiText(aQueryKeyTitle) set reqURLStr to "https://en.wikipedia.org/w/api.php" set aRec to {action:"parse", page:aQueryKeyTitle, |prop|:"wikitext", format:"json"} set aURL to retURLwithParams(reqURLStr, aRec) of me set aRes to callRestGETAPIAndParseResults(aURL, 5) of me if aRes = missing value then return false set bRes to (aRes’s valueForKeyPath:"parse.wikitext.*") as string return bRes end getWikiText –GET methodのREST APIを呼ぶ on callRestGETAPIAndParseResults(reqURLStr as string, timeoutSec as integer) set (my retData) to NSMutableData’s alloc()’s init() set (my retCode) to 0 set (my retHeaders) to {} set (my drecF) to false –if my aCache = missing value then set cachePath to (POSIX path of (path to library folder from user domain)) & "/Caches/AppleScriptURLCache" set my aCache to NSURLCache’s alloc()’s initWithMemoryCapacity:512000 diskCapacity:1024 * 1024 * 5 diskPath:cachePath –end if NSURLCache’s setSharedURLCache:(my aCache) set aURL to |NSURL|’s URLWithString:reqURLStr set aRequest to NSMutableURLRequest’s requestWithURL:aURL aRequest’s setHTTPMethod:"GET" aRequest’s setTimeoutInterval:timeoutSec aRequest’s setValue:"gzip" forHTTPHeaderField:"Content-Encoding" aRequest’s setValue:"AppleScript/Cocoa" forHTTPHeaderField:"User-Agent" aRequest’s setValue:"application/json; charset=UTF-8" forHTTPHeaderField:"Content-Type" set aConfig to NSURLSessionConfiguration’s defaultSessionConfiguration() aConfig’s setRequestCachePolicy:(NSURLRequestReturnCacheDataElseLoad) aConfig’s setURLCache:(my aCache) –どちらでも速度差がない set my aSession to NSURLSession’s sessionWithConfiguration:aConfig delegate:(me) delegateQueue:(NSOperationQueue’s mainQueue()) –set my aSession to NSURLSession’s sessionWithConfiguration:aConfig delegate:(me) delegateQueue:(missing value) set aTask to aSession’s dataTaskWithRequest:aRequest aTask’s resume() –Start URL Session repeat (1000 * timeoutSec) times if (my drecF) is not equal to false then exit repeat end if delay "0.001" as real end repeat –delegateの無効化 my aSession’s finishTasksAndInvalidate() set my aSession to missing value return my parseSessionResults() end callRestGETAPIAndParseResults on URLSession:tmpSession dataTask:tmpTask didReceiveData:tmpData (my retData)’s appendData:tmpData end URLSession:dataTask:didReceiveData: on URLSession:tmpSession task:tmpTask didCompleteWithError:tmpError if tmpError = missing value then set (my drecF) to true else error "Donwload Failed" end if end URLSession:task:didCompleteWithError: on parseSessionResults() set resStr to NSString’s alloc()’s initWithData:(my retData) encoding:(NSUTF8StringEncoding) set jsonString to NSString’s stringWithString:(resStr) set jsonData to jsonString’s dataUsingEncoding:(NSUTF8StringEncoding) set aJsonDict to NSJSONSerialization’s JSONObjectWithData:jsonData options:0 |error|:(missing value) return aJsonDict end parseSessionResults on retURLwithParams(aBaseURL, aRec) set aDic to NSMutableDictionary’s dictionaryWithDictionary:aRec set aKeyList to (aDic’s allKeys()) as list set aValList to (aDic’s allValues()) as list set aLen to length of aKeyList set qList to {} repeat with i from 1 to aLen set aName to contents of item i of aKeyList set aVal to contents of item i of aValList set the end of qList to (NSURLQueryItem’s queryItemWithName:aName value:aVal) end repeat set aComp to NSURLComponents’s alloc()’s initWithString:aBaseURL aComp’s setQueryItems:qList set aURL to (aComp’s |URL|()’s absoluteString()) as text return aURL end retURLwithParams |
REST API呼び出しに欠かせない部品をアップデートしました。macOS 10.15でクラッシュしないように書き換えています。
–> GET method REST API v4.4
–> GET method REST API v4.3
–> GET method REST API v4.1
–> GET method REST API v4
前バージョンではmacOS 10.15上でクラッシュするかわりにmacOS 10.14上で高速という特徴がありました。ただ、macOS 10.15上でクラッシュしないように調整を行っていくと、処理速度については別段速くもなく(キャッシュがヒットしていない雰囲気)、よりいっそうキャッシュについて調べる必要が出てきています。
一応、キャッシュの作り方については調べてみたものの、
キャッシュが効いている雰囲気がまったくありません。
キャッシュの内容を確認してみると、からっぽですね(^ー^; もう少し(時間のあるときに)キャッシュの連携などを調べておくといいのかもしれません。最低限、macOS 10.15でクラッシュしなくなったので、NSURLConnectionを呼び出してリジェクトを喰らわないように準備しておくという感じでしょうか。
AppleScript名:GET method REST API v4.4.1_wikipedia.scptd |
— Created 2019-05-02 by Takaaki Naganoya — Modified 2020-04-02 by Takaaki Naganoya — 2020 Piyomaru Software use AppleScript version "2.5" use scripting additions use framework "Foundation" use framework "AppKit" property |NSURL| : a reference to current application’s |NSURL| property NSString : a reference to current application’s NSString property NSURLSession : a reference to current application’s NSURLSession property NSMutableData : a reference to current application’s NSMutableData property NSJSONSerialization : a reference to current application’s NSJSONSerialization property NSMutableURLRequest : a reference to current application’s NSMutableURLRequest property NSUTF8StringEncoding : a reference to current application’s NSUTF8StringEncoding property NSURLSessionConfiguration : a reference to current application’s NSURLSessionConfiguration property retData : missing value property retCode : 0 property retHeaders : 0 property drecF : false property aSession : missing value property aCache : missing value on run set retData to missing value set drecF to false set aSession to missing value set aQueryKeyTitle to "AppleScript" set aRes to getWikiText(aQueryKeyTitle) of me return aRes end run on getWikiText(aQueryKeyTitle) set reqURLStr to "https://en.wikipedia.org/w/api.php" set aRec to {action:"parse", page:aQueryKeyTitle, |prop|:"wikitext", format:"json"} set aURL to retURLwithParams(reqURLStr, aRec) of me set aRes to callRestGETAPIAndParseResults(aURL, 5) of me if aRes = missing value then return "Error" set bRes to (aRes’s valueForKeyPath:"parse.wikitext.*") as string return bRes end getWikiText –GET methodのREST APIを呼ぶ on callRestGETAPIAndParseResults(reqURLStr as string, timeoutSec as integer) set (my retData) to NSMutableData’s alloc()’s init() set (my retCode) to 0 set (my retHeaders) to {} set (my drecF) to false –if my aCache = missing value then set cachePath to (POSIX path of (path to library folder from user domain)) & "/Caches/AppleScriptURLCache" set my aCache to current application’s NSURLCache’s alloc()’s initWithMemoryCapacity:512000 diskCapacity:1024 * 1024 * 5 diskPath:cachePath –end if current application’s NSURLCache’s setSharedURLCache:(my aCache) set aURL to |NSURL|’s URLWithString:reqURLStr set aRequest to NSMutableURLRequest’s requestWithURL:aURL aRequest’s setHTTPMethod:"GET" aRequest’s setTimeoutInterval:timeoutSec aRequest’s setValue:"gzip" forHTTPHeaderField:"Content-Encoding" aRequest’s setValue:"AppleScript/Cocoa" forHTTPHeaderField:"User-Agent" aRequest’s setValue:"application/json; charset=UTF-8" forHTTPHeaderField:"Content-Type" set v2 to system attribute "sys2" –> 14 if v2 < 15 then –macOS 10.14まで set aConfig to NSURLSessionConfiguration’s defaultSessionConfiguration() else –macOS 10.15以降。10.15でdefaultSessionConfigurationを使うとクラッシュしやすい??? set identifier to "BackgroundSessionConfiguration" set aConfig to NSURLSessionConfiguration’s backgroundSessionConfiguration:(identifier) end if aConfig’s setRequestCachePolicy:(current application’s NSURLRequestReturnCacheDataElseLoad) aConfig’s setURLCache:(my aCache) –aConfig’s setTimeoutIntervalForRequest:0.5 –aConfig’s setTimeoutIntervalForResource:timeoutSec –aConfig’s setWaitsForConnectivity:false –どちらでも速度差がない set my aSession to NSURLSession’s sessionWithConfiguration:aConfig delegate:(me) delegateQueue:(current application’s NSOperationQueue’s mainQueue()) –set my aSession to NSURLSession’s sessionWithConfiguration:aConfig delegate:(me) delegateQueue:(missing value) set aTask to aSession’s dataTaskWithRequest:aRequest aTask’s resume() –Start URL Session repeat (1000 * timeoutSec) times if (my drecF) is not equal to false then exit repeat end if delay "0.001" as real end repeat –aSession’s invalidateAndCancel() –delegateの無効化 my aSession’s finishTasksAndInvalidate() set my aSession to missing value return my parseSessionResults() end callRestGETAPIAndParseResults on URLSession:tmpSession dataTask:tmpTask didReceiveData:tmpData (my retData)’s appendData:tmpData end URLSession:dataTask:didReceiveData: on URLSession:tmpSession dataTask:tmpTask willCacheResponse:cacheRes completionHandler:aHandler set (my drecF) to true end URLSession:dataTask:willCacheResponse:completionHandler: on URLSession:tmpSession task:tmpTask didCompleteWithError:tmpError if tmpError = missing value then set (my drecF) to true else error "Donwload Failed" end if end URLSession:task:didCompleteWithError: on parseSessionResults() set resStr to NSString’s alloc()’s initWithData:(my retData) encoding:(NSUTF8StringEncoding) set jsonString to NSString’s stringWithString:(resStr) set jsonData to jsonString’s dataUsingEncoding:(NSUTF8StringEncoding) set aJsonDict to NSJSONSerialization’s JSONObjectWithData:jsonData options:0 |error|:(missing value) return aJsonDict end parseSessionResults on retURLwithParams(aBaseURL, aRec) set aDic to current application’s NSMutableDictionary’s dictionaryWithDictionary:aRec set aKeyList to (aDic’s allKeys()) as list set aValList to (aDic’s allValues()) as list set aLen to length of aKeyList set qList to {} repeat with i from 1 to aLen set aName to contents of item i of aKeyList set aVal to contents of item i of aValList set the end of qList to (current application’s NSURLQueryItem’s queryItemWithName:aName value:aVal) end repeat set aComp to current application’s NSURLComponents’s alloc()’s initWithString:aBaseURL aComp’s setQueryItems:qList set aURL to (aComp’s |URL|()’s absoluteString()) as text return aURL end retURLwithParams |
Keynote、Pages、Numbersのv10.0が配信されました。Keynoteの前バージョンはv9.2、Pagesはv8.2、Numbersはv6.2.1でしたが、バージョン番号を別々に採番することがめんどくさくなったのか、内部のエンジンが一新されたのか、バージョン番号をそろえてきました。
おそらく、マーケティング的な要請やユーザーサポートの手間を省くために番号をそろえてきたのでしょう。
Pages/NumbersについてはmacOS 10.14以上が対象。KeynoteはmacOS 10.15.4が必要とのことですが、いろいろ致命的なバグの修正を含んでいるため、後日macOS 10.14.6向けのKeynoteアップデートが配信されることを期待したいところです。
# アップデートに出てこないだけで、Mac App Storeから個別にKeynote v10.0のダウンロードが(macOS 10.14.6上でも)できました。なんででしょう?
■Numbers v10.0:変更点なし
Numbers v6.2で「特定の行数の表を作るとエラーになる」というバグがありましたが、これはv6.2.1で修正されました。これが修正されています(記憶違いだったので、記事を修正しておきます)。
documentにfacing pagesというプロパティが新設されました。見開き表示時にページの左右(奇数ページ、偶数ページ)を考慮して2ページ表示のペアを変更するという機能のようです。
AppleScript名:Pages v10.0で追加されたfacing pagesの操作を行う |
tell application "Pages" tell front document set curStat to facing pages set facing pages to not curStat –反転 delay 2 set facing pages to curStat –元に戻す end tell end tell |
全言語的に普遍的な内容ではあるのですが、日本語ユーザー的には縦書き/横書きの設定属性値を持ってくれたほうがありがたいところです。たぶん、自分は使うことはないと思います。
Keynoteは前バージョンでかなりおかしなバグが発覚していたので、その修正が行われているかどうか、というのが見所です。
結論からいえば、バグは修正されています。macOSに関して久しぶりに前向きなニュースといえます。彼らにバグを修正する意思と能力が残されていたことについては喜びたいところです。同時に残念なニュースとして、この修正に関連して新たなバグが生まれています(このあたりがいかにもAppleらしい)。
5行の表を作るとエラーになり、他の行数でもいろいろエラーになる組み合わせが確認されていました。テスト用のAppleScriptを書いて、行数を2〜100行で変更しつつ作成、列数を2〜30列で変更しつつ作成、行数および列数を順次ループで2〜20まで変更しつつ新規作成するなどのテストを実施。無事、修正を確認できました。
ただし、本テストは表の新規作成についてのみ確認したものであり、既存の表の行数/列数の変更を確認したものではありません。Appleの仕事に関しては、修正点の周囲に新たなバグを生む可能性が高く、修正時と修正後が一番危険な状態です。彼らに「自分の作った機能を動作確認する」という能力を期待してはいけません。信じてもいけません。つねに、疑いの目で見ることが重要です。
AppleScript名:Keynote書類上に表を作成、行数を2から100まで可変 |
tell application "Keynote" tell front document tell current slide repeat with i from 2 to 100 set aTable to make new table with properties {header column count:0, header row count:0, row count:i, column count:3} delay 1 delete aTable end repeat end tell end tell end tell |
AppleScript名:Keynote書類上に表を作成、列数を2から30まで可変 |
tell application "Keynote" tell front document tell current slide repeat with i from 2 to 30 set aTable to make new table with properties {header column count:0, header row count:0, row count:5, column count:i} delay 1 delete aTable end repeat end tell end tell end tell |
AppleScript名:Keynote書類上に表を作成、行数および列数を2から20まで可変 |
tell application "Keynote" tell front document tell current slide repeat with x from 2 to 20 repeat with y from 2 to 20 set aTable to make new table with properties {header column count:0, header row count:0, row count:y, column count:x} delay 0.01 delete aTable end repeat end repeat end tell end tell end tell |
数値ではじまる予約語や記号を含む予約語はAppleScriptの言語処理系では宣言できません。エラーになります。それをAppleの(おそらくKeynoteの)担当者がKeynote v7.1のアップデート時に、従来の「small」「midium」「large」といったEnumによる指定から、「360p」「540p」「720p」「1080p」「2160p」という指定を行えるように変更を加えました。
より大きな解像度の書き出しに対処したことは評価できると思いますが、そもそもAppleScriptの処理系で認識できない「数字で始まる予約語」に変えたのはダメダメです(スクリプトエディタ上で構文確認を行うとエラーになる=使えない)。つまり、この担当者はsdefの改変を行なっただけで、実際にコードを書いて動作確認を行っていないことがわかります。
Appleにバグレポートを書きつつ、このような処理が必要な場合にはnative sizeで書き出して、そのあとでムービーをリサイズするような処理で回避していました(GUI Scriptingで乗り切ったScripterもいるようですが)。
Keynote v10.0では「format360p」「format540p」「format720p」「format1080p」「format2160p」と変更され、AppleScriptの構文確認時にエラーでハネられることはなくなりました。この点についてはバグ修正が行われたものと判断してよいと思われます。
ただし、従来動作していたEnum「native size」を指定するとエラーになるようになってしまいました。互換性のために残したが動作していない、といったコメントが書かれているわけでもないため、これはバグだと判断します。
■Keynote書類フォーマットとムービー書き出し時の解像度の対応表(Piyomaru Software独自調査による)
movie export formats | 標準(4:3) | ワイド(16:9) |
format360p | 480 × 360 | 640 × 360 |
format540p | 720 × 540 | 960 × 540 |
format720p | 960 × 720 | 1280 × 720 |
format1080p | 1440 × 1080 | 1920 × 1080 |
format2160p | 2880 × 2160 | 3840 × 2160 |
AppleScript名:Keynote 360p movie export test |
set targetFileHFSPath to (choose file name) as string –かならずファイル拡張子に「.m4v」を指定する必要がある if targetFileHFSPath does not end with ".m4v" then set targetFileHFSPath to targetFileHFSPath & ".m4v" end if with timeout of 3600 seconds tell application "Keynote" export front document to file targetFileHFSPath as QuickTime movie with properties {movie format:format360p} end tell end timeout |
AppleScript名:Keynote 540p movie export test |
set targetFileHFSPath to (choose file name) as string –かならずファイル拡張子に「.m4v」を指定する必要がある if targetFileHFSPath does not end with ".m4v" then set targetFileHFSPath to targetFileHFSPath & ".m4v" end if with timeout of 3600 seconds tell application "Keynote" export front document to file targetFileHFSPath as QuickTime movie with properties {movie format:format540p} end tell end timeout |
AppleScript名:Keynote 720p movie export test |
set targetFileHFSPath to (choose file name) as string –かならずファイル拡張子に「.m4v」を指定する必要がある if targetFileHFSPath does not end with ".m4v" then set targetFileHFSPath to targetFileHFSPath & ".m4v" end if with timeout of 3600 seconds tell application "Keynote" export front document to file targetFileHFSPath as QuickTime movie with properties {movie format:format720p} end tell end timeout |
AppleScript名:Keynote 1080p movie export test |
set targetFileHFSPath to (choose file name) as string –かならずファイル拡張子に「.m4v」を指定する必要がある if targetFileHFSPath does not end with ".m4v" then set targetFileHFSPath to targetFileHFSPath & ".m4v" end if with timeout of 3600 seconds tell application "Keynote" export front document to file targetFileHFSPath as QuickTime movie with properties {movie format:format1080p} end tell end timeout |
AppleScript名:Keynote 2160p movie export test |
set targetFileHFSPath to (choose file name) as string –かならずファイル拡張子に「.m4v」を指定する必要がある if targetFileHFSPath does not end with ".m4v" then set targetFileHFSPath to targetFileHFSPath & ".m4v" end if with timeout of 3600 seconds tell application "Keynote" export front document to file targetFileHFSPath as QuickTime movie with properties {movie format:format2160p} end tell end timeout |
AppleScript名:Keynote native size movie export test (Bug) |
set targetFileHFSPath to (choose file name) as string –かならずファイル拡張子に「.m4v」を指定する必要がある if targetFileHFSPath does not end with ".m4v" then set targetFileHFSPath to targetFileHFSPath & ".m4v" end if with timeout of 3600 seconds tell application "Keynote" –export front document to file targetFileHFSPath as QuickTime movie with properties {movie format:native size} end tell end timeout |
REST API呼び出しに欠かせない部品をアップデートしました。
–> GET method REST API v4.3
–> GET method REST API v4.1
–> GET method REST API v4
従来、NSURLConnectionを使って同期処理を行っていましたが、これがDepreceted扱いになりました。すぐになくなることはありませんが、いつかなくなる可能性があります。
このため、NSURLConnectionから動作原理の異なるNSURLSessionを使うよう移行する必要があります(curlコマンドを利用するというルートもあります)。
とはいえ、blocks構文の使えないAppleScriptでこのNSURLSessionを使えるようにするのは大変です。非同期通信を行うNSURLSessionを非同期処理のしにくいAppleScriptで、同期処理っぽく書かなくてはなりません。
NSURLSessionによる通信では、サーバーとの間で何回かのやりとりがあって、データを小分けに複数回受信する必要がありました。NSMutableDataというオブジェクトの存在を知ったのでいい感じに処理できるようになってきました。
NSURLSessionのメリットとして喧伝されている非同期処理は、AppleScriptから呼び出しているかぎりはそれほどメリットになりません。もう少し使い込めば活用できるかもしれませんが、まだそういう段階にはありません。
NSURLSessionの導入によって得られるメリットで最大のものは、キャッシュ機構です。NSURLConnectionでもキャッシュ機構は利用できましたが、NSURLSessionではよりAPIに深く統合されているものと理解しました。
キャッシュの効き方については、設定で何段階かに設定できるとAppleのオンラインドキュメントに書かれています。本Scriptでは、キャッシュ優先でキャッシュが存在している場合にはキャッシュ内のデータを返し、キャッシュが存在していない場合にはWebサーバーに問い合わせを行うように設定しています。
キャッシュが効いている状況なら、NSURLConnectionで1.8秒ぐらいの処理がNSURLSessionで0.1秒程度と大幅に速く処理できます。キャッシュが効いていない状況(初回実行時)だと、NSURLSessionのほうがやや処理に時間がかかるぐらいです。
処理結果について、NSURLConnection版とNSURLSession版で結果の比較を行ってみましたが、とくに差異は見られませんでした。
本サンプルScriptでは、Wikipedia(英語版)へのキーワードの問い合わせを行います。NSURLConnection版では複数回実行しても同じぐらいの速度で実行されますが、NSURLSession版では2度目から早く終わります(Script Debugger上で動かすと秒以下の精度の時間がわかります)。
macOS 10.14上で作ってmacOS 10.14.6/10.13.6上で動作確認しています(スクリプトエディタ、Script Debuggerで動作確認)。ただし、macOS 10.15上だとクラッシュします。Xcodeのプロジェクトに入れてみましたが、同様にmacOS 10.15上ではクラッシュします。
ただし、自分のmacOS 10.14.6環境はSIP解除しているのと、macOS 10.15環境のクラッシュログにSIP関連のメッセージが残っているので、OSバージョンではなくSIPのためかもしれません(macOS 10.14.6でもSIPを有効にするとクラッシュするかもしれない & macOS 10.15.xでもSIPを無効にするとクラッシュしないかもしれない)。
AppleScript名:GET method REST API v4.4_wikipedia |
— Created 2019-05-02 by Takaaki Naganoya — Modified 2020-03-29 by Takaaki Naganoya — 2020 Piyomaru Software use AppleScript version "2.5" use scripting additions use framework "Foundation" use framework "AppKit" property |NSURL| : a reference to current application’s |NSURL| property NSString : a reference to current application’s NSString property NSURLSession : a reference to current application’s NSURLSession property NSMutableData : a reference to current application’s NSMutableData property NSJSONSerialization : a reference to current application’s NSJSONSerialization property NSMutableURLRequest : a reference to current application’s NSMutableURLRequest property NSUTF8StringEncoding : a reference to current application’s NSUTF8StringEncoding property NSURLSessionConfiguration : a reference to current application’s NSURLSessionConfiguration property retData : missing value property retCode : 0 property retHeaders : 0 property drecF : false set aQueryKeyTitle to "Steve Jobs" –キーワードの正規化が必要("戦場の絆"→"機動戦士ガンダム 戦場の絆" ) set reqURLStr to "https://en.wikipedia.org/w/api.php" set aRec to {action:"parse", page:aQueryKeyTitle, |prop|:"wikitext", format:"json"} set aURL to retURLwithParams(reqURLStr, aRec) of me set aRes to callRestGETAPIAndParseResults(aURL, 10) of me set bRes to (aRes’s valueForKeyPath:"parse.wikitext.*") as string return bRes –GET methodのREST APIを呼ぶ on callRestGETAPIAndParseResults(reqURLStr as string, timeoutSec as integer) set (my retData) to NSMutableData’s alloc()’s init() set (my retCode) to 0 set (my retHeaders) to {} set (my drecF) to false set aURL to |NSURL|’s URLWithString:reqURLStr set aRequest to NSMutableURLRequest’s requestWithURL:aURL aRequest’s setHTTPMethod:"GET" aRequest’s setTimeoutInterval:timeoutSec aRequest’s setValue:"gzip" forHTTPHeaderField:"Content-Encoding" aRequest’s setValue:"AppleScript/Cocoa" forHTTPHeaderField:"User-Agent" aRequest’s setValue:"application/json; charset=UTF-8" forHTTPHeaderField:"Content-Type" set aConfig to NSURLSessionConfiguration’s defaultSessionConfiguration() aConfig’s setRequestCachePolicy:(current application’s NSURLRequestReturnCacheDataElseLoad) set aSession to NSURLSession’s sessionWithConfiguration:aConfig delegate:(me) delegateQueue:(missing value) set aTask to aSession’s dataTaskWithRequest:aRequest aTask’s resume() –Start URL Session repeat (10 * timeoutSec) times if (my drecF) = true then exit repeat end if delay 0.1 end repeat return my parseSessionResults() end callRestGETAPIAndParseResults on URLSession:tmpSession dataTask:tmpTask didReceiveData:tmpData (my retData)’s appendData:tmpData end URLSession:dataTask:didReceiveData: on URLSession:tmpSession dataTask:tmpTask didCompleteWithError:tmpError set (my drecF) to true end URLSession:dataTask:didCompleteWithError: on URLSession:tmpSession dataTask:tmpTask willCacheResponse:cacheRes completionHandler:aHandler set (my drecF) to true end URLSession:dataTask:willCacheResponse:completionHandler: on parseSessionResults() set resStr to NSString’s alloc()’s initWithData:(my retData) encoding:(NSUTF8StringEncoding) set jsonString to NSString’s stringWithString:(resStr) set jsonData to jsonString’s dataUsingEncoding:(NSUTF8StringEncoding) set aJsonDict to NSJSONSerialization’s JSONObjectWithData:jsonData options:0 |error|:(missing value) return aJsonDict end parseSessionResults on retURLwithParams(aBaseURL, aRec) set aDic to current application’s NSMutableDictionary’s dictionaryWithDictionary:aRec set aKeyList to (aDic’s allKeys()) as list set aValList to (aDic’s allValues()) as list set aLen to length of aKeyList set qList to {} repeat with i from 1 to aLen set aName to contents of item i of aKeyList set aVal to contents of item i of aValList set the end of qList to (current application’s NSURLQueryItem’s queryItemWithName:aName value:aVal) end repeat set aComp to current application’s NSURLComponents’s alloc()’s initWithString:aBaseURL aComp’s setQueryItems:qList set aURL to (aComp’s |URL|()’s absoluteString()) as text return aURL end retURLwithParams on urlencodeStr(aStr) set aString to current application’s NSString’s stringWithString:aStr set aString to (aString’s stringByAddingPercentEncodingWithAllowedCharacters:(current application’s NSCharacterSet’s URLQueryAllowedCharacterSet())) as text return aString end urlencodeStr –リストに入れたレコードを、指定の属性ラベルの値で抽出 on filterRecListByLabel1(aRecList as list, aPredicate as string) set aArray to current application’s NSArray’s arrayWithArray:aRecList set aPredicate to current application’s NSPredicate’s predicateWithFormat:aPredicate set filteredArray to aArray’s filteredArrayUsingPredicate:aPredicate set bList to filteredArray as {list, missing value} return bList end filterRecListByLabel1 |
AppleScript名:GET method REST API_wikipedia |
— Created 2016-03-03 by Takaaki Naganoya — 2016 Piyomaru Software use AppleScript version "2.4" use scripting additions use framework "Foundation" set aQueryKeyTitle to "Steve Jobs" –キーワードの正規化が必要("戦場の絆"→"機動戦士ガンダム 戦場の絆" ) set reqURLStr to "https://en.wikipedia.org/w/api.php" set aRec to {action:"parse", page:aQueryKeyTitle, |prop|:"wikitext", format:"json"} set aURL to retURLwithParams(reqURLStr, aRec) of me set aRes to callRestGETAPIAndParseResults(aURL, 10) of me set aRESTres to json of aRes set bRes to (aRESTres’s valueForKeyPath:"parse.wikitext.*") as string –GET methodのREST APIを呼ぶ on callRestGETAPIAndParseResults(aURL, timeoutSec) set aRequest to current application’s NSMutableURLRequest’s requestWithURL:(current application’s |NSURL|’s URLWithString:aURL) aRequest’s setHTTPMethod:"GET" aRequest’s setTimeoutInterval:timeoutSec aRequest’s setValue:"gzip" forHTTPHeaderField:"Content-Encoding" aRequest’s setValue:"AppleScript/Cocoa" forHTTPHeaderField:"User-Agent" aRequest’s setValue:"application/json; charset=UTF-8" forHTTPHeaderField:"Content-Type" set aRes to current application’s NSURLConnection’s sendSynchronousRequest:aRequest returningResponse:(reference) |error|:(missing value) set resList to aRes as list set bRes to contents of (first item of resList) set resStr to current application’s NSString’s alloc()’s initWithData:bRes encoding:(current application’s NSUTF8StringEncoding) set jsonString to current application’s NSString’s stringWithString:resStr set jsonData to jsonString’s dataUsingEncoding:(current application’s NSUTF8StringEncoding) set aJsonDict to current application’s NSJSONSerialization’s JSONObjectWithData:jsonData options:0 |error|:(missing value) –Get Response Code set dRes to contents of second item of resList set resCode to (dRes’s statusCode()) as integer –Get Response Header set resHeaders to (dRes’s allHeaderFields()) as record return {json:aJsonDict, responseCode:resCode, responseHeader:resHeaders} end callRestGETAPIAndParseResults on retURLwithParams(aBaseURL, aRec) set aDic to current application’s NSMutableDictionary’s dictionaryWithDictionary:aRec set aKeyList to (aDic’s allKeys()) as list set aValList to (aDic’s allValues()) as list set aLen to length of aKeyList set qList to {} repeat with i from 1 to aLen set aName to contents of item i of aKeyList set aVal to contents of item i of aValList set the end of qList to (current application’s NSURLQueryItem’s queryItemWithName:aName value:aVal) end repeat set aComp to current application’s NSURLComponents’s alloc()’s initWithString:aBaseURL aComp’s setQueryItems:qList set aURL to (aComp’s |URL|()’s absoluteString()) as text return aURL end retURLwithParams on urlencodeStr(aStr) set aString to current application’s NSString’s stringWithString:aStr set aString to (aString’s stringByAddingPercentEncodingWithAllowedCharacters:(current application’s NSCharacterSet’s URLQueryAllowedCharacterSet())) as text return aString end urlencodeStr –リストに入れたレコードを、指定の属性ラベルの値で抽出 on filterRecListByLabel1(aRecList as list, aPredicate as string) set aArray to current application’s NSArray’s arrayWithArray:aRecList set aPredicate to current application’s NSPredicate’s predicateWithFormat:aPredicate set filteredArray to aArray’s filteredArrayUsingPredicate:aPredicate set bList to filteredArray as list return bList end filterRecListByLabel1 |
Xcode上で記述するAppleScriptアプリケーションにおいて、マウスカーソルを変更するAppleScriptです。
NSCursorにアクセスして、macOS側で用意しているカーソルにマウスカーソルのイメージを変更します。
あらかじめmacOSが用意しているカーソルはいくつかあるわけですが、これで満足するわけがなくて……任意のNSImageをカーソルに指定する方法について調べていたものの、なかなかうまくいかなかったので、このOS側で用意しているカーソルへの切り替えのみまとめておきました。
他のアプリケーションに切り替えると通常のカーソルに戻ってしまうので、本プロジェクト側でアプリケーション切り替えのイベントハンドラでカーソルの再変更などを行うべきなのかも。
set aImage to current application's NSImage's imageNamed:(current application's NSImageNameComputer) set tmpCursor to current application's NSCursor's alloc()'s initWithImage:aImage hotSpot:{0,15} tmpCursor's |set|()
結局、コンピュータのアイコンをマウスカーソルに指定するというところまでは持って行けた(カーソル画像の差し替えができた)わけなんですが、NSCursorの挙動を評価してみたら、カーソルが標準のものに戻るタイミングがバラバラ(他のディスプレイにカーソルが移動したとき、というわけでもない)で、挙動が謎すぎであります。
その後、対象のビュー(NSViewなど、その派生クラス)にマウスカーソルが入った/出たことを検出するイベントハンドラでマウスカーソルの形状変更を明示的に指定するような実装で落ち着きました(Edama2さんから教えていただきました。ありがとうございます)。
AppleScript名:AppDelegate.applescript |
— — AppDelegate.applescript — CursorTest — — Created by Takaaki Naganoya on 2020/03/24. — Copyright © 2020 Takaaki Naganoya. All rights reserved. — script AppDelegate property parent : class "NSObject" — IBOutlets property theWindow : missing value on applicationWillFinishLaunching:aNotification — Insert code here to initialize your application before any files are opened end applicationWillFinishLaunching: on applicationShouldTerminate:sender — Insert code here to do any housekeeping before your application quits return current application’s NSTerminateNoww end applicationShouldTerminate: on clicked:aSender set aTag to (aSender’s tag) as integer if aTag = 100 then current application’s NSCursor’s arrowCursor()’s |set|() else if aTag = 101 then current application’s NSCursor’s disappearingItemCursor()’s |set|() else if aTag = 102 then current application’s NSCursor’s contextualMenuCursor()’s |set|() else if aTag = 103 then current application’s NSCursor’s openHandCursor()’s |set|() end if end clicked: end script |
指定画像をICNS書類に変換するAppleScriptです。
ICNS書類自体は、Classic Mac OS時代にあったアイコンの画像リソースである「icn#」リソースをファイル化したもの、と理解しています(位置付け的に、データ形式とか厳密なレベルで比較したわけではなくて)。
現在では、ICNS書類を作ってアプリケーションアイコンに指定したり、
Xcode上で解像度の異なる画像ファイルをドラッグ&ドロップすると、アプリケーションのICNSファイルを作ってくれます。
Finder上で解像度の異なる画像をフォルダ構造に入れてフォルダの拡張子をつけかえるだけでICNSと認識してくれたような記憶もあります。
そのため、ICNS書類をわざわざScriptから作る需要もないだろうと考えて掲載してこなかったのですが、意外なところでICNS書類の作成を求められました。
自作アプリケーションのカスタム書類を定義して、書類にカスタムアイコンをつけようとしたところ、PNGなどの画像では認識されず、結局ICNS書類を指定する必要に迫られました(意外)。
そんなわけで、いろいろICNS作成Scriptをまとめておきました。
こちらのScriptは実際には実行していませんが、/usr/bin/tiff2icnsコマンドがmacOS 10.15にも含まれていることを確認しています。
AppleScript名:TIFF画像をicnsに変換 v2scpt |
tell application "Finder" set a to choose file end tell set theTiffPath to POSIX path of a –128 x 128のtiff画像が必要 do shell script "/usr/bin/tiff2icns -noLarge " & (quoted form of theTiffPath) |
こちらは、オープンソースの「IcnsFactory」をフレームワーク化したIconFactory.frameworkを呼び出すバージョンです。自分が実際に使ったのはこちらです。
macOS 10.14以降ではScript Debugger上で実行するか、Script Debuggerから書き出したアプレット(Application(Enhanced))で実行するか、あるいはMacのSIPを解除して実行するなどの方法で実行できます。
Xcode上のAppleScript Applicationプロジェクトに突っ込んで呼び出せば、SIPの解除やScript Debuggerのインストールは必要ありません。
–> Download iconFactory.framework
AppleScript名:画像をICNS書類に |
— Created 2017-04-14 by Takaaki Naganoya — 2017 Piyomaru Software use AppleScript version "2.4" use scripting additions use framework "Foundation" use framework "AppKit" use framework "IconFactory" –https://github.com/kgn/IcnsFactory set aFile to POSIX path of (choose file) set outFile to POSIX path of (choose file name) set anImage to current application’s NSImage’s alloc()’s initWithContentsOfFile:aFile set iconFam to current application’s IcnsFactory’s writeICNSToFile:outFile withImages:anImage |
AppleScriptで指定URLのページを表示する「WebKit Utilities」がmacOS 10.13以降の環境で動かなくなっていたので、書き換えて動くようにしておきました。
–> Download WebKit Utilities_archive v2
これ自体が役に立ったということはなく、単にAppleScript Librariesの書き方のサンプルと理解しています。Webサイトの表示を行うよりも1枚ものの画像やPDFにレンダリングする処理のほうがバッチ処理の中では相性がいいと思います。
また、このScriptはWkWebViewではなく古いWebViewを使っているので、じきに動かなくなります。
AppleScript名:WebKit Utilitiesで指定URLをウィンドウ表示 |
— Created 2017-03-24 by Takaaki Naganoya — 2017 Piyomaru Software use AppleScript version "2.4" use scripting additions use webLib : script "WebKit Utilities" set targetURL to "http://www.piyocast.com/" display URL targetURL window size {1024, 1024} |
AppleScript名:WebKit Utilities |
— An example AppleScript/Objective-C library that uses the WebKit framework.
use AppleScript use framework "AppKit" use framework "WebKit" property NSURL : a reference to current application’s NSURL property WebView : a reference to current application’s WebView property NSScreen : a reference to current application’s NSScreen property NSThread : a reference to current application’s NSThread property NSWindow : a reference to current application’s NSWindow property NSURLRequest : a reference to current application’s NSURLRequest property NSMutableDictionary : a reference to current application’s NSMutableDictionary on _DisplayWebWindowWithURL:theURLString windowSize:theWindowSize — Create and display a horizontally centered window with a WebView set {thisWindowWidth, thisWindowHeight} to theWindowSize set screenBounds to the NSScreen’s mainScreen’s visibleFrame as any if class of screenBounds = record then set screenWidth to screenBounds’s |size|’s width set screenHeight to screenBounds’s |size|’s height set windowLeft to ((screenWidth – thisWindowWidth) / 2) + (screenBounds’s origin’s x) set windowBottom to screenHeight – thisWindowHeight + (screenBounds’s origin’s y) – 40 else copy screenBounds to {{originX, originY}, {screenWidth, screenHeight}} set windowLeft to ((screenWidth – thisWindowWidth) / 2) + originX set windowBottom to screenHeight – thisWindowHeight + originY – 40 end if set theStyleMask to (get current application’s NSTitledWindowMask) + (get current application’s NSClosableWindowMask) + (get current application’s NSMiniaturizableWindowMask) + (get current application’s NSResizableWindowMask) set webWindow to NSWindow’s alloc()’s initWithContentRect:(current application’s NSMakeRect(windowLeft, windowBottom, thisWindowWidth, thisWindowHeight)) ¬ styleMask:theStyleMask backing:(current application’s NSBackingStoreBuffered) defer:false — set webWindow’s title to "WebView Created with AppleScript – " & theURLString set theWebView to WebView’s alloc()’s initWithFrame:(current application’s NSMakeRect(0, 0, thisWindowWidth, thisWindowHeight)) ¬ frameName:"WebKit Frame" groupName:"WebKit Group" set webWindow’s contentView to theWebView tell webWindow to makeKeyAndOrderFront:(missing value) — Start loading the URL set theURL to NSURL’s URLWithString:theURLString set theURLRequest to NSURLRequest’s requestWithURL:theURL tell theWebView’s mainFrame to loadRequest:theURLRequest webWindow end _DisplayWebWindowWithURL:windowSize: — The following two handlers exist to deal with running on a background thread. — If we’re on a background thread, we have to run the UI code on the main thread. on _DisplayWebWindowMainThread:parameterDictionary try set theURLString to parameterDictionary’s objectForKey:"url string" set theWindowSize to parameterDictionary’s objectForKey:"window size" set webWindow to my _DisplayWebWindowWithURL:theURLString windowSize:theWindowSize tell parameterDictionary to setObject:webWindow forKey:"result" on error errMsg number errNum using terms from scripting additions display alert "Error!" message errMsg & " (" & errNum & ")" end using terms from tell parameterDictionary to setObject:{errMsg, errNum} forKey:"error" end try end _DisplayWebWindowMainThread: on display URL theURLString window size theWindowSize — All UI methods must be invoked on the main thread. if (NSThread’s isMainThread) then my _DisplayWebWindowWithURL:theURLString windowSize:theWindowSize else — Parameters, results and error information are communicated between threads via a dictionary. set parameterDictionary to NSMutableDictionary’s dictionary() tell parameterDictionary to setObject:theURLString forKey:"url string" tell parameterDictionary to setObject:theWindowSize forKey:"window size" its performSelectorOnMainThread:"_DisplayWebWindowMainThread:" withObject:parameterDictionary waitUntilDone:true — Propagate errors from the other thread set errInfo to parameterDictionary’s objectForKey:"error" if errInfo is not missing value then error errInfo’s first item number errInfo’s second item parameterDictionary’s objectForKey:"result" end if end display URL |
正方形セルの表を形成する1次元配列上で、指定セルに隣接する指定データの入っているセルブロックを検出するAppleScriptです。
テストデータは11×11のサイズで作ってみました。本Scriptを作ることが最終目的ではないので、実戦投入はしていませんが、それなりに動いているようです。11セルの対象データを検出するのに、0.002秒ぐらい(10回実行時の平均値)。
本プログラムではセルアドレス=58のセルと隣接する「9」のデータが入っているセルのアドレスをすべて求めています。基準セルの8方向のセルアドレスを計算したうえでデータにアクセスして、「9」が入っているセルを検出。それらをまとめてすべての検出セルの8方向のアドレスを検査し、それ以上検出セル数が増加しない状態までループ処理を続けています。
本プログラムは本番プログラムを作るための習作だったので、ものすごくオーソドックスな組み方をしています。本番のプログラムでは、この「無駄に長い」処理を大幅に簡略化して3分の1ぐらいの長さになりました。
AppleScript名:findBlock2.scptd |
— – Created by: Takaaki Naganoya – Created on: 2020/03/10 — – Copyright © 2020 Piyomaru Software, All Rights Reserved — use AppleScript version "2.4" — Yosemite (10.10) or later use framework "Foundation" use scripting additions property dataWidth : 11 property dataHeight : 11 property dataMax : 121 property testData : {"1", "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", ¬ "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", ¬ "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", ¬ "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", ¬ "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", ¬ "1", "1", "9", "1", "1", "1", "1", "1", "1", "1", "1", ¬ "1", "9", "9", "9", "1", "1", "1", "1", "1", "1", "1", ¬ "1", "9", "9", "9", "9", "1", "1", "1", "1", "1", "1", ¬ "1", "9", "9", "9", "1", "1", "1", "1", "1", "1", "1", ¬ "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", ¬ "1", "1", "1", "1", "1", "1", "1", "1", "1", "1", "1"} set foundList to {58} set prevFound to 1 repeat set foundList to findAllDirection(foundList) of me set aLen to length of foundList if aLen = prevFound then return foundList else copy aLen to prevFound end if end repeat return foundList –> {58, 69, 68, 70, 80, 79, 81, 82, 91, 90, 92} on findAllDirection(foundList) repeat with sCanCell in foundList set nRes to findN(sCanCell) of me set sRes to findS(sCanCell) of me set eRes to findE(sCanCell) of me set wRes to findW(sCanCell) of me set nwRes to findNW(sCanCell) of me set neRes to findNE(sCanCell) of me set swRes to findSW(sCanCell) of me set seRes to findSE(sCanCell) of me if nRes is not equal to false then set newAddr to contents of (item 2 of nRes) if newAddr is not in foundList then set the end of foundList to newAddr else set nRes to false end if end if if sRes is not equal to false then set newAddr to contents of (item 2 of sRes) if newAddr is not in foundList then set the end of foundList to newAddr else set sRes to false end if end if if eRes is not equal to false then set newAddr to contents of (item 2 of eRes) if newAddr is not in foundList then set the end of foundList to newAddr else set eRes to false end if end if if wRes is not equal to false then set newAddr to contents of (item 2 of wRes) if newAddr is not in foundList then set the end of foundList to newAddr else set wRes to false end if end if if nwRes is not equal to false then set newAddr to contents of (item 2 of nwRes) if newAddr is not in foundList then set the end of foundList to newAddr else set nwRes to false end if end if if neRes is not equal to false then set newAddr to contents of (item 2 of neRes) if newAddr is not in foundList then set the end of foundList to newAddr else set neRes to false end if end if if swRes is not equal to false then set newAddr to contents of (item 2 of swRes) if newAddr is not in foundList then set the end of foundList to newAddr else set swRes to false end if end if if seRes is not equal to false then set newAddr to contents of (item 2 of seRes) if newAddr is not in foundList then set the end of foundList to newAddr else set seRes to false end if end if end repeat return foundList end findAllDirection ————————————— on findS(anAddress) copy anAddress to tmpAddr set tmpAddr to incLine(tmpAddr) of me if tmpAddr = false then return false set tmpCon to contents of item tmpAddr of testData if tmpCon = "1" then return false return {true, tmpAddr} end findS on findN(anAddress) copy anAddress to tmpAddr set tmpAddr to decLine(tmpAddr) of me if tmpAddr = false then return false set tmpCon to contents of item tmpAddr of testData if tmpCon = "1" then return false return {true, tmpAddr} end findN on findE(anAddress) copy anAddress to tmpAddr set tmpAddr to incCell(tmpAddr) of me if tmpAddr = false then return false set tmpCon to contents of item tmpAddr of testData if tmpCon = "1" then return false return {true, tmpAddr} end findE on findW(anAddress) copy anAddress to tmpAddr set tmpAddr to decCell(tmpAddr) of me if tmpAddr = false then return false set tmpCon to contents of item tmpAddr of testData if tmpCon = "1" then return false return {true, tmpAddr} end findW on findNW(anAddress) copy anAddress to tmpAddr set tmpAddr to decLine(tmpAddr) of me if tmpAddr = false then return false set tmpAddr to decCell(tmpAddr) of me if tmpAddr = false then return false set tmpCon to contents of item tmpAddr of testData if tmpCon = "1" then return false return {true, tmpAddr} end findNW on findNE(anAddress) copy anAddress to tmpAddr set tmpAddr to decLine(tmpAddr) of me if tmpAddr = false then return false set tmpAddr to incCell(tmpAddr) of me if tmpAddr = false then return false set tmpCon to contents of item tmpAddr of testData if tmpCon = "1" then return false return {true, tmpAddr} end findNE on findSW(anAddress) copy anAddress to tmpAddr set tmpAddr to incLine(tmpAddr) of me if tmpAddr = false then return false set tmpAddr to decCell(tmpAddr) of me if tmpAddr = false then return false set tmpCon to contents of item tmpAddr of testData if tmpCon = "1" then return false return {true, tmpAddr} end findSW on findSE(anAddress) copy anAddress to tmpAddr set tmpAddr to incLine(tmpAddr) of me if tmpAddr = false then return false set tmpAddr to incCell(tmpAddr) of me if tmpAddr = false then return false set tmpCon to contents of item tmpAddr of testData if tmpCon = "1" then return false return {true, tmpAddr} end findSE ————————————— on incLine(anAddress) if anAddress + dataWidth > dataMax then set anAddress to false else set anAddress to anAddress + dataWidth end if return anAddress end incLine on decLine(anAddress) if anAddress – dataWidth < 1 then set anAddress to false else set anAddress to anAddress – dataWidth end if return anAddress end decLine on incCell(anAddress) set tmpMax to anAddress div dataWidth if ((anAddress + 1) div dataWidth) is not equal to tmpMax then set anAddress to false else if anAddress + 1 < dataMax then set anAddress to anAddress + 1 else return false end if end if return anAddress end incCell on decCell(anAddress) set tmpMax to anAddress div dataWidth set tmp2Max to (anAddress – 1) div dataWidth if anAddress – 1 > 0 then if (tmp2Max is equal to tmpMax) then set anAddress to anAddress – 1 end if else set anAddress to false end if return anAddress end decCell |