GtkMenuButton F10

Y901x 1.1.2 公開、日は変わってしまったけど。

y901x112

メニューバーを取り払ったよ。
ラジオメニューじゃ解りづらい再生速度アスペクト比切り替えもステータスバーに移した。

GtkHeaderBar and GStreamer | PaePoi
Mini GtkButton | PaePoi

つーか、今頃になってやっと上記を実装した、半年も前だったのか。
使い道の例だと思ってください、多分作った本人しか使っていないアプリなので。

所で F10 キーでメインメニューがドロップするのは知っているよね。
GtkMenuButton ではこれを自分で実装しないといけないみたい。
こんな感じでいいみたい、F10 ってシステム管理じゃなかったんだ。
ちょっと具体的に追記

#!/usr/bin/env python3

class Win(Gtk.ApplicationWindow):
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, application=app)
        # GtkAccelGroup
        accelgroup = Gtk.AccelGroup.new()
        self.add_accel_group(accelgroup)
        # Preference button
        preferencebutton = Gtk.MenuButton.new()
        image_gear = Gtk.Image.new_from_icon_name("emblem-system-symbolic", Gtk.IconSize.MENU)
        preferencebutton.set_image(image_gear)
        # Set F10 Accel
        preferencebutton.add_accelerator("clicked", accelgroup, Gdk.KEY_F10, 0, Gtk.AccelFlags.VISIBLE)
        # HeaderBar
        headerbar = Gtk.HeaderBar()
        headerbar.pack_end(preferencebutton)

ついでに今頃気が付いた。
Alt+F10 で最大化切り替えできる、これは便利だ。
まあシステム設定で変更できるんだけど。

後 Y901 伝統の Z キーでフルスクリーンは実装できなかった。
一応強引な手段もあるけど GNOME デフォルトの F11 のみでいいかなと。

次は Alt+Enter でプロパティ表示を実装したいな。
一度弄り始めると急に色々思い付くんだよね。

