Googleマップのルート検索結果をカレンダーに追加するサービスメニュー

iOS 版の Google Maps アプリは電車やバスを使う場合のルート検索結果の詳細表示画面で、再下方にスクロールすると「カレンダーに追加」ボタンがありカレンダーアプリに予定を追加することができますが、MacOS のブラウザを使った場合に同じことができないものかと模索した結果、Automator でサービスメニューに追加するべくワークフローを作成することにしました。ファイルは Dropbox に置いたのでご自由にお使いください。

GMapルート検索結果をカレンダーに追加.workflow

使い方は、

  1. ダウンロード後、ZIP 解凍してダブルクリックをするとサービスインストーラーのダイアログが表示されるのでインストールボタンを押してください。
  2. ルート検索の詳細を表示した画面で、サービス>GMapルート検索結果をカレンダーに追加をメニューから選択してください。現在、Safari と Chrome のみ対応しています。

    電車/バスルート検索の詳細画面以外でサービスを実行するとエラーを返します。

  3. カレンダーアプリが前面に来て一覧を表示するので、予定を入れるカレンダー名を選択して OK を押してください。
  4. 予定の内容は、可能な限り iOS 版に近いものにしました。日跨ぎの予定に対応し、出発/到着時間の指定により一週間内で日付を判定して追加するようにしました。

出発/到着時間の設定にカレンダーで選択ができるにも関わらずその指定日へ予定が追加できないのは、グーグルが電車やバスの運行状況を曜日毎でのみデータを保持しているからなのか、検索結果に曜日しか表示されないという理由があります。AppleScript のソースコードは最初の手順で Automator で開くと見ることができるので、改変などご自由にどうぞ。

技術的な余談で言うと、Yosemite から利用できる JavaScript for Automation (JXA) での記述も検討したのですが、2つのブラウザの処理の違いとコード量、結局 JavaScript で DOM 操作をすることなどを考えると従来の方法になってしまいました。

参考:

WebViewのフォーム入力でImmersiveモードを維持する

前回、Javascript の alertconfirm 関数を使って WebView から AlertDialog を呼び出した際に全画面表示を止めてしまう問題について取り組んでいた時に、入力フィールドへフォーカスした際に Immersive モードが解除されるのは仕方がないとしても、その扱いについて考えていたところ、こちらも一癖ある挙動だったので紹介します。

通常 Immersive モードではない場合、WebView にキーボードが被るという問題を解決すべく、AndroidManifest.xml の activity ノード属性として android:windowSoftInputMode="adjustResize" と書くのですが、Immersive モードの場合はこの方法がうまくいきません。そこで試してみたのが、入力フィールドがフォーカスした時に Immersive モードを解除する、という方法です。

まず、MainActivity クラスの中に次のような JavaScript インターフェースを用意し、

public class WebViewJavaScriptInterface{
  private Context context;
  public WebViewJavaScriptInterface(Context context){
    this.context = context;
  }
  @JavascriptInterface
  public void disableFullScreen() {
    getWindow().getDecorView().setSystemUiVisibility(0);
  }
}

WebView について色々と書いてある onCreate の中で JavaScript から呼び出せるように設定しました。

webView.addJavascriptInterface(new WebViewJavaScriptInterface(this), "app");

そしてテキスト入力フィールドを1つ持っている HTML ではこのように JavaScript から Immersive モードを解除するようにしました。

document.querySelector('input[type=text]').ontouchend = function(){
  app.disableFullScreen();
};

ところが、Logcat に “Uncaught Error: Java exception was raised during method invocation” というエラーを出力しうまく動かなかったので、MainActivity で Handler を呼び出し、JavaScript インターフェース内の関数を次のようにすると期待通りの動作をエミュレータで確認しました。

@JavascriptInterface
public void disableFullScreen() {
  handler.postDelayed(new Runnable() {
    @Override
    public void run() {
      getWindow().getDecorView().setSystemUiVisibility(0);
    }
  }, 0);
}

また、JavaScript インターフェース内にもう一つ次のようなものを用意して(前回不要だと思った enableImmersiveMode 関数を再度用意)、

@JavascriptInterface
public void enableFullScreen() {
  handler.postDelayed(new Runnable() {
    @Override
    public void run() {
      enableImmersiveMode();
    }
  }, 0);
}

送信ボタンを押した時に Immersive モードとなるようにしました。

document.querySelector('#submit').ontouchend = function(){
  app.enableFullScreen();
};

実際にこのような挙動で動くようになったのですが、

