【修正】スライドっぽいものを作る - Kivy Advent Calendar 2013

Carouselというウィジェットは、ドラッグしてウィジェットを次々と表示できます。すでに2日目 (Tofu issue) で何の解説もなく使ってますが...
これとRstDocumentを組み合わせればスライドっぽいものができますね。
 

ということで作ってみました。横方向にスライドすれば次の画面が出てきます。
(一部白い斑点がありますが、これはAndroidの開発者オプションでタップ位置を表示しているものです)


(/sdcard/kivy/slides/)

docutils.zip docutils-*.tar.gzの中の「docutils」ディレクトリをZIP圧縮したもの
android.txt Androidの場合必要
fonts_ja.py 2日目「Tofu issue」のfonts_ja.pyと同じ
main.py
slides.kv
title.txt スライド表紙
page01.rst 1ページ目
page02.rst 2ページ目
page03.rst 3ページ目



(android.txt)

title=slides
author=cheeseshop
orientation=landscape



(main.py)

# -*- coding: utf-8 -*-
import os, sys
sys.path.insert(0, os.path.join(os.getcwd(), 'docutils.zip'))

from kivy.app import App
from kivy.uix.carousel import Carousel
from kivy.uix.label import Label
from kivy.uix.rst import RstDocument
from kivy.uix.textinput import TextInput
from kivy.uix.popup import Popup
import codecs
import fonts_ja

class TLabel(Label):

    def __init__(self, **kwargs):
        if kwargs.has_key('source'):
            kwargs['text'] = codecs.open(kwargs['source'], 'r', 'utf-8').read()
            del kwargs['source']
        super(TLabel, self).__init__(**kwargs)

class Rst(RstDocument):

    source_encoding = 'utf-8'

    def on_touch_up(self, touch):
        if not self.parent.current_slide is self:
            return
        if touch.is_double_tap:
            popup = Popup(
                title='Source',
                content=TextInput(text=self.text),
                auto_dismiss=True,
                size_hint=(0.8, 0.8))
            popup.open()

    def __init__(self, **kwargs):
        if kwargs.has_key('source'):
            kwargs['text'] = codecs.open(kwargs['source'], 'r', 'utf-8').read()
            del kwargs['source']
        super(Rst, self).__init__(**kwargs)

class SlidesApp(App):

    def build(self):
        carousel = Carousel(direction='right')
        carousel.add_widget(TLabel(source='title.txt'))
        carousel.add_widget(Rst(source='page01.rst'))
        carousel.add_widget(Rst(source='page02.rst'))
        carousel.add_widget(Rst(source='page03.rst'))
        return carousel

if __name__ == '__main__':
    SlidesApp().run()



(slides.kv)

:
    font_size: '20pt'
    halign: 'center'
    valign: 'middle'
    color: 0.2, 0.2, 0.0, 1
    canvas.before:
        Color:
            rgb: 0.9, 0.9, 0.8
        Rectangle:
            pos: self.pos
            size: self.size

:
    font_size: '28pt'
    halign: 'center'
    valign: 'middle'
    color: 0.2, 0.2, 0.0, 1

:
    cols: 1
    content: content
    size_hint_y: None
    height: content.texture_size[1] + 30
    canvas:
        Color:
            rgb: parse_color('#cccccc')
        Rectangle:
            pos: self.x - 1, self.y - 1
            size: self.width + 2, self.height + 2
        Color:
            rgb: parse_color('#eeeeee')
        Rectangle:
            pos: self.pos
            size: self.size
    Label:
        id: content
        markup: True
        text_size: self.width - 10, None
        font_name: '/system/fonts/DroidSansFallback.ttf'
        color: (0, 0, 0, 1)



(title.txt)

Kivy / Python for Androidでプレゼンツール

2013.12.05
http://d.hatena.ne.jp/cheeseshop/



(page01.rst)

================================================
CarouselとScreenManagerの違い
================================================

Carousel
                                                                                              • -
スライドして次/前ページへ移動 + 上下スクロールと干渉しないよう左右にスライドさせることが多い + イベント記述なしで、スライド操作を受け付ける + 左右または上下方向のスライド表示しかできない ScreenManager
                                                                                              • -
別の画面へ遷移する + 画面転換に使う (ゲームオーバー、設定画面、ログイン要求) + ScreenManager自体はスライド操作は受けつけない + OpenGLのエフェクトが使え、シェーダー効果も自作できる - スライド (SlideTransition) - 前後を入替えるように (SwapTransition) - ワイプ (WipeTransition) フェード (FadeTransition) - 中央からズームで現れる (RiseInTransition) 中央に消えていく (FallOutTransition)



(page02.rst)

================================================
このプレゼンツールのポイント
================================================

Tofu issue

  + 等幅フォントのところだけ化けるのに対応

# コードsample print 'Hello!' ... docutilsが入ってない + ZIP圧縮していれる方法を採用 ダブルタップすると + まるで編集画面っぽくreSTテキストを開きます + でも保存できません... orz



(page03.rst)

================================================
まとめ
================================================

.. image:: .kivy/icon/kivy-icon-128.png

解説

docutils.zip

2日目でRstDocument使ったときは「docutilsをそのままプロジェクトにつっこんで」とか書きましたが、1MB以上あるんですね。Androidへの転送は面倒だし。
今回はライブラリ本体だけZIP圧縮しておいています。Pythonはこの手が使えるので便利ですね。
ただこれでも480KBくらいありますので、他のプロジェクトと共通で使うのなら、KivyLauncherをビルドしてdocutilsを最初から入れていた方がいいかもしれません。

source引数

TLabelとRstでsourceにファイル名を入れていますが、なぜかその場でファイルを読んでtextに入れ直しています。
これはRstDocumentウィジェットは「source_encoding='utf-8'」としているのに、全然設定が渡されておらず、docutilsは「input_encoding='ascii'」でrstファイルを読もうとしてUnicodeエラーに。
いろいろ試行錯誤しましたが時間切れのため、codecs.openで読み込んでtextにUnicode文字列をつっこむ方法を取っています(Unicode文字列を渡せば正しくレンダリングするのですが)。
ひょっとしたらSphinxの日本語パッチか何かで、スマートな解決方法をやっているのかもしれません。


ダブルタップ

ページをダブルタップをするとTextInputが開き、reSTソースが表示されます(グレーアウトした背景をタップすれば閉じます)。
本当は「プレゼン中にその場で編集→即時更新」なんてかっこいいこと考えたのですが、これも時間切れ。今はただ表示するだけです。
ちなみに最後のreturn Trueを忘れると挙動がおかしくなるようです。ダブルタップのイベントが他のページにも渡されてしまうのかもしれません。

(追記 2013-12-13) ごめんなさい。イベント処理についての知識が足りてなかったです。この件について後で解説します。
(追記 2013-12-21) http://d.hatena.ne.jp/cheeseshop/20131221 で解説を書きました
(追記 2013-12-21) 上のmain.pyを修正しました。今はスライド自身のreSTソースが表示されているはずです。

追記

あーやっと理解しました。
masterブランチでは直っていますが、KivyLaucher 1.7.2はdocutilsにエンコーディングを渡してないです(preloadメソッドにencoding引数がない!)
この問題も1.8.0リリース待ちですかね...

https://twitter.com/mathieuvirbel/status/407950627046948864

昨日、作者さんが「うまくいけば今週中にKivy 1.8出すよ、しかもPython 3コンパチだ」とか呟いてるし。
期待していいんだね? Python for Androidだけ遅れるとかないよね?