Ubuntu での動作確認は明日やろう(ぉい!
個人的には超有名な某ゲームエンジンと同じ名前であるあの糞デスクトップ環境はガン無視したいんだけどユーザー数がなぁ…

GtkToggleButton and keyboard

GtkToggleButton のシグナルには activate と clicked がある。

activate は On 時のみ、clicked は On/Off 両方シグナルが発生する。
マウス等でクリック時だけでなく gtk_button_clicked() 関数にも反応する。

なので GtkToggleButton を Off させる場合に困る場合がある。
トグルなので二度押しで上るわけだが複数ボタンで排他制御を行う場合に。
更にキーボードでも同様に動作するようにとなると…

activate では二度押しで上るのを検知できない。
clicked では排他で上るボタンからもシグナルが飛んでくる。

activate は使えそうもないので clicked にするしかない。
Off 検知を逃すのとキーボードでつじつまを合わせるのに一苦労。
そのためアクロバチックなコードを今まで書いていた。

class Y901window(Gtk.ApplicationWindow):
    def __init__(self, app):
        #
        # etc...
        #
        self.toolbox.one.connect("clicked", self.on_rep_btn, 1)
        self.toolbox.all.connect("clicked", self.on_rep_btn, 2)
        self.toolbox.rdm.connect("clicked", self.on_rep_btn, 3)
        #
        # etc...
        #

    def on_keydown(self, accelGroup, window, keyval, modifier):
        if keyval == Gdk.KEY_F7:
            self.toolbox.one.clicked()
        elif keyval == Gdk.KEY_F8:
            self.toolbox.all.clicked()
        elif keyval == Gdk.KEY_F9:
            self.toolbox.rdm.clicked()
        ''' old
        if keyval == Gdk.KEY_F7:
            self.on_rep_btn(None, 1)
        elif keyval == Gdk.KEY_F8:
            self.on_rep_btn(None, 2)
        elif keyval == Gdk.KEY_F9:
            self.on_rep_btn(None, 3)'''

    def on_rep_btn(self, widget, num):
        if widget.get_active():
            if self.settingwin.repeat != num:
                self.toolbox.one.set_active(num == 1)
                self.toolbox.all.set_active(num == 2)
                self.toolbox.rdm.set_active(num == 3)
                self.settingwin.repeat = num
        else:
            if not self.toolbox.one.get_active() and not self.toolbox.all.get_active() and not self.toolbox.rdm.get_active():
                self.settingwin.repeat = 0
        ''' old
        if widget == None:
            if num == 0:
                self.toolbox.one.set_active(False)
                self.toolbox.all.set_active(False)
                self.toolbox.rdm.set_active(False)
            elif num == 3:
                self.toolbox.one.set_active(False)
                self.toolbox.all.set_active(False)
                self.toolbox.rdm.set_active(True)
            elif num == 2:
                self.toolbox.one.set_active(False)
                self.toolbox.all.set_active(True)
                self.toolbox.rdm.set_active(False)
            elif num == 1:
                self.toolbox.one.set_active(True)
                self.toolbox.all.set_active(False)
                self.toolbox.rdm.set_active(False)
        else:
            if widget.get_active():
                if self.settingwin.repeat != num:
                    action = self.actiongroup.get_action(ac2_str[num])
                    action.set_current_value(num)
                    if num != 3:
                        self.toolbox.rdm.set_active(False)
                    if num != 2:
                        self.toolbox.all.set_active(False)
                    if num != 1:
                        self.toolbox.one.set_active(False)
            else:
                if self.settingwin.repeat == num:
                    action = self.actiongroup.get_action(ac2_str[0])
                    action.set_current_value(0)
                    self.toolbox.one.set_active(False)
                    self.toolbox.all.set_active(False)
                    self.toolbox.rdm.set_active(False)'''

最初に書いたとおり gtk_button_clicked() 関数にも反応だからコッチを使う。
これでマウスクリックと直接呼び出しとを振り分けする必要はなくなった。

排他を if 文で振り分けしていた部分はこの手があった。
今まで何をやってきたのかと情け無くなった。

排他トグルで All Off は All Off の場合のみ処理すればいい。
それ以外だったら必ずどこかが On になっているということだから。
散々悩んだことなのに解ってしまえばこんなにアッサリ。

ということで、こんなにコードが短くなった。
強引に短くするドヤ顔コードは吐き気がするけど正攻法で短くするのは気持ちいい。

GtkActionGroup の処理が取り除かれているけど気にしないで。
メニューバーを GNOME の意向に合わせ取っ払っているだけなので。
iPhone を使うようになってから更にメニューバーが邪魔に思えてきた。
何故あんなのが普及したのか謎と考えるくらいに。

gst direction next frame

JavaScript でスマホゲームもいいけどメインの GTK+ もやらねば。
久々に Y901x の更新、ポーズからコマ戻しが上手くいく方法を見つけたので。

# self.player @ playbin
# self.settingwin @ My Setting

def set_next_frame(self, rate):
    """
        1 frame up down (rate 1.0 or -1.0)
    """
    if self.settingwin.direction != rate:
        self.settingwin.direction = rate
        self.set_playback_direction(rate)
    event = Gst.Event.new_step(Gst.Format.BUFFERS, 1, 1.0, True, False)
    self.player.send_event(event)

def set_playback_direction(self, rate):
    """
        Change direction
    """
    pos = self.player.query_position(Gst.Format.TIME)[1]
    if rate >= 0.0:
        self.player.seek(
                rate,
                Gst.Format.TIME,
                Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE,
                Gst.SeekType.SET, pos,
                Gst.SeekType.SET, -1)
    else:
        self.player.seek(
                rate,
                Gst.Format.TIME,
                Gst.SeekFlags.FLUSH | Gst.SeekFlags.ACCURATE,
                Gst.SeekType.SET, 0,
                Gst.SeekType.SET, pos)

def on_play(self, widget, data=None):
    if self.settingwin.direction == -1.0:
        self.settingwin.direction = 1.0
        self.set_playback_direction(1.0)
    if self.player.get_state(1)[1] == Gst.State.PLAYING:
        self.player.set_state(Gst.State.PAUSED)
    else:
        self.player.set_state(Gst.State.PLAYING)

逆転させる手続きを分離して send_event するだけだった。
GLib.idle_add とか色々試したのに実はこんなに簡単だった…

これなら関数を抜けるので逆転した後で 1 コマ移動ということになるのかな。
再生開始で正転に戻すのを忘れないようにと。

それと手段が全然解らず困り果てていたシークバー上のマウスホイール。
event.direction 引数が Gdk.ScrollDirection.SMOOTH になるのよ。
Gdk.ScrollDirection.UP, DOWN になる記事以外見当たらなかったし。

http://nullege.com/codes/show/src%40p%40i%40pitivi-HEAD%40pitivi%40utils%40widgets.py/1035/gi.repository.Gdk.ScrollDirection.UP/python

そういうことだったのか!
やっぱり GPL のコードを見たほうが圧倒的に参考になるね。
何も作品を作っていなさそうな人って何故勉強しているのか、日本人に多過ぎ。

def on_seek_scroll_event(self, widget, event):
    """
        Mouse Wheel event
        widget @ GtkScale
    """
    delta = 0
    if event.direction in [Gdk.ScrollDirection.UP, Gdk.ScrollDirection.RIGHT]:
        delta = 1
    elif event.direction in [Gdk.ScrollDirection.DOWN, Gdk.ScrollDirection.LEFT]:
        delta = -1
    elif event.direction in [Gdk.ScrollDirection.SMOOTH]:
        unused_res, delta_x, delta_y = event.get_scroll_deltas()
        if delta_x:
            delta = math.copysign(1, delta_x)
        elif delta_y:
            delta = math.copysign(1, -delta_y)
    if delta:
        location = widget.get_value() - (delta * 10000000000)
        if location > 0 and location < self.playinfo.duration:
            self.set_play_position(location)
            self.put_time_status(location)

これで約 10 秒送りなホイールスクロールが可能になった。
Fedora 20 でしか試していないけど多分 GNOME なら大丈夫だろう。
次バージョン早く出ないかなぁ、20 が安定しているからいいけど半年サイクルに慣れ過ぎた。

start, stop, clear

打ち出したボールが何かに衝突して跳ね返るみたいなコードを探す。
究極かもしれないと思えるサンプルコードを発見!

endo blog: HTML5 – Canvas 円同士の衝突アニメーション

しかし例のごとく setInterval で clearInterval していない。
無限に増殖する円が延々衝突し続けるという究極の CPU 殺しでもあった。
一生懸命自己解析していたら CPU ファンが凄い勢いで回り初めて焦った。
もしファンの無いスマホなんかで見ていたら…

しかたがないので停止や初期化のボタンを付けてコピペ。
TimerClass をインスタンス化したら start(func), stop() メソッドで簡単に使えるようにしてみた。
それをスマートフォン用にレイアウト、のつもり。
肝心のアルゴリズムはコピペです、すんません。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>球の衝突(負荷強)</title>
<!-- for Smart Phone -->
<meta name="viewport" content="
    width=device-width,
    initial-scale=1.0,
    minimum-scale=1.0,
    maximum-scale=1.0,
    user-scalable=no" />
<style>
body {
    margin: 0;
    overflow: hidden;
    -webkit-text-size-adjust: 100%;
}
img {
    margin: 0;
    padding: 0;
    overflow: hidden;
}
#ID_CANVAS {
    background-color: #FFFFFF;
}
</style>
<script type="text/javascript"><!--

    var canvas = null;
    var context = null;
    var timer = null;
    var cw = 0;
    var ch = 0;

    var g = 0.1;
    const INTERVAL = 10;
    var circles = [];
    var colorList = ['0','1','2','3','4','5','6','7',
                     '8','9','a','b','c','d','e','f'];

    var TimerClass = function() {
        this.count = 0;
        this.id = -1;
        this.start = function(func) {
            if (this.id == -1) {
                this.id = setInterval(func, INTERVAL);
            }
        }
        this.stop = function() {
            if (this.id > -1) {
                clearInterval(this.id);
                this.id = -1;
            }
        }
    }

    var onTimer = function() {
        context.clearRect(0, 0, cw, ch);
        if (timer.count>100 || timer.count==0) {
            timer.count = 0;
            circles.push(new Circle());
            circles[circles.length-1].init(circles.length % 2);
        }
        for (var i=0; i<circles.length; i++) {
            for(var j=i+1; j<circles.length; j++) {
                collisionCircleCircle(circles[i], circles[j]);
            }
        }
        for (var i=0; i<circles.length; i++) {
            circles[i].move();
            circles[i].collisionWall();
            circles[i].view();
        }
        timer.count++;
    }

    function Circle(){
        this.h = 0.9;
        this.x = 0;
        this.y = 100;
        this.vx = 0;
        this.vy = 0;
        this.r = 0;
        this.color = '#';

        this.init = function(vec) {
            this.vx = Math.random() * 9 + 1;
            this.r = Math.random() * 25 + 5;
            if (vec) {
                this.x = cw + 100;
                this.vx *= -1;
            } else {
                this.x = -100;
            }
            this.color += colorList[Math.floor(Math.random() * 3) + 3];
            this.color += colorList[Math.floor(Math.random() * 5) + 5];
            this.color += colorList[Math.floor(Math.random() * 7) + 9];
        }
        this.move = function() {
            this.x += this.vx;
            this.y += this.vy;
            this.vy += g;
        }
     
        this.view = function() {
            context.beginPath();
            context.fillStyle = this.color;
            context.arc(this.x, this.y, this.r, 0, 360, false);
            context.fill();
        }
     
        this.collisionWall = function() {
            if (this.x+this.r > cw && this.vx > 0 || this.x-this.r < 0 && this.vx < 0) {
                this.vx *= -this.h;
            } else if (this.y+this.r > ch && this.vy > 0 || this.y-this.r < 0 && this.vy < 0) {
                this.vy *= -this.h;
            }
        }
    }

    function collisionCircleCircle(a,b){
        if( (b.x-a.x)*(b.x-a.x)+(b.y-a.y)*(b.y-a.y) < (a.r+b.r)*(a.r+b.r) ){
            var vx = a.x-b.x;
            var vy = a.y-b.y;
            var len = Math.sqrt(vx*vx+vy*vy);
            var d = a.r+b.r-len;
            if (len>0)
                len =1/len;

            vx *= len;
            vy *= len;

            d /= 2.0;
            a.x += vx*d;
            a.y += vy*d;
            b.x -= vx*d;
            b.y -= vy*d;

            var t;
            t = -(vx*a.vx+vy*a.vy)/(vx*vx+vy*vy);
            var arx = a.vx+vx*t;
            var ary = a.vy+vy*t;

            t = -(-vy*a.vx+vx*a.vy)/(vy*vy+vx*vx);
            var amx = a.vx-vy*t;
            var amy = a.vy+vx*t;

            t = -(vx*b.vx+vy*b.vy)/(vx*vx+vy*vy);
            var brx = b.vx+vx*t;
            var bry = b.vy+vy*t;

            t = -(-vy*b.vx+vx*b.vy)/(vy*vy+vx*vx);
            var bmx = b.vx-vy*t;
            var bmy = b.vy+vx*t;

            var e = 0.8;
            var adx = (a.r*amx+b.r*bmx+bmx*e*b.r-amx*e*b.r)/(a.r+b.r);
            var bdx = -e*(bmx-amx)+adx;
            var ady = (a.r*amy+b.r*bmy+bmy*e*b.r-amy*e*b.r)/(a.r+b.r);
            var bdy = -e*(bmy-amy)+ady;

            a.vx = adx+arx;
            a.vy = ady+ary;
            b.vx = bdx+brx;
            b.vy = bdy+bry;
        }
    }
    var init = function() {
        if (window.TouchEvent) {
            cw = window.innerWidth;
            ch = window.innerHeight - 60;
            timer = new TimerClass();
            // Create Canvas and Context
            canvas = document.getElementById("ID_CANVAS");
            canvas.width = cw;
            canvas.height = ch;
            context = canvas.getContext("2d");
            // Create ToolBar
            var back = document.getElementById("ID_BACK");
            back.style.top = ch + "px";
            back.style.left = 0 + "px";
            back.addEventListener("touchend", function() {
                document.location = ".";
            });
            var start = document.getElementById("ID_START");
            start.style.top = ch + "px";
            start.style.left = 80 + "px";
            start.addEventListener("touchend", function() {
                timer.start(onTimer);
            });
            var stop = document.getElementById("ID_STOP");
            stop.style.top = ch + "px";
            stop.style.left = 160 + "px";
            stop.addEventListener("touchend", function() {
                timer.stop();
            });
            var clear = document.getElementById("ID_CLEAR");
            clear.style.top = ch + "px";
            clear.style.left = 240 + "px";
            clear.addEventListener("touchend", function() {
                timer.stop();
                circles = [];
                context.clearRect(0, 0, cw, ch);
            });
        }
    }
    //-->
