GTK+」タグアーカイブ

PyGObject CSS

PyGObject 覚書ページの更新状況。
CSS 関連が色々と変更されていてワケワカメになっている。
y901x 1.1 の時に解っていて放置していたけどそろそろキチンと調べることに。

#!/usr/bin/env python3
 
import sys, gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
 
OLD_CSS = '''
GtkLabel {
    color: blue;
}'''
NEW_CSS = '''
#MyLabel {
    color: red;
}'''
 
class Win(Gtk.ApplicationWindow):
    '''
        Font Set
    '''
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, application=app, title='Py')
        # set the Widget Name
        label = Gtk.Label(label='ABCDEFG', name='MyLabel')
        self.add(label)
        # GtkCssProvider
        provider = Gtk.CssProvider()
        #provider.load_from_data(OLD_CSS.encode('utf-8'))
        provider.load_from_data(NEW_CSS.encode('utf-8'))
        label.get_style_context().add_provider(provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
        #
        self.show_all()

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(self)

    def do_startup(self):
        Gtk.Application.do_startup(self)
        Win(self)

    def do_activate(self):
        self.props.active_window.present()

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

と。

GtkWidget に名前を付けて # の接頭子でなんとか適用された。
GtkEntry や .entry という以前の指定は無視される、全部名前を付けてねと。
他にも手段があるのだろうけどコレが一番確実みたい。

GTK+ CSS Properties: GTK+ 3 Reference Manual

が指定できる CSS だけど、解り辛いったらありゃしない。
手探りで色々書きまくって覚えるしかない、Linux の世界はそういうものだ。

ところで GtkTextView のフォントを変更するにも CSS しか手段が無いようだ。
探しても探しても古い手段しか出てこない、適用できない。
こんな時は、Gedit のソースコードを見るのが一番、GPL 万歳。
gedit-view.c と gedit-pango.c を解析、ってほど難しいコードではないけど。

Fonts: Pango Reference Manual

PangoFontDescription を得て個別に CSS 文字列に変換していた。
Gedit のソースをよく見ると Italic かどうかを調べていない、あれ?
試したら Gedit は Italic 表示できないジャン、使わないとはいえ初めて知った!

PangoFontDescription には Italic かを調べる手段があるようだ。
それならということで。

#!/usr/bin/env python3

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

TPL = '''
#MyTextView {{
    font: {0}pt "{1}";
    font-weight: {2};
    font-style: {3};
}}'''

class Win(Gtk.ApplicationWindow):
    '''
        Font Set
    '''
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, application=app, title='Py')
        # GtkTextView
        self.view = Gtk.TextView(name='MyTextView')
        # deprecated
        #font_desc = Pango.font_description_from_string('Monospace 10')
        #view.override_font(font_desc)
        #
        # read the this script file
        with open(__file__) as f:
            s = f.read()
            self.view.get_buffer().set_text(s)
        # GtkFontButton
        button = Gtk.FontButton()
        button.connect('font-set', self.on_font_set)
        # GtkScrolledWindow
        sw = Gtk.ScrolledWindow(child=self.view)
        vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
        vbox.pack_start(button, False, False, 0)
        vbox.pack_start(sw, True, True, 0)
        self.add(vbox)
        self.resize(300, 300)
        self.show_all()

    def on_font_set(self, button):
        '''
        font = button.get_font()
        n = font.rfind(' ')
        name = font[:n]
        size = font[n+1:]
        css = '#MyTextView {{ font: {0}pt "{1}"; }}'.format(size, name)
        '''
        desc = button.get_font_desc()
        size = desc.get_size() // Pango.SCALE
        name = desc.get_family()
        weight = desc.get_weight() // 1 # enum to int
        if weight == Pango.Weight.NORMAL:
            w = 'normal'
        elif weight == Pango.Weight.BOLD:
            w = 'bold'
        else:
            w = weight
        style = desc.get_style()
        if style == Pango.Style.OBLIQUE:
            s = 'oblique'
        elif style == Pango.Style.ITALIC:
            s = 'italic'
        else:
            s = 'normal'
        self.props.title = f'{size}|{name}|{w}|{s}'
        css = TPL.format(size, name, w, s)
        # GtkCssProvider
        provider = Gtk.CssProvider()
        provider.load_from_data(css.encode('utf-8'))
        self.view.get_style_context().add_provider(provider, Gtk.STYLE_PROVIDER_PRIORITY_USER)
        #'''

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(self)

    def do_startup(self):
        Gtk.Application.do_startup(self)
        Win(self)

    def do_activate(self):
        self.props.active_window.present()

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

よし bold も italic も適用されるようになったぞと。
コメントアウト部分だけでイケるかなと思ったけど駄目だった。
しかし GUI アプリなのに CSS でフォント指定って、時代の流れなんでしょうか。
とにかくコレで覚書ページの更新が進む、はず。