ソフトウェアキーボード以外の場所で画面を押すと入力フィールドからフォーカスが外れてキーボードが隠れるという OS の設計上、このように加えておくと更に安心です。

document.querySelector('input[type=text]').onblur = function(){
  app.disableFullScreen();
};

実機で検証してみると、処理能力が劣る古い機種ではフルスクリーンになる際にステータスバーが途中で止まってしまったりということが確認できたので、postDelayed で遅延を750〜1000と与えると(動作的にはギクシャクする感は否めないのですが)解決しました。そして、最初に書いていた android:windowSoftInputMode="adjustResize" を外してみると…なんと入力フィールドが隠れずにキーボードに合わせて移動するという世にも奇妙な…。

参考:

WebViewのAlertDialogでImmersiveモードを維持する

Android の上部にあるツールバーと下部にあるナビゲーションバーを隠しフルスクリーンで表示する「Immersive モード」を使ってハイブリッドアプリを開発している際に、WebView 内の JavaScript で confirm を呼び出すと、全画面表示が解除されてツールバー及びナビゲーションバーが再び表示されてしまうという問題に直面したので調べました。

元々は confirm のボタンラベル名を拡張すべく WebChromeClient 内でこのようなコードを使っていました。

@Override
public boolean onJsConfirm(WebView view, String url, String message, final JsResult result)  {
  new AlertDialog.Builder(MainActivity.this)
    .setMessage(message)
    .setPositiveButton("確認",
      new DialogInterface.OnClickListener() {
        public void onClick(DialogInterface dialog, int id) {
          result.confirm();
        }
      })
    .setNegativeButton("消去",
      new DialogInterface.OnClickListener() {
        public void onClick(DialogInterface dialog, int id) {
          result.cancel();
        }
      })
    .setCancelable(false).create().show();
  return true;
};

この問題の根本にある原因はダイアログがフォーカスを持ってしまうことにあるようなので、参考にした文献のコードを利用し(ワンライナーフェチとしては少し残念な) AlertDialog を一旦 alertToShow に入れるようにして addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE) を追加しました。

@Override
public boolean onJsConfirm(WebView view, String url, String message, final JsResult result)  {
  AlertDialog alertToShow = new AlertDialog.Builder(MainActivity.this)
(中略)
  alertToShow.getWindow().addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE);
  alertToShow.show();
  return true;
};

今回の失敗談を一つ紹介すると、MainActivity に次のような全画面設定関数を用意して setPositiveButtonsetNegativeButton のそれぞれの onClick で呼び出したのですが、案の定ボタンを押した後に Immersive モードを再開するという間抜けな結果になり、上述したコードで不要になりました汗。

@SuppressLint("NewApi")
private void enableImmersiveMode() {
  if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
    getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_IMMERSIVE | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
  }
}

この例は JavaScript の confirm でしたが、もちろん alert でも同じような処理で解決できると思います。更に調べているとテキストフォーム入力時の input にフォーカスした際に下部からニュッと現れるソフトキーボードと共に Immersive モードが解除されるという問題について Stackoverflow で多く議論されていることが分かりました。こちらも原因は同様なのですが WebView と連携した場合にどうなるのか、別投稿で紹介したいと思います。

参考:

Safari 9.1.1でAppleScriptからJavaScriptを実行する

以前、Ingress のプレイ記録用途に AppleScript を書いたのだけど、半径3キロメートルに及ぶノバ作戦を近所で開催した時にもっと大きなインテルマップを表示しようと、次のようなコードを追加していました。

on fullScreenIntelMap()
	tell application "/Applications/Safari.app"
		tell document 1
			do JavaScript "document.getElementById('dashboard_container').style.left='0';document.getElementById('dashboard_container').style.top='0';document.getElementById('dashboard_container').style.right='0';document.getElementById('dashboard_container').style.bottom='0';document.getElementById('comm').style.display='none';document.getElementById('player_stats').style.display='none';document.getElementById('rs_box').style.display='none';"
		end tell
	end tell
end fullScreenIntelMap

この関数を起動時の on run と再読み込み時の on idle で実行すると、ヘッダ部分やフッタ部分を隠しブラウザのフルスクリーンモードにして可能な限り広域な地図状況を記録しておくことができるのですが、最近になってこのスクリプトを使うと Safari が次のようなエラーを返してきました。
スクリーンショット 2016-06-01 9.03.48

You must enable the ‘Allow JavaScript from Apple Events’ option in Safari’s Develop menu to use ‘do JavaScript’.