</script>

</head>
<body onLoad="init()">

<canvas id="ID_CANVAS"></canvas>
<img id="ID_BACK" src="button1/back.png" style="position:absolute">
<img id="ID_START" src="button1/start.png" style="position:absolute">
<img id="ID_STOP" src="button1/stop.png" style="position:absolute">
<img id="ID_CLEAR" src="button1/clear.png" style="position:absolute">

</body>
</html>

球の衝突(負荷強)

clash

はっきりいって何がどうなってこうなるかよくワカンネェ!
重力で落下するみたいな処理も多分入っているよな、どこだか解らないけど。
何日かチマチマ弄っていればそのうち少しは理解できる、といいな。
今回は覚書とタイマーの簡単な利用方法、ということで。

それにしても神様、私にもっと絵心というものを下さい。
ボタンが我ながらダサすぎる、今後の課題はイラストかも。

JavaScript, Python3 like format

JavaScriptの可変長引数: ぺるたごブログ

なんだ、JavaScript も可変長引数が使えたんだ。
arguments という特殊な変数を利用すればいいんだね。
って Gjs の /usr/share/gjs-1.0/format.js もそうやっていた。

それなら Python3 っぽい文字列成形が自作できるかも。
いや、すっかり Python3 に慣れてしまったので。

完全に同じでなくてもいいんだ、”{0}, {1}” みたいな手段って便利じゃん。
Gjs なら format.js でいいけど Web で使いたいし参考にして。

