GtkSourceView Insert spaces instead of tabs

タブ幅とスペース切り替えで上手く行かなかった理由が解った。
indent-width を弄くっただけではなんか変な動作になるのね。

indent-width Property が -1 なら tab-width Property に従う。
デフォルトが -1 なので最初から tab-width のみで指定する、つまり
gtk_source_view_set_indent_width ではなく
gtk_source_view_set_tab_width で指定する。

ただ tab-width は困ったことに guint (unsignde int のマクロ) である。
ちなみに WindowsSDK でのマクロは UINT、なら最初から uint でいいのに…
C 言語を作った人達は何故こんな長い型名にしてしまったのやら。

Python ってつまり guint が無い。
型として記憶するようだし、旧文字列フォーマッタにも %u が何故かあるが。
しかし試してみると PyGI は guint を long として扱うようだ。
gint, guint の違いを long と扱って吸収するようだ。

int で型チェックを行っているモジュールに渡す場合は当然弾かれるのだが。
その場合は abs(int_value), int(uint_value) キャストでイケるみたい。
つまり我が自作の inifile8.py とかを使う場合、ココでしばし悩んだ。

gtk_source_view_set_insert_spaces_instead_of_tabs
の真偽値でタブかスペースかを切り替え。

gtk_source_view_set_indent_on_tab
の真偽値では Shift+Tab での動作。
False にすると Shift+Tab でもインデントしたり BackSpace でインデントが先頭まで削除されたりでワケが解らないよ。
思えばそんな動作をするエディタもある、私的に Shift+Tab は逆インデントが直感的だと思うのだが逆にそうなっているエディタのほうが少ないという現実。

更に GtkSpinButton を使うと値が float だったりする。
しかし面白いことに guint のまま値を突っ込むことができる。
そうなるようにバインドされているだけだが Python って簡単だね。
C 言語で同じことやっちゃダメだよ。

実際にアプリを作っている人でないと気がつきもしないだろうな。
こんなに簡単だとメモリ内でどうなっているかとかが勉強できないよ。

#!/usr/bin/env python
#-*- coding:utf-8 -*-

from gi.repository import Gtk, Gio, GtkSource, Pango

class TabTest(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self)
        # GtkSourceView
        self.view = GtkSource.View()
        desc = Pango.font_description_from_string("Monospace 11")
        self.view.override_font(desc)
        self.view.set_show_line_numbers(True)
        self.view.set_wrap_mode(Gtk.WrapMode.CHAR)
        self.view.set_draw_spaces(GtkSource.DrawSpacesFlags.ALL)
        # Tab Width
        spin = Gtk.SpinButton.new_with_range(0.0, 100.0, 1.0)
        spin.set_value(self.view.get_tab_width()) # uint to float ?
        spin.connect("value-changed", self.on_spin_changed)
        # Indent of Tab or Space
        check = Gtk.CheckButton.new_with_mnemonic("Insert _spaces instead of tabs")
        check.set_active(self.view.get_insert_spaces_instead_of_tabs())
        check.connect("toggled", self.on_toggled)
        # Test Button
        button = Gtk.Button.new_with_label("Type is Integer ?")
        button.connect("clicked", self.on_clicked)
        # pack
        vbox = Gtk.VBox()
        vbox.pack_start(self.view, True, True, 0)
        vbox.pack_start(spin, False, True, 0)
        vbox.pack_start(check, False, True, 0)
        vbox.pack_start(button, False, False, 0)
        # self
        self.add(vbox)
        self.resize(300, 300)
        self.connect("delete-event", Gtk.main_quit)
        self.show_all()
        self.set_focus(self.view)

    def on_spin_changed(self, widget, data=None):
        """
            There is no need to UINT
            self.view.set_tab_width( abs(widget.get_value_as_int()) )
        """
        self.view.set_tab_width( widget.get_value_as_int() )

    def on_toggled(self, widget, data=None):
        self.view.set_insert_spaces_instead_of_tabs(widget.get_active())

    def on_clicked(self, widget, data=None):
        """
            tab-width Type Check
        """
        value = self.view.get_tab_width()
        widget.set_label("Type is {0}".format(type(value)))

if __name__ == "__main__":
    TabTest()
    Gtk.main()

てなわけで、なんとかなった。
memopoli 0.1.1 をめでたく公開、多分自分しか使わないと思うけど。

GtkSourceDrawSpacesFlags

あけましておめでとうございます。
ということで GtkSourceView でアプリを作っているのだが。

GtkSourceView