この内容に従って「開発」メニューを覗いてみると次のように「Apple Events からの JavaScript を許可」というコマンドがありました。

スクリーンショット 2016-06-01 9.04.24
この見慣れぬコマンドについて確認しようと Apple のサポートページを見ると未だ記述がないので、どうやら最新のバージョン 9.1.1 から追加されたものと思われます。そこでもう少しおググりなさってみると、数日前に “do JavaScript 記述のある AppleScript が動かなくなったよ〜” という内容で、とあるマックサイトのフォーラムに投稿している人がいました。

その内容によると、どうやら Safari の「開発」メニューを表示しておかないと上述したようなエラー内容を表示せず、コンソールに不可解なエラーメッセージだけが残るそうなので注意が必要です。

http://hkitago.tumblr.com/post/145244516611

確かに近年 JavaScript の台頭と安全性について語られることが増えていますが、わざわざ開発メニューから設定する必要があるのは柔軟性という点で開発者にとってはなかなか厳しい変更点なのではないかと思いました。またこのフォーラムでも懸念されていますが、Safari 以外で AppleScript と連携ができる(特にウェブ開発で使うような)アプリケーションの対応についても今後の動向に少し注目しておく必要がありそうです。

参考:

ポップアップするステータスバー表示を止める

Google Chrome ブラウザをフルスクリーンモードやプレゼンテーションモードで使用していると、リンクにマウスを乗せた際、画面左下にポップアップするステータスバーが表示されるのだけど、プレゼン資料用スライドショーアプリを使う場合など、これを表示しないようにする必要があり方法について検討しました。

スクリーンショット 2016-04-25 10.25.56

解決策は非常に簡単で、A タグに href 属性を与えないということでした。

window.addEventListener('load', function() {
	var aNodes = document.getElementsByTagName('a');
	for (var i = 0, max = aNodes.length; i < max; i++){
		aNodes[i].removeAttribute('href');
	}
}, false);

もちろん、マウスを乗せた場合に形状は変えたいという状況には、a:hover {cursor:pointer;} とスタイルを設定することで対応ができます。

元々のリンク遷移を生かしたい場合も、A ノードのループ処理内で aNodes[i].onclick = "location.href='" + aNodes[i].getAttribute('href') + "'"; と先に処理を入れると良いかと思います。フレーム処理している場合は target 属性の付与が必要になるのでご注意。

P.S. 最近は 直前に記述するので window.addEventListener が不要なことが増えました。

Chromeのブックマークバーにショートカットを当てる機能拡張

OS X で Safari をメインブラウザとして使っていた理由の一つに、コマンド+数字というショートカットでブックマークバーの項目を呼び出せることがあったのだけど、次のバージョンの開発プレビュー版に更新してから暫くこのショートカットが Chrome のようにタブを呼び出すようになってしまったことに加え、SafariDAVClient が暴走するという正式リリース前にはよくある問題に直面し、更にグレードダウンもできないということから、既定のブラウザを Chrome にし、ブックマークバー(Safari では「お気に入りバー」という)に並ぶブックマークレットをショートカットで呼び出す機能拡張を自作することにした。

http://hkitago.tumblr.com/post/123624450876


(この機能設定はようやく v9 10601.1.50 Seed 5で実装された)

実際にマニュフェストファイル(manifest.json)と本体の JavaScript ファイル(background.js)はこのようなものを書いた。

{
    "name": "Bookmarks Bar Launcher",
    "version": "1",
    "manifest_version": 2,
    "permissions": [
    "bookmarks",
    "tabs",
    "<all_urls>"
  ],
    "background": {
        "scripts": ["background.js"],
        "persistent": false
  },
    "commands": {
        "bblCommand1": {
            "suggested_key": {
                "default": "Ctrl+1",
                "mac": "Command+1"
            },
            "description": "Item 1"
        },
        "bblCommand2": {
            "suggested_key": {
                "default": "Ctrl+2",
                "mac": "Command+2"
            },
            "description": "Item 2"
        },
        "bblCommand3": {
            "suggested_key": {
                "default": "Ctrl+3",
                "mac": "Command+3"
            },
            "description": "Item 3"
        },
        "bblCommand4": {
            "suggested_key": {
                "default": "Ctrl+4",
                "mac": "Command+4"
            },
            "description": "Item 4"
        }
    }
}
chrome.commands.onCommand.addListener(function(command) {
	var url;
	switch(command) {
		case 'bblCommand1': url = url_items[0]; break;
		case 'bblCommand2': url = url_items[1]; break;
		case 'bblCommand3': url = url_items[2]; break;
		case 'bblCommand4': url = url_items[3]; break;
	}
	if((/^javascript:/).test(url)) {
		url = url.replace(/javascript:/g, '');
		chrome.tabs.query({active: true, currentWindow: true}, function(tabs) {
			chrome.tabs.executeScript(tabs[0].id, {code: decodeURI(url)});
		});	
	} else {
		window.open(url);	
	}
});
var url_items = new Array();
chrome.bookmarks.getTree(process_bookmark);
function process_bookmark(bookmarks) {
	for(var i = 0; i < bookmarks.length; i++) {
		var bookmark = bookmarks[i];
		if(bookmark.url && (bookmark.url).indexOf('chrome://bookmarks/')) {
			url_items.push(bookmark.url);
		}
		if(bookmark.children) {
			process_bookmark(bookmark.children);
		}
	}
	url_items.splice(4);
}

