SeeMe」タグアーカイブ

HTML5 Dynamic Select

HTML5 で SeeMe(Opera serch.ini 編集アプリ) を作りたい。

Opera 使いは全員最新版を使うのでバージョンを気にする必要が無い。
ぶっちゃけ Opera にだけ対応させればいいので面倒が無い、素晴らしい。
まさに HTML5 で作るにはうってつけではないか!

というのは建前で…
Windows をまったく使っていないので Windows 版を作りたくないから。
HTML5 なら Linux で作ってツジツマを合わせればいいだけだもの。

後々で更新が面倒くさいからセクション丸ごと textarea に書き出すだけにでも…
それはどうでもよくて。

とりあえず ListBox みたいなのを作るには <select> タグでいいようだ。
コレを動的に作成して順番の入れ替えなんかの処理を入れればイケる。

Select in HTML 5
Dynamic Select in HTML with Ajax

何故か動的な JavaScript 編集コードはフランスで見つける。
日本の HTML5 解説サイトって何故かみんな似たようなのばかりでこういうのが見つからない、探し方が悪いのだろうけど英語で探したほうが早いのはたしか。
どうでもいいがドイツからコメントスパムが来るようになったぞ。

とにかく SeeMe に必要な新規作成、上下移動、削除のテストコード。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>SeeMe for HTML5</title>

<style>
#id_name { float: left; }
#id_edit { float: left; }
</style>

<script>
function on_up(listbox) {
    var i = listbox.selectedIndex;
    if (i > 0) {
        var str = listbox.options[i].text;
        listbox.options[i].text = listbox.options[i-1].text;
        listbox.options[i-1].text = str;
        listbox.selectedIndex = listbox.selectedIndex - 1;
    }
}
function on_down(listbox) {
    var i = listbox.selectedIndex;
    var len = listbox.length;
    if (i < len-1) {
        var str = listbox.options[i].text;
        listbox.options[i].text = listbox.options[i+1].text;
        listbox.options[i+1].text = str;
        listbox.selectedIndex = listbox.selectedIndex + 1;
    }
}
function on_new(listbox) {
    var len = listbox.length;
    listbox[len] = new Option("new!");
    listbox.size = len + 1;
    listbox.selectedIndex = len;
}
function on_delete(listbox) {
    var i = listbox.selectedIndex;
    if (i == -1) return;
    listbox.options[i] = null;
    listbox.size = listbox.size - 1;
    var len = listbox.length;
    if (len == 0) {
        return;
    }
    else if (i == len) {
        listbox.selectedIndex = i-1;
    }
    else {
        listbox.selectedIndex = i;
    }
}
function on_select(form) {
    var i = form.listbox.selectedIndex;
    form.edit1.value = form.listbox.options[i].text;
    form.message1.value = form.listbox.options[i].text;
}
function init() {
    var listbox = document.getElementById("id_select");
    for (var i=0; i<10; i++) {
        listbox[i] = new Option("Item" + i);
    }
    listbox.size = 10;
}
</script>

</head>

<body onLoad="init()">


<form>
<div>
<input type="button" value="up" onclick="on_up(this.form.listbox)">
<input type="button" value="down" onclick="on_down(this.form.listbox)">
<input type="button" value="new item" onclick="on_new(this.form.listbox)">
<input type="button" value="delete item" onclick="on_delete(this.form.listbox)">
</div>
<div id="id_name">
<select id="id_select" name="listbox" onchange="on_select(this.form)"></select>
</div>
<div id="id_edit">
<input type="text" name="edit1" size=20><br />
<textarea name="message1" rows="6" cols="60px"></textarea>
</div>
</form>

</body>
</html>

問題なく移動な削除ができるようだ。
ということで早速こんな感じにしようと作っていた。
ユーザー名と OS さえ解れば ini のパスは解るからと思ったけど…

File オブジェクトはパスや URI の文字列からは作成できないんだね。
間に PHP あたりを噛ませてなんとかするしか無いかな…
挫折したらごめん、多分もう Windows 版は更新しない。

