Clutter」タグアーカイブ

ClutterSwipeAction

マルチタッチを実装してみたくなった。
具体的には Comipoli にてフリックで次ページ移動をさせたい。
スマートフォンに慣れてしまうとどうしてもね。

swipe

本当は指一本フリックにしたいけど指定する手段が無いようだ。
当然か、マウスカーソルがある OS ではそれマウスカーソルの移動だ。
指一本で操作するには OS をマウスカーソル無しにするしか手段が無い。

上記一つだけでもデスクトップとタブレットで共通 OS は不可能と解る。
iOS と Mac OS みたいにイメージだけ共通にするべき。
結局 GNOME は Mac OS のようにしたいってことなんだろう。

でも GNOME のマルチタッチは本当に動くのかな?
そうだ、筆者は MacBook Air を持っているじゃないか。
MacBook Air に DVD ドライブを付けて Fedora LiveDVD で試す。

更に外付け HDD を繋いで Gedit にてテキストファイルを開く。
うん、二本指で上下スクロールは可能だ、慣性スクロールにも対応している。
次に EoG で画像を表示。
回転も拡大もできない、フリックで次ファイルのつもりがスクロールに。
駄目だこりゃ、多分 GNOME は Windows 10 のマルチタッチ仕様なんだろう。

他にも Mac で Fedora はキーボードのバックライトが点灯しないとか。

Fedora 24 でも WiFi には繋がらなかった。
多分下記と同じようにすればイケると思うけど。
MacBookへLinux(Fedora)をインストール – Qiita

ところで command キーがそのまま Super キーとして使えるのね。
control が Ctrl のままだ、Function キーは fn キーを押す必要あり。
fn+delete で Delete キーなんかは Mac OS と同じ。
うん、キーボードに関しては何も問題ない。

マルチタッチさえ気にしないならなんとか使えそうだが、うーん。
MacBook Air はやはり Mac のまま使うのが正解だろうな。
そもそも現状で何も困っていない。

脱線した、フリックで次ページ移動の話ね。

実はマウスで次ページに移動する手段を作ろうと考えた。
EoG のように画像に被さるボタンは嫌い、ならばフリックだ!
という理由でこんなことになった。
ClutterSwipeAction を使えば簡単にフリック動作を得られるようだ。

swipe シグナルの direction 引数は
CLUTTER_SWIPE_DIRECTION_UP | CLUTTER_SWIPE_DIRECTION_RIGHT
のように or 演算で届くので and 演算を使って確認する。

class ComipoliWindow(Gtk.ApplicationWindow):
    def __init__(self, app):
        #
        # etc...
        #
        # Gesture
        swipe = Clutter.SwipeAction()
        swipe.connect("swipe", self.on_swipe)
        self.stage.add_action(swipe)
        self.stage.set_reactive(True)
        #
        # etc...

    def on_swipe(self, action, actor, direction):
        if direction & Clutter.SwipeDirection.RIGHT:
            self.change_pixbuf(2)
        elif direction & Clutter.SwipeDirection.LEFT:
            self.change_pixbuf(-2)
        return True

なんだ、これだけでマウス左ボタンでのフリック動作が可能だ。
Mac のタッチパッドフリックじゃやっぱり動かないな。

EoG は同じようにマウスでは動かないので別の実装が必要なのかな?
マルチタッチ対応マシンを買わないと解らない、いつか気が向いたら。

PyGObject CBZ Viewer

MComix のデザインや操作性が古すぎて GNOME3 に合わない。
Python2 は諦めるけど PyGtk はもう入れたくない。

キーバインドが eog と同じように使える cbz コミックビューアが欲しい。
特に [→] キーで改ページがやりたい、筆者が eog で一番使うキーである。

Evince でも見開き表示にはできるが左ページから始まるようにしか設定できない。
次ページが [→] キーなのはこのアプリも同じ。

etc…

だったら自分で作ればいいじゃないか!

今から作るなら当然 ClutterImage を使って OpenGL 表示だよね。
昔ながらのノウハウが使えないという意味でもあるけど。

#!/usr/bin/env python3

import sys, zipfile, gi
gi.require_version('Gtk', '3.0')
gi.require_version('Clutter', '1.0')
gi.require_version('GtkClutter', '1.0')
from gi.repository import Gtk, Gio, GLib, Gdk, GdkPixbuf, Clutter, GtkClutter, Cogl

PATH = "gf(kari).cbz";

