アラートダイアログ上にWkWebViewを配置し、Google Chartsを用いてPie Chartを表示するAppleScriptです。
自分の開発環境(MacBook Pro Retina 2012, Core i7 2.6GHz)で10日間のアクセス履歴を処理して1.38秒程度でダイアログ表示してグラフ描画に入ります。3秒強ぐらいで描画が終了します。
WkWebViewに対して、文字列で組み立てたHTMLを読み込んでグラフ表示しています。Pie Chart上にマウスカーソルを乗せると反応します。一応表示できているぐらいで、デザイン的には余計な余白ができたままで、いまひとつな感じです。せめて、WkWebViewの背景を透明化できると大味な表示をいくらか緩和できるかと思っているのですが、WkWebViewの透明背景にはNSColorではなく(AppleScriptから作成できない)CGColorでclearColor(透明色)を指定する必要があるようで、、、、
サンプル表示データは、Safariの10日間のアクセス履歴からドメイン単位で計算したヒストグラムを、その場で計算したものを使っています。
本Scriptは、①NSAlertのダイアログ表示の経験 ②WkWebView上へのコンテンツ表示の経験 ③グラフ表示用FrameworkやSVGベースのグラフ作成の経験 を経て、2・3年越しで完成したものです。とくに、WkWebViewについてはサンプル数がそれほど多くなく、用途の方向性がObjective-CやSwiftと若干異なるために、参考にできるサンプルの数が多くない状況。必然的に、いろいろ試しては失敗を重ねてきました。急に出来上がってきたものではありません。
Safariの履歴集計ScriptをScriptオブジェクトの中に放り込んだら、scripting additionsの用語をusing terms from句で囲っておく必要がありました。何回か経験していますが、知らないと対処に困る現象です。
現状(macOS 10.14以降)では、Scripting AdditionはmacOS標準搭載のStandard Additionsしか存在していないし認識も行われないため、エラー内容がいまひとつわかりにくく、不正確であるように思えます。Apple純正のStandard Additionsしか存在していないんだから自分で判断しろやコラ、と言いたくなります。
AppleScript名:アラートダイアログ上にWebViewでGoogle Chartを表示 v2.scptd |
— – Created by: Takaaki Naganoya – Created on: 2019/03/02 — – Copyright © 2019 Piyomaru Software, All Rights Reserved — use AppleScript version "2.4" — Yosemite (10.10) or later use framework "Foundation" use framework "AppKit" use framework "WebKit" use scripting additions property |NSURL| : a reference to current application’s |NSURL| property NSAlert : a reference to current application’s NSAlert property NSString : a reference to current application’s NSString property NSButton : a reference to current application’s NSButton property WKWebView : a reference to current application’s WKWebView property WKUserScript : a reference to current application’s WKUserScript property NSURLRequest : a reference to current application’s NSURLRequest property NSRunningApplication : a reference to current application’s NSRunningApplication property NSUTF8StringEncoding : a reference to current application’s NSUTF8StringEncoding property WKUserContentController : a reference to current application’s WKUserContentController property WKWebViewConfiguration : a reference to current application’s WKWebViewConfiguration property WKUserScriptInjectionTimeAtDocumentEnd : a reference to current application’s WKUserScriptInjectionTimeAtDocumentEnd property returnCode : 0 –Calc Safari access domain from history set aRes to calcSafariHistoryBest10Domain(10) of safariHistoryLib set aList to {{"Domain", "Access"}} repeat with i in aRes set aDom to theName of i set accessNum to numberOfTimes of i set the end of aList to {aDom, accessNum} end repeat –set aList to {{"Task", "Hours per Day"}, {"Work", 8}, {"Eat", 2}, {"TV", 4}, {"Gym", 2}, {"Sleep", 8}} set aJsonArrayStr to array2DToJSONArray(aList) of me –Pie Chart Template HTML set myStr to "<!DOCTYPE html> <html lang=\"UTF-8\"> <body> <div id=\"piechart\"></div> <script type=\"text/javascript\" src=\"https://www.gstatic.com/charts/loader.js\"></script> <script type=\"text/javascript\"> // Draw the chart and set the chart values // Optional; add a title and set the width and height of the chart var options = {’title’:’’, ’width’:600, ’height’:400}; // Display the chart inside the <div> element with id=\"piechart\" var chart = new google.visualization.PieChart(document.getElementById(’piechart’)); chart.draw(data, options); } </script> </body> –my browseStrWebContents:paramObj–for debug my performSelectorOnMainThread:"browseStrWebContents:" withObject:(paramObj) waitUntilDone:true on browseStrWebContents:paramObj set aMainMes to myMessage of paramObj set aSubMes to mySubMessage of paramObj set htmlString to (htmlStr of paramObj) set aWidth to 600 set aHeight to 450 –WebViewをつくる set aConf to WKWebViewConfiguration’s alloc()’s init() –指定HTML内のJavaScriptをFetch set jsSource to pickUpFromToStr(htmlString, "<script type=\"text/javascript\">", "</script>") of me set userScript to WKUserScript’s alloc()’s initWithSource:jsSource injectionTime:(WKUserScriptInjectionTimeAtDocumentEnd) forMainFrameOnly:true set userContentController to WKUserContentController’s alloc()’s init() userContentController’s addUserScript:(userScript) aConf’s setUserContentController:userContentController set aWebView to WKWebView’s alloc()’s initWithFrame:(current application’s NSMakeRect(0, 0, aWidth, aHeight – 100)) configuration:aConf aWebView’s setNavigationDelegate:me aWebView’s setUIDelegate:me aWebView’s setTranslatesAutoresizingMaskIntoConstraints:true set bURL to |NSURL|’s fileURLWithPath:(POSIX path of (path to me)) aWebView’s loadHTMLString:htmlString baseURL:(bURL) — 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:aWebView set myWindow to its |window| end tell — show alert in modal loop NSRunningApplication’s currentApplication()’s activateWithOptions:0 my performSelectorOnMainThread:"doModal:" withObject:(theAlert) waitUntilDone:true –Stop Web View Action set bURL to |NSURL|’s URLWithString:"about:blank" set bReq to NSURLRequest’s requestWithURL:bURL aWebView’s loadRequest:bReq if (my returnCode as number) = 1001 then error number -128 end browseStrWebContents: on doModal:aParam set (my returnCode) to (aParam’s runModal()) as number end doModal: on viewDidLoad:aNotification return true end viewDidLoad: on fetchJSSourceString(aURL) set jsURL to |NSURL|’s URLWithString:aURL set jsSourceString to NSString’s stringWithContentsOfURL:jsURL encoding:(NSUTF8StringEncoding) |error|:(missing value) return jsSourceString end fetchJSSourceString on pickUpFromToStr(aStr as string, s1Str as string, s2Str as string) set a1Offset to offset of s1Str in aStr if a1Offset = 0 then return false set bStr to text (a1Offset + (length of s1Str)) thru -1 of aStr set a2Offset to offset of s2Str in bStr if a2Offset = 0 then return false set cStr to text 1 thru (a2Offset – (length of s2Str)) of bStr return cStr as string end pickUpFromToStr –リストを任意のデリミタ付きでテキストに on retArrowText(aList, aDelim) set aText to "" set curDelim to AppleScript’s text item delimiters set AppleScript’s text item delimiters to aDelim set aText to aList as text set AppleScript’s text item delimiters to curDelim return aText end retArrowText on array2DToJSONArray(aList) set anArray to current application’s NSMutableArray’s arrayWithArray:aList set jsonData to current application’s NSJSONSerialization’s dataWithJSONObject:anArray options:(0 as integer) |error|:(missing value) –0 is set resString to current application’s NSString’s alloc()’s initWithData:jsonData encoding:(current application’s NSUTF8StringEncoding) return resString end array2DToJSONArray –Safariのn日前からの履歴をドメイン別に集計してソート script safariHistoryLib use scripting additions use framework "Foundation" property parent : AppleScript property |NSURL| : a reference to current application’s |NSURL| script spd property sList : {} property nList : {} property sRes : {} end script on calcSafariHistoryBest10Domain(daysBefore) –現在日時のn日前を求める using terms from scripting additions set origDate to (current date) – (daysBefore * days) end using terms from set dStr to convDateObjToStrWithFormat(origDate, "yyyy-MM-dd hh:mm:ss") of me set aDBpath to "~/Library/Safari/History.db" set pathString to current application’s NSString’s stringWithString:aDBpath set newPath to pathString’s stringByExpandingTildeInPath() set aText to "sqlite3 " & newPath & " ’SELECT datetime(history_visits.visit_time+978307200, \"unixepoch\", \"localtime\"), history_visits.title || \" @ \" || substr(history_items.URL,1,max(length(history_items.URL)*(instr(history_items.URL,\" & \")=0),instr(history_items.URL,\" & \"))) as Info FROM history_visits INNER JOIN history_items ON history_items.id = history_visits.history_item where history_visits.visit_time>(julianday(\"" & dStr & "\")*86400-211845068000) ORDER BY visit_time ASC LIMIT 999999;’" using terms from scripting additions set (sRes of spd) to do shell script aText end using terms from set (sList of spd) to (paragraphs of (sRes of spd)) set (nList of spd) to {} repeat with i in (sList of spd) set j to contents of i –Parse each field set j2 to parseByDelim(j, {"|", "@ "}) of me –Calculate URL’s domain only set j3 to contents of last item of j2 set aURL to (|NSURL|’s URLWithString:j3) if aURL = missing value then set j4 to "" else set j4 to (aURL’s |host|()) as string end if set the end of (nList of spd) to j4 end repeat –登場頻度でURLを集計 set aList to countItemsByItsAppearance((nList of spd)) of me –Best 10の計算のために10位以下をまとめる set best9 to items 1 thru 9 of aList set best10 to items 10 thru -1 of aList set otherTimes to 0 repeat with i in best10 set otherTimes to otherTimes + (numberOfTimes of i) end repeat set the end of best9 to {theName:"Other", numberOfTimes:otherTimes} return best9 end calcSafariHistoryBest10Domain on parseByDelim(aData, aDelim) set curDelim to AppleScript’s text item delimiters set AppleScript’s text item delimiters to aDelim set dList to text items of aData set AppleScript’s text item delimiters to curDelim return dList end parseByDelim –出現回数で集計 on countItemsByItsAppearance(aList) set aSet to current application’s NSCountedSet’s alloc()’s initWithArray:aList set bArray to current application’s NSMutableArray’s array() set theEnumerator to aSet’s objectEnumerator() repeat set aValue to theEnumerator’s nextObject() if aValue is missing value then exit repeat bArray’s addObject:(current application’s NSDictionary’s dictionaryWithObjects:{aValue, (aSet’s countForObject:aValue)} forKeys:{"theName", "numberOfTimes"}) end repeat –出現回数(numberOfTimes)で降順ソート set theDesc to current application’s NSSortDescriptor’s sortDescriptorWithKey:"numberOfTimes" ascending:false bArray’s sortUsingDescriptors:{theDesc} return bArray as list end countItemsByItsAppearance on convDateObjToStrWithFormat(aDateO as date, aFormatStr as string) set aDF to current application’s NSDateFormatter’s alloc()’s init() set aLoc to current application’s NSLocale’s currentLocale() set aLocStr to (aLoc’s localeIdentifier()) as string aDF’s setLocale:(current application’s NSLocale’s alloc()’s initWithLocaleIdentifier:aLocStr) aDF’s setDateFormat:aFormatStr set dRes to (aDF’s stringFromDate:aDateO) as string return dRes end convDateObjToStrWithFormat end script |