iter_previous @ GTK+ 3 only

seemex を Ubuntu 11.04 で動かすテスト。
あらら色々問題が。
まず前回書いた StyleContext は GTK+ 2 では使えない。

# Toolbar Style
toolbar = self.uimanager.get_widget('/ToolBar')
toolbar.set_style(gtk.ToolbarStyle.ICONS)
try:
    style = toolbar.get_style_context()
    gtk.StyleContext.add_class(style, gtk.STYLE_CLASS_PRIMARY_TOOLBAR)
except Exception, s:
    pass # GTK+ 2

コレでいいや、GNOME 3 に合わせたいだけの処理なのだし。

AboutDialog の get_major_version() メソッドも使えなかった。
MAJOR_VERSION とかの定数はどちらでも使えるからコッチに変更しよう。

更に GtkTreeView 順番を上に移動する iter_previous もダメか。
GtkTreePath には確か prev メソッドがあったよな。

def on_item_up(self, widget, event=None):
    selection = self.custome_treeview.get_selection()
    model, it = selection.get_selected()
    p = model.get_path(it)
    if it:
        path = model.get_path(it)
        if path.prev():
            itprev = model.get_iter(path)
            model.swap(it, itprev)
            self.change()
        """ GTK+ 3
        itprev = it.copy()
        if model.iter_previous(itprev):
            model.swap(it, itprev)
            self.change()"""
    else:
        self.messagebox(MESSAGE_NON_SELECT)

せっかく簡単になったけど GTK+ 2 のままの Ubuntu を考慮しなきゃとは…

しかし Fedora の VirtualBox ose アップデートはまだか。
右 Ctrl+F でもそのまんま XGA サイズだし \ は打てないし辛い。

pygtk3 pack_start

自作 PyGtk アプリを Gtk3 に切り替えしたい。
よし、まずは SeeMe for Linux だ。
現行 Opera の仕様と合わないまま放置もどうかと思うし。

名前を SeeMe for Linux という無駄に長い名前から seemex に変更。
単純に打つのが面倒なだけです、バージョンは 2.0 でいいな。

まず gtk 名前空間の変更、面倒だから変名しちゃえ。
というか大文字の名前空間ってやっぱり変だよね。

#import pygtk
#pygtk.require("2.0")
#import gtk
from gi.repository import Gtk as gtk

定数の大半が列挙体名のアトリビュートに変わっているのは以前書いた。
gtk.FILL が Gtk.AttachOptions.FILL になったのを見つけるのに苦労したぞ。

pack_start の引数は expand, fill, padding を C 同様に全部指定する必要あり。
デフォルト引数は現行方式では用意されていない、バインディング方法が違うし。

# old pack_start(child, expand=True, fill=True, padding=0)
v = Gtk.VBox()
v.pack_start(widget, True, True, 0)
#v.pack_start(widget) Error

アイコンは GdkPixbuf を作らなくてもパスで良かったのね、前から…

path = os.path.join(os.path.dirname( __file__ ), "seeme.xpm")
#self.icon = gtk.gdk.pixbuf_new_from_file(path)
#self.set_icon(self.icon)
self.set_icon_from_file(path)

でも GdkPixbuf をファイルから作らないと Y901x のボタンが利用できない。
んー cairo を使うみたいなんだけど今はよく解らない。
seeme からやりはじめて助かった…

レンダラのコールバック引数とかに user_data が入って飛んでくる。
とにかく何をやるのも C 言語で書くのと同じようにしなきゃいけなくいなった。

GtkDialog に vbox アトリビュートなんかも当然無い。
C 言語同様に get_content_area で GtkBox を得る必要あり。
PyGtk だからこそ便利だった拡張が丸ごと利用できなくなった。

他 PyGtk2 との細かい差はここから辿って見つけて。
GTK+ 3 Reference Manual

GtkAboutDialog は set_url_hook せずともリンクボタンになる。

VERSION = "1.9.0"