class ComipoliWindow(Gtk.ApplicationWindow):
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, application=app)
        # var
        self.num = 0
        self.datas = []
        self.is_fullscreen = False
        # Clutter
        embed = GtkClutter.Embed()
        self.add(embed)
        self.stage = embed.get_stage()
        #
        self.actor1 = Clutter.Actor()
        self.stage.add_child(self.actor1)
        self.image1 = Clutter.Image()
        self.actor1.set_content(self.image1)
        # signal
        self.stage.connect("allocation-changed", self.on_stage_allocation_changed)
        # unzip
        with zipfile.ZipFile(PATH) as f:
            l = f.namelist()
            l.sort()
            for name in l:
                try:
                    data = f.read(name)
                    stream = Gio.MemoryInputStream.new_from_data(data)
                    p = GdkPixbuf.Pixbuf.new_from_stream(stream)
                    self.datas.append(p)
                except Exception as e:
                    pass
        # set
        self.set_pixbuf(0)
        #
        self.show_all()

    def set_pixbuf(self, num):
        pixbuf = self.datas[num]
        self.image1.set_data(
            pixbuf.get_pixels(),
            Cogl.PixelFormat.RGB_888,
            pixbuf.get_width(),
            pixbuf.get_height(),
            pixbuf.get_rowstride()
        )
        self.num = num

    def change_pixbuf(self, bool_next):
        """
            TODO: Spread display
        """
        if bool_next:
            if len(self.datas) > self.num + 1:
                self.set_pixbuf(self.num + 1)
        else:
            if self.num > 0:
                self.set_pixbuf(self.num - 1)

    def on_stage_allocation_changed(self, actor, box, flags):
        """
            TODO: Aspect ratio
        """
        self.actor1.set_size(box.x2 , box.y2)

    def do_key_press_event(self, event):
        """
            eog like Key Bind
        """
        if event.keyval == Gdk.KEY_Down:
            self.change_pixbuf(True)
        elif event.keyval == Gdk.KEY_Right:
            self.change_pixbuf(True)
        elif event.keyval == Gdk.KEY_space:
            self.change_pixbuf(True)

        elif event.keyval == Gdk.KEY_Up:
            self.change_pixbuf(False)
        elif event.keyval == Gdk.KEY_Left:
            self.change_pixbuf(False)
        elif event.keyval == Gdk.KEY_BackSpace:
            self.change_pixbuf(False)

        elif event.keyval == Gdk.KEY_F11:
            if self.is_fullscreen:
                self.unfullscreen()
                self.is_fullscreen = False
            else:
                self.fullscreen()
                self.is_fullscreen = True
        elif event.keyval == Gdk.KEY_Escape:
            if self.is_fullscreen:
                self.unfullscreen()
                self.is_fullscreen = False
            else:
                self.close()

class ComipoliApp(Gtk.Application):
    def __init__(self):
        GLib.set_prgname("Comipoli");
        Gtk.Application.__init__(
            self,
            application_id="apps.sasakima.comipoli",
            flags=Gio.ApplicationFlags.FLAGS_NONE )

    def do_activate(self):
        ComipoliWindow(self)

#Clutter.init(); @ Error
GtkClutter.init();
app = ComipoliApp()
app.run(sys.argv)

comipoli000

とりあえず読み込みと表示関連はなんとかなった。
後はアスペクト比の保持と見開き表示等々。

実は、キーバインドが eog と同じだと少々問題が。

