Programming」カテゴリーアーカイブ

MessageBox.show @ Gjs and PyGObject

JavaScript の static メソッドはインスタンスからは呼び出しできない。

#!/usr/bin/gjs
 
class A {
    constructor(arg) {
        this.num = 'instance';
    }
    static static_method() {
        return 'staticmethod';
    }
}
A.num = 'static';
a = new A();
print(a.num); //=> instance
print(A.num); //=> static

print(A.static_method()); //=> staticmethod
print(a.static_method()); //=> ERROR!

当然である。
JavaScript の class は prototype の別表現でしかないから。

#!/usr/bin/gjs

function A() {}
A.func = function() {
    return 'staticmethod';
}
A.prototype.func = function() {
    return 'instancemethod';
}

print(A.func()); //=> staticmethod
let a = new A();
print(a.func()); //=> instancemethod

Python,C# 等の他言語ではインスタンスからでも呼び出しできる。

#!/usr/bin/env python3
 
class A:
    def __init__(self):
        self.num = 'instance'
    @staticmethod
    def static_method():
        return 'staticmethod'

A.num = 'static'
a = A()
print(a.num) #=> instance
print(A.num) #=> static

print(a.static_method()) #=> staticmethod
print(A.static_method()) #=> staticmethod

特に C# でよく見るけど、わざわざインスタンスを作ってからスタティックメソッドを使っている人の異様な多さを見ると JavaScript みたいなアクセスのほうがいいような。

そんなことより。
スタティックメソッドとインスタンスメソッドで同じ名前を指定してもいいんだ。
あまり自分のコードでスタティックを作らないから知らなかったよ。

ということで。
GtkMessageDialog で MessageBox.show を作ってみよう。
MessageBox.Show ではどちらの言語のコーディングスタイルにも合わないからね。
Gir の Gtk.Widget には show メソッドがあるけど無視できる。

#!/usr/bin/gjs

const GObject = imports.gi.GObject;
const Gtk = imports.gi.Gtk;

var MessageBox = GObject.registerClass({
    GTypeName: "MessageBox"
}, class MessageBox extends Gtk.MessageDialog {
    _init(text) {
        super._init({
            buttons: Gtk.ButtonsType.OK,
            text: text,
            secondary_text: "test"
        });
    }
    static show(text) {
        //let dlg = new MessageBox(text); @ error!
        let dlg = new this(text);
        dlg.run();
        dlg.destroy();
    }
});

Gtk.init(null);
MessageBox.show("Message");

new this(text) って変な感じ。

#!/usr/bin/env python3

import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk

class MessageBox (Gtk.MessageDialog):
    def __init__(self, text):
        Gtk.MessageDialog.__init__(
            self,
            buttons=Gtk.ButtonsType.OK,
            text=text,
            secondary_text="test" )
    @staticmethod
    def show(text):
        #dlg = self(text) @ error!
        dlg = MessageBox(text)
        dlg.run()
        dlg.destroy()

#Gtk.init(None) @ no need
MessageBox.show("Message")

PyGObject だとこう。

GUI を作っている時の print 代わりに。

Gjs: GdkPixbuf Memory leak

comipoli がガッツリメモリリークしてる、と今更気がつく。
抜き出しすると、001.jpg – 200.jpg を用意して。
JavaScriptでゼロ埋め処理 – Qiita
ゼロ詰めはこの方法を利用しました。

#!/usr/bin/gjs

const System = imports.system;
const GObject = imports.gi.GObject;
const GLib = imports.gi.GLib;
const Gtk = imports.gi.Gtk;
const Gdk = imports.gi.Gdk;
const GdkPixbuf = imports.gi.GdkPixbuf;
const PATH = '/home/sasakima-nao/erohon/';

