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

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 ぐるぐる(同

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

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

gzip, zip

zip 書庫の中にある画像を表示したい。
もちろん展開ファイルを作らずメモリ内でやりくり。

って Gjs ではどうすればいいんだ?
Java なら ZipInputStream という便利なクラスがあるんだが。

ホイール欲しい ハンドル欲しい ? データ圧縮 zlib と gzip と zip (zlib で zip にアクセスする)

zlib でできるっぽい、サイト名が関係なさすぎでワロタ。
Gio で zlib はアクセスできるはず。

Projects/Vala/GIOCompressionSample – GNOME Wiki!

Vala コードだけど gir なら同様に扱えるはず。
ということでこんなコードを書いてみた。

#!/usr/bin/gjs

const Gtk = imports.gi.Gtk;
const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
const Lang = imports.lang;
const GdkPixbuf = imports.gi.GdkPixbuf;

//const PATH = "akazukin.zip";
const PATH = "akazukin.gz";

const UnzipTestWin = new Lang.Class({
    Name: 'UnzipTestWin',
    Extends: Gtk.ApplicationWindow,

    _init: function(app) {
        this.parent({
            application: app
        });
        // Read gzip file
        let source = Gio.File.new_for_path(PATH);
        let stream = source.read(null);
        let converter = new Gio.ZlibDecompressor({
            //format: Gio.ZlibCompressorFormat.RAW
            format: Gio.ZlibCompressorFormat.GZIP
        });
        let cnv_stream = new Gio.ConverterInputStream ({
            base_stream: stream,
            converter: converter
        });
        // Create Pixbuf
        let pixbuf = GdkPixbuf.Pixbuf.new_from_stream(cnv_stream, null);
        let image = new Gtk.Image({
            pixbuf: pixbuf
        });
        this.add(image);
        this.show_all();
    }
});

const UnzipApp = new Lang.Class({
    Name: 'UnzipApp',
    Extends: Gtk.Application,

    _init: function() {
        GLib.set_prgname("UnzipApp");
        this.parent({
            application_id: 'org.sasakima.unzip',
            flags: Gio.ApplicationFlags.FLAGS_NONE
        });
    },
    vfunc_activate: function() {
        new UnzipTestWin(this);
    }
});
let application = new UnzipApp();
application.run(null);

gzip は上手くいったけど zip はダメだ。
そんなに甘くはなかった、いや筆者が無知なだけかも。
てか gzip は普通 tar とセットだ、tar も展開しないと…

いや違うだろ、zip でなきゃ意味が無いんだ。
何を作ろうとしているかバレバレ臭いのは気にしない。

ええい面倒だ、Python3 の zipfile を使ってしまえ!
スクリプト言語の速度で大丈夫かな?試してみるべ。

#!/usr/bin/env python3

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

PATH = "なえコレ.zip";

class UnzipTestWin(Gtk.ApplicationWindow):
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, application=app)
        fbox = Gtk.FlowBox(valign=Gtk.Align.START, min_children_per_line=5)
        datas = []
        # unzip
        with zipfile.ZipFile(PATH) as f:
            l = f.namelist()
            for name in l:
                d = f.read(name)
                datas.append(d)
        for data in datas:
            stream = Gio.MemoryInputStream.new_from_data(data)
            p = GdkPixbuf.Pixbuf.new_from_stream(stream)
            minp = p.scale_simple(80, 100, GdkPixbuf.InterpType.BILINEAR)
            image = Gtk.Image(pixbuf=minp)
            fbox.add(image)
        self.add(fbox)
        self.show_all()


class UnzipApp(Gtk.Application):
    def __init__(self):
        GLib.set_prgname("UnzipApp");
        Gtk.Application.__init__(
            self,
            application_id="apps.test.naecore",
            flags=Gio.ApplicationFlags.FLAGS_NONE )

    def do_activate(self):
        UnzipTestWin(self)

app = UnzipApp()
app.run(sys.argv)

naekore

なんだ一瞬だった。
これなら速度も問題ないし簡単だし Python で作ることにしよう。
しかし PyGObject でもプロパティ指定がすっかり Gjs 風になってしまった。