参考したスタックオーバーフローで知ったのだけど、Google Chrome Extension では最大4つまでしかコマンドを上書きできないそうで、Safari では1〜9までのショートカットになるところを、今回は1〜4までとなってしまった。左手でのショートカットキー操作を考えるとそれでも良いかなという気もしたので、名前を変えた同様の機能拡張を2つ用意することは止めておいた。

また、頻繁に利用する4つ共にブックマークレットを使うことから単に window.open しないで chrome.tabs.executeScript と振り分けをするところが肝。更にブックマークバーの初期状態では先頭に Bookmark という項目があるのでそれをスキップする処理も入れた。今のところの問題は、ブラウザを最初に起動した時に限りこの機能を読み込むので、起動中ブックマークバーの項目を入れ替えた場合に順序の整合性が取れない点にあるのでご注意。

最後に、この機能拡張を利用するには適当な場所にフォルダを作成し2つのファイルを保存した後、Chrome の環境設定>機能拡張と進み、右上の「デベロッパーモード」にチェックを入れると表示される「パッケージ化されていない機能拡張を読み込む…」ボタンを押し、先のフォルダを選択する。
スクリーンショット 2015-08-20 10.19.17

ここまでやってみてうまく動かない場合は、機能拡張環境設定画面の最下方右にある「キーボードショートカット」というリンクをクリックすると、改めてお好みのショートカットを割り当てることができるので設定すると良い。
スクリーンショット 2015-08-20 10.20.50

面倒臭いからパッケージ化された .crx ファイルをよこせ!という方がいらしたら遠慮なくご連絡ください。

http://hkitago.tumblr.com/post/124450780756

P.S. ちなみにウィンドウズ版でも動作することを確認していますが、Safari 体験が少ないと思うので手厚いサポートは遠慮したいところです笑

参考:

SELECT要素のテキストを中央揃えにする

Webkit で表示するウェブアプリ製作の過程で SELECT (正確には OPTION になるかな)の要素内に表示されるテキストを中央揃えにする必要があって調べてみたところ面白いやり方が Stack Overflow にあったので紹介します。

もちろん最初に思いつくのは、スタイルシートを使って text-align: -webkit-center; とすることなのだけど、iOS では使えないことがそもそも知られている上に、開発環境の検証に使っている OS X の Safari 8 でも廃止されているようで上手く表現できずに却下することにしました。

次に考えたのは OPTION 要素のテキスト幅に合わせて SELECT 要素の幅を JavaScript から CSS で制御するという方法で、これと同じ発想を持った人がいたということです。

具体的には、まず SELECT の CSS を display: block; としておきつつ、width: 0 auto; を使って、あらかじめ用意しておいた固定幅を持つ親要素内で中央に配置するようにします。

それから、適当な要素(SPAN が適任!)をどこにでもいいので用意(詳細は後述)し、SELECT の ONCHANGE イベントで、選択された OPTION の TEXT 値を取得し、その要素に一時的に入れます。

ここまでくると結果は容易に想像できると思うのですが、その幅を読み取り、SELECT に適応するという手筈になります。

参考にしたコメントでは jQuery を使っていたのですが、今回のプロジェクトでは利用していなかったため次のような関数を使って SELECT の ONCHANGE イベントで呼ばれる関数内で発動することにしました。

var setSelectWidth = function(elm) {
  if(elm.selectedIndex < 0) {
    return;
  }
  var elm_tmp = document.getElementById('width_tmp');
  elm_tmp.innerHTML = elm.options[elm.selectedIndex].text;
  elm.style.width = elm_tmp.offsetWidth + 'px';
};

