Archive for the 'iCal' Category

01/13 iCalで選択中のイベントのカレンダー情報を取得する

iCal上で選択中のイベントが所属しているカレンダーの名称を取得するAppleScriptです。

iCalのAppleScript用語辞書には致命的な欠陥が山のようにあって……GUI上で選択中のイベント(selection)を取得できません。普通のやり方では、選択中のイベントのカレンダー名を取得できません。もっと基本的なところで、いま表示中の月が何月なのかも分かりません。

日時を指定してイベントを登録したり、フィルタ参照でイベントを抽出したりするぐらいで、画面上で選択したイベントから対話的に何かの処理を行うようなAppleScriptを作りにくい(というよりも、作れない)状況にあります。

ical1.jpg

はい、おしまいおしまい〜

……などと言っていては海外の強豪Scripter連中に顔向けできません。

選択中のイベントが存在するのであれば、コピーを実行すると何らかの情報が得られるはずです。

実際にiCal上でコピーを実行してみると、

<ここから>
退去確認@ヴィラ山田
2011年1月22日 を 16:00 〜 17:00 にスケジュールしました
</ここまで>

のような内容が得られました。この情報から日付、開始時刻、終了時刻、イベント名 などを取得できます。

これらの情報を手がかりに、すべてのカレンダー上で検索(フィルタ参照による抽出)を行ってみました。

実行条件は、GUI Scriptingがオンになっていることと、iCal上でなにがしかのイベントが選択状態になっていることです。

ical2.jpg

GUI Scripting(UI Element Scripting)経由で選択イベントのコピー動作を行い、クリップボードの内容を解析。得られた情報をもとに今度はiCalに正攻法ですべてのカレンダーに対して「日付、開始時刻、終了時刻、イベント名」を手がかりにイベントの検索を行います。条件に合致するイベントが見つかったら、カレンダーの名称を取得して返します。これを複数のイベントに対してループで実行してみました。

実験してみたところ、複数のイベントを一度に選択した場合、その中に「繰り返しイベント」が含まれているとiCalがダイアログで警告します。このダイアログが邪魔なので、ダイアログのクローズ処理も行わなくてはなりません。

ical3.jpg

作成時間はトータルで40分程度でしょうか。かなり作り込みましたが、驚くべきことに、さほど実用性がありません、、、

スクリプト名:iCalで選択中のイベントの情報を取得する
set aRes to retSelectedEvent() of me

set icalRes to {}
repeat with i in aRes
  set j to contents of i
  
set the end of icalRes to getCalName(j) of me
end repeat
icalRes
–> {”ホーム”}

–選択中のiCal上のイベントが所属しているカレンダーの名称を取得する
on getCalName(aStr)
  
  
set aList to paragraphs of aStr
  
set aTitle to contents of item 1 of aList
  
set dStr to contents of item 2 of aList
  
  
set dOffset to offset of “日 を “ in dStr
  
set dDate to text 1 thru dOffset of dStr
  
–set dDateObj to date dDate
  
  
set tSeparator to offset of ” 〜 “ in dStr
  
if tSeparator = 0 then
    –「〜」がないことから、終日のスケジュールと判断
    
set timeFrom to “0:00″
    
set timeTo to “23:59:59″
  else
    –開始時刻と終了時刻のあるスケジュールと判断
    
set timeFrom to text (dOffset + 4) thru (tSeparator - 1) of dStr
    
set timeTo to text (tSeparator + 3) thru ((length of dStr) - 12) of dStr
  end if
  
  
set timeFromObj to date (dDate & ” “ & timeFrom)
  
set timeToObj to date (dDate & ” “ & timeTo)
  
  
  
  
tell application “iCal”
    set cnList to name of every calendar
    
set cList to every calendar
    
    
set cCount to 1
    
set findF to false
    
repeat with i in cList
      tell i
        set eList to (every event whose start date is not less than timeFromObj and start date is not greater than timeToObj and summary of it is aTitle)
        
if eList is not equal to {} then
          set findF to true
          
exit repeat
        end if
      end tell
      
set cCount to cCount + 1
    end repeat
    
    
if findF = true then
      set aName to contents of item cCount of cnList
    else
      set aName to “”
    end if
  end tell
  
  
return aName
end getCalName

–選択中のイベント情報をコピー(エラー時にはfalseをリターン)
on retSelectedEvent()
  activate application “iCal”
  
tell application “System Events”
    tell process “iCal”
      –「編集」メニューの項目「コピー」がイネーブルかどうかを取得
      