child Property

お盆休みが終わってしまった。
この間に PyGObject Tips ページの全書き換えが、終わらなかった。。。。。

しかし想像以上に内容が古かった、特に

#label = Gtk.Label('text') # Compatibility PyGtk => stderr
label = Gtk.Label(label='text')

みたいな箇所が沢山あって全部 stderr を吐くという。
もう PyGtk は闇に葬らないといけないみたいね。

ところで GtkEntryCompletion の所に書いているんだけーが。
Gtk(PyGObject) Tips | 一行 EDIT – L’Isola di Niente

text_column をプロパティで指定したら何も表示されない。
関数で指定すれば問題なく表示されるんだけーが、見つけるのに超時間喰った。
いくら検索しても出てこないし、C だと関数しか手段が無いし当然かもだが。
今月中には全部終わらせたい、明日から仕事だけど。

そういえば、Stock Items が知らぬ間に Deprecated にと気が付く。
と思ったら 3.10 からかよ、何故今まで気が付かなかったんだよ俺!
ダイアログを作るのページで使ってしまったんだが、いつ書き換えよう?

他、こんなページを作っていてあることに気がつく。
GtkContainer には child というプロパティがある。
add で重ねられる Widget は全部コレの派生だ、つまり。

#scroll = Gtk.ScrolledWindow()
#scroll.add(flowbox)
scroll = Gtk.ScrolledWindow(child=flowbox)

これでいいジャン!
毎回毎回作って add してを書いていたのが一行で終わる、素晴らしい。
唯一の大発見であった。

PyGObject HANDLES_COMMAND_LINE

前回 *.desktop に追記で ApplicationMenu だった位置に項目追加の件。
あの後アップデート通知から適用して再起動をしたら表示されるようになった。
なんだコレ、*.desktop 項目ってメモリキャッシュでもしているのだろうか?
よくわからないけど前回のリンク先の方法のみで項目追加は可能だと解った。

それより肝心な –new-window オプションの適用手段はなんとかしたい。
GSettings にフラグを用意して転送先 GApplication から参照では強引すぎる。
他に手段があるはず、Nautilus や Gedit はそんな変なことしていないけどメニューにあるし。
ダメだ、筆者の経験値では他の手段を思いつかない。。。。。