GtkShortcutWindow

GTK+ (GNOME) 3.20 の目玉は当然 GtkShortcutWindow です。

メニューバーの中に表示しなければいけないなんて誰も決めていないぞ。
まさか「メニューバーの中のほうが直感的に使える!」なんて言う無知はいないよね。
今だに Ctrl+C すら知らない人が大多数という事実をヲタは知らない。

って、それはどうでもよくて。
実際に GtkShortcutWindow を作ってみよう。

#!/usr/bin/gjs

const Gtk = imports.gi.Gtk;
const GLib = imports.gi.GLib;
const Gio = imports.gi.Gio;
const Lang = imports.lang;

const TestWindow = new Lang.Class({
    Name: 'TestWindow',
    Extends: Gtk.ApplicationWindow,

    _init: function(app) {
        this.parent({
            application: app
        });
        this.resize(300, 300);
        this.show_all();
    }
});

const ShortcutApp = new Lang.Class({
    Name: 'ShortcutApp',
    Extends: Gtk.Application,

    _init: function() {
        // TaskBar Title
        GLib.set_prgname("ShortcutApp");
        // property
        this.parent({
            application_id: 'org.sasakima.shortcutapp',
            flags: Gio.ApplicationFlags.FLAGS_NONE
        });
    },
    vfunc_startup: function() {
        this.parent();
        //
        // Create ShortcutWindow
        this.scwin = new Gtk.ShortcutsWindow();
        let sec = new Gtk.ShortcutsSection({
            visible: true,
            section_name: "Name"
        });
        let doc = new Gtk.ShortcutsGroup({
            title: "Document"
        });
        doc.add(new Gtk.ShortcutsShortcut({
            accelerator: "<ctl>Q",
            title: "Ctrl : <ctl>,  Shift : <shift>, Alt : <alt>"
        }));
        doc.add(new Gtk.ShortcutsShortcut({
            accelerator: "Escape",
            title: "Single Key"
        }));
        doc.add(new Gtk.ShortcutsShortcut({
            accelerator: "1",
            title: "Num Key"
        }));
        sec.add(doc);
        this.scwin.add(sec);
        //
        // Menu
        let menu = new Gio.Menu();
        menu.append("_Keyboard Shortcuts", "app.shortcut_action");
        menu.append("_Quit", "app.quit_action");
        this.set_app_menu(menu);
        // Accel
        this.set_accels_for_action("app.shortcut_action", ["<Control>F1", "question"]);
        this.set_accels_for_action("app.quit_action", ["<Control>Q"]);
        // Action
        let shortcut_action = new Gio.SimpleAction({
            name: "shortcut_action"
        });
        shortcut_action.connect("activate", Lang.bind(this, function(action) {
            this.scwin.show_all();
        }));
        let quit_action = new Gio.SimpleAction({
            name: "quit_action"
        });
        quit_action.connect("activate", Lang.bind(this, function(action) {
            this.quit();
        }));
        this.add_action(shortcut_action);
        this.add_action(quit_action);
    },
    vfunc_activate: function() {
        let w = new TestWindow(this);
        // Set ShortcutWindow Palent
        w.set_help_overlay(this.scwin);
    }
});
let application = new ShortcutApp();
application.run(ARGV);

gtk_shortcutswindow

アレ?と思ったのが GtkShortcutsGroup に配置が add だったこと。
GtkBox のようなパッキングだと思っていた。

ShortcutsSection に max_height プロパティがある。
この数を超える場合はグループを次ペインに全自動で移すようです。

gtk_application_window_set_help_overlay

は凄く重要、GtkApplicationWindow 指定を必ず行うこと。
実際に GNOME アプリで GtkShortcutWindow を出して移動してみよう。

後はこのブログを見ているような人なら説明不要かと。
スクリプト言語から最新 API を試せるって本当に面白いよね。

GTK+ 3.20 GtkHeaderBar and CSS

GTK+ 3.20 で GtkHeaderBar を使う場合の注意。
GtkWindow のリサイズがクライアント領域に変更されたようです。
というか GtkHeaderBar があってもなくても同じ扱いということ。