var AWindow = GObject.registerClass({
    GTypeName: "AWindow"
}, class AWindow extends Gtk.ApplicationWindow {
    _init(app) {
        super._init({application: app});
        this.d = new Gtk.DrawingArea();
        this.d.connect('draw', (widget, cr)=> {
            if (this.pixbuf) {
                let aw = widget.get_allocated_width();
                let ah = widget.get_allocated_height();
                let buf = this.pixbuf.scale_simple(aw, ah, GdkPixbuf.InterpType.BILINEAR);
                Gdk.cairo_set_source_pixbuf(cr, buf, 0, 0);
                cr.paint();
            }
        });
        this.add(this.d);
        this.pixbuf = null;
        this.count = 0;
        GLib.timeout_add(GLib.PRIORITY_DEFAULT, 200, ()=> {
            this.count += 1;
            if (this.count == 200)
                return false;
            let f = `${PATH}${('00'+this.count).slice(-3)}.jpg`;
            this.pixbuf = GdkPixbuf.Pixbuf.new_from_file(f);
            this.d.queue_draw();
            return true;
        });
        this.show_all();
    }
});

var AApplication = GObject.registerClass({
    GTypeName: "FlApplication"
}, class FlApplication extends Gtk.Application {
    _init(v) {
        GLib.set_prgname("AApplication");
        super._init();
    }
    vfunc_startup() {
        super.vfunc_startup();
        new AWindow(this);
    }
    vfunc_activate() {
        this.active_window.present();
    }
});

new AApplication().run([System.programInvocationName].concat(ARGV));

動かす。

ガベージコレクションは仕事をしろよ。。。。。
完全に GdkPixbuf が原因だよな、自前で破棄しなきゃいけなかったのか。

Reference Counting and Memory Mangement: GDK-PixBuf Reference Manual

buf.unref() では非推奨の gdk_pixbuf_unref を呼び出ししそうだ。
でも JavaScript には call があるじゃないか。
$dispose() を呼び出ししたほうがいいとかもどこかで見つけた。

//buf.unref();
GObject.Object.prototype.unref.call(buf);
cr.$dispose();

結果

動かしてしばらくたってからこんなエラーを吐いて落ちる。
どうやらガベージコレクタが GdkPixbuf を見つけられない意味っぽい。
つまりガベージコレクションは仕事をしているけど実は破棄できない。
結果ゾンビになったメモリ領域が大量発生、なのね。
だけど自前で破棄するとエラー、どうしろっての。。。。。

javascript の delete は単なるプロパティの削除だ。
this にくっつけたりあれやこれや十日間やったけど破棄する手段が見つからない。

もう C で作り直しするしか手段が無いかなぁとも。
こんだけスクリプト言語押しをしといて今更感が凄いけど。
って PyGObject だとどうなんだろう?どうせ同じだと思うけど実験。

#!/usr/bin/env python3

import sys, gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GLib, Gdk, GdkPixbuf

PATH = '/home/sasakima-nao/erohon/'

class AWindow(Gtk.ApplicationWindow):
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, application=app)
        self.d = Gtk.DrawingArea()
        self.d.connect('draw', self.on_draw)
        self.add(self.d)
        self.pixbuf = None
        self.count = 0
        GLib.timeout_add(200, self.read_pixbuf)
        self.show_all()

    def on_draw(self, widget, cr):
        if (self.pixbuf):
            aw = widget.get_allocated_width()
            ah = widget.get_allocated_height()
            buf = self.pixbuf.scale_simple(aw, ah, GdkPixbuf.InterpType.BILINEAR)
            Gdk.cairo_set_source_pixbuf(cr, buf, 0, 0)
            cr.paint()

    def read_pixbuf(self):
        self.count += 1
        if self.count == 200:
            return False
        f = '{0}{1:03d}.jpg'.format(PATH, self.count)
        self.pixbuf = GdkPixbuf.Pixbuf.new_from_file(f)
        self.d.queue_draw()
        return True

class AApplication(Gtk.Application):
    __gtype_name__ = 'AApplication'
    def __init__(self):
        GLib.set_prgname('AApplication')
        Gtk.Application.__init__(self)

    def do_startup(self):
        Gtk.Application.do_startup(self)
        AWindow(self)
    
    def do_activate(self):
        self.props.active_window.present()

AApplication().run(sys.argv)

まさかの問題無し!

やっていることは完全に同じなのに PyGObject では普通に破棄されている。
自前でバイナリを扱えるかどうかの差なのですかね。
筆者はいったい十日間何をやっていたんだ!
まあ検索しまくって色々勉強になったけど。
comipoli は PyGObject に戻します、メソッド名付けるのメンドイけど。

“webkit2” download

とあるサイトで連番ファイルをダウンロードしようとした。
Fedora な筆者は当然 wget を使った、見事にアクセス拒否された。