先に「後述」と書いたのは、この一時的にテキストを入れて幅を読み取る SPAN 要素の扱いで、これまた参考したものでは display: none; としつつも、jQuery の width() で幅を取得できているのですが、offsetWidth を使った場合は上手くいかず、代わりに position: absolute; として画面領域外にぶっ飛ばしておくという alert のタイトルを省いた時のような小技を使いました。

更に参考文では、SELECT に適用する幅の値に余白を与えていますが、SELECT に PADDING: 0; の設定と、両方の要素のフォントを揃えておくことで不要になるかと思います。また、 -webkit-appearance の設定の仕方で結果が若干変わってくることも考えられるので、SELECT の CSS に BORDER を付けて逐一確認するのが良いかと思います。

最後に疑問点としては、offsetWidth ではなくて clientWidth を使うとどうなるのか、ということと、display: none; の指定がある要素の幅を jQuery の width() はどのように取得しているのか、ということで、毎度ながら時間が許せば調べてみたいところです。

P.S. 右揃えだけなら dir 属性を使うと簡単に解決します。

参考:

SafariのMinimal-Ui Viewportメタタグ

具体的にはこのようなコードを読み込み時に加えていたものが必要なくなるとのことで、Android の対応も期待されます。

setTimeout(function() { window.scrollTo(0, 1); }, 0);


https://twitter.com/DonaldASuth/status/413019703825924096


ホーム画面に登録したウェブアプリから外部リンクへ誘導しない問題も解消したようで高い iOS7 普及率に応じることができそうです。
P.S. 出かけることが増える休日前にβは禁物…

「なんでやねん」ボタン

そう,Facebookでは「ぼけ」はできても「つっ込み」は無理ですね。「へえそうなんだ」の文化圏の人には「いいね!」ボタンで十分かもしれません。

そこで,私は関西人のために,以下のようなfacebookボタンを用意することを提案します。

引用元: Facebookは関西人のコミュニケーションスタイルに合っていない | オブジェクト脳@kcg.

表示だけならこのブックマークレットを Facebook の表示中に使うといいです。

javascript:document.body.innerHTML=document.body.innerHTML.replace(/いいね/g,'なんでやねん');focus();

スクリーンショット 2013-10-10 9.51.06
具体的には上記の文字列を選択して、PCのブラウザならブックマークバーにドラッグ&ドロップ、モバイルならブックマーク編集でペーストしてお使いください。
P.S. Chrome 機能拡張の参考を追加

参考:

UIWebViewのAlert Titleを省く

ウェブアプリが主戦場の開発者には悩ましい iOS の UIwebView 内の JavaScript から Alert や Confirm といった関数を使う場合にタイトル箇所がファイル名称になってしまう問題の解決方法を、偶然にも PhoneGap 関連のコードを眺めていて見つけたので紹介します。

通常の手段で Confirm 関数はこのような表示になるのですが、
IMG_2215

上述したコードを参考にこのような関数を用意して呼び出すと、

var setConf = function(str) {
  var tmpFrame = document.createElement('iframe');
  tmpFrame.setAttribute('src', 'data:text/plain,');
  document.documentElement.appendChild(tmpFrame);
  var conf = window.frames[0].window.confirm(str);
  tmpFrame.parentNode.removeChild(tmpFrame);
  return conf;
};

上部太字のタイトル部分を省いてこのような表示になります。
IMG_2214

1つ気を付けたい点は、繰り返し高負荷な処理を与えてみたところ次のように IFRAME の枠らしき物体が現れたので、
IMG_2278

このようにスタイルを指定して回避しました。(位置指定はどちらかだけでも十分ですね…)

iframe {
  position:absolute;
  width:1px;
  height:1px;
  top:-1px;
  left:-1px;
}

本来ならば太字のタイトル部分をまず表示することが望ましいので別の方法として UIWebView の JavaScript から Objective-C の stringByEvaluatingJavaScriptFromString 関数を呼び出して表示を整形する若干面倒なやり方があります。また Stack Overflow にも同様の回答がありましたのでウェブアプリ開発の参考になれば幸いです。

P.S. ちなみに Android (エミュレータ) の WebView ではタイトルの制御が不可能だったのですが、技術的には問題なく動作しましたので WebChromeClient クラスの onJsAlertonJsConfirm 関数でタイトルを省く処理を入れておけば同等のコードで大丈夫かと思います。

参考: