神アップデートされた Pythonista 3.3 の新機能をざっくり紹介するよ

20220227_ogp

驚きました。

今日 Pythonisita のアップデートが来てたんだけど、まさかこんなに進化するとは思ってなかったんですよ。
Pythonisita ってある程度完成されたアプリだったし、でもまあ Python の進化に合わせてたまに更新する程度なんだろうなと思っていたので。

今回のリリースノートの「新機能」の出だしにしたって、

Support for dark mode on iOS 13 – you can now select separate themes for light/dark mode, and Pythonista will switch automatically between them. Switching between themes is also a bit faster now.

「iOS13のダークモードに対応しました」ですよ。もう普通すぎて(真顔)

ところがですよ、次の一節に気になる言葉が…

New custom keyboard (“PyKeys”) for running scripts in any app with text input. Have a look at the sample scripts in the Examples/Keyboard folder for some ideas of what you can do with this.

カスタムキーボード?
TextExpander みたいなあれ?

ちょっと気になったんでリリースノート見ながら新機能チェックしてみました。

結果、ごめんなさい神アップデートでした。

目玉はなんと言ってもカスタムキーボード

もうね、こんな感じですよ。

日付貼り付け

デフォルトのメモアプリですからね、これ。

挙句、メモの中で Python 実行できたりしますからね。

REPL実行

サンプルも結構入ってて、全部羅列しても仕方ないので軽く導入法だけ紹介。
といっても、Pythonista使うような人に事細かな説明いらないだろうけど、
普通に設定 → 一般 → キーボードと進んでいって、

設定1

新しいキーボード追加から Pythonista キーボードを追加するだけですね。

設定2

「フルアクセスの許可」は必ずしも必要ないそうですが、クリップボードの使用やネットワークアクセスなどで必要となるそうです。

それからアプデ後にまだ一度もPythonistaを起動させていなければ、必ず一度起動させておきます。
カスタムキーボードの使用時に起動している必要はありません。

スクリプトを作ってみる

どのようなサンプルがあるのかは各々試してもらうとして、軽くスクリプトの作り方に触れておきます。

どう書くかは公式のモジュールリファレンスに「keyboard」という項目が新たに追加されてますね。

公式マニュアル目次

http://omz-software.com/pythonista/docs/ios/keyboard.html

ここがカスタムキーボードに関するリファレンスのようです。
ざっと見ていきます。

  • 選択されたテキストを取得する
    keyboard.get_selected_text()
  • テキスト text を挿入する
    keyboard.insert_text(text)

この2つだけでもかなりのことができそうですね。

新規作成

ではスクリプトを新規作成します。スクリプト名はなんでもいいけど “tag” としておきました。

新規作成
拡張子は Pythonista が付けてくれます

書く

#!python3
# coding: UTF-8

import keyboard

if keyboard.is_keyboard():
    select = keyboard.get_selected_text()
    if select:
        keyboard.insert_text(f"<p>{select}</p>")
  1. Pythonistaキーボード用のモジュール “keyboard” をインポートします。
  2. keyboard.is_keyboard() はカスタムキーボードで実行されているか判定します。
  3. あとはさっきの命令ですね。keyboard.get_selected_text() はあれば選択文字、なければ空を返します。
  4. 最近知ったけど、今は f”文字列{変数}文字列” とすれば変数を埋め込めるんですね。

キーボードから使えるよう登録する

書き終わったらカスタムキーボードから使えるように登録します。

まず、必ず登録したいスクリプトを開いた状態で右上のレンチアイコンをタップします。

赤マーカーするほどかと思うかもしれませんが重要ですからね、ここ。
PCなんかで書いてクラウド経由でコピーしたり、他人のスクリプトそのまま使う時には手順忘れてますから。

登録1

次はこのようにさくさく進めていきましょう。

登録2

登録画面となります。

  • Custom Title: 好きなタイトルに変更できるけど、空のままでもいい
  • Icon: 好みで

よければ「Add」をタップ。

登録3

きちんと登録できてるか確認して「Done」。

登録4

動作確認してみる

テキスト入力できる適当なアプリで動作確認してみます。
ここではメモアプリでやってみました。

動作タグ

問題なさそうですね。
スクリプト自体はベタすぎるタグ挟みで面白くもなんともありませんが。

他のスクリプトも登録してみる

登録の方法がわかったところで、すでにある他のスクリプトを登録してみるのも面白いかもしれません。
たとえばサンプルの、

Examples → User Interface → Calculator.py

を登録するとこうなります。

動作電卓
Calculator.pyはExamples → Widgetにもありますが、そっちではありません。それはWidget内でしか動かないです。

元のサイズが違うのでレイアウトこそ崩れてますが、意外と動くものですね。
他にもほんの少しの修正で使えるスクリプトがあるかもしれません。

プログラミング時の注意点

ところで他アプリからテキストを取得する際の注意点ですが、本家のモジュールリファレンスに次のようなWarning: があります。

公式Worning

なにが書いてるかというと、

  • 2行以上や1000文字以上のテキストは切り捨てられることがある
  • システムによるものなので Pythonista側ではどうしようもない
  • また、切り捨てがあったかどうかもPythonista側で知ることは不可能

なので、テキストまるごと取得して加工みたいなことはできませんね、残念。

その他の新機能

カスタムキーボードについておおかた見終わったところで、他の新機能についても軽く流しておきます。

外部キーボードのサポートが大幅に改善

Significantly improved support for external keyboards (more contextual shortcuts, arrow-key navigation almost everywhere…)

これについては、なにしろ以前のバージョンで外部キーボードでPythonistaを使ったことがないので、どこがどう変わったのかはわかりません。

エディターのアウトラインをフィルターできるようになりました

The outline (list of functions) in the editor can now be filtered — just start typing if the keyboard is already active, or drag down the list to reveal the filter text field. The filter supports fuzzy matching, and you can enter line numbers as well.

これについては実際見てもらった方が早いでしょう。
サンプルスクリプトの Examples → Animation → AnalogClock.py を例に説明します。

まずファイル名部分をタップします。

関数フィルタ1

次のような関数リストが現れます。指で押さえて下方向にスライドしましょう。

関数フィルタ2

入力欄が現れます。

関数フィルタ3

文字を入力するとフィルタリングされます。タップすればそこに飛べます。

関数フィルタ4

数字を入力すると行番号にジャンプできます。

関数フィルタ5

ショートカット作成のためのUI

Unified UI (and documentation) for creating script shortcuts in various places of iOS (“Shortcuts” option in the “wrench” menu).

すでにキーボードのスクリプト登録で見ましたよね。こういうのです。

ショートカットUI

アプリ間自動化を容易にする新しいURLジェネレーター

New URL generator for easier inter-app automation (you can also use this with the Shortcuts app, but full Shortcuts support will come later).

サンプルスクリプトの Examples → Animation → AnalogClock.py を例に説明します。
スクリプトを開いてこの順に進んで「Copy URL」をタップします。

URLジェネレーター

他のアプリにペーストして、そのリンクをタップすると即座にスクリプトを実行できます。

URLジェネレーター2

この機能はショートカットアプリからも利用できるそうになるそうです。後日。

他の場所も開けるファイルダイアログ

Support for opening external folders using the system’s file picker on iOS 13 (this was possible on iOS 12, but not easily discoverable).

こんな感じのファイルダイアログが使えるようになります。

新しいファイルダイアログ

ちょっとリファレンス(dialogs — Easy-to-use UI Dialogs)を確かめてみたんだけど、驚きの簡単さです。

import dialog

dialogs.pick_document(types=['public.data'])

たったこれだけですよ。
実はobj_cでやる方法は以前から出回ってたけど、↓これだけのコードが必要だったんです。
(長いので畳んであります)

import ui
from objc_util import *
import urllib.parse

#===================== delegate of UIDocumentPickerViewController: begin
def documentPickerWasCancelled_(_self, _cmd, _controller):
    #print('documentPickerWasCancelled_')
    UIDocumentPickerViewController = ObjCInstance(_controller)
    UIDocumentPickerViewController.uiview.close()
    if UIDocumentPickerViewController.callback:
        UIDocumentPickerViewController.callback('canceled')
    UIDocumentPickerViewController.picked = 'canceled'
    UIDocumentPickerViewController.done = True

def documentPicker_didPickDocumentsAtURLs_(_self, _cmd, _controller, _urls):
    #print('documentPicker_didPickDocumentsAtURLs_')
    UIDocumentPickerViewController = ObjCInstance(_controller)
    UIDocumentPickerViewController.uiview.close()
    urls = ObjCInstance(_urls)
    if len(urls) == 1:
        url = urllib.parse.unquote(str(urls[0]))
    else:
        url = []
        for i in range(0,len(urls)):
            url.append(urllib.parse.unquote(str(urls[i])))
    if UIDocumentPickerViewController.callback:
        UIDocumentPickerViewController.callback(url)
    UIDocumentPickerViewController.picked = url
    UIDocumentPickerViewController.done = True

methods = [documentPicker_didPickDocumentsAtURLs_,documentPickerWasCancelled_]
protocols = ['UIDocumentPickerDelegate']
try:
        MyUIDocumentPickerViewControllerDelegate = ObjCClass('MyUIDocumentPickerViewControllerDelegate')
except:
    MyUIDocumentPickerViewControllerDelegate = create_objc_class('MyUIDocumentPickerViewControllerDelegate', methods=methods, protocols=protocols)
#===================== delegate of UIDocumentPickerViewController: end

#def handler(_cmd):
#   print('here')
#handler_block = ObjCBlock(handler, restype=None, argtypes=[c_void_p])