Chrome のロケーションバーに一つづつ打ち込むと普通に表示できる。
どうやらウェブブラウザからのアクセス以外は弾くようにしているようだ。
当然多重アクセスの対策もしているっぽい。

まてよ、ウェブブラウザなら大丈夫だったら Headless で wget の代わりに。。。
Headless Chromeでファイルをダウンロード – Qiita
やっぱりダメか、筆者ですら思い付くレベルの話だし当然悪用されるだろう。
面倒臭いけど一つづつ落とすしかないか。

そういえば、ウェブブラウザなら大丈夫なんだよね。
もしかして Gir の WebKit2 でアクセスすればイケるんでね?
悪用のススメではない、あくまで興味があったから。

[“webkit2” download] で検索すると、おぉ目的の記事が。
日本語がゼロなことに苦笑。

[webkit-gtk] how to download something with webkit ?

なるほど、WebKit2 でファイルをダウンロードするには
webkit_web_context_download_uri() 関数でイケるのね。
Python3 と Gir で簡単に作れそうだ。

WebKit2.WebContext – Classes – WebKit2 4.0

当然非同期だろうから for 文なんかを使うとトンデモアクセスになってしまう。
一つのダウンロードが終ってから次のリクエストを送る必要がある。
戻り値 WebKit2.Download のシグナルを使う、メインループがいるな。
エラーが戻る場合もあるだろうから Ctrl+C で終了できたほうがいいかと。
以下初心者向きな解説はしない。

#!/usr/bin/env python3

"""
    python3 wk_download.py uri start_number end_number
"""

import gi, signal, sys, os
signal.signal(signal.SIGINT, signal.SIG_DFL)

gi.require_version('WebKit2', '4.0')
gi.require_version('Gtk', '3.0')
from gi.repository import WebKit2, Gtk

ctx = WebKit2.WebContext()

d = os.path.dirname(sys.argv[1])
i = int(sys.argv[2])
end = int(sys.argv[3])

def on_finished(dl):
    global i, end
    i += 1
    if i <= end:
        do_download(i)
    else:
        Gtk.main_quit()

def on_failed(dl, error):
    print(error.message);
    Gtk.main_quit()

def do_download(n):
    s = "{0}/{1}.jpg".format(d, n)
    print(s)
    dl = ctx.download_uri(s)
    dl.connect("finished", on_finished);
    dl.connect("failed", on_failed);

do_download(i)
Gtk.main()

イケちゃった。

XDG_DOWNLOAD_DIR が自動的にダウンロード場所に指定される。
変更できるだろうけど筆者はコレでいいから別にいいや。

久々に Python をやったら結構忘れていて自分でびっくり。
argv の何番目とか行末コロンを忘れたりとかつい // とか。

GTK+ Overlay

Gedit を F11 でフルスクリーンにする。
その状態でマウスカーソルを上部に持って行くとフルスクリーンコントローラがスルスルと降りてくる。

これをどうやって実現しているのか今まで謎だった。
Clutter や非推奨の GtkFixed を使えば実は簡単なんだけど。
実際 0.1 や拙作 Clutter 動画プレイヤーのシークバーでやっているし。
Cocoa は GtkFixed みたいな古臭い API という事実は置いておいて。

GTK+ は基本的に Widget を配置すると引き伸ばしされる。
加えて GtkBin のサブクラスには Widget を一つしか配置できない。
つまり Widget 同士を重ねて配置することは不可能。
しかし Gedit 等は実現している、手段を調べてみよう。

実は Widget の引き伸ばしは単なるデフォルト動作だった。
知らなかったよマジで。
valign, halign プロパティのデフォルトが GTK_ALIGN_FILL なのだ。
つまり。

#!/usr/bin/gjs

const System = imports.system;
const GObject = imports.gi.GObject;
const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
const Gtk = imports.gi.Gtk;

var FlWindow = GObject.registerClass({
    GTypeName: "FlWindow"
}, class FlWindow extends Gtk.ApplicationWindow {
    _init(app) {
        super._init({application: app});
        //
        let entry = new Gtk.Entry({
            text: "this is a Entry",
            valign: Gtk.Align.END
        });
        this.add(entry);
        //
        this.show_all();
    }
});

