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

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 に画像を指定して文字入れ、なんてのもこの応用で可能。
フォトレタッチとはとても言えないけど手段は解ったということで。

GtkDrawingArea GdkEvent

あけましておめでとうございます。
昨日出した comipoli がバグだらけで今年も正月からプログラミング。

まだおかしい、button-press-event がタイトルバーに反応する。
多分 GtkHeaderBar 部分もクライアントエリアとして認識されているかと。
GNOME は GtkHeaderBar を推奨だけど使うとこんな弊害が色々出るんだよなぁ。

GtkDrawingArea で GdkEvent を取ればいいのだが、手段が解らなかった。
普段あまりネタが無いのに元旦にネタができるという。

日本語で見つかったサイトはココだけだった、GTK2 だけど。
GtkDrawingAreaに図形を表示する
2019 年にもなって手打ち HTML かつスマホ用タグ無しサイトってどうなんだ。。。。。
いやそれはどうでもよくて。

c – In GTK+3, how do I get a drawingarea to respond to mouse events? – Stack Overflow

やはり GTK3 も gtk_widget_add_events と can-focus プロパティか。
とにかくやってみよう。

Events: GDK 3 Reference Manual

GdkEventMask を調べたら GDK_BUTTON1_MOTION_MASK なんて便利そうなものが。
マウス左ボタンを押しっ放しで動かした場合のみ飛んでくるシグナルのようだ。
自分でフラグを用意しなくてもいいのね、早速。

#!/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;
const Gdk = imports.gi.Gdk;

var DrawingWindow = GObject.registerClass({
    GTypeName: "DrawingWindow"
}, class DrawingWindow extends Gtk.ApplicationWindow {
    _init(app) {
        super._init({application: app});
        // var
        this.xy = [false, 0, 0];
        // HeadreBar
        let hb = new Gtk.HeaderBar({show_close_button: true});
        this.set_titlebar(hb);
        // DrawingArea
        let da = new Gtk.DrawingArea();//{can_focus: true});
        //da.set_events(
        da.add_events(
            Gdk.EventMask.BUTTON_PRESS_MASK |
            Gdk.EventMask.BUTTON_RELEASE_MASK |
            Gdk.EventMask.BUTTON1_MOTION_MASK |
            Gdk.EventMask.STRUCTURE_MASK );
        da.connect("button-release-event", (widget, event)=> {
            return false;
        });
        da.connect("button-press-event", (widget, event)=> {
            if (event.get_button()[1] === 1) {
                this.xy = event.get_coords();
                hb.set_title(`${this.xy[1]}/${this.xy[2]}`);
                widget.queue_draw();
            }
            return false;
        });
        da.connect("motion-notify-event", (widget, event)=> {
            this.xy = event.get_coords();
            hb.set_title(`${this.xy[1]}/${this.xy[2]}`);
            widget.queue_draw();
            return false;
        });
        da.connect("draw", (widget, cr)=> {
            cr.arc(this.xy[1], this.xy[2], 10.0, 0.0, 2*Math.PI);
            cr.fill();
        });
        this.add(da);
        // this
        this.resize(400, 400);
        this.show_all();
    }
});

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));

あれ?

cairo のクリアをしていないので追加描写だと思ったら書き換えされる。
逆にフォトレタッチみたく追加で描くにはどうすればいいんだ?
あぁヤルことが増えてしまった、それは後日ということで。
とにかくマウス左ボタンに追従する円のできあがり。

マウスだけなら can-focus プロパティはセットする必要が無いみたい。
GDK_KEY_PRESS_MASK 等を使うなら当然必須になると思うけど。

add_events, set_events のどちらでも動いた。
set は初期化、add は既にあるイベントに追加という振り分けみたい。
GtkDrawingArea は元々何もないので同じということね。

今年もこんな感じでいきます。

Automator JXA and shell

サービスがいいMacBookにしておく – ザリガニが見ていた…。

Automator ってコンテキストメニューが作れるんだ!
この方法なら筆者が書いた Finder Script より簡単にシェルが使える。
macOS アプリを JXA (AppleScript) で拡張 – L’Isola di Niente
いや Nautilus スクリプトと同じように使えるというべきか。

では早速。
Mojave ではサービスではなく「クイックアクション」になっています。

リンク先ではテキストの選択だけど Finder での選択もイケそう。
「Finder 項目を表示」を挟む必要があるみたい。
早速同じ AppleScript を試すと、、、、、

あぁ、やはりパス名は古い mac 形式、いつものことだ。
しかしこの input は CR 改行区切りの文字列なのか配列なのか?
JXA でないと筆者は解らないので「JavaScript を実行」に置き換えて。

function run(input, parameters) {
	
	// Your script goes here
	let app = Application.currentApplication();
	app.includeStandardAdditions = true;
	//let s = input.toString().split('\r').join('\n');
	let s = input.join('\n');
	app.displayAlert(s);

	//return input;
}

うん、JXA だと input は JavaScript の配列になるようだ。
こっちだと UNIX パスになるな、何故かはワカンネ。
最後の return は次に続くチェーンが無いなら不要みたい。

んで、名前を付けて保存すると。

~/Library/Services/

以下に保存される、*.workfrow はディレクトリのようだ。
中にある info.plist の NSMenuItem キーからメニューを変名できる。

まて、よく見ると「シェルスクリプトを実行」のアクションがある。
AppleScript, JXA だけでなく直接 bash も使えるようで。
入力を「引数として」の選択肢もある、これならどうなる?

ところでカレントディレクトリは又してもルートなんだろうか。
と思って調べたら $HOME だった、どっちにしろ移動しなきゃ。
ファイル実行ではないので $0 では調べられない、と最初思ったけど。
ファイルを選択してコンテキストメニューなんだから $1 でいいヤン。

cd `dirname $1`
echo "$@" > out.txt

で保存して Finder で何か選択して実行。
選択したファイルがある位置に UNIX 形式パスが IFS 区切りでリダイレクトされるのが確認できるはず。
これでシェルを簡単に使えるようになるのでお好きなコマンドを。
JXA とチェーンを繋げればかなり高度な自動化ができそう。

colon command

Bash, no-arguments warning, and case decisions – Stack Overflow

このページの

: ${1?"Usage: $0 ARGUMENT"}

がよく解らなかった。
先頭のコロンって何も起こらないコマンドだったはずだけど。
変数中の ? も今まで見たことがなかった。

何もしない組み込みコマンド ":" (コロン)の使い道 – Qiita

凄く詳しい解説をありがとう。

: echo 1
# 無視される

: | echo 1
# 1 が出力される

: touch one.txt > tow.txt
# 空の tow.txt だけが造られる

: ${var=3}
echo $var
# 3 が出力される

なるほど、先頭コロンが何もしないのは最初の引数だけなんだ。
でも :=, :? の場合は副作用が起こるということらしい

それと変数中の ? の正体は :? のようだ、イコールも同様。
:- と同様にコロンは実は無くてもいいということね。

つまり最初のは第一引数が無い場合は ? 以下をエラー出力して終了という意味。
こんな短いコードで実現、って Bash 解りづれぇYO!