設定ファイルを複雑にしたくないがための「history」@tkinter

ネタとしては tkinter なんだけれど、発想そのものはわりと汎用。

ffchopreview.pyはなんだか Rev128 にまで育っちまったが、雑にガシガシ連ねってってるだけなので、番号に期待するだけの「すんげーこと/もの」に仕立て上がっているわけではない。

その ffchopreview.py はまぁ設定ファイルを持っていて、ここからデフォルト値とかを取ってるんだけれど、これからどうしていこうかなぁと悩んでおってね。まず Rev128 に至ってもなお、いまだ「GUI で編集した設定をセーブする機能がない」かったりするのは、これから言う悩みの現れ。

そもそも設定ファイルに書くようなものって、「数秒ごとに変更しうる」ものじゃのーて、当然ある程度の時間固定したまま使っていられるものを管理するよね、ふつー。そうなんだけど、やっぱグレーなやつってのはあらわれるわけでね。「下手すりゃぁ頻繁に切り替えたい」なんてものが。ffchopreview.py では最も顕著なのが 「audio_visualize_if_no_input_vstream」で、これ、「プレビューとして音声可視化を使う」ためのフィルタなんだけれど、「ただ一つの絶対的正解」があるわけじゃなくて、最低でも少なくとも二つ三つくらいのお気に入りはあり、なおかつ「まれにすんげー凝ったものが欲しい」かったりもして、「絶賛試行錯誤時間」も含め、結構頻繁に切り替えたいものなわけよ。ゆえにこうしたものは、微妙に「設定ファイル」に馴染みにくい。てなことがあり、「設定ファイル変更のセーブ」を実装するのをためらってんのよね。

無論設定ファイル内で「リストで管理する」しつつ、設定変更 UI は編集可能なプルダウンを使う…という方向性はありえる。それが良ければそうするのはやぶさかではない。ただ「簡単切り替え」の術がほかにあるのならば、設定ファイルを拡張せずともありがたいてことはないかいね、と。

これってのはもうあれよ、ブラウザの検索ボックスなんかにある「オートコンプリート」みたいな発想よ。emacs でも昔から abbrev-expand なんてのがあって、これは「かつてどこぞのバッファに現れたテキスト」を拾ってテキスト補完に使ってくれる。こういうものがあれば「設定ファイルのアイテムをリストにする」などの複雑化はいらんことにならんかいな、と。

tkinter ではたぶん自作するしかないんだと思うんだよね、たとえばこんなかなぁと思った:

python 2.7 対応は「ふり」だけ。動くかわかんない。
 1 #! py -3
 2 # -*- coding: utf-8 -*-
 3 from __future__ import unicode_literals
 4 
 5 try:
 6     import Tkinter as tkinter  # python 2.x, but sorry, I'm not testing it.
 7     import ttk
 8     from Tkinter import E, W, N, S  # for sticky
 9     def _askopenfilename(master):
10         import tkFileDialog
11         return tkFileDialog.askopenfilename()
12     from tkSimpleDialog import Dialog
13     from ScrolledText import ScrolledText
14 except ImportError:
15     import tkinter
16     from tkinter import ttk
17     from tkinter import E, W, N, S  # for sticky
18     def _askopenfilename(master):
19         from tkinter.filedialog import askopenfilenames
20         return askopenfilenames(multiple=0)
21     from tkinter.simpledialog import Dialog
22     from tkinter.scrolledtext import ScrolledText
23 
24 #
25 import json, os, io, functools
26 
27 class HistCompletionPopupMng(object):
28     def __init__(self):
29         # key: ident, value: a list of history items
30         self._histories = self.load_history()
31 
32         # key: ident, value: instance of control
33         self._controls = {}
34 
35     def _append_history(self, ident, *args):
36         v = self._controls[ident].get().strip()
37         if ident not in self._histories:
38             self._histories[ident] = []
39         if v not in self._histories[ident]:
40             self._histories[ident].append(v)
41 
42     def _popup_history_selection(self, ident, *args):
43         entry = self._controls[ident]
44         hist = self._histories.get(ident, [])
45         if not hist:
46             return
47         menu = tkinter.Menu(entry, tearoff=0)
48         for item in hist:
49             menu.add_command(
50                 label=item,
51                 command=lambda *args: (
52                     entry.delete(0, "end"), entry.insert(0, item)))
53         ppos_x = entry.winfo_rootx()
54         ppos_y = entry.winfo_rooty()
55         try:
56             menu.tk_popup(ppos_x, ppos_y)
57         finally:
58             menu.grab_release()
59 
60     def register_control(
61             self,
62             ident,
63             entry  # the instance of tkinter.Entry (etc.)
64     ):
65         self._controls[ident] = entry
66         entry.bind(
67             "<FocusOut>", functools.partial(self._append_history, ident), add=1)
68         entry.bind(
69             "<Control-space>", functools.partial(self._popup_history_selection, ident), add=1)
70 
71     def save_history(self):
72         pass
73         
74     def load_history(self):
75         # key: ident, value: a list of history items
76         return {}
77 
78 
79 # -------------------
80 # example
81 pm = HistCompletionPopupMng()
82 tkroot = tkinter.Tk()
83 ent = tkinter.Entry()
84 ent.pack()
85 pm.register_control("ent", ent)
86 ent2 = tkinter.Entry()
87 ent2.pack()
88 tkroot.mainloop()

load, save はまだ書いてない。書けばまぁ使えるものにはなる。ただ「FocusOut」がほんとは問題でなぁ。こいつ、期待したタイミング全部では入ってくれないんよね…。でもまぁ…、「まぁまぁ」かな、きっと。


12:15追記:
ごめん、ちとサボり過ぎた。上の lambda でやってる部分、item インスタンスの共有問題が起こっちゃって、期待通り振る舞わない。実際に ffchopreview.py に組み込んだのでそっち参照。

にしても、これだけだと「まだまだだなぁ」って感じね。いらない履歴を削除したいとも思うし、そもそも編集の際って結構無意識にフォーカスアウトしがちで、そのたびに「中途のアイテム」が挿入されちまうのも考えもの。ただ…、予想通り、これのおかげで「あんまし凝った設定ファイルの構造は考えなくていい」にはなった。まぁひとまず前進、と考えておきますか…。


11-13追記:
上で書いたままだと tkinter.TclError: No more menus can be allocated が起こる。すまん。