class SeeMe4(gtk.Window):
    #...
    def on_about(self, widget, event=None):
        url = "http://palepoli.skr.jp/"
        w = gtk.AboutDialog()
        w.set_transient_for(self)
        w.set_program_name("seemex")
        w.set_version(VERSION)
        w.set_copyright("Copyright(C) 2009-2011 sasakima-nao")
        w.set_license("GPL")
        w.set_logo(self.get_icon())
        v = "GTK+ Version {0}.{1}.{2}".format(
                gtk.get_major_version(),
                gtk.get_minor_version(),
                gtk.get_micro_version())
        w.set_comments(v)
        w.set_website(url)
        w.set_website_label(url)
        w.run()
        w.destroy()

よし、とりあえず GTK+ 3.0 なアプリになったぞ。
Ubuntu 11.04 で動かしていないけど多分 Ubuntu では GTK2 になるはずだが。
後は現行 Opera で無意味になったセパレータ指定とかを消したり色々。

バックアップ
seeme-1.9.0.tar.gz

コイツが一段落したら Windows 版をどうしよう…

v1 とある理由から Delphi でチャチャッと作成
v2 動画プレイヤー作りが嫌になった反動で無意味にコレの製作を続ける
v3 WPF で何か作りたいという理由だけで C# 化
v4 インストーラを作ってみたいという理由だけでインストーラ化
v5 IronPython で何かを作りたいという理由だけで IronPython 化
v6 自分でビルドしろ第二段にしたい理由だけで C# に戻す、かも…

Windows 版は完全に作者のおもちゃ。
いやコレを作ったから私は十年も生き残っているのは間違いないんだが。

SeeMe 5.0.1 and 1.0.5

GtkSharp TreeView Tutorial – Mono

まだ SeeMe for Linux を GTK# で作っていた頃の参考にしたトコ。
よく見ると自作クラスを型として GtkListStore を作成しているコードがあった。
Controlling how the model is used 以下のところね。

こういう型指定って PyGtk でもできるのかな?
可能であればもっと本体のコード量を減らすことができるんだが。
とりあえず Python で同じように SeeMe コードを書きかえてみた。

class Engine():
    def __init__(self, deleted=True, name="New Item", key="", url="", query="",
                post=False, endsp=False, encode="utf8", stype=0, pos=0,
                nameid=0, verb=0, icon="", unique="", usetld=0):
        self.delete = deleted
        self.name = name
        self.key = key
        self.url = url
        self.query = query
        self.post = post
        self.endsep = endsp
        self.encode = encode
        self.stype = stype
        self.pos = pos
        self.nameid = nameid
        self.verb = verb
        self.icon = icon
        self.unique = unique
        self.usetld = usetld

class SeeMe4(gtk.Window):
    def __init__(self, sset):
        gtk.Window.__init__(self)
        #...
        #self.default_liststore = gtk.ListStore(bool, str, str, str, str, bool, bool, str, int, int, int, int, str, str, int)
        self.custome_liststore = gtk.ListStore(Engine)

型指定の時点で駄目ジャン…
property にしてみたり小細工してみたりしたけど無駄な努力だった。
Python では変数宣言しただけでは型が決まっていないので当然なのかも。

gtk.ListStore

ま、公式の Constructor 解説には C 言語と同じ型を全部書く方法しか書かれていない。
できないことは素直に諦めて、せめて GtkListStore への append をもう少し簡単にやれないか?

てか、そうしておかないと後々のメンテで沢山書き換えを行わなければいけなくなる。
後で書き換えが必要だろう箇所が少なければ少ないほどミスが減るのよね。
というかソレがオブジェクト指向最大のメリットなのだし。

よく考えたら SeeMe で GtkListStore の型は一つしか無い。
だったら GtkListStore サブクラスを作ってコンストラクタでとっとと型を指定。
ついでに Engine クラスを受け取る add メソッドを作れば簡単になるかな?