GANMA! むさむらだけ読んでいる
となりのヤングジャンプ えびなちゃんだけ(同
ガンガンONLINE ぐるぐる(同

と筆者が利用しているマンガサイトはことごとく改ページが [←] キーなのだ。
縦書き文化の国で生まれたマンガなのだからそのほうが自然といえる。

横書きの英語文化で作られたアプリとキーバインドを共通にするべきかどうか。
もう完全に好みの問題、左右キーのみ設定で入れ替えできるようにするのが一番かなと。
タッチパネルでもどちらにフリックかで問題になりそう。

Clutter Box and Scroll

Clutter をチマチマやっているけど絶対値配置はやはり古い!
やはり BoxLayout で配置したい、スクロール機能も欲しい。

色々試して驚いた。
なんと Clutter はスクロールバーに相当するものが無い!

どうしても使いたいなら自分で作れってことでしょうか。
いや違う、そもそも Button Widget 相当すら無いではないか。
スマートフォンではいらないよね、そういうこと。

古臭い考え方をバッサリ捨てないと今の Mac と GNOME は使えない。
つか昔風に戻るなんてありえない、パソコン離れは加速する一方だし。

もしかしてスマートフォンのようなスクロールを想定しているのかも。
だったらやったろうジャン!

#!/usr/bin/gjs

const Clutter = imports.gi.Clutter;
const Lang = imports.lang;

const ListTest = new Lang.Class({
    Name: 'ListTest',
    Extends: Clutter.Stage,

    _init: function() {
        this.parent();
        let layout = new Clutter.BoxLayout({
            orientation: Clutter.Orientation.VERTICAL,
            spacing: 2
        });
        // ScrollActor
        this.scroll = new Clutter.ScrollActor({
            layout_manager: layout,
            scroll_mode: Clutter.ScrollMode.VERTICALLY,
            x_expand: true
        });
        // append
        for (let i=0; i<100; i++) {
            let m = new Clutter.Text({
                text: "TextLine: " + i,
                x_expand: true
            });
            m.set_background_color(Clutter.Color.new(255, 0, 0, 125));
            this.scroll.add_child(m);
        }
        // Scroll Action
        this.x_point = 0;
        this.y_point = 0;
        this.x_diff = 0;
        this.y_diff = 0;
        let gesture = new Clutter.GestureAction();
        gesture.connect("gesture-begin", Lang.bind(this, function(action, actor) {
            this.x_point = action.get_press_coords(0)[0] + this.x_diff;
            this.y_point = action.get_press_coords(0)[1] + this.y_diff;
            return true;
        }));
        gesture.connect("gesture-progress", Lang.bind(this, function(action, actor) {
            let x = action.get_motion_coords(0)[0];
            let y = action.get_motion_coords(0)[1];
            this.x_diff = this.x_point - x;
            this.y_diff = this.y_point - y
            let point = new Clutter.Point({
                x: this.x_diff,
                y: this.y_diff
            });
            this.scroll.scroll_to_point(point);
            return true;
        }));
        this.scroll.add_action(gesture);
        this.scroll.set_reactive(true);
        // this
        this.add_child(this.scroll);
        this.set_layout_manager(new Clutter.BoxLayout()); // Child Fill
        this.connect("hide", Clutter.main_quit);
        this.show_all();
    }
});

Clutter.init(null);
new ListTest();
Clutter.main();

scroll_actor

ClutterBoxLayout を使うことで GtkBox 相当のようだ。
これで fill, expand を利用したレイアウトができる。
しかし ClutterActor に割り当てなのか、GTK+ と随分違うなと。

子 Actor を親サイズに広げたい時は Child Fill の部分のように。
これだけで GtkWindow を使うのとと同じ感覚になる。

スクロールは上記で上手くいった。
スマートフォンみたいにマウスで掴んで動かした分だけスクロール。
やろうと思えば iPhone のような慣性やポヨンを入れることもできる。
でもあの動きは特許だったような…

このスクロールをパソコンで使うかというと疑問もあるけれど。
コマンドが優秀な Linux もタブレットの時代が、ってワカンネーけど。
GNOME プロジェクトはガッツリ備えているということでしょう。

ClutterImage

今回は Clutter で画像をクルクル回してみよう。
3D ライブラリの定番ですね、ClutterImage を使うようだ。

ClutterImage: Clutter Reference Manual

画像ファイルをセットする方法は公式のサンプルコードがある。
コレを clutter_actor_set_content すればテクスチャになるってことかな。
画像の原寸と ClutterActor のサイズはどんな関係なんだろう。
チャチャッと実験コードを書いて試してみよう。

このサンプルを作っている時に気が付いたけど ClutterScore は deprecated だ。
CLutterTimeline に同様の property がある時点で気が付けよって感じですが。

#!/usr/bin/gjs

const Clutter = imports.gi.Clutter;
const GdkPixbuf = imports.gi.GdkPixbuf;
const Cogl = imports.gi.Cogl;
const Lang = imports.lang;

// Own Rewrite
const FILENAME = "/home/sasakima-nao/pic/game/gf/nae_yuki_ssr/ [ハロウィン13]優木苗.jpg";

const ImageTest = new Lang.Class({
    Name: 'ImageTest',
    Extends: Clutter.Stage,

    _init: function() {
        this.parent();
        // Image
        let image = new Clutter.Image();
        let pixbuf = GdkPixbuf.Pixbuf.new_from_file(FILENAME);
        let alpha = pixbuf.get_has_alpha() ? Cogl.PixelFormat.RGBA_8888 : Cogl.PixelFormat.RGB_888;
        image.set_data(
            pixbuf.get_pixels(),
            alpha,
            pixbuf.get_width(),
            pixbuf.get_height(),
            pixbuf.get_rowstride
        );
        // Rotate Actor
        this.actor = new Clutter.Actor();
        this.actor.set_pivot_point(0.5, 0.5);
        let xy = pixbuf.get_height() / pixbuf.get_width();
        this.actor.set_size(200, 200 * xy);
        this.actor.set_position(70, 50);
        this.actor.set_content(image);
        // Timer
        this.timeline = new Clutter.Timeline({
            duration: 100,
            loop: true
        });
        this.rotation = 0;
        this.timeline.connect("new-frame", Lang.bind(this, function() {
            this.rotation += 0.3
            this.actor.set_rotation_angle(Clutter.RotateAxis.Y_AXIS, this.rotation);
            this.actor.set_rotation_angle(Clutter.RotateAxis.Z_AXIS, this.rotation);
        }));
        this.timeline.start();
        // this
        this.add_child(this.actor);
        this.connect("hide", Clutter.main_quit);
        //this.set_user_resizable(true);
        this.show_all();
    }
});

Clutter.init(null);
new ImageTest();
Clutter.main();

clutter_image

公式サンプルの方法で画像全体のデータは取り込めるようだ。
ClutterActor の大きさに合わせてアスペクト比無視で拡縮される。
上記は一応大雑把にアスペクト比を合わせるようにしている。

PivotPoint や回転は全部 ClutterActor 側の仕事。
ClutterImage は ClutterActor の角度に合わせてレンダリングされる。
簡単すぎて拍子抜け、3D ってもっと面倒なイメージがあったのにな。

ClutterGestureAction

Clutter @ Mouse Drag | PaePoi
にてシークバーを motion-event で動かすようにした。

しかしこの方法は問題があった。
マウスカーソルが ClutterActor から外れるとシグナルが無効化される。
カーソルのキャプチャが必要だが手段が解らない。

いくら手段を探しても見つからない、もしかして無いのかも。
もう別のシーク方法を考えたほうが良さそうだ。

ClutterDragAction でなく ClutterGestureAction ならどうだ?
タッチパネル向けアクションだと思って無視していたけど調べてみよう。

ClutterGestureAction: Clutter Reference Manual

clutter_gesture_action_get_motion_coords
で普通に x, y の値が抜けるみたいだね。
問題はマウスカーソルが外れてもキャプチャするかどうかだ、実験。

#!/usr/bin/gjs

const Clutter = imports.gi.Clutter;
const Lang = imports.lang;

const GestureTest = new Lang.Class({
    Name: 'GestureTest',
    Extends: Clutter.Stage,

    _init: function() {
        this.parent();
        // var
        this.is_drag = false;
        // text
        this.text = new Clutter.Text();
        this.text.set_text("x = NULL; y = NULL");
        this.add_child(this.text);
        // Action
        let gesture = new Clutter.GestureAction();
        gesture.connect("gesture-begin", Lang.bind(this, function(action, actor) {
            let x = action.get_press_coords(0)[0] / this.width;
            let y = action.get_press_coords(0)[1] / this.height;
            this.text.set_text("x = " + x + "; y = " + y);
            return true;
        }));
        gesture.connect("gesture-progress", Lang.bind(this, function(action, actor) {
            let x = action.get_motion_coords(0)[0] / this.width;
            let y = action.get_motion_coords(0)[1] / this.height;
            this.text.set_text("x = " + x + "; y = " + y);
            return true;
        }));
        gesture.connect("gesture-end", Lang.bind(this, function(action, actor) {
            this.text.set_text("x = NULL; y = NULL");
        }));
        this.add_action(gesture);
        this.set_reactive(true);
        // this
        this.connect("hide", Clutter.main_quit);
        this.show_all();
    }
});

Clutter.init(null);
new GestureTest();
Clutter.main();

にてマウスをウインドウの外まで移動

clutter_gesture_action

しっかりマイナス値も取得できていますね。
ウインドウ外でマウスボタンを離すとしっかり NULL に戻る。

なんだよ、こんなに簡単な手段があったじゃないか。
昔の手段で思考停止していたら駄目だね、どんどん新しいことをやらないと。
ついでに。

this.player = new ClutterGst.Playback();
this.player.set_seek_flags(ClutterGst.SeekFlags.ACCURATE);

これだけでシークバーの追従がナイスになる。
思い通りに動かなかったものがキチンと動くようになっていくのはマジ楽しい。