set copyEnabled to (enabled of menu item 5 of menu 1 of menu bar item 4 of menu bar 1)
      
if copyEnabled = false then return false
      
      
–コピーしてみる
      
try
        
        
keystroke “c” using {command down}
      on error
        –何らかの都合でコピー等の動作を行えなかった場合
        
return false
      end try
      
      
–ダイアログ表示時のボタンクリック
      
clickiCalDialog() of me
      
      
–クリップボードから文字情報を取得
      
set aConList to the clipboard
      
set aCon to aConList as string
      
    end tell
  end tell
  
  
set aList to parseByDelim(aCon, (ASCII character 13) & (ASCII character 13)) of me
  
  
return aList
  
end retSelectedEvent

–iCalでイベントをコピー時に、繰り返しイベントが選択されていた時に表示されるダイアログを乗り越えるためのルーチン
–(「キャンセル」ではない方のボタンを押す)
on clickiCalDialog()
  –ダイアログ検出
  
tell application “System Events”
    tell process “iCal”
      if (count every window) > 1 then
        if subrole of window 1 = “AXDialog” then
          tell window 1
            –キャンセル「ではない」方のボタンを取得
            
set bList to every button whose title is not equal to “キャンセル”
            
if bList is not equal to {} then
              set aButton to first item of bList
              
tell aButton
                click
              end tell
            end if
          end tell
        end if
      end if
    end tell
  end tell
end clickiCalDialog

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

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

12/11 iCal上で指定月の重複イベントを削除する

iCalデータが、さまざまなデバイスとのシンクロを繰り返した末に重複しまくってしまった、という友人から頼まれて作ったScriptです。

ical.jpeg

Script Menuなどに入れておいて実行し、実行対象月を入力すると(期間指定も可能)重複しているスケジュールを削除します。このあたり、iCalのAppleScript仕様がいまひとつ実用的でなく、「iCalで表示中の月」というのがAppleScript側から取得できないため、やむなくダイアログから入力してもらっています。

ical2.jpeg

対象月を入力すると重複イベントが削除されます。

ical3.jpeg

普通の環境であれば、「あっ!」という間に実行されてしまう程度のAppleScriptですが、残念なことに友人の環境ではデータ内容がすでにこわれかけていて、イベントの件数を求めたり1件削除するだけでも1日以上かかる状態に。

結局、このAppleScriptでは役に立たず、さらにiCalendarファイルを解析して重複イベントを削除するという、超強力で手の込んだものを作る羽目に。世界中探しまわっても、そこまで作り込んだAppleScriptにお目にかからなかったので、ちょっとした達成感はあったものの……友人にそのScriptを渡したところ、またスケジュールが重複したとか(汗) それはきっと、何か壊れているか、そうでなければたたられているに違いありません。

スクリプト名:iCal上で指定月の重複イベントを削除する v4
global g_tmpList, g_tmpList_ref

指定期間のイベントを取得
set todayDat to current date
set targYear to (year of todayDat) as string
set targMonth to (month of todayDat as number) as string
set aTarg to text returned of (display dialog "重複イベントの削除対象月は?(YYYY/MM)" default answer (targYear & "/" & targMonth))
set {sDate, eDate} to getRangeFromDateText(aTarg) of me

with timeout of 3600 seconds
  tell application "iCal"
    set theEvents to properties of every event of every calendar whose start date is greater than sDate and end date is less than eDate and summary is not equal to ""
  end tell
end timeout

入り組んだリストをフラットなリストに
set g_tmpList to {} 初期化
set g_tmpList_ref to a reference to g_tmpList
makeFlatList(theEvents) of me

set dIDList to {}

with timeout of 3600 seconds
  tell application "iCal"
    repeat with i from 1 to (length of g_tmpList_ref)
      set anItem to first item of g_tmpList_ref
      
set anRec to {start date of anItem, end date of anItem, summary of anItem}
      
set g_tmpList to rest of g_tmpList
      
      
repeat with ii in g_tmpList_ref
        set bRec to {start date of ii, end date of ii, summary of ii}
        
if anRec = bRec then
          set anID to uid of ii
          
if anID is not in dIDList then
            display dialog (("重複イベント" & return & (item 1 of anRec) as string) & return & (item 2 of anRec) as string) & return & item 3 of anRec
            
set the end of dIDList to anID
          end if
        end if
      end repeat
    end repeat
    
    
    
set delSucCount to 0
    