素直に Gedit のソースコードを参考にすることにする。
Gedit の About を開いてリンクをクリックすればホームページが開くのでそこから落とす。
当然 C 言語、Python でアプリを作れる人は C でも作れるけど面倒だから(以下略

GtkApplikation の flags に G_APPLICATION_HANDLES_OPEN だけでなく
G_APPLICATION_HANDLES_COMMAND_LINE を OR 演算している、あれ?
この指定って CUI アプリを作る時に使うと思っていた、思い込みだったようだ。

command-line シグナルとのセットのようだ、–new-window をコチラで処理している。
なるほど、–version の場合は転送前の GApplication にて処理。
–new-window は転送先の GApplication で処理ってことね。
handle-local-options と command-line シグナルってそういうことみたい。
こんな便利なものを GApplication は提供していたとは。

ただ同様に G_APPLICATION_HANDLES_COMMAND_LINE を OR で色々試してみたんだけど。。。。。
どうやっても open シグナルが発行されない、これじゃファイル名を得られない。
devhelp には G_APPLICATION_HANDLES_OPEN 指定なら残りの引数で open シグナルとあるんだが。
Google 翻訳なので完全に正しいとはいえないんだけど、とにかく困った。

よく見ると Gedit もファイル引数は独自で処理しているみたいだし。
あきらめて command-line ハンドラ内で処理するしかないみたい。
get_arguments で引数は全部取得できる、存在するファイルかどうかを自前で調べ
g_application_open を発行という手もあるけど二度手間だよね。

経緯を全部書いたら長くなってしまった。
ということをふまえて早速サンプルコードを書いてみる。

#!/usr/bin/env python3

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

class Win(Gtk.ApplicationWindow):
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, application=app, title='Chikubi')
        self.resize(150, 60)
        self.show_all()

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(
            self,
            application_id='bura.oppai.chikubi',
            flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
            # nonsense
            #flags=Gio.ApplicationFlags.HANDLES_OPEN | Gio.ApplicationFlags.HANDLES_COMMAND_LINE)
        # add --version, -v option
        self.add_main_option('version', b'v', GLib.OptionFlags.NONE, GLib.OptionArg.NONE, 'Show Chikubi Version', None)
        # add --new-window, -n option
        self.add_main_option('new-window', b'n', GLib.OptionFlags.NONE, GLib.OptionArg.NONE, 'New Chikubi Window', None)

    def do_command_line(self, command_line):
        # --new-window Option Handler
        options = command_line.get_options_dict()
        if options.lookup_value('new-window', GLib.VariantType.new('b')):
            w = Win(self)
        # get filename
        arg = command_line.get_arguments()[1:]
        for s in arg:
            # Option ignored
            if not s.startswith('-'):
                # URI? (file:///...)
                if '//' in s:
                    f = Gio.file_new_for_uri(s)
                else:
                    f = Gio.file_new_for_path(s)
                if f.query_exists():
                    self.props.active_window.set_title(s)
                    break
        return 0

    def do_handle_local_options(self, options):
        # --version Option Handler
        if options.lookup_value('version', GLib.VariantType.new('b')):
            print('Chikubi 0.0.1')
            return 0
        return -1

    def do_startup(self):
        Gtk.Application.do_startup(self)
        Win(self)

''' needless
    def do_activate(self):
        self.props.active_window.present()

    def do_open(self, files, n_file, hint):
        self.props.active_window.present()
'''

if __name__ == '__main__':
    app = App()
    app.run(sys.argv)

狙ったとおりになった。

*.desktop で %U 指定だと file:///… の URI で渡ってくるので注意ね。
パス名は二連スラッシュに絶対できないはずだから振り分けはコレでイケると思う。
もう少し自前 debug してから自アプリは更新します。

PyGObject handle_local_options

GNOME 3.32 から ApplicationMenu は無くなったはずだけど。
Gedit や Nautilus の Global Menu には「新しいウインドウ」がある。
自作 GTK+ アプリにもコレを付けてみたい。

“New terminal” desktop action does nothing on Gnome ? Issue #427 ? gnunn1/tilix ? GitHub

Desktop Entry Specification

色々探してこんなのを見つける。
*.desktop のほうに定義するのね、なるほど。

いやまて。
つまり –new-window というオプションを用意しなきゃいけないヤン!

手抜きなオプション定義にしていたけどキチンと作らないと。
GApplication の handle_local_options シグナルを使えばいいようだ。

Using g_application_add_main_option_entries() with PyGObject ? GitHub

GOptionEntry を使おうとしたら PyGObject だとメンドクサ!
g_application_add_main_option のほうが短いコードでイケるのでコッチで。
とりあえず –version を表示するオプションで上手くいったサンプルコード。

#!/usr/bin/env python3

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

class Win(Gtk.ApplicationWindow):
    def __init__(self, app):
        Gtk.ApplicationWindow.__init__(self, application=app, title='Oppai')
        self.show_all()

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(self)
        # add --version, -v option
        self.add_main_option('version', b'v', GLib.OptionFlags.NONE, GLib.OptionArg.NONE, 'Show Oppai Version', None)

    def do_handle_local_options(self, options):
        # get option
        if options.lookup_value('version', GLib.VariantType.new('b')):
            self.show_version()
            return 0
        return -1

    def do_activate(self):
        self.window = Win(self)
        self.window.present()

    def show_version(self):
        print('Oppai 0.0.1')

if __name__ == '__main__':
    app = App()
    app.run(sys.argv)

にて。

–help は全自動で定義される。
add_main_option の第二引数を使う場合はバイナリにするのをお忘れなく。
handle_local_options ハンドラはゼロを戻すとそのまま終了してくれる。
それ以外の説明はいらないよね。

さて肝心の –new-window を定義したら色々困ったぞ。
handle_local_options は startup シグナルより前に飛んでくる。
なので何も定義されていない状態。
しかも GApplication は同じ application-id の app に引数を転送して終了する。
handle_local_options ハンドラからは転送先 app を参照することは不可能。
つまり GtkApplicationWindow はこのハンドラ内では作れないってことだ。

GSettings にフラグを用意するというかなり強引な方法で定義はできた。
コマンドでは上手くいった、よし comipoli.desktop に追記だ。
表示されないんですけど。。。。。
まだ他にやらないといけないことがあるのだろうか?

GLib(PyGObject) Tips

内容が古いと解っていながら放置していた PyGObject Tips 以下。
整理したり文字列の fstring 化等をやってとりあえず GLib のページだけ。
GLib(PyGObject) Tips – L’Isola di Niente

それでは寂しいので GIOChannel 関連を Gjs のページからもってくる。
GIOChannel(PyGObject) Tips – L’Isola di Niente

いざ書き換えしてみたらマジで内容が古かった、こりゃアカン。
盆休みくらいまでには全部書き換えしたいな、ポケ GO も飽きたし。

PyGObject で作りたいものは今は無いけどページを作っていたら何か思い付くかも。
PyObjC では実はあるんだけど手段で詰まってアプリもページも止まっていることは内緒。
zsh 関連は次の macOS アップデートから再開、今やってもなぁって感じだし。

しかし Python を始める人が増えた感じだよね。
C 言語のポインタを理解できない人がオブジェクトなんて理解できるのか疑問なんだけど。
Python でアプリが作れる人って実は C でも作れるけど面倒だから Python を使っている。
っていう人しかいないんだよね。