var FlApplication = GObject.registerClass({
    GTypeName: "FlApplication"
}, class FlApplication extends Gtk.Application {
    _init(v) {
        GLib.set_prgname("MyProgram");
        super._init({
            application_id: "palepoli.skr.jp",
            flags: Gio.ApplicationFlags.HANDLES_OPEN
        });
    }
    vfunc_startup() {
        super.vfunc_startup();
        new FlWindow(this);
    }
    vfunc_open(files, hint) {
        this.active_window.present();
    }
    vfunc_activate() {
        this.active_window.present();
    }
});

new FlApplication().run([System.programInvocationName].concat(ARGV));

で。

と valign, halign プロパティで Widget を上下左右に貼り付けることができる。
ウインドウをリサイズすると追従することが確認できる。

次は Widget 同士を重ねる方法。
なんてことはない、GtkOverlay というズバリな Widget があった。

#!/usr/bin/gjs

const System = imports.system;
const GObject = imports.gi.GObject;
const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
const Gtk = imports.gi.Gtk;

var FlWindow = GObject.registerClass({
    GTypeName: "FlWindow"
}, class FlWindow extends Gtk.ApplicationWindow {
    _init(app) {
        super._init({application: app});
        // Widgets
        this.entry = new Gtk.Entry({
            no_show_all: true,
            text: "this is a Entry",
            valign: Gtk.Align.START
        });
        let view = new Gtk.TextView();
        view.buffer.set_text("1\n2\n3\n4\n5", -1);
        // Overlay
        let ol = new Gtk.Overlay();
        ol.add(view);
        ol.add_overlay(this.entry);
        this.add(ol);
        // Signal
        this.connect("motion-notify-event", (widget, event)=> {
            let [b, x, y] = event.get_coords();
            this.entry.visible = y < 50;
        });
        this.show_all();
    }
});

var FlApplication = GObject.registerClass({
    GTypeName: "FlApplication"
}, class FlApplication extends Gtk.Application {
    _init(v) {
        GLib.set_prgname("MyProgram");
        super._init({
            application_id: "palepoli.skr.jp",
            flags: Gio.ApplicationFlags.HANDLES_OPEN
        });
    }
    vfunc_startup() {
        super.vfunc_startup();
        new FlWindow(this);
    }
    vfunc_open(files, hint) {
        this.active_window.present();
    }
    vfunc_activate() {
        this.active_window.present();
    }
});

new FlApplication().run([System.programInvocationName].concat(ARGV));

結果。

クライアント領域上部にマウスカーソルを持って行くと Widget が重なって出て来る。
こんな感じでフルスクリーン時のみ Widget を重ねて上部に出してやればオケ。

後は Gedit のようにアニメーションで出す方法だけど。
ソースを見ると GtkRevealer を使っているので同じ配置をしてみたんだけど。
GTK_REVEALER_TRANSITION_TYPE_CROSSFADE 以外動かない、何故だ?
おまけに motion-notify-event で得る値がおかしくなる、ワカンネェ!
これについては気が向いたら後日。

GtkDrawingArea Custom Drawing

前回の続き、GtkDrawingArea です。
フォトレタッチのようにマウス軌跡の連続線を GTK+ で描きたい。
って探すまでもなく GTK+ 3 Reference Manual に書いてあった。

Custom Drawing: GTK+ 3 Reference Manual

つまり軌跡を残すには draw シグナル引数にある cairo_t の中に描写しない。
サーフェス(面の意味)を別に作ってそちらに描写する、当然追記になる。
そのまま gtk_widget_queue_draw 関数を呼び出し draw シグナルを送る。
ハンドラにて先程のサーフェスを cairo_t にセットする。

解ってしまえばそりゃそうだ、みたいな。

しかし gjs へのバインディング記法がサッパリ解らない。
Gir の cairo は何もできないし、使おうとした人なら解ってくれるよね。
Gdk.cairo*** も GTK+ と同様に書き換えるとエラー。
cr.set_source_rgb みたいなバインドではないようだ、うーん。

installed-tests/js/testCairo.js ? master ? GNOME / gjs ? GitLab

散々遠回りをしてやっとこんなのを捜し出した。
リソースにある cairo を使えばいいってことね。
しかしコッチは GTK+ と違って C 言語とかなり違う表記を強いられる。