GtkSourceDrawSpacesFlags というのがある。
gtk_source_view_set_draw_spaces で指定するのね。
以前書いたコードに追記して試しに使ってみる。

#!/usr/bin/env python
#-*- coding:utf-8 -*-

from gi.repository import Gtk, Gio, GtkSource, Pango

class TextEditor(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self)
        # GtkUIManager and GtkAccelGroup
        self.uimanager = Gtk.UIManager()
        accelgroup = self.uimanager.get_accel_group()
        self.add_accel_group(accelgroup)
        # GtkActionGroup
        self.actiongroup = Gtk.ActionGroup("editor_menu")
        # GtkActionEntry
        ae = [  ("open", Gtk.STOCK_OPEN, "_Open", "O", "open", self.on_open),
                ("save", Gtk.STOCK_SAVE, "_Save", "S", "save", self.on_save),
                ("save_as", Gtk.STOCK_SAVE_AS, "Save as...", "S", "save as", self.on_save_as),
                ("quit", Gtk.STOCK_QUIT, "Quit", "Q", "quit", self.on_quit),
                ("file", None, "_File") ]
        self.actiongroup.add_actions(ae)
        self.uimanager.insert_action_group(self.actiongroup, 0)
        self.uimanager.add_ui_from_string(menu_xml)
        # MenuBar
        menubar = self.uimanager.get_widget('/MenuBar')
        # Toolbar and Style
        toolbar = self.uimanager.get_widget('/ToolBar')
        toolbar.set_style(Gtk.ToolbarStyle.ICONS)
        style = toolbar.get_style_context()
        Gtk.StyleContext.add_class(style, Gtk.STYLE_CLASS_PRIMARY_TOOLBAR)
        # StatusBar
        statusbar = Gtk.Statusbar()
        # GtkSourceView
        self.view = GtkSource.View()
        desc = Pango.font_description_from_string("Monospace 11")
        self.view.override_font(desc)
        self.view.set_show_line_numbers(True)
        self.view.set_wrap_mode(Gtk.WrapMode.CHAR)
        self.view.set_draw_spaces(GtkSource.DrawSpacesFlags.ALL)
        # Add
        sw = Gtk.ScrolledWindow()
        sw.add(self.view)
        vbox = Gtk.VBox()
        vbox.pack_start(menubar, False, True, 0)
        vbox.pack_start(toolbar, False, True, 0)
        vbox.pack_start(sw, True, True, 0)
        vbox.pack_start(statusbar, False, True, 0)
        self.add(vbox)
        # self
        self.open_filename = ""
        self.resize(300, 300)
        self.set_title("Text Editor")
        self.show_all()
        self.set_focus(self.view)

    def on_open(self, widget, data=None):
        dlg = Gtk.FileChooserDialog(
                "Open File",
                self,
                Gtk.FileChooserAction.OPEN,
                (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, 
                Gtk.STOCK_OPEN, Gtk.ResponseType.ACCEPT))
        r = dlg.run()
        if r == Gtk.ResponseType.ACCEPT:
            self.open_filename = dlg.get_filename()
            f = open(self.open_filename, "r")
            t = f.read()
            f.close()
            buf = self.view.get_buffer()
            buf.set_text(t)
        dlg.destroy()

    def on_save(self, widget, data=None):
        if self.open_filename == "":
            self.on_save_as(widget)
        else:
            buf = self.view.get_buffer()
            t = buf.get_text(buf.get_start_iter(), buf.get_end_iter(), False)
            f = open(self.open_filename, "w")
            f.write(t)
            f.close()

    def on_save_as(self, widget, data=None):
        dlg = Gtk.FileChooserDialog(
                "Save File",
                self,
                Gtk.FileChooserAction.SAVE,
                (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, 
                Gtk.STOCK_SAVE, Gtk.ResponseType.ACCEPT))
        r = dlg.run()
        if r == Gtk.ResponseType.ACCEPT:
            buf = self.view.get_buffer()
            t = buf.get_text(buf.get_start_iter(), buf.get_end_iter(), False)
            self.open_filename = dlg.get_filename()
            f = open(self.open_filename, "w")
            f.write(t)
            f.close()
        dlg.destroy()

    def on_quit(self, widget, data=None):
        self.emit("destroy")

menu_xml = """<ui>
    <menubar name="MenuBar">
        <menu action="file">
            <menuitem action="open"/>
            <menuitem action="save"/>
            <menuitem action="save_as"/>
            <separator/>
            <menuitem action="quit"/>
        </menu>
    </menubar>
    <toolbar name="ToolBar">
        <toolitem action="open"/>
        <toolitem action="save"/>
    </toolbar>
</ui>"""

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(
                self,
                application_id="apps.test.editor",
                flags=Gio.ApplicationFlags.FLAGS_NONE)
        self.connect("activate", self.on_activate)
        
    def on_activate(self, data=None):
        w = TextEditor()
        w.set_application(self)
    
if __name__ == "__main__":
    app = App()
    app.run(None)

これは、まるで秀丸ではないか。
なんだよ、GtkSourceView には改行やタブ文字表示機能があるんだ。
dconf-editor で Gedit の設定を漁ったけど無かった…

GTK_SOURCE_DRAW_SPACES_NBSP とかは試してもよく解らないのだけど。
日本語圏内では解らないことなのだろうか、うーん。

ついでに、上記コードのようにフォント指定を作っている。
いや、デフォルトに戻したくなる場合もあるわけで。

# Get Default Font
settings = Gtk.Settings.get_default()
defaultfont = settings.props.gtk_font_name
desc = Pango.font_description_from_string(defaultfont)

これでデフォルトの PangoFontDescription が得られるわけだ。

どうでもいいけど Fedora の Monospace では半角全角で幅が違う。
VL ゴシックにすればいいわけだがコレは人によるからな。

ぶっちゃけ設定は Gedit の劣化パクリである。
括弧の強調表示方法がイマイチ解らない、GtkSourceBuffer 側なのよね。

現在インデントが全部半角スペースになる現象に悩まされている…
明日には出したいな、休みは明日までだし。

PangoFontDescription

memopoli の読み書きとツリー移動がなんとかなったので本サイトで公開。
まだ実装していないメニュー項目があるし設定も何もないけど「まあいいや」で。

どうしても 2011 年中に間に合わせたかっただけだ!
こんな最小限の実装までにとうとう大晦日まで掛かってしまった…

memopoli ヘルプ – L’Isola di Niente

私的にたいしたことはやっていないと思うので blog に書くことが無い。
後は自分で利用(バグ出し)しながら細かい機能を実装していくつもり。

次にフォント設定をやろうと考えていきなり困った。
GTK+ ではフォント情報をどうやって保存するのだろう?

Fonts

Pango にpango_font_description_to_string という関数があるね。
そういえば以前フォント変更する方法だけはやっていた。

GtkFontChooserDialog | PaePoi

今頃気がついたが gtk_widget_modify_font は古いようだ。
gtk_widget_override_font にすればいいのね。
よし、この前回コードをフォント情報が保存できるように書き換えてみよう。

#!/usr/bin/env python
#-*- coding:utf-8 -*-

from gi.repository import Gtk, Gio, Pango

class Win(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self)
        self.set_title("font test")
        entry = Gtk.Entry()
        entry.set_text("homurachan")
        button = Gtk.Button("Select Font")
        button.connect("clicked", self.on_clicked, entry)
        vbox = Gtk.VBox()
        vbox.pack_start(button, False, False, 0)
        vbox.pack_start(entry, False, False, 0)
        self.add(vbox)
        self.connect("delete-event", self.on_delete)
        # Config File Read
        try:
            f = open("font.conf", "r")
            s = f.read()
            f.close()
        except:
            s = "Monospace 16"
        # PangoFontDescription
        self.desc = Pango.font_description_from_string(s)
        entry.override_font(self.desc)
        self.show_all()

    def on_delete(self, widget, event):
        c = self.desc.to_string()
        f = open("font.conf", "w")
        f.write(c)
        f.close()

    def on_clicked(self, widget, entry):
        dlg = Gtk.FontChooserDialog("Select Font")
        if dlg.run() == Gtk.ResponseType.OK:
            self.desc = dlg.get_font_desc()
            #entry.modify_font(self.desc) # old Func
            entry.override_font(self.desc)
        dlg.destroy()

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(
                self,
                application_id="apps.test.font",
                flags=Gio.ApplicationFlags.FLAGS_NONE)
        self.connect("activate", self.on_activate)

    def on_activate(self, data=None):
        w = Win()
        w.set_application(self)

if __name__ == "__main__":
    app = App()
    app.run(None)

なるほど、単純に「フォント名 サイズ」文字列だけでいいのか。
これなら簡単だ、早速 memopoli に実装することにしよう。
メニューに乗せるかダイアログに入れるか、うーん…

ということで。

2011 年イッパイになったのでめでたく Y901, Palepoli 等の公開を終了。
早く消したかったけど我慢して一年も間をおいたので多分大丈夫だろう。
それでは良いお年を。

Dark Theme

GNOME 3.2 から導入された Dark Theme って簡単なんですね。
gtk_settings_get_default ()
で得られる Settings のプロパティを弄くるだけ。

ついでだから他にどんなプロパティがあるかを書き出す処理も。

#!/usr/bin/env python
#-*- coding:utf-8 -*-

from gi.repository import Gtk

# Sharing settings Default Value Check
settings = Gtk.Settings.get_default()
for s in dir(settings.props):
    if not s.startswith("_"):
        print "{0}: {1}".format(s, settings.get_property(s))

# Set Dark Theme
settings.props.gtk_application_prefer_dark_theme = True
# or
#settings.set_property('gtk-application-prefer-dark-theme', True)

w = Gtk.Window()
w.connect("delete-event", Gtk.main_quit)
w.show()
Gtk.main()

こんなに簡単な処理で Totem 等と同じ見た目になる。
他色々プロパティがあるけど利用するかどうかは微妙かな。

ついでに memopolix の現状。
memopoli に名前変更する、メモポリエックスじゃ言いにくい。
クリポリエックスも次で x を取る、Windows 版と名前を分ける必要はなかった。
ワイキュウマルイチエックスだけは x を付けないと混乱する人が出る。

ガシガシ例外を吐いてくれる、もう少しまともになったら何か書く。

destroy and delete-event Signal

現在作っている PyGI アプリで Window size を保存したい。
GtkApplication から起動なら delete-event で Gtk.main_quit せずに閉じる。
ちなみに delete-event は標準の「閉じる」ボタンか吐くシグナルです。

なので destroy Signal で処理しようとしたのだけど。

#!/usr/bin/env python
#-*- coding:utf-8 -*-

from gi.repository import Gtk, Gio

class DestroyTest(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self)
        cx, cy = self.get_size()
        print "initialize size {0},{1}".format(cx, cy)
        self.connect("delete-event", self.on_delete_event)
        self.connect("destroy", self.on_destroy)
        # Resize
        self.resize(320, 240)
        self.show_all()

    def on_destroy(self, widget):
        cx, cy = self.get_size()
        print "destroy size {0},{1}".format(cx, cy)

    def on_delete_event(self, widget, event):
        cx, cy = self.get_size()
        print "delete-event size {0},{1}".format(cx, cy)

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(
                self,
                application_id="apps.test.destroy",
                flags=Gio.ApplicationFlags.FLAGS_NONE)
        self.connect("activate", self.on_activate)
        
    def on_activate(self, data=None):
        w = DestroyTest()
        w.set_application(self)
    
if __name__ == "__main__":
    app = App()
    app.run(None)

""" output
initialize size 200,200
delete-event size 320,240
destroy size 200,200
"""

こうなっってしまった。
破棄された後に値を得ようとしているのだからそりゃそうだ。
.NET とかなら例外、って GTK+ は C 言語だったね。

それなら delete-event で設定保持を行えばいい。
んでもって emit も destroy から変更して。
で、又困ってしまった。

#!/usr/bin/env python
#-*- coding:utf-8 -*-

from gi.repository import Gtk, Gio, Gdk, GdkX11

class DestroyTest(Gtk.Window):
    def __init__(self):
        Gtk.Window.__init__(self)
        button = Gtk.Button("emit delete-event")
        button.connect("clicked", self.on_button_clicked)
        self.add(button)
        self.connect("delete-event", self.on_delete_event, button)
        self.show_all()

    def on_button_clicked(self, button, data=None):
        """
            emit is No Close
        """
        event = Gdk.Event(Gdk.EventType.DELETE)
        self.emit("delete-event", event)
        # Enable a Close
        #self.emit("destroy")

    def on_delete_event(self, widget, event, button):
        button.set_label("catch delete-event")
        return False

class App(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(
                self,
                application_id="apps.test.destroy",
                flags=Gio.ApplicationFlags.FLAGS_NONE)
        self.connect("activate", self.on_activate)
        
    def on_activate(self, data=None):
        w = DestroyTest()
        w.set_application(self)
    
if __name__ == "__main__":
    app = App()
    app.run(None)

delete-event の emit だけじゃ終了してくれない。
メニューやボタンから終了するなら destroy も投げる必要あり?

GtkApplication はどうやって閉じているのだろう。
やっぱり普通に Gtk.main() を使ったほうが楽かも。