@on_main_thread
def MyPickDocument(w, h, mode='sheet', popover_location=None, callback=None, title=None, UTIarray=['public.item'], allowsMultipleSelection=False, PickerMode=1,tint_color=None):
    # view needed for picker
    uiview = ui.View()
    uiview.frame = (0,0,w,h)
    if mode == 'sheet':
        uiview.present('sheet',hide_title_bar=True)
    elif mode == 'popover':
        if popover_location:
            uiview.present('popover', hide_title_bar=True, popover_location=popover_location)
        else:
            return
    else:
        return

    UIDocumentPickerMode = PickerMode
                                                        # 1 = UIDocumentPickerMode.open
                                                        #   this mode allows a search field
                                                        #   and url in delegate is the original one
                                                        # 0 = UIDocumentPickerMode.import
                                                        #   url is url of a copy
    UIDocumentPickerViewController = ObjCClass('UIDocumentPickerViewController').alloc().initWithDocumentTypes_inMode_(UTIarray,UIDocumentPickerMode)
    #print(dir(UIDocumentPickerViewController))

    objc_uiview = ObjCInstance(uiview)
    SUIViewController = ObjCClass('SUIViewController')
    vc = SUIViewController.viewControllerForView_(objc_uiview)

    if title:
        l = ui.Label()
        wb = 80
        wl = uiview.width - 2*wb        # title width
        #l.border_width = 1                 # for tests only
        l.text = title
        l.alignment = ui.ALIGN_CENTER
        # find greatest font size allowing to display title between buttons
        fs = 16
        while True:
            wt,ht = ui.measure_string(title,font=('Menlo',fs))
            if wt <= wl:
                break
            fs = fs - 1
        l.frame = (wb,0,wl,fs)
        l.text_color = 'green'
        UIDocumentPickerViewController.view().addSubview_(ObjCInstance(l))

    UIDocumentPickerViewController.setModalPresentationStyle_(3) #currentContext

    # Use new delegate class:
    delegate = MyUIDocumentPickerViewControllerDelegate.alloc().init()
    UIDocumentPickerViewController.delegate = delegate
    UIDocumentPickerViewController.callback = callback  # used by delegate
    UIDocumentPickerViewController.uiview   = uiview        # used by delegate
    UIDocumentPickerViewController.done = False
    UIDocumentPickerViewController.allowsMultipleSelection = allowsMultipleSelection
    vc.presentViewController_animated_completion_(UIDocumentPickerViewController, True, None)#handler_block)

    if tint_color != None:
        r,g,b = tint_color
        UIDocumentPickerViewController.view().setTintColor_(ObjCClass('UIColor').colorWithRed_green_blue_alpha_(r,g,b,1.0))

    return UIDocumentPickerViewController

def main():
        # demo code
        def callback(param):
            # you could check if file save at hoped place...
            print(param)
        MyPickDocument(600,500, callback=callback, title='test', allowsMultipleSelection=True)
        #MyPickDocument(600,500, mode ='popover', popover_location=(mv.width-40,60))

if __name__ == '__main__':
    main()

通知の充実

Revamped notifications module with custom action buttons, support for attachments, location triggers, and more – see the new “Notification Quiz.py” sample code for a demo. The module also works in the share sheet extension now.

通知に関する部分が充実して自由度がかなり上がってるようです。
カスタムアクションボタン、添付ファイルのサポート、ロケーショントリガーなどを備えた、とのことです。

speechモジュールの新しいオプション

New “on device” option for speech recognition in the speech module (please note that this can be very slow though!)

正直使ったことのない機能なのでどうなのかはわかりません。
このオプションはかなり処理が重いそうです。

今いる場所や任意の場所の衛星画像を取得

New location.render_map_snapshot() function to generate an image from a location (see new “Satellite Image.py” sample code).

任意の場所情報を元にこんな感じの写真を撮るということです。
サンプルスクリプトは Examples → Misc → Satellite Image.py です。

衛星写真

コンソール履歴がずっと残るようになった

Console history is now persistent (you can clear it by tapping and holding the ^ button).

履歴を消すには ^ (履歴呼び出しボタン)をロングタップします。

まとめ

ざっとこんなところでしょうか。

このところのセキュリティ強化による締め付けか、iOSアプリの自由度も大きく失われていく傾向だったのですが、よくこんなものを投入できたものです。間違えて審査通しちゃったってことないよね? 次のアップデートは機能が削られてないか確認してからのやった方がいいかもしれません。

ところで冒頭では「今日アップデートされた」と書いてますが、かなりの量で書くのに手こずって実際記事をアップしたのは翌日となってしまいました。

‎Pythonista 3
‎Pythonista is a complete scripting environment for Python, running directly on your iPad or iPhone. It includes support for both Python 3.6 and 2.7, so you can...
タイトルとURLをコピーしました