// C
// JavaScript

cairo_create (surface);
new Cairo.Context(surface);

cairo_set_source_surface (cr, surface, 0, 0);
cr.setSourceSurface(surface, 0, 0);

cairo_set_source_rgb (cr, 1, 1, 1);
cr.setSource(Cairo.SolidPattern.createRGB(1, 1, 1));

gdk_window_create_similar_surface
widget.window.create_similar_surface
//Gdk.Window.create_similar_surface // Error

CAIRO_CONTENT_COLOR
Cairo.Content.COLOR

てな感じ。

ということで、実際に上手くいったソース。

#!/usr/bin/gjs

// Resource
const System = imports.system;
const Cairo = imports.cairo;
// Gir
const GObject = imports.gi.GObject;
const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
const Gtk = imports.gi.Gtk;
const Gdk = imports.gi.Gdk;

var DrawingWindow = GObject.registerClass({
    GTypeName: "DrawingWindow"
}, class DrawingWindow extends Gtk.ApplicationWindow {
    _init(app) {
        super._init({application: app});
        // var
        this.surface = null;
        // HeadreBar
        let hb = new Gtk.HeaderBar({show_close_button: true});
        let clearButton = new Gtk.Button({label: "Clear"});
        clearButton.connect("clicked", ()=> {
            this.clearSurface();
            this.canvas.queue_draw();
        });
        hb.pack_start(clearButton);
        this.set_titlebar(hb);
        // DrawingArea
        this.canvas = new Gtk.DrawingArea();
        this.canvas.set_events(
            Gdk.EventMask.BUTTON_PRESS_MASK |
            Gdk.EventMask.BUTTON_RELEASE_MASK |
            Gdk.EventMask.BUTTON1_MOTION_MASK |
            Gdk.EventMask.STRUCTURE_MASK );
        this.canvas.connect("configure-event", (widget, event)=> {
            this.surface = this.canvas.window.create_similar_surface(
                Cairo.Content.COLOR,
                widget.get_allocated_width(),
                widget.get_allocated_height() );
            this.clearSurface();
            return true;
        });
        this.canvas.connect("button-release-event", (widget, event)=> {
            return false;
        });
        this.canvas.connect("button-press-event", (widget, event)=> {
            if (event.get_button()[1] === 1) {
                let [res, x, y] = event.get_coords();
                hb.set_title(`${x}/${y}`);
                this.drawBrush(x, y);
            }
            return false;
        });
        this.canvas.connect("motion-notify-event", (widget, event)=> {
            let [res, x, y] = event.get_coords();
            hb.set_title(`${x}/${y}`);
            this.drawBrush(x, y);
            return false;
        });
        this.canvas.connect("draw", (widget, cr)=> {
            cr.setSourceSurface(this.surface, 0, 0);
            cr.paint();
            return false;
        });
        this.add(this.canvas);
        // this
        this.resize(400, 400);
        this.show_all();
    }
    drawBrush(x, y) {
        let cr = new Cairo.Context(this.surface);
        cr.arc(x, y, 10.0, 0.0, 2*Math.PI);
        cr.fill();
        //this.canvas.queue_draw();
        this.canvas.queue_draw_area(x-10, y-10, 20, 20);
    }
    clearSurface() {
        let cr = new Cairo.Context(this.surface);
        cr.setSource(Cairo.SolidPattern.createRGB(1, 1, 1));
        cr.paint();
    }
});

var DrawingApplication = GObject.registerClass({
    GTypeName: "DrawingApplication"
}, class DrawingApplication extends Gtk.Application {
    _init(v) {
        GLib.set_prgname("Program");
        super._init({
            application_id: "org.sasakima.drawwin",
            flags: Gio.ApplicationFlags.HANDLES_OPEN
        });
    }
    vfunc_startup() {
        super.vfunc_startup();
        new DrawingWindow(this);
    }
    vfunc_open(files, hint) {
        this.active_window.present();
    }
    vfunc_activate() {
        this.active_window.present();
    }
});

new DrawingApplication().run([System.programInvocationName].concat(ARGV));

で。

となります。
ImageSurface に画像を指定して文字入れ、なんてのもこの応用で可能。
フォトレタッチとはとても言えないけど手段は解ったということで。