let w = 320;
let h = 240;
/* under 3.18 in GtkHeaderBar
let diff_x = window.get_allocated_width() - window.vbox.get_allocated_width();
let diff_y = window.get_allocated_height() - window.vbox.get_allocated_height();
window.resize(w + diff_x, h + diff_y);
*/
window.resize(w, h); // 3.20

3.18 以前にも対応させるには振り分け処理が必要。
我がアプリは GtkShortcutWindow を使う予定なので 3.20 専用にする。

*****

GStreamer に変更があったのか他の要因か解らないけど。
皮肉なことに Y901x 1.1.3 で動画がリサイズできるようになった。
せっかく ClutterGst で丸ごと作り替えはじめたのにな。

ただ CSS の仕様が 3.20 で丸々変更されている。
3.18 以前で Style Properties を使っていた場合は WARNING だらけに。

gtk_style

こいつは 3.18 以前との振り分けは無理っぽいな。
詳しいことは今度調べる。

*****

Gjs で ARGV の仕様は変わっていないみたい。
相変わらず Clutter を使って多重起動すると落ちる。
つまり Gjs は筆者の解る範囲では何も変わっていない。

ClutterGst Aspect Rate | PaePoi

多重起動で不具合が出まくるなら原点に戻って多重起動防止だ!
って Totem も多重起動防止だった、知らなかった…

つか、多重起動って実は問題があるのよね。
設定を変更した時に既に起動しているウインドウへの適用をどうするか。
今の GNOME アプリは GSettings を使って見事に適用させている。
ini ファイルの奴でも手段はあるけど、ほぼ誰もやっていない。

まあそれはいいとして、大ボケに気が付いた。
y901x beta ではインストールする起動スクリプトをこうしていた。

#!/bin/sh
cd /usr/share/y901x
gjs y901main.js $*

これだと端末で cd 移動からの起動がおかしいジャン。
引数がファイル名だけだと[カレントディレクトリ + ファイル名]だよ。

#!/bin/sh
gjs /usr/share/y901x/y901main.js $*

に変更してソースには以下を追記。

imports.searchPath.unshift("/usr/share/y901x");

うん、大ボケはとりあえずなんとかなった。
もう少し調べて明日には 3.20 対応版を出そう。

JXA NSString

久々に Mac で JavaScript(JXA) を。
JavaScript 文字列と NSString の相互変換が今まで解らなかった。

OS?X 10.10 Release Notes

公式の解説にて $() だけで NSString に変換できるのは理解。
でも逆がドコにも書いていないんですけど。
散々探してやっと以下を見つける。

Home ? dtinth/JXA-Cookbook Wiki ? GitHub
Shell and CLI Interactions ? dtinth/JXA-Cookbook Wiki ? GitHub

んと、ObjC.unwrap で NSArray を JavaScript 形式に変換できるのか。
ならば NSString もイケるかな?と適当にやったら出来ちゃった。
ということで JXA, NSString 関連の覚書。

ObjC.import("Cocoa");

console.log("日本語も大丈夫");
$.NSLog("%@ %@", $("NSString"), $("はこうする"));

var jsStr = "変数も $() で変換できる\n";
var nsStr = $(jsStr);
$.NSLog("%@", nsStr);

nsStr = $("NS* から JavaScript 形式へは ObjC.unwrap()");
jsStr = ObjC.unwrap(nsStr);
console.log(jsStr);

var saveStr = "保存します\n";
saveStr += "戻り値が表示されるので変数に入れています\n";
var data = $(saveStr).dataUsingEncoding($.NSUTF8StringEncoding);
var res = data.writeToFileAtomically("output.txt", true);

if (res) {
    var terminal = Application("Terminal");
    var fm = $.NSFileManager.defaultManager;
    var cwd = fm.currentDirectoryPath;
    var nil = terminal.doScript("cat " + ObjC.unwrap(cwd) + "/output.txt");
}

jxa_nsstring

よし日本語もこれでバッチリ(死語)
Cookbook は他にも色々試したいコードが沢山あって素晴らしい。

しかし Cookbook も var とセミコロンを全部書いているんだね。
JXA では不要と言われても無いとキモいのは皆同じようである。