set delFailCount to 0
    
    
repeat with i in dIDList
      set delEvents to (every event of every calendar whose uid is equal to i)
      
makeFlatList(delEvents) of me
      
      
repeat with ii in g_tmpList_ref
        set aDelEv to contents of ii
        
if aDelEv is not equal to {} then
          try
            delete (contents of aDelEv)
            
set delSucCount to delSucCount + 1
          on error
            set delFailCount to delFailCount + 1
          end try
        end if
      end repeat
    end repeat
    
    
if delSucCount = 0 then
      display dialog "重複スケジュールは存在しませんでした。" buttons {"OK"} default button 1 with icon 1
    else
      set aMes to (delSucCount as string) & "件のスケジュールを削除。"
      
(*
    if delFailCount is not equal to 0 then
      set aMes to aMes & "うち、" & (delFailCount as string) & "件の削除に失敗しました。"
    end if
    
*)
      display dialog aMes buttons {"OK"} default button 1 with icon 1
    end if
  end tell
end timeout

再帰でフラットなリストを作成する
on makeFlatList(aList)
  repeat with i in aList
    set aClass to (class of i) as string 2行に分けたり、ここでstringにcastしなかったりすると……AppleScript Studio環境に持っていったときに動かなくなる!!
    
if aClass = "list" then
      makeFlatList(i) of me
    else
      set the end of g_tmpList to (contents of i)
    end if
  end repeat
end makeFlatList

on getRangeFromDateText(aText)
  if aText does not contain "" then
    普通にYYYY/MM指定のみ行った場合
    
set sDate to (aText & "/1")
    
set eDate to (date sDate) + (getMlen(year of (date sDate), month of (date sDate)) of me) * days
    
set sDate to date sDate
    
  else
    期間を「YYYY/MM…YYYY/MM」  で指定した場合
    
set curDelim to AppleScript’s text item delimiters
    
set AppleScript’s text item delimiters to ""
    
set tList to text items of aText
    
set AppleScript’s text item delimiters to curDelim
    
    
set item1T to contents of item 1 of tList
    
set item2T to contents of item 2 of tList
    
    
set sDate to date retYYYYMDD(item1T) of me
    
set eDate1 to date retYYYYMDD(item2T) of me
    
set eDate to eDate1 + (getMlen(year of sDate, month of eDate1) of me) * days
  end if
  
  
return {sDate, eDate}
end getRangeFromDateText

on retYYYYMDD(aText)
  if aText contains "/" then
    set bText to aText & "/1"
  else
    set thisYear to (year of (current date)) as string
    
set bText to thisYear & "/" & aText & "/1"
  end if
  
return bText
end retYYYYMDD

指定月の長さを得る(日数)
on getMlen(aYear, aMonth)
  
  
set aYear to aYear as number
  
set aMonth to aMonth as number
  
  
set aDat to (aYear as text) & "/" & (aMonth as text) & "/1"
  
if aMonth is not equal to 12 then
    set eDat to ((aYear as text) & "/" & (aMonth + 1) as text) & "/1"
  else
    set eDat to ((aYear + 1) as text) & "/" & (1 as text) & "/1"
  end if
  
  
set sDat to date aDat
  
set eDat to date eDat
  
set eDat to eDat - 1
  
  
set mLen to day of eDat
  
return mLen
  
end getMlen

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

07/28 iCalに「日本の祝日」を追加

iCalに、「Japanese Holidays」というAppleが配布しているWebcalの登録確認を行い、登録していない場合には登録するという処理を行います。

一部、GUI Scriptingによる処理を含んでいるため、Mac OS X 10.6以降でそのまま動くという保証はまったくないのですが、自前で日本の祝日を計算しなくてもiCalに問い合わせるだけでいい(しかも、祝日が増えた場合でも自動対応してくれる)ため、実装実験を行ってみたものです。

結局、このルーチンは実戦投入されませんでしたが、何かの折に使ってみたいものです。

スクリプト名:iCalに日本の祝日を追加
set aRes to addJapaneseHolidays() of iCalLib

script iCalLib
  iCalに「日本の休日」のwebcalを追加する
  
on addJapaneseHolidays()
    OSバージョンの確認を行う
    
set v2 to system attribute sys2 > 4/5
    
if v2 < 4 then return false 10.3や10.2などであれば実行禁止
    
    
GUI scriptingの有効チェックを行う
    
set guiRes to retGUIScriptingEnabled() of me
    