String.prototype.format = function() {
    var result = this;
    for (var i = 0; i < arguments.length; i++) {
        var s = "{" + i + "}";
        var r = String(arguments[i]); // not tostring();
        result = result.replace(s, r);
    }
    return result;
}

超手抜き、{0:.2f} みたいなのはできない!

それと文字列の tostring だとエラーになるけどキャストなら通るようだ。
これで文字列も整数も実数も対応できる、しかし本当にいいかげんな言語だ。

javascript_format

Fedora は最初から Gjs が入っているから確認が簡単でいいですよ。
SpiderMonkey ですからつまり Firefox でも使えるってことだし。

いや、Web てか iPhone で使えないと意味がない、確認だ。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Python3 っぽい文字列の成形</title>
<!-- for Smart Phone -->
<meta name="viewport" content="
    width=device-width,
    initial-scale=1.0,
    minimum-scale=1.0,
    maximum-scale=1.0,
    user-scalable=no" />
<style>
body {
    -webkit-text-size-adjust: 100%;
}
</style>
<script type="text/javascript"><!--

    const STR = "Test : {0}, {1}, {2}";

    String.prototype.format = function() {
        var result = this;
        for (var i = 0; i < arguments.length; i++) {
            var s = "{" + i + "}";
            var r = String(arguments[i]); // not tostring();
            result = result.replace(s, r);
        }
        return result;
    }

    var init = function() {
        // Test
        var text = STR.format("str", 1, 0.5);
        document.getElementById("ID_TEXT").innerHTML = text;
    }
    //-->
</script>

</head>
<body onLoad="init()">

<p>const STR = "Test : {0}, {1}, {2}";<br />
var text = STR.format("str", 1, 0.5);<br />
document.getElementById("ID_TEXT").innerHTML = text;<br />
↓</p>

<p id="ID_TEXT"></p>

<p><a href=".">戻る</a></p>
</body>
</html>

Python3 っぽい文字列の成形

Safari, Google Chrome でも問題ないね、○E は知らないしどうでもいい。
ゲームの “Round {0} Start!” みたいな時に使うと便利かも。