class Engine():
    def __init__(self, deleted=True, name="New Item", key="", url="", query="",
                post=False, endsp=False, encode="utf8", stype=0, pos=0,
                nameid=0, verb=0, icon="", unique="", usetld=0):
        self.delete = deleted
        self.name = name
        self.key = key
        self.url = url
        self.query = query
        self.post = post
        self.endsep = endsp
        self.encode = encode
        self.stype = stype
        self.pos = pos
        self.nameid = nameid
        self.verb = verb
        self.icon = icon
        self.unique = unique
        self.usetld = usetld

class CreateListStore(gtk.ListStore):
    def __init__(self):
        gtk.ListStore.__init__(self, bool, str, str, str, str, bool, bool, str, int, int, int, int, str, str, int)

    def add(self, en):
        return self.append( [en.delete, en.name, en.key, en.url, en.query,
                en.post, en.endsep, en.encode, en.stype, en.pos,
                en.nameid, en.verb, en.icon, en.unique, en.usetld] )

class SeeMe4(gtk.Window):
    def __init__(self, sset):
        gtk.Window.__init__(self)
        #...
        self.custome_liststore = CreateListStore()
        #...

    def read_default_searchini(self):
        self.default_liststore.clear()
        inipath = self.sset.default_path
        lngpath = self.sset.lang_path
        if os.path.exists(inipath):
            ini = inifile8.Inifile(inipath)
            lng = inifile8.InifileReader(lngpath)
            try:
                self.iniver = ini.read_int("Version", "File Version", 0)
                i = 0
                while 1:
                    i += 1
                    s = "Search Engine %i" % i
                    if not ini.section_exists(s):
                        break
                    t = ini.read_str(s, "Key", "")
                    if t == "":
                        continue
                    en = Engine();
                    en.nameid = ini.read_int(s, "Nameid", 0);
                    if en.nameid != 0:
                        # Get View Name from *.lng
                        if en.nameid == 17171:
                            en.name = lng.read_str("Translation", "1632215285", "")
                        elif en.nameid == 17183:
                            en.name = lng.read_str("Translation", "-1971470391", "")
                        elif en.nameid == 71103:
                            en.name = lng.read_str("Translation", "-1453429782", "")
                        else:
                            en.name = lng.read_str("Translation", str(en.nameid), "")
                        if en.name == "":
                            n = -1752227277 - en.nameid;
                            en.name = lng.read_str("Translation", str(n), "")
                    else:
                        en.name = ini.read_str(s, "Name", "")
                    en.key = t
                    en.url = ini.read_str(s, "URL", "")
                    en.query = ini.read_str(s, "Query", "")
                    en.post = ini.read_bool(s, "Is post", False)
                    en.endsep = ini.read_bool(s, "Has endseparator", False)
                    en.encode = ini.read_str(s, "Encoding", "utf-8")
                    en.stype = ini.read_int(s, "Search Type", 0)
                    en.pos = ini.read_int(s, "Position", -1)
                    en.verb = ini.read_int(s, "Verbtext", 0)
                    en.delete = True #!
                    en.unique = ini.read_str(s, "UNIQUEID", "")
                    en.icon = ini.read_str(s, "ICON", "")
                    en.usetld = ini.read_int(s, "UseTLD", 0)
                    """self.default_liststore.append( [
                            en.delete, en.name, en.key, en.url, en.query,
                            en.post, en.endsep, en.encode, en.type, en.pos,
                            en.nameid, en.verb, en.icon, en.unique, en.usetld] )"""
                    self.default_liststore.add(en)

    def on_item_new(self, widget, event=None):
        uid = self.create_uuid()
        num = self.deditor.get_radio_num()
        if num == 2:
            num = -1988219522
        else:
            num = 0
        en = Engine(verb=num, unique= uid)
        #it = self.custome_liststore.append( [True, "", "", "", "", False, False, "utf-8", 0, -1, 0, num, "", uid, 0] )
        it = self.custome_liststore.add(en)

コードは全然短くならないけど Engine を直で渡して展開できるようになった。
デフォルト引数によってアトリビュートの型も大半が気にしないでいいという。
読み込みや新規アイテムで順番を気にする必要が無くなったのはデカい。
これなら後々で順番の入れ替えや追加の必要があっても型側で調節できる。

ただこのコードでは展開するために for ループにすると

for row in self.default_liststore:
    if row[0] == False:
        i += 1
        s = "Search Engine %i" % i
        ini.write_str(s, "UNIQUEID", row[13])

添字アクセスにするしか無いんだなぁこれが、意味ネェ。
イテレーターは整数による順番という概念を使わないことに意義があるのだが。
この部分については WPF は徹底的にやっている感じ、よく考えて作られているよ。
ここをアトリビュートで取り出すサブクラスをどうすれば作れるか考え中。

ということで重複キーチェック追加や不具合修正で Linux 版更新。
キーボードショートカットを増やしたり不具合修正で Windows 版も更新。

「こんなことができると楽」
な方法を考えている時がアプリケーション作りで一番楽しいです。

SeeMe for Windows 5.0.0

SeeMe for Windows 5.0.0 公開。
実は大半が二日前に終わっていたのですが、たった一箇所で二日掛かった。
Deleted プロパティ変更を SeeMe 本体でどうやって検知するかで。

INotifyPropertyChanged によって ListView にはバインディングされているんだけど…

<Window.Resources>
<DataTemplate x:Key="tpl_show">
    <CheckBox IsChecked="{Binding Path=delete}" IsTabStop="False" Click="CheckBox_Click" />
</DataTemplate>

と C# で作っていた時には DataTemplate で CheckBox.Click イベントのハンドラを指定していた。
こうしておけばこの DataTemplate を利用した CheckBox 全てに同一ハンドラがコネクトされる。
クリックで property が変更されましたというのを本体で知りたいだけなのでコレで十分。

IronPython では partial class にする方法が解らない(方法あるの?)というのと
SeeMe for Linux の GTK+ と限界までソックリにしたいということで ListView をパーツ化した。
結果イベントハンドラのコネクトはコードで行う以外に手が無い。

ListView サブクラスを作って partial にして XAML の x:Name 属性を…
散々試してみたんだがどうしても上手くいかない、やはり無理か?

えっと、ハンドラを含めたリソースをコードで書いて…
方法が解らない、つかそんなことできるのかいな?

res = self.custome_listview.FindResource("tpl_show")
print res
checkbox = res.LoadContent()
print checkbox
checkbox.Click += self.on_modification

例外にならないけど反映されない、困った。
海外を探しまくったけどやはり IronPython の細かい情報は少ない…

まてよ、INotifyPropertyChanged を利用しているんだ。
それならコイツを引っ張り出して CheckBox の変更を感知すればいいかも。
無理に Click イベントに拘る必要は無かったのではないか?

と気が付くまで二日も無駄にしたというわけです…

while 1:
    i += 1
    s = "Search Engine %i" % i
    if not ini.section_exists(s):
        break
    en = Engine()
    en.name   = ini.read_str(s, "Name", "")
    en.key    = ini.read_str(s, "Key", "")
    en.url    = ini.read_str(s, "URL", "")
    en.delete = not ini.read_bool(s, "Deleted", False)
    #...
    en.PropertyChanged += self.on_modification
    self.custome_liststore.Add(en)

という感じで PropertyChanged にハンドラをコードで追加メソッドにできる。
しかもコレなら全部のプロパティ変更を監視できるので自前検知処理を減らすことができる。
でもコレだけでは ObservableCollection への追加や削除は検知できないので

self.custome_liststore = ObservableCollection[Engine]()
#...
self.custome_liststore.CollectionChanged += self.on_modification

と CollectionChanged イベントにもハンドラを指定。
コレでやっと予定していたとおりの動作になった。
WPF が複雑すぎるのではなかった、自分が方法に気が付かなかっただけだ。
細かいことは SeeMe スクリプトの Python コードを見てください。

ということで予定を大幅に遅れて Windows 版公開。
メソッド名やアトリビュート名は Linux 版とだいたい合わせたので今後更新は早くなる。
と思う、つか Aspire 1410 ミニノートだけで作るのは結構辛い…