if guiRes = false then return false
    
    
repeat
      using terms from application iCal
        tell application iCal
          set cList to name of every calendar
          
          
if Japanese Holidays is not in cList then
            インターネット接続確認
            
set a to connection_check() of me
            
if a = false then return false
            
            
ignoring application responses
              GetURL webcal://ical.mac.com/ical/Japanese32Holidays.ics
            end ignoring
          else
            return true
          end if
        end tell
      end using terms from
      
      
activate application iCal
      
tell application System Events
        tell process iCal
          tell window 1
            tell sheet 1
              click button 2
            end tell
            
            
repeat
              delay 1
              
tell sheet 1
                set bCount to count every button
                
if bCount is not equal to 1 then exit repeat
              end tell
            end repeat
            
            
tell sheet 1
              if v2 = 4 then
                Mac OS X 10.4の場合
                
set value of checkbox 1 to 1
                
set value of checkbox 2 to 1
                
                
click button 2 of list 1 OKボタン
              else if v2 = 5 then
                Mac OS X 10.5の場合
                
tell list 1
                  set value of checkbox 1 to 1
                  
set value of checkbox 2 to 1
                  
set value of checkbox 3 to 1
                end tell
                
                
click button 1 OK
                
              else
                return false 10.6などの想定外のバージョンであった場合    
              end if
            end tell
          end tell
        end tell
      end tell
    end repeat
  end addJapaneseHolidays
  
  
on connection_check()
    try
      tell application System Events
        do shell script sbin/ping -c 1 www.google.com
      end tell
      
set the connection_status to true
    on error
      set the connection_status to false
    end try
    
return connection_status
  end connection_check
  
  
GUI Scriptingの設定判定。10.4/10.5対応
  
on retGUIScriptingEnabled()
    set v2 to system attribute sys2 > 4, 5
    
    
Mac OS X 10.4以上なら実行
    
if v2 > 3 then
      tell application System Events
        set anUIe to UI elements enabled
      end tell
      
      
if anUIe = true then
        return true UI Element Scripting (GUI Scripting)がイネーブルならtrueを返す
      else
        beep
        
display dialog UI Element Scriptingが有効になっていません。 & return & return & 「システム環境設定」の「ユニバーサルアクセス」で、「補助装置を使用可能にする」のチェックボックスにチェックを入れてから再実行してください。 with icon stop
        
if button returned of result is OK then
          tell application System Preferences
            activate
            
set current pane to pane com.apple.preference.universalaccess
          end tell
          
return false
        end if
      end if
    end if
  end retGUIScriptingEnabled
end script

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

04/07 iCalでReadOnlyではないカレンダーの番号を取得

iCalのカレンダーのうち、ローカルで情報を持っているもの(書き換え可能)のほか、Webサーバー上からダウンロードしてくるもの(書き換え不可)があり、後者は書き換えできないのでAppleScriptで処理するには向いていないケースが多いので、処理対象から外すためにこのようなサブルーチンを作ったものと思われます。

スクリプト名:iCalでReadOnlyではないカレンダーの番号を取得

set iCalList to getWritableCalendarNo() of me

R/Oではないカレンダーの番号を取得
on getWritableCalendarNo()
  set aList to {}
  
tell applicationiCal
    set calCount to count every calendar
    
repeat with i from 1 to calCount
      set writableF to writable of calendar i
      
if writableF is equal to true then
        set the end of aList to i
      end if
    end repeat
  end tell
  
return aList
end getWritableCalendarNo

▼新規書類に ▼カーソル位置に ▼ドキュメント末尾に

03/28 1月分のワークログ作成(iCal連携Ver)

ワークログ作成3部作の最終バージョンです。結局、カレンダーどおりに休めるわけでもなく、休みの情報はiCal上に休日カレンダーを作成していたので、それを参照してワークログを作ってくれるようにしたのがこのバージョンです。かといって、それほど時間をかけて作ったわけでもなく……実に効率よく作成できました。

iCalに各日の休み情報を問い合わせると、たとえば31日ある月であれば31回iCalにリクエストを投げる必要があるわけで、アプリケーションへの問い合わせオーバーヘッドを考えるとあまり得策とはいえません。AppleScriptそのものの処理速度はそんなに遅くはないのですが、アプリケーションとの通信を行うとずいぶん待たされます。そのため、本Scriptでは1か月分をまるごと問い合わせて、31回問い合わせるべきところを1回で済ませています。そのことで、初期試作バージョンよりも大幅に処理速度を向上